added git and edited settings.py
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2008, 2012 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014, 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import sys
|
||||
|
||||
from .__pkginfo__ import version as __version__
|
||||
|
||||
def run_pylint():
|
||||
"""run pylint"""
|
||||
from pylint.lint import Run
|
||||
Run(sys.argv[1:])
|
||||
|
||||
|
||||
def run_epylint():
|
||||
"""run pylint"""
|
||||
from pylint.epylint import Run
|
||||
Run()
|
||||
|
||||
def run_pyreverse():
|
||||
"""run pyreverse"""
|
||||
from pylint.pyreverse.main import Run
|
||||
Run(sys.argv[1:])
|
||||
|
||||
def run_symilar():
|
||||
"""run symilar"""
|
||||
from pylint.checkers.similar import Run
|
||||
Run(sys.argv[1:])
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
#!/usr/bin/env python
|
||||
import pylint
|
||||
pylint.run_pylint()
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Florian Bruhin <git@the-compiler.org>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2017 Hugo <hugovk@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
# pylint: disable=W0622,C0103
|
||||
"""pylint packaging information"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from os.path import join
|
||||
from sys import version_info as py_version
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from setuptools import __version__ as setuptools_version
|
||||
|
||||
modname = distname = 'pylint'
|
||||
|
||||
numversion = (1, 9, 2)
|
||||
version = '.'.join([str(num) for num in numversion])
|
||||
|
||||
install_requires = [
|
||||
'astroid>=1.6,<2.0',
|
||||
'six',
|
||||
'isort >= 4.2.5',
|
||||
'mccabe',
|
||||
]
|
||||
|
||||
dependency_links = []
|
||||
|
||||
extras_require = {}
|
||||
extras_require[':sys_platform=="win32"'] = ['colorama']
|
||||
|
||||
|
||||
def has_environment_marker_range_operators_support():
|
||||
"""Code extracted from 'pytest/setup.py'
|
||||
https://github.com/pytest-dev/pytest/blob/7538680c/setup.py#L31
|
||||
The first known release to support environment marker with range operators
|
||||
it is 17.1, see: https://setuptools.readthedocs.io/en/latest/history.html#id113
|
||||
"""
|
||||
return parse_version(setuptools_version) >= parse_version('17.1')
|
||||
|
||||
|
||||
if has_environment_marker_range_operators_support():
|
||||
extras_require[':python_version=="2.7"'] = ['configparser', 'backports.functools_lru_cache']
|
||||
extras_require[':python_version<"3.4"'] = ['singledispatch']
|
||||
else:
|
||||
if (py_version.major, py_version.minor) == (2, 7):
|
||||
install_requires.extend(['configparser', 'backports.functools_lru_cache'])
|
||||
if py_version < (3, 4):
|
||||
install_requires.extend(['singledispatch'])
|
||||
|
||||
|
||||
license = 'GPL'
|
||||
description = "python code static checker"
|
||||
web = 'https://github.com/PyCQA/pylint'
|
||||
mailinglist = "mailto:code-quality@python.org"
|
||||
author = 'Python Code Quality Authority'
|
||||
author_email = 'code-quality@python.org'
|
||||
|
||||
classifiers = ['Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Software Development :: Debuggers',
|
||||
'Topic :: Software Development :: Quality Assurance',
|
||||
'Topic :: Software Development :: Testing'
|
||||
]
|
||||
|
||||
|
||||
long_desc = """\
|
||||
Pylint is a Python source code analyzer which looks for programming
|
||||
errors, helps enforcing a coding standard and sniffs for some code
|
||||
smells (as defined in Martin Fowler's Refactoring book)
|
||||
.
|
||||
Pylint can be seen as another PyChecker since nearly all tests you
|
||||
can do with PyChecker can also be done with Pylint. However, Pylint
|
||||
offers some more features, like checking length of lines of code,
|
||||
checking if variable names are well-formed according to your coding
|
||||
standard, or checking if declared interfaces are truly implemented,
|
||||
and much more.
|
||||
.
|
||||
Additionally, it is possible to write plugins to add your own checks.
|
||||
.
|
||||
Pylint is shipped with "pyreverse" (UML diagram generator)
|
||||
and "symilar" (an independent similarities checker)."""
|
||||
|
||||
scripts = [join('bin', filename)
|
||||
for filename in ('pylint', "symilar", "epylint",
|
||||
"pyreverse")]
|
||||
|
||||
include_dirs = [join('pylint', 'test')]
|
||||
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) 2006-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities methods and classes for checkers
|
||||
|
||||
Base id of standard checkers (used in msg and report ids):
|
||||
01: base
|
||||
02: classes
|
||||
03: format
|
||||
04: import
|
||||
05: misc
|
||||
06: variables
|
||||
07: exceptions
|
||||
08: similar
|
||||
09: design_analysis
|
||||
10: newstyle
|
||||
11: typecheck
|
||||
12: logging
|
||||
13: string_format
|
||||
14: string_constant
|
||||
15: stdlib
|
||||
16: python3
|
||||
17: refactoring
|
||||
18-50: not yet used: reserved for future internal checkers.
|
||||
51-99: perhaps used: reserved for external checkers
|
||||
|
||||
The raw_metrics checker has no number associated since it doesn't emit any
|
||||
messages nor reports. XXX not true, emit a 07 report !
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
import warnings
|
||||
|
||||
from pylint.config import OptionsProviderMixIn
|
||||
from pylint.reporters import diff_string
|
||||
from pylint.utils import register_plugins
|
||||
from pylint.interfaces import UNDEFINED
|
||||
|
||||
|
||||
def table_lines_from_stats(stats, old_stats, columns):
|
||||
"""get values listed in <columns> from <stats> and <old_stats>,
|
||||
and return a formated list of values, designed to be given to a
|
||||
ureport.Table object
|
||||
"""
|
||||
lines = []
|
||||
for m_type in columns:
|
||||
new = stats[m_type]
|
||||
format = str # pylint: disable=redefined-builtin
|
||||
if isinstance(new, float):
|
||||
format = lambda num: '%.3f' % num
|
||||
old = old_stats.get(m_type)
|
||||
if old is not None:
|
||||
diff_str = diff_string(old, new)
|
||||
old = format(old)
|
||||
else:
|
||||
old, diff_str = 'NC', 'NC'
|
||||
lines += (m_type.replace('_', ' '), format(new), old, diff_str)
|
||||
return lines
|
||||
|
||||
|
||||
class BaseChecker(OptionsProviderMixIn):
|
||||
"""base class for checkers"""
|
||||
# checker name (you may reuse an existing one)
|
||||
name = None
|
||||
# options level (0 will be displaying in --help, 1 in --long-help)
|
||||
level = 1
|
||||
# ordered list of options to control the ckecker behaviour
|
||||
options = ()
|
||||
# messages issued by this checker
|
||||
msgs = {}
|
||||
# reports issued by this checker
|
||||
reports = ()
|
||||
# mark this checker as enabled or not.
|
||||
enabled = True
|
||||
|
||||
def __init__(self, linter=None):
|
||||
"""checker instances should have the linter as argument
|
||||
|
||||
linter is an object implementing ILinter
|
||||
"""
|
||||
self.name = self.name.lower()
|
||||
OptionsProviderMixIn.__init__(self)
|
||||
self.linter = linter
|
||||
|
||||
def add_message(self, msg_id, line=None, node=None, args=None, confidence=UNDEFINED):
|
||||
"""add a message of a given type"""
|
||||
self.linter.add_message(msg_id, line, node, args, confidence)
|
||||
|
||||
# dummy methods implementing the IChecker interface
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
|
||||
def close(self):
|
||||
"""called after visiting project (i.e set of modules)"""
|
||||
|
||||
|
||||
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 """
|
||||
register_plugins(linter, __path__[0])
|
||||
|
||||
__all__ = ('BaseChecker', 'initialize')
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2017 Derek Gustafson <degustaf@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for anything related to the async protocol (PEP 492)."""
|
||||
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from astroid import exceptions
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils as checker_utils
|
||||
from pylint import interfaces
|
||||
from pylint import utils
|
||||
|
||||
|
||||
class AsyncChecker(checkers.BaseChecker):
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
name = 'async'
|
||||
msgs = {
|
||||
'E1700': ('Yield inside async function',
|
||||
'yield-inside-async-function',
|
||||
'Used when an `yield` or `yield from` statement is '
|
||||
'found inside an async function.',
|
||||
{'minversion': (3, 5)}),
|
||||
'E1701': ("Async context manager '%s' doesn't implement __aenter__ and __aexit__.",
|
||||
'not-async-context-manager',
|
||||
'Used when an async context manager is used with an object '
|
||||
'that does not implement the async context management protocol.',
|
||||
{'minversion': (3, 5)}),
|
||||
}
|
||||
|
||||
def open(self):
|
||||
self._ignore_mixin_members = utils.get_global_option(self, 'ignore-mixin-members')
|
||||
|
||||
@checker_utils.check_messages('yield-inside-async-function')
|
||||
def visit_asyncfunctiondef(self, node):
|
||||
for child in node.nodes_of_class(astroid.Yield):
|
||||
if child.scope() is node and (sys.version_info[:2] == (3, 5) or
|
||||
isinstance(child, astroid.YieldFrom)):
|
||||
self.add_message('yield-inside-async-function', node=child)
|
||||
|
||||
@checker_utils.check_messages('not-async-context-manager')
|
||||
def visit_asyncwith(self, node):
|
||||
for ctx_mgr, _ in node.items:
|
||||
infered = checker_utils.safe_infer(ctx_mgr)
|
||||
if infered is None or infered is astroid.YES:
|
||||
continue
|
||||
|
||||
if isinstance(infered, astroid.Instance):
|
||||
try:
|
||||
infered.getattr('__aenter__')
|
||||
infered.getattr('__aexit__')
|
||||
except exceptions.NotFoundError:
|
||||
if isinstance(infered, astroid.Instance):
|
||||
# If we do not know the bases of this class,
|
||||
# just skip it.
|
||||
if not checker_utils.has_known_bases(infered):
|
||||
continue
|
||||
# Just ignore mixin classes.
|
||||
if self._ignore_mixin_members:
|
||||
if infered.name[-5:].lower() == 'mixin':
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
self.add_message('not-async-context-manager',
|
||||
node=node, args=(infered.name, ))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(AsyncChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2010, 2012-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
|
||||
# Copyright (c) 2018 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""check for signs of poor design"""
|
||||
|
||||
from collections import defaultdict
|
||||
import re
|
||||
|
||||
from astroid import If, BoolOp
|
||||
from astroid import decorators
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils as checker_utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint import utils
|
||||
|
||||
|
||||
MSGS = {
|
||||
'R0901': ('Too many ancestors (%s/%s)',
|
||||
'too-many-ancestors',
|
||||
'Used when class has too many parent classes, try to reduce \
|
||||
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 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 \
|
||||
really worth it.'),
|
||||
'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 simpler (and so easier to use) class.'),
|
||||
|
||||
'R0911': ('Too many return statements (%s/%s)',
|
||||
'too-many-return-statements',
|
||||
'Used when a function or method has too many return statement, \
|
||||
making it hard to follow.'),
|
||||
'R0912': ('Too many branches (%s/%s)',
|
||||
'too-many-branches',
|
||||
'Used when a function or method has too many branches, \
|
||||
making it hard to follow.'),
|
||||
'R0913': ('Too many arguments (%s/%s)',
|
||||
'too-many-arguments',
|
||||
'Used when a function or method takes too many arguments.'),
|
||||
'R0914': ('Too many local variables (%s/%s)',
|
||||
'too-many-locals',
|
||||
'Used when a function or method has too many local variables.'),
|
||||
'R0915': ('Too many statements (%s/%s)',
|
||||
'too-many-statements',
|
||||
'Used when a function or method has too many statements. You \
|
||||
should then split it in smaller functions / methods.'),
|
||||
'R0916': ('Too many boolean expressions in if statement (%s/%s)',
|
||||
'too-many-boolean-expressions',
|
||||
'Used when a if statement contains too many boolean '
|
||||
'expressions'),
|
||||
}
|
||||
SPECIAL_OBJ = re.compile('^_{2}[a-z]+_{2}$')
|
||||
|
||||
|
||||
def _count_boolean_expressions(bool_op):
|
||||
"""Counts the number of boolean expressions in BoolOp `bool_op` (recursive)
|
||||
|
||||
example: a and (b or c or (d and e)) ==> 5 boolean expressions
|
||||
"""
|
||||
nb_bool_expr = 0
|
||||
for bool_expr in bool_op.get_children():
|
||||
if isinstance(bool_expr, BoolOp):
|
||||
nb_bool_expr += _count_boolean_expressions(bool_expr)
|
||||
else:
|
||||
nb_bool_expr += 1
|
||||
return nb_bool_expr
|
||||
|
||||
|
||||
class MisdesignChecker(BaseChecker):
|
||||
"""checks for sign of poor/misdesign:
|
||||
* number of methods, attributes, local variables...
|
||||
* size, complexity of functions, methods
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'design'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
# configuration options
|
||||
options = (('max-args',
|
||||
{'default' : 5, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of arguments for function / method'}
|
||||
),
|
||||
('max-locals',
|
||||
{'default' : 15, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of locals for function / method body'}
|
||||
),
|
||||
('max-returns',
|
||||
{'default' : 6, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of return / yield for function / '
|
||||
'method body'}
|
||||
),
|
||||
('max-branches',
|
||||
{'default' : 12, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of branch for function / method body'}
|
||||
),
|
||||
('max-statements',
|
||||
{'default' : 50, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of statements in function / method '
|
||||
'body'}
|
||||
),
|
||||
('max-parents',
|
||||
{'default' : 7,
|
||||
'type' : 'int',
|
||||
'metavar' : '<num>',
|
||||
'help' : 'Maximum number of parents for a class (see R0901).'}
|
||||
),
|
||||
('max-attributes',
|
||||
{'default' : 7,
|
||||
'type' : 'int',
|
||||
'metavar' : '<num>',
|
||||
'help' : 'Maximum number of attributes for a class \
|
||||
(see R0902).'}
|
||||
),
|
||||
('min-public-methods',
|
||||
{'default' : 2,
|
||||
'type' : 'int',
|
||||
'metavar' : '<num>',
|
||||
'help' : 'Minimum number of public methods for a class \
|
||||
(see R0903).'}
|
||||
),
|
||||
('max-public-methods',
|
||||
{'default' : 20,
|
||||
'type' : 'int',
|
||||
'metavar' : '<num>',
|
||||
'help' : 'Maximum number of public methods for a class \
|
||||
(see R0904).'}
|
||||
),
|
||||
('max-bool-expr',
|
||||
{'default': 5,
|
||||
'type': 'int',
|
||||
'metavar': '<num>',
|
||||
'help': 'Maximum number of boolean expressions in a if '
|
||||
'statement'}
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self._returns = None
|
||||
self._branches = None
|
||||
self._stmts = 0
|
||||
|
||||
def open(self):
|
||||
"""initialize visit variables"""
|
||||
self.stats = self.linter.add_stats()
|
||||
self._returns = []
|
||||
self._branches = defaultdict(int)
|
||||
|
||||
@decorators.cachedproperty
|
||||
def _ignored_argument_names(self):
|
||||
return utils.get_global_option(self, 'ignored-argument-names', default=None)
|
||||
|
||||
@check_messages('too-many-ancestors', 'too-many-instance-attributes',
|
||||
'too-few-public-methods', 'too-many-public-methods')
|
||||
def visit_classdef(self, node):
|
||||
"""check size of inheritance hierarchy and number of instance attributes
|
||||
"""
|
||||
nb_parents = len(list(node.ancestors()))
|
||||
if nb_parents > self.config.max_parents:
|
||||
self.add_message('too-many-ancestors', node=node,
|
||||
args=(nb_parents, self.config.max_parents))
|
||||
|
||||
if len(node.instance_attrs) > self.config.max_attributes:
|
||||
self.add_message('too-many-instance-attributes', node=node,
|
||||
args=(len(node.instance_attrs),
|
||||
self.config.max_attributes))
|
||||
|
||||
@check_messages('too-few-public-methods', 'too-many-public-methods')
|
||||
def leave_classdef(self, node):
|
||||
"""check number of public methods"""
|
||||
my_methods = sum(1 for method in node.mymethods()
|
||||
if not method.name.startswith('_'))
|
||||
|
||||
# Does the class contain less than n public methods ?
|
||||
# This checks only the methods defined in the current class,
|
||||
# since the user might not have control over the classes
|
||||
# from the ancestors. It avoids some false positives
|
||||
# for classes such as unittest.TestCase, which provides
|
||||
# a lot of assert methods. It doesn't make sense to warn
|
||||
# when the user subclasses TestCase to add his own tests.
|
||||
if my_methods > self.config.max_public_methods:
|
||||
self.add_message('too-many-public-methods', node=node,
|
||||
args=(my_methods,
|
||||
self.config.max_public_methods))
|
||||
# stop here for exception, metaclass and interface classes
|
||||
if node.type != 'class' or checker_utils.is_enum_class(node):
|
||||
return
|
||||
|
||||
all_methods = sum(1 for method in node.methods()
|
||||
if not method.name.startswith('_'))
|
||||
# Special methods count towards the number of public methods,
|
||||
# but don't count towards there being too many methods.
|
||||
for method in node.mymethods():
|
||||
if SPECIAL_OBJ.search(method.name) and method.name != '__init__':
|
||||
all_methods += 1
|
||||
|
||||
# Does the class contain more than n public methods ?
|
||||
# This checks all the methods defined by ancestors and
|
||||
# by the current class.
|
||||
if all_methods < self.config.min_public_methods:
|
||||
self.add_message('too-few-public-methods', node=node,
|
||||
args=(all_methods,
|
||||
self.config.min_public_methods))
|
||||
|
||||
@check_messages('too-many-return-statements', 'too-many-branches',
|
||||
'too-many-arguments', 'too-many-locals',
|
||||
'too-many-statements', 'keyword-arg-before-vararg')
|
||||
def visit_functiondef(self, node):
|
||||
"""check function name, docstring, arguments, redefinition,
|
||||
variable names, max locals
|
||||
"""
|
||||
# init branch and returns counters
|
||||
self._returns.append(0)
|
||||
# check number of arguments
|
||||
args = node.args.args
|
||||
ignored_argument_names = self._ignored_argument_names
|
||||
if args is not None:
|
||||
ignored_args_num = 0
|
||||
if ignored_argument_names:
|
||||
ignored_args_num = sum(1 for arg in args if ignored_argument_names.match(arg.name))
|
||||
|
||||
argnum = len(args) - ignored_args_num
|
||||
if argnum > self.config.max_args:
|
||||
self.add_message('too-many-arguments', node=node,
|
||||
args=(len(args), self.config.max_args))
|
||||
else:
|
||||
ignored_args_num = 0
|
||||
# check number of local variables
|
||||
locnum = len(node.locals) - ignored_args_num
|
||||
if locnum > self.config.max_locals:
|
||||
self.add_message('too-many-locals', node=node,
|
||||
args=(locnum, self.config.max_locals))
|
||||
# init statements counter
|
||||
self._stmts = 1
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
@check_messages('too-many-return-statements', 'too-many-branches',
|
||||
'too-many-arguments', 'too-many-locals',
|
||||
'too-many-statements')
|
||||
def leave_functiondef(self, node):
|
||||
"""most of the work is done here on close:
|
||||
checks for max returns, branch, return in __init__
|
||||
"""
|
||||
returns = self._returns.pop()
|
||||
if returns > self.config.max_returns:
|
||||
self.add_message('too-many-return-statements', node=node,
|
||||
args=(returns, self.config.max_returns))
|
||||
branches = self._branches[node]
|
||||
if branches > self.config.max_branches:
|
||||
self.add_message('too-many-branches', node=node,
|
||||
args=(branches, self.config.max_branches))
|
||||
# check number of statements
|
||||
if self._stmts > self.config.max_statements:
|
||||
self.add_message('too-many-statements', node=node,
|
||||
args=(self._stmts, self.config.max_statements))
|
||||
|
||||
leave_asyncfunctiondef = leave_functiondef
|
||||
|
||||
def visit_return(self, _):
|
||||
"""count number of returns"""
|
||||
if not self._returns:
|
||||
return # return outside function, reported by the base checker
|
||||
self._returns[-1] += 1
|
||||
|
||||
def visit_default(self, node):
|
||||
"""default visit method -> increments the statements counter if
|
||||
necessary
|
||||
"""
|
||||
if node.is_statement:
|
||||
self._stmts += 1
|
||||
|
||||
def visit_tryexcept(self, node):
|
||||
"""increments the branches counter"""
|
||||
branches = len(node.handlers)
|
||||
if node.orelse:
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
self._stmts += branches
|
||||
|
||||
def visit_tryfinally(self, node):
|
||||
"""increments the branches counter"""
|
||||
self._inc_branch(node, 2)
|
||||
self._stmts += 2
|
||||
|
||||
@check_messages('too-many-boolean-expressions')
|
||||
def visit_if(self, node):
|
||||
"""increments the branches counter and checks boolean expressions"""
|
||||
self._check_boolean_expressions(node)
|
||||
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)):
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
self._stmts += branches
|
||||
|
||||
def _check_boolean_expressions(self, node):
|
||||
"""Go through "if" node `node` and counts its boolean expressions
|
||||
|
||||
if the "if" node test is a BoolOp node
|
||||
"""
|
||||
condition = node.test
|
||||
if not isinstance(condition, BoolOp):
|
||||
return
|
||||
nb_bool_expr = _count_boolean_expressions(condition)
|
||||
if nb_bool_expr > self.config.max_bool_expr:
|
||||
self.add_message('too-many-boolean-expressions', node=condition,
|
||||
args=(nb_bool_expr, self.config.max_bool_expr))
|
||||
|
||||
def visit_while(self, node):
|
||||
"""increments the branches counter"""
|
||||
branches = 1
|
||||
if node.orelse:
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
|
||||
visit_for = visit_while
|
||||
|
||||
def _inc_branch(self, node, branchesnum=1):
|
||||
"""increments the branches counter"""
|
||||
self._branches[node.scope()] += branchesnum
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(MisdesignChecker(linter))
|
||||
@@ -0,0 +1,416 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2011-2014 Google, Inc.
|
||||
# Copyright (c) 2012 Tim Hatch <tim@timhatch.com>
|
||||
# Copyright (c) 2013-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Rene Zhang <rz99@cornell.edu>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Steven Myint <hg@stevenmyint.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Martin von Gagern <gagern@google.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checks for various exception related errors."""
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
from six.moves import builtins
|
||||
|
||||
import astroid
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils
|
||||
from pylint import interfaces
|
||||
|
||||
|
||||
def _builtin_exceptions():
|
||||
def predicate(obj):
|
||||
return isinstance(obj, type) and issubclass(obj, BaseException)
|
||||
|
||||
members = inspect.getmembers(six.moves.builtins, predicate)
|
||||
return {exc.__name__ for (_, exc) in members}
|
||||
|
||||
|
||||
def _annotated_unpack_infer(stmt, context=None):
|
||||
"""
|
||||
Recursively generate nodes inferred by the given statement.
|
||||
If the inferred value is a list or a tuple, recurse on the elements.
|
||||
Returns an iterator which yields tuples in the format
|
||||
('original node', 'infered node').
|
||||
"""
|
||||
if isinstance(stmt, (astroid.List, astroid.Tuple)):
|
||||
for elt in stmt.elts:
|
||||
inferred = utils.safe_infer(elt)
|
||||
if inferred and inferred is not astroid.YES:
|
||||
yield elt, inferred
|
||||
return
|
||||
for infered in stmt.infer(context):
|
||||
if infered is astroid.YES:
|
||||
continue
|
||||
yield stmt, infered
|
||||
|
||||
|
||||
PY3K = sys.version_info >= (3, 0)
|
||||
OVERGENERAL_EXCEPTIONS = ('Exception',)
|
||||
BUILTINS_NAME = builtins.__name__
|
||||
|
||||
MSGS = {
|
||||
'E0701': ('Bad except clauses order (%s)',
|
||||
'bad-except-order',
|
||||
'Used when except clauses are not in the correct order (from the '
|
||||
'more specific to the more generic). If you don\'t fix the order, '
|
||||
'some exceptions may not be caught by the most specific handler.'),
|
||||
'E0702': ('Raising %s while only classes or instances are allowed',
|
||||
'raising-bad-type',
|
||||
'Used when something which is neither a class, an instance or a \
|
||||
string is raised (i.e. a `TypeError` will be raised).'),
|
||||
'E0703': ('Exception context set to something which is not an '
|
||||
'exception, nor None',
|
||||
'bad-exception-context',
|
||||
'Used when using the syntax "raise ... from ...", '
|
||||
'where the exception context is not an exception, '
|
||||
'nor None.',
|
||||
{'minversion': (3, 0)}),
|
||||
'E0704': ('The raise statement is not inside an except clause',
|
||||
'misplaced-bare-raise',
|
||||
'Used when a bare raise is not used inside an except clause. '
|
||||
'This generates an error, since there are no active exceptions '
|
||||
'to be reraised. An exception to this rule is represented by '
|
||||
'a bare raise inside a finally clause, which might work, as long '
|
||||
'as an exception is raised inside the try block, but it is '
|
||||
'nevertheless a code smell that must not be relied upon.'),
|
||||
'E0710': ('Raising a new style class which doesn\'t inherit from BaseException',
|
||||
'raising-non-exception',
|
||||
'Used when a new style class which doesn\'t inherit from \
|
||||
BaseException is raised.'),
|
||||
'E0711': ('NotImplemented raised - should raise NotImplementedError',
|
||||
'notimplemented-raised',
|
||||
'Used when NotImplemented is raised instead of \
|
||||
NotImplementedError'),
|
||||
'E0712': ('Catching an exception which doesn\'t inherit from Exception: %s',
|
||||
'catching-non-exception',
|
||||
'Used when a class which doesn\'t inherit from \
|
||||
Exception is used as an exception in an except clause.'),
|
||||
'W0702': ('No exception type(s) specified',
|
||||
'bare-except',
|
||||
'Used when an except clause doesn\'t specify exceptions type to \
|
||||
catch.'),
|
||||
'W0703': ('Catching too general exception %s',
|
||||
'broad-except',
|
||||
'Used when an except catches a too general exception, \
|
||||
possibly burying unrelated errors.'),
|
||||
'W0705': ('Catching previously caught exception type %s',
|
||||
'duplicate-except',
|
||||
'Used when an except catches a type that was already caught by '
|
||||
'a previous handler.'),
|
||||
'W0710': ('Exception doesn\'t inherit from standard "Exception" class',
|
||||
'nonstandard-exception',
|
||||
'Used when a custom exception class is raised but doesn\'t \
|
||||
inherit from the builtin "Exception" class.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W0711': ('Exception to catch is the result of a binary "%s" operation',
|
||||
'binary-op-exception',
|
||||
'Used when the exception to catch is of the form \
|
||||
"except A or B:". If intending to catch multiple, \
|
||||
rewrite as "except (A, B):"'),
|
||||
'W0715': ('Exception arguments suggest string formatting might be intended',
|
||||
'raising-format-tuple',
|
||||
'Used when passing multiple arguments to an exception \
|
||||
constructor, the first of them a string literal containing what \
|
||||
appears to be placeholders intended for formatting'),
|
||||
}
|
||||
|
||||
|
||||
class BaseVisitor(object):
|
||||
"""Base class for visitors defined in this module."""
|
||||
|
||||
def __init__(self, checker, node):
|
||||
self._checker = checker
|
||||
self._node = node
|
||||
|
||||
def visit(self, node):
|
||||
name = node.__class__.__name__.lower()
|
||||
dispatch_meth = getattr(self, 'visit_' + name, None)
|
||||
if dispatch_meth:
|
||||
dispatch_meth(node)
|
||||
else:
|
||||
self.visit_default(node)
|
||||
|
||||
def visit_default(self, node): # pylint: disable=unused-argument
|
||||
"""Default implementation for all the nodes."""
|
||||
|
||||
|
||||
class ExceptionRaiseRefVisitor(BaseVisitor):
|
||||
"""Visit references (anything that is not an AST leaf)."""
|
||||
|
||||
def visit_name(self, name):
|
||||
if name.name == 'NotImplemented':
|
||||
self._checker.add_message(
|
||||
'notimplemented-raised',
|
||||
node=self._node)
|
||||
|
||||
def visit_call(self, call):
|
||||
if isinstance(call.func, astroid.Name):
|
||||
self.visit_name(call.func)
|
||||
if (len(call.args) > 1 and
|
||||
isinstance(call.args[0], astroid.Const) and
|
||||
isinstance(call.args[0].value, six.string_types)):
|
||||
msg = call.args[0].value
|
||||
if ('%' in msg or
|
||||
('{' in msg and '}' in msg)):
|
||||
self._checker.add_message(
|
||||
'raising-format-tuple',
|
||||
node=self._node)
|
||||
|
||||
|
||||
class ExceptionRaiseLeafVisitor(BaseVisitor):
|
||||
"""Visitor for handling leaf kinds of a raise value."""
|
||||
|
||||
def visit_const(self, const):
|
||||
if not isinstance(const.value, str):
|
||||
# raising-string will be emitted from python3 porting checker.
|
||||
self._checker.add_message('raising-bad-type', node=self._node,
|
||||
args=const.value.__class__.__name__)
|
||||
|
||||
def visit_instance(self, instance):
|
||||
# pylint: disable=protected-access
|
||||
cls = instance._proxied
|
||||
self.visit_classdef(cls)
|
||||
|
||||
# Exception instances have a particular class type
|
||||
visit_exceptioninstance = visit_instance
|
||||
|
||||
def visit_classdef(self, cls):
|
||||
if (not utils.inherit_from_std_ex(cls) and
|
||||
utils.has_known_bases(cls)):
|
||||
if cls.newstyle:
|
||||
self._checker.add_message('raising-non-exception', node=self._node)
|
||||
else:
|
||||
self._checker.add_message('nonstandard-exception', node=self._node)
|
||||
|
||||
def visit_tuple(self, tuple_node):
|
||||
if PY3K or not tuple_node.elts:
|
||||
self._checker.add_message('raising-bad-type',
|
||||
node=self._node,
|
||||
args='tuple')
|
||||
return
|
||||
|
||||
# On Python 2, using the following is not an error:
|
||||
# raise (ZeroDivisionError, None)
|
||||
# raise (ZeroDivisionError, )
|
||||
# What's left to do is to check that the first
|
||||
# argument is indeed an exception. Verifying the other arguments
|
||||
# is not the scope of this check.
|
||||
first = tuple_node.elts[0]
|
||||
inferred = utils.safe_infer(first)
|
||||
if not inferred or inferred is astroid.Uninferable:
|
||||
return
|
||||
|
||||
if (isinstance(inferred, astroid.Instance)
|
||||
and inferred.__class__.__name__ != 'Instance'):
|
||||
# TODO: explain why
|
||||
self.visit_default(tuple_node)
|
||||
else:
|
||||
self.visit(inferred)
|
||||
|
||||
def visit_default(self, node):
|
||||
name = getattr(node, 'name', node.__class__.__name__)
|
||||
self._checker.add_message('raising-bad-type',
|
||||
node=self._node,
|
||||
args=name)
|
||||
|
||||
|
||||
class ExceptionsChecker(checkers.BaseChecker):
|
||||
"""Exception related checks."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
|
||||
name = 'exceptions'
|
||||
msgs = MSGS
|
||||
priority = -4
|
||||
options = (('overgeneral-exceptions',
|
||||
{'default' : OVERGENERAL_EXCEPTIONS,
|
||||
'type' : 'csv', 'metavar' : '<comma-separated class names>',
|
||||
'help' : 'Exceptions that will emit a warning '
|
||||
'when being caught. Defaults to "%s"' % (
|
||||
', '.join(OVERGENERAL_EXCEPTIONS),)}
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
self._builtin_exceptions = _builtin_exceptions()
|
||||
super(ExceptionsChecker, self).open()
|
||||
|
||||
@utils.check_messages('nonstandard-exception', 'misplaced-bare-raise',
|
||||
'raising-bad-type', 'raising-non-exception',
|
||||
'notimplemented-raised', 'bad-exception-context',
|
||||
'raising-format-tuple')
|
||||
def visit_raise(self, node):
|
||||
if node.exc is None:
|
||||
self._check_misplaced_bare_raise(node)
|
||||
return
|
||||
|
||||
if PY3K and node.cause:
|
||||
self._check_bad_exception_context(node)
|
||||
|
||||
expr = node.exc
|
||||
try:
|
||||
inferred_value = next(expr.infer())
|
||||
except astroid.InferenceError:
|
||||
inferred_value = None
|
||||
|
||||
ExceptionRaiseRefVisitor(self, node).visit(expr)
|
||||
|
||||
if inferred_value:
|
||||
ExceptionRaiseLeafVisitor(self, node).visit(inferred_value)
|
||||
|
||||
def _check_misplaced_bare_raise(self, node):
|
||||
# Filter out if it's present in __exit__.
|
||||
scope = node.scope()
|
||||
if (isinstance(scope, astroid.FunctionDef)
|
||||
and scope.is_method()
|
||||
and scope.name == '__exit__'):
|
||||
return
|
||||
|
||||
current = node
|
||||
# Stop when a new scope is generated or when the raise
|
||||
# statement is found inside a TryFinally.
|
||||
ignores = (astroid.ExceptHandler, astroid.FunctionDef, astroid.TryFinally)
|
||||
while current and not isinstance(current.parent, ignores):
|
||||
current = current.parent
|
||||
|
||||
expected = (astroid.ExceptHandler,)
|
||||
if not current or not isinstance(current.parent, expected):
|
||||
self.add_message('misplaced-bare-raise', node=node)
|
||||
|
||||
def _check_bad_exception_context(self, node):
|
||||
"""Verify that the exception context is properly set.
|
||||
|
||||
An exception context can be only `None` or an exception.
|
||||
"""
|
||||
cause = utils.safe_infer(node.cause)
|
||||
if cause in (astroid.YES, None):
|
||||
return
|
||||
|
||||
if isinstance(cause, astroid.Const):
|
||||
if cause.value is not None:
|
||||
self.add_message('bad-exception-context',
|
||||
node=node)
|
||||
elif (not isinstance(cause, astroid.ClassDef) and
|
||||
not utils.inherit_from_std_ex(cause)):
|
||||
self.add_message('bad-exception-context',
|
||||
node=node)
|
||||
|
||||
def _check_catching_non_exception(self, handler, exc, part):
|
||||
if isinstance(exc, astroid.Tuple):
|
||||
# Check if it is a tuple of exceptions.
|
||||
inferred = [utils.safe_infer(elt) for elt in exc.elts]
|
||||
if any(node is astroid.YES for node in inferred):
|
||||
# Don't emit if we don't know every component.
|
||||
return
|
||||
if all(node and (utils.inherit_from_std_ex(node) or
|
||||
not utils.has_known_bases(node))
|
||||
for node in inferred):
|
||||
return
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
# Don't emit the warning if the infered stmt
|
||||
# is None, but the exception handler is something else,
|
||||
# maybe it was redefined.
|
||||
if (isinstance(exc, astroid.Const) and
|
||||
exc.value is None):
|
||||
if ((isinstance(handler.type, astroid.Const) and
|
||||
handler.type.value is None) or
|
||||
handler.type.parent_of(exc)):
|
||||
# If the exception handler catches None or
|
||||
# the exception component, which is None, is
|
||||
# defined by the entire exception handler, then
|
||||
# emit a warning.
|
||||
self.add_message('catching-non-exception',
|
||||
node=handler.type,
|
||||
args=(part.as_string(), ))
|
||||
else:
|
||||
self.add_message('catching-non-exception',
|
||||
node=handler.type,
|
||||
args=(part.as_string(), ))
|
||||
return
|
||||
|
||||
if (not utils.inherit_from_std_ex(exc) and
|
||||
exc.name not in self._builtin_exceptions):
|
||||
if utils.has_known_bases(exc):
|
||||
self.add_message('catching-non-exception',
|
||||
node=handler.type,
|
||||
args=(exc.name, ))
|
||||
|
||||
@utils.check_messages('bare-except', 'broad-except',
|
||||
'binary-op-exception', 'bad-except-order',
|
||||
'catching-non-exception', 'duplicate-except')
|
||||
def visit_tryexcept(self, node):
|
||||
"""check for empty except"""
|
||||
exceptions_classes = []
|
||||
nb_handlers = len(node.handlers)
|
||||
for index, handler in enumerate(node.handlers):
|
||||
if handler.type is None:
|
||||
if not utils.is_raising(handler.body):
|
||||
self.add_message('bare-except', node=handler)
|
||||
# check if a "except:" is followed by some other
|
||||
# except
|
||||
if index < (nb_handlers - 1):
|
||||
msg = 'empty except clause should always appear last'
|
||||
self.add_message('bad-except-order', node=node, args=msg)
|
||||
|
||||
elif isinstance(handler.type, astroid.BoolOp):
|
||||
self.add_message('binary-op-exception',
|
||||
node=handler, args=handler.type.op)
|
||||
else:
|
||||
try:
|
||||
excs = list(_annotated_unpack_infer(handler.type))
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
for part, exc in excs:
|
||||
if exc is astroid.YES:
|
||||
continue
|
||||
if (isinstance(exc, astroid.Instance)
|
||||
and utils.inherit_from_std_ex(exc)):
|
||||
# pylint: disable=protected-access
|
||||
exc = exc._proxied
|
||||
|
||||
self._check_catching_non_exception(handler, exc, part)
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
exc_ancestors = [anc for anc in exc.ancestors()
|
||||
if isinstance(anc, astroid.ClassDef)]
|
||||
|
||||
for previous_exc in exceptions_classes:
|
||||
if previous_exc in exc_ancestors:
|
||||
msg = '%s is an ancestor class of %s' % (
|
||||
previous_exc.name, exc.name)
|
||||
self.add_message('bad-except-order',
|
||||
node=handler.type, args=msg)
|
||||
if (exc.name in self.config.overgeneral_exceptions
|
||||
and exc.root().name == utils.EXCEPTIONS_MODULE
|
||||
and not utils.is_raising(handler.body)):
|
||||
self.add_message('broad-except',
|
||||
args=exc.name, node=handler.type)
|
||||
|
||||
if exc in exceptions_classes:
|
||||
self.add_message('duplicate-except',
|
||||
args=exc.name, node=handler.type)
|
||||
|
||||
exceptions_classes += [exc for _, exc in excs]
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(ExceptionsChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,807 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013 buck@yelp.com <buck@yelp.com>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2015 Dmitry Pribysh <dmand@yandex.ru>
|
||||
# Copyright (c) 2015 Cezar <celnazli@bitdefender.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Noam Yorav-Raphael <noamraph@gmail.com>
|
||||
# Copyright (c) 2015 James Morgensen <james.morgensen@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Jared Garst <cultofjared@gmail.com>
|
||||
# Copyright (c) 2016 Maik Röder <maikroeder@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Michka Popoff <michkapopoff@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Erik Wright <erik.wright@shopify.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""imports checkers for Python code"""
|
||||
|
||||
import collections
|
||||
from distutils import sysconfig
|
||||
import os
|
||||
import sys
|
||||
import copy
|
||||
|
||||
import six
|
||||
|
||||
import astroid
|
||||
from astroid import are_exclusive
|
||||
from astroid.modutils import (get_module_part, is_standard_module)
|
||||
import isort
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.utils import get_global_option
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
check_messages,
|
||||
node_ignores_exception,
|
||||
is_from_fallback_block
|
||||
)
|
||||
from pylint.graph import get_cycles, DotBackend
|
||||
from pylint.reporters.ureports.nodes import VerbatimText, Paragraph
|
||||
|
||||
|
||||
def _qualified_names(modname):
|
||||
"""Split the names of the given module into subparts
|
||||
|
||||
For example,
|
||||
_qualified_names('pylint.checkers.ImportsChecker')
|
||||
returns
|
||||
['pylint', 'pylint.checkers', 'pylint.checkers.ImportsChecker']
|
||||
"""
|
||||
names = modname.split('.')
|
||||
return ['.'.join(names[0:i+1]) for i in range(len(names))]
|
||||
|
||||
|
||||
def _get_import_name(importnode, modname):
|
||||
"""Get a prepared module name from the given import node
|
||||
|
||||
In the case of relative imports, this will return the
|
||||
absolute qualified module name, which might be useful
|
||||
for debugging. Otherwise, the initial module name
|
||||
is returned unchanged.
|
||||
"""
|
||||
if isinstance(importnode, astroid.ImportFrom):
|
||||
if importnode.level:
|
||||
root = importnode.root()
|
||||
if isinstance(root, astroid.Module):
|
||||
modname = root.relative_to_absolute_name(
|
||||
modname, level=importnode.level)
|
||||
return modname
|
||||
|
||||
|
||||
def _get_first_import(node, context, name, base, level, alias):
|
||||
"""return the node where [base.]<name> is imported or None if not found
|
||||
"""
|
||||
fullname = '%s.%s' % (base, name) if base else name
|
||||
|
||||
first = None
|
||||
found = False
|
||||
for first in context.body:
|
||||
if first is node:
|
||||
continue
|
||||
if first.scope() is node.scope() and first.fromlineno > node.fromlineno:
|
||||
continue
|
||||
if isinstance(first, astroid.Import):
|
||||
if any(fullname == iname[0] for iname in first.names):
|
||||
found = True
|
||||
break
|
||||
elif isinstance(first, astroid.ImportFrom):
|
||||
if level == first.level:
|
||||
for imported_name, imported_alias in first.names:
|
||||
if fullname == '%s.%s' % (first.modname, imported_name):
|
||||
found = True
|
||||
break
|
||||
if name != '*' and name == imported_name and not (alias or imported_alias):
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if found and not are_exclusive(first, node):
|
||||
return first
|
||||
return None
|
||||
|
||||
|
||||
def _ignore_import_failure(node, modname, ignored_modules):
|
||||
for submodule in _qualified_names(modname):
|
||||
if submodule in ignored_modules:
|
||||
return True
|
||||
|
||||
return node_ignores_exception(node, ImportError)
|
||||
|
||||
# utilities to represents import dependencies as tree and dot graph ###########
|
||||
|
||||
def _make_tree_defs(mod_files_list):
|
||||
"""get a list of 2-uple (module, list_of_files_which_import_this_module),
|
||||
it will return a dictionary to represent this as a tree
|
||||
"""
|
||||
tree_defs = {}
|
||||
for mod, files in mod_files_list:
|
||||
node = (tree_defs, ())
|
||||
for prefix in mod.split('.'):
|
||||
node = node[0].setdefault(prefix, [{}, []])
|
||||
node[1] += files
|
||||
return tree_defs
|
||||
|
||||
|
||||
def _repr_tree_defs(data, indent_str=None):
|
||||
"""return a string which represents imports as a tree"""
|
||||
lines = []
|
||||
nodes = data.items()
|
||||
for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])):
|
||||
if not files:
|
||||
files = ''
|
||||
else:
|
||||
files = '(%s)' % ','.join(sorted(files))
|
||||
if indent_str is None:
|
||||
lines.append('%s %s' % (mod, files))
|
||||
sub_indent_str = ' '
|
||||
else:
|
||||
lines.append(r'%s\-%s %s' % (indent_str, mod, files))
|
||||
if i == len(nodes)-1:
|
||||
sub_indent_str = '%s ' % indent_str
|
||||
else:
|
||||
sub_indent_str = '%s| ' % indent_str
|
||||
if sub:
|
||||
lines.append(_repr_tree_defs(sub, sub_indent_str))
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _dependencies_graph(filename, dep_info):
|
||||
"""write dependencies as a dot (graphviz) file
|
||||
"""
|
||||
done = {}
|
||||
printer = DotBackend(filename[:-4], rankdir='LR')
|
||||
printer.emit('URL="." node[shape="box"]')
|
||||
for modname, dependencies in sorted(six.iteritems(dep_info)):
|
||||
done[modname] = 1
|
||||
printer.emit_node(modname)
|
||||
for depmodname in dependencies:
|
||||
if depmodname not in done:
|
||||
done[depmodname] = 1
|
||||
printer.emit_node(depmodname)
|
||||
for depmodname, dependencies in sorted(six.iteritems(dep_info)):
|
||||
for modname in dependencies:
|
||||
printer.emit_edge(modname, depmodname)
|
||||
printer.generate(filename)
|
||||
|
||||
|
||||
def _make_graph(filename, dep_info, sect, gtype):
|
||||
"""generate a dependencies graph and add some information about it in the
|
||||
report's section
|
||||
"""
|
||||
_dependencies_graph(filename, dep_info)
|
||||
sect.append(Paragraph('%simports graph has been written to %s'
|
||||
% (gtype, filename)))
|
||||
|
||||
|
||||
# the import checker itself ###################################################
|
||||
|
||||
MSGS = {
|
||||
'E0401': ('Unable to import %s',
|
||||
'import-error',
|
||||
'Used when pylint has been unable to import a module.',
|
||||
{'old_names': [('F0401', 'import-error')]}),
|
||||
'E0402': ('Attempted relative import beyond top-level package',
|
||||
'relative-beyond-top-level',
|
||||
'Used when a relative import tries to access too many levels '
|
||||
'in the current package.'),
|
||||
'R0401': ('Cyclic import (%s)',
|
||||
'cyclic-import',
|
||||
'Used when a cyclic import between two or more modules is \
|
||||
detected.'),
|
||||
|
||||
'W0401': ('Wildcard import %s',
|
||||
'wildcard-import',
|
||||
'Used when `from module import *` is detected.'),
|
||||
'W0402': ('Uses of a deprecated module %r',
|
||||
'deprecated-module',
|
||||
'Used a module marked as deprecated is imported.'),
|
||||
'W0403': ('Relative import %r, should be %r',
|
||||
'relative-import',
|
||||
'Used when an import relative to the package directory is '
|
||||
'detected.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W0404': ('Reimport %r (imported line %s)',
|
||||
'reimported',
|
||||
'Used when a module is reimported multiple times.'),
|
||||
'W0406': ('Module import itself',
|
||||
'import-self',
|
||||
'Used when a module is importing itself.'),
|
||||
|
||||
'W0410': ('__future__ import is not the first non docstring statement',
|
||||
'misplaced-future',
|
||||
'Python 2.5 and greater require __future__ import to be the \
|
||||
first non docstring statement in the module.'),
|
||||
|
||||
'C0410': ('Multiple imports on one line (%s)',
|
||||
'multiple-imports',
|
||||
'Used when import statement importing multiple modules is '
|
||||
'detected.'),
|
||||
'C0411': ('%s should be placed before %s',
|
||||
'wrong-import-order',
|
||||
'Used when PEP8 import order is not respected (standard imports '
|
||||
'first, then third-party libraries, then local imports)'),
|
||||
'C0412': ('Imports from package %s are not grouped',
|
||||
'ungrouped-imports',
|
||||
'Used when imports are not grouped by packages'),
|
||||
'C0413': ('Import "%s" should be placed at the top of the '
|
||||
'module',
|
||||
'wrong-import-position',
|
||||
'Used when code and imports are mixed'),
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_STANDARD_LIBRARY = ()
|
||||
DEFAULT_KNOWN_THIRD_PARTY = ('enchant',)
|
||||
|
||||
|
||||
class ImportsChecker(BaseChecker):
|
||||
"""checks for
|
||||
* external modules dependencies
|
||||
* relative / wildcard imports
|
||||
* cyclic imports
|
||||
* uses of deprecated modules
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = 'imports'
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
|
||||
if six.PY2:
|
||||
deprecated_modules = ('regsub', 'TERMIOS', 'Bastion', 'rexec')
|
||||
elif sys.version_info < (3, 5):
|
||||
deprecated_modules = ('optparse', )
|
||||
else:
|
||||
deprecated_modules = ('optparse', 'tkinter.tix')
|
||||
options = (('deprecated-modules',
|
||||
{'default' : deprecated_modules,
|
||||
'type' : 'csv',
|
||||
'metavar' : '<modules>',
|
||||
'help' : 'Deprecated modules which should not be used,'
|
||||
' separated by a comma'}
|
||||
),
|
||||
('import-graph',
|
||||
{'default' : '',
|
||||
'type' : 'string',
|
||||
'metavar' : '<file.dot>',
|
||||
'help' : 'Create a graph of every (i.e. internal and'
|
||||
' external) dependencies in the given file'
|
||||
' (report RP0402 must not be disabled)'}
|
||||
),
|
||||
('ext-import-graph',
|
||||
{'default' : '',
|
||||
'type' : 'string',
|
||||
'metavar' : '<file.dot>',
|
||||
'help' : 'Create a graph of external dependencies in the'
|
||||
' given file (report RP0402 must not be disabled)'}
|
||||
),
|
||||
('int-import-graph',
|
||||
{'default' : '',
|
||||
'type' : 'string',
|
||||
'metavar' : '<file.dot>',
|
||||
'help' : 'Create a graph of internal dependencies in the'
|
||||
' given file (report RP0402 must not be disabled)'}
|
||||
),
|
||||
('known-standard-library',
|
||||
{'default': DEFAULT_STANDARD_LIBRARY,
|
||||
'type': 'csv',
|
||||
'metavar': '<modules>',
|
||||
'help': 'Force import order to recognize a module as part of'
|
||||
' the standard compatibility libraries.'}
|
||||
),
|
||||
('known-third-party',
|
||||
{'default': DEFAULT_KNOWN_THIRD_PARTY,
|
||||
'type': 'csv',
|
||||
'metavar': '<modules>',
|
||||
'help': 'Force import order to recognize a module as part of'
|
||||
' a third party library.'}
|
||||
),
|
||||
('analyse-fallback-blocks',
|
||||
{'default': False,
|
||||
'type': 'yn',
|
||||
'metavar': '<y_or_n>',
|
||||
'help': 'Analyse import fallback blocks. This can be used to '
|
||||
'support both Python 2 and 3 compatible code, which means that '
|
||||
'the block might have code that exists only in one or another '
|
||||
'interpreter, leading to false positives when analysed.'},
|
||||
),
|
||||
('allow-wildcard-with-all',
|
||||
{'default': False,
|
||||
'type': 'yn',
|
||||
'metavar': '<y_or_n>',
|
||||
'help': 'Allow wildcard imports from modules that define __all__.'}),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self.import_graph = None
|
||||
self._imports_stack = []
|
||||
self._first_non_import_node = None
|
||||
self.__int_dep_info = self.__ext_dep_info = None
|
||||
self.reports = (('RP0401', 'External dependencies',
|
||||
self._report_external_dependencies),
|
||||
('RP0402', 'Modules dependencies graph',
|
||||
self._report_dependencies_graph),
|
||||
)
|
||||
|
||||
self._site_packages = self._compute_site_packages()
|
||||
|
||||
@staticmethod
|
||||
def _compute_site_packages():
|
||||
def _normalized_path(path):
|
||||
return os.path.normcase(os.path.abspath(path))
|
||||
|
||||
paths = set()
|
||||
real_prefix = getattr(sys, 'real_prefix', None)
|
||||
for prefix in filter(None, (real_prefix, sys.prefix)):
|
||||
path = sysconfig.get_python_lib(prefix=prefix)
|
||||
path = _normalized_path(path)
|
||||
paths.add(path)
|
||||
|
||||
# Handle Debian's derivatives /usr/local.
|
||||
if os.path.isfile("/etc/debian_version"):
|
||||
for prefix in filter(None, (real_prefix, sys.prefix)):
|
||||
libpython = os.path.join(prefix, "local", "lib",
|
||||
"python" + sysconfig.get_python_version(),
|
||||
"dist-packages")
|
||||
paths.add(libpython)
|
||||
return paths
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
self.linter.add_stats(dependencies={})
|
||||
self.linter.add_stats(cycles=[])
|
||||
self.stats = self.linter.stats
|
||||
self.import_graph = collections.defaultdict(set)
|
||||
self._excluded_edges = collections.defaultdict(set)
|
||||
self._ignored_modules = get_global_option(
|
||||
self, 'ignored-modules', default=[])
|
||||
|
||||
def _import_graph_without_ignored_edges(self):
|
||||
filtered_graph = copy.deepcopy(self.import_graph)
|
||||
for node in filtered_graph:
|
||||
filtered_graph[node].difference_update(self._excluded_edges[node])
|
||||
return filtered_graph
|
||||
|
||||
def close(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
if self.linter.is_message_enabled('cyclic-import'):
|
||||
graph = self._import_graph_without_ignored_edges()
|
||||
vertices = list(graph)
|
||||
for cycle in get_cycles(graph, vertices=vertices):
|
||||
self.add_message('cyclic-import', args=' -> '.join(cycle))
|
||||
|
||||
@check_messages(*MSGS.keys())
|
||||
def visit_import(self, node):
|
||||
"""triggered when an import statement is seen"""
|
||||
self._check_reimport(node)
|
||||
|
||||
modnode = node.root()
|
||||
names = [name for name, _ in node.names]
|
||||
if len(names) >= 2:
|
||||
self.add_message('multiple-imports', args=', '.join(names), node=node)
|
||||
|
||||
for name in names:
|
||||
self._check_deprecated_module(node, name)
|
||||
imported_module = self._get_imported_module(node, name)
|
||||
if isinstance(node.parent, astroid.Module):
|
||||
# Allow imports nested
|
||||
self._check_position(node)
|
||||
if isinstance(node.scope(), astroid.Module):
|
||||
self._record_import(node, imported_module)
|
||||
|
||||
if imported_module is None:
|
||||
continue
|
||||
|
||||
self._check_relative_import(modnode, node, imported_module, name)
|
||||
self._add_imported_module(node, imported_module.name)
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_importfrom(self, node):
|
||||
"""triggered when a from statement is seen"""
|
||||
basename = node.modname
|
||||
imported_module = self._get_imported_module(node, basename)
|
||||
|
||||
self._check_misplaced_future(node)
|
||||
self._check_deprecated_module(node, basename)
|
||||
self._check_wildcard_imports(node, imported_module)
|
||||
self._check_same_line_imports(node)
|
||||
self._check_reimport(node, basename=basename, level=node.level)
|
||||
|
||||
if isinstance(node.parent, astroid.Module):
|
||||
# Allow imports nested
|
||||
self._check_position(node)
|
||||
if isinstance(node.scope(), astroid.Module):
|
||||
self._record_import(node, imported_module)
|
||||
if imported_module is None:
|
||||
return
|
||||
modnode = node.root()
|
||||
self._check_relative_import(modnode, node, imported_module, basename)
|
||||
|
||||
for name, _ in node.names:
|
||||
if name != '*':
|
||||
self._add_imported_module(node, '%s.%s' % (imported_module.name, name))
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def leave_module(self, node):
|
||||
# Check imports are grouped by category (standard, 3rd party, local)
|
||||
std_imports, ext_imports, loc_imports = self._check_imports_order(node)
|
||||
|
||||
# Check imports are grouped by package within a given category
|
||||
met = set()
|
||||
current_package = None
|
||||
for import_node, import_name in std_imports + ext_imports + loc_imports:
|
||||
if not self.linter.is_message_enabled('ungrouped-imports', import_node.fromlineno):
|
||||
continue
|
||||
package, _, _ = import_name.partition('.')
|
||||
if current_package and current_package != package and package in met:
|
||||
self.add_message('ungrouped-imports', node=import_node,
|
||||
args=package)
|
||||
current_package = package
|
||||
met.add(package)
|
||||
|
||||
self._imports_stack = []
|
||||
self._first_non_import_node = None
|
||||
|
||||
def compute_first_non_import_node(self, node):
|
||||
if not self.linter.is_message_enabled('wrong-import-position', node.fromlineno):
|
||||
return
|
||||
# if the node does not contain an import instruction, and if it is the
|
||||
# first node of the module, keep a track of it (all the import positions
|
||||
# of the module will be compared to the position of this first
|
||||
# instruction)
|
||||
if self._first_non_import_node:
|
||||
return
|
||||
if not isinstance(node.parent, astroid.Module):
|
||||
return
|
||||
nested_allowed = [astroid.TryExcept, astroid.TryFinally]
|
||||
is_nested_allowed = [
|
||||
allowed for allowed in nested_allowed if isinstance(node, allowed)]
|
||||
if is_nested_allowed and \
|
||||
any(node.nodes_of_class((astroid.Import, astroid.ImportFrom))):
|
||||
return
|
||||
if isinstance(node, astroid.Assign):
|
||||
# Add compatibility for module level dunder names
|
||||
# https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names
|
||||
valid_targets = [
|
||||
isinstance(target, astroid.AssignName) and
|
||||
target.name.startswith('__') and target.name.endswith('__')
|
||||
for target in node.targets]
|
||||
if all(valid_targets):
|
||||
return
|
||||
self._first_non_import_node = node
|
||||
|
||||
visit_tryfinally = visit_tryexcept = visit_assignattr = visit_assign = \
|
||||
visit_ifexp = visit_comprehension = visit_expr = visit_if = \
|
||||
compute_first_non_import_node
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
if not self.linter.is_message_enabled('wrong-import-position', node.fromlineno):
|
||||
return
|
||||
# If it is the first non import instruction of the module, record it.
|
||||
if self._first_non_import_node:
|
||||
return
|
||||
|
||||
# Check if the node belongs to an `If` or a `Try` block. If they
|
||||
# contain imports, skip recording this node.
|
||||
if not isinstance(node.parent.scope(), astroid.Module):
|
||||
return
|
||||
|
||||
root = node
|
||||
while not isinstance(root.parent, astroid.Module):
|
||||
root = root.parent
|
||||
|
||||
if isinstance(root, (astroid.If, astroid.TryFinally, astroid.TryExcept)):
|
||||
if any(root.nodes_of_class((astroid.Import, astroid.ImportFrom))):
|
||||
return
|
||||
|
||||
self._first_non_import_node = node
|
||||
|
||||
visit_classdef = visit_for = visit_while = visit_functiondef
|
||||
|
||||
def _check_misplaced_future(self, node):
|
||||
basename = node.modname
|
||||
if basename == '__future__':
|
||||
# check if this is the first non-docstring statement in the module
|
||||
prev = node.previous_sibling()
|
||||
if prev:
|
||||
# consecutive future statements are possible
|
||||
if not (isinstance(prev, astroid.ImportFrom)
|
||||
and prev.modname == '__future__'):
|
||||
self.add_message('misplaced-future', node=node)
|
||||
return
|
||||
|
||||
def _check_same_line_imports(self, node):
|
||||
# Detect duplicate imports on the same line.
|
||||
names = (name for name, _ in node.names)
|
||||
counter = collections.Counter(names)
|
||||
for name, count in counter.items():
|
||||
if count > 1:
|
||||
self.add_message('reimported', node=node,
|
||||
args=(name, node.fromlineno))
|
||||
|
||||
def _check_position(self, node):
|
||||
"""Check `node` import or importfrom node position is correct
|
||||
|
||||
Send a message if `node` comes before another instruction
|
||||
"""
|
||||
# if a first non-import instruction has already been encountered,
|
||||
# it means the import comes after it and therefore is not well placed
|
||||
if self._first_non_import_node:
|
||||
self.add_message('wrong-import-position', node=node,
|
||||
args=node.as_string())
|
||||
|
||||
def _record_import(self, node, importedmodnode):
|
||||
"""Record the package `node` imports from"""
|
||||
importedname = importedmodnode.name if importedmodnode else None
|
||||
if not importedname:
|
||||
if isinstance(node, astroid.ImportFrom):
|
||||
importedname = node.modname
|
||||
else:
|
||||
importedname = node.names[0][0].split('.')[0]
|
||||
if isinstance(node, astroid.ImportFrom) and (node.level or 0) >= 1:
|
||||
# We need the impotedname with first point to detect local package
|
||||
# Example of node:
|
||||
# 'from .my_package1 import MyClass1'
|
||||
# the output should be '.my_package1' instead of 'my_package1'
|
||||
# Example of node:
|
||||
# 'from . import my_package2'
|
||||
# the output should be '.my_package2' instead of '{pyfile}'
|
||||
importedname = '.' + importedname
|
||||
self._imports_stack.append((node, importedname))
|
||||
|
||||
@staticmethod
|
||||
def _is_fallback_import(node, imports):
|
||||
imports = [import_node for (import_node, _) in imports]
|
||||
return any(astroid.are_exclusive(import_node, node)
|
||||
for import_node in imports)
|
||||
|
||||
def _check_imports_order(self, _module_node):
|
||||
"""Checks imports of module `node` are grouped by category
|
||||
|
||||
Imports must follow this order: standard, 3rd party, local
|
||||
"""
|
||||
std_imports = []
|
||||
third_party_imports = []
|
||||
first_party_imports = []
|
||||
# need of a list that holds third or first party ordered import
|
||||
external_imports = []
|
||||
local_imports = []
|
||||
third_party_not_ignored = []
|
||||
first_party_not_ignored = []
|
||||
local_not_ignored = []
|
||||
isort_obj = isort.SortImports(
|
||||
file_contents='', known_third_party=self.config.known_third_party,
|
||||
known_standard_library=self.config.known_standard_library,
|
||||
)
|
||||
for node, modname in self._imports_stack:
|
||||
if modname.startswith('.'):
|
||||
package = '.' + modname.split('.')[1]
|
||||
else:
|
||||
package = modname.split('.')[0]
|
||||
nested = not isinstance(node.parent, astroid.Module)
|
||||
ignore_for_import_order = not self.linter.is_message_enabled('wrong-import-order',
|
||||
node.fromlineno)
|
||||
import_category = isort_obj.place_module(package)
|
||||
node_and_package_import = (node, package)
|
||||
if import_category in ('FUTURE', 'STDLIB'):
|
||||
std_imports.append(node_and_package_import)
|
||||
wrong_import = (third_party_not_ignored or first_party_not_ignored
|
||||
or local_not_ignored)
|
||||
if self._is_fallback_import(node, wrong_import):
|
||||
continue
|
||||
if wrong_import and not nested:
|
||||
self.add_message('wrong-import-order', node=node,
|
||||
args=('standard import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string()))
|
||||
elif import_category == 'THIRDPARTY':
|
||||
third_party_imports.append(node_and_package_import)
|
||||
external_imports.append(node_and_package_import)
|
||||
if not nested and not ignore_for_import_order:
|
||||
third_party_not_ignored.append(node_and_package_import)
|
||||
wrong_import = first_party_not_ignored or local_not_ignored
|
||||
if wrong_import and not nested:
|
||||
self.add_message('wrong-import-order', node=node,
|
||||
args=('third party import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string()))
|
||||
elif import_category == 'FIRSTPARTY':
|
||||
first_party_imports.append(node_and_package_import)
|
||||
external_imports.append(node_and_package_import)
|
||||
if not nested and not ignore_for_import_order:
|
||||
first_party_not_ignored.append(node_and_package_import)
|
||||
wrong_import = local_not_ignored
|
||||
if wrong_import and not nested:
|
||||
self.add_message('wrong-import-order', node=node,
|
||||
args=('first party import "%s"' % node.as_string(),
|
||||
'"%s"' % wrong_import[0][0].as_string()))
|
||||
elif import_category == 'LOCALFOLDER':
|
||||
local_imports.append((node, package))
|
||||
if not nested and not ignore_for_import_order:
|
||||
local_not_ignored.append((node, package))
|
||||
return std_imports, external_imports, local_imports
|
||||
|
||||
def _get_imported_module(self, importnode, modname):
|
||||
try:
|
||||
return importnode.do_import_module(modname)
|
||||
except astroid.TooManyLevelsError:
|
||||
if _ignore_import_failure(importnode, modname, self._ignored_modules):
|
||||
return None
|
||||
|
||||
self.add_message('relative-beyond-top-level', node=importnode)
|
||||
|
||||
except astroid.AstroidBuildingException:
|
||||
if _ignore_import_failure(importnode, modname, self._ignored_modules):
|
||||
return None
|
||||
if not self.config.analyse_fallback_blocks and is_from_fallback_block(importnode):
|
||||
return None
|
||||
|
||||
dotted_modname = _get_import_name(importnode, modname)
|
||||
self.add_message('import-error', args=repr(dotted_modname),
|
||||
node=importnode)
|
||||
|
||||
def _check_relative_import(self, modnode, importnode, importedmodnode,
|
||||
importedasname):
|
||||
"""check relative import. node is either an Import or From node, modname
|
||||
the imported module name.
|
||||
"""
|
||||
if not self.linter.is_message_enabled('relative-import'):
|
||||
return None
|
||||
if importedmodnode.file is None:
|
||||
return False # built-in module
|
||||
if modnode is importedmodnode:
|
||||
return False # module importing itself
|
||||
if modnode.absolute_import_activated() or getattr(importnode, 'level', None):
|
||||
return False
|
||||
if importedmodnode.name != importedasname:
|
||||
# this must be a relative import...
|
||||
self.add_message('relative-import',
|
||||
args=(importedasname, importedmodnode.name),
|
||||
node=importnode)
|
||||
return None
|
||||
return None
|
||||
|
||||
def _add_imported_module(self, node, importedmodname):
|
||||
"""notify an imported module, used to analyze dependencies"""
|
||||
module_file = node.root().file
|
||||
context_name = node.root().name
|
||||
base = os.path.splitext(os.path.basename(module_file))[0]
|
||||
|
||||
# Determine if we have a `from .something import` in a package's
|
||||
# __init__. This means the module will never be able to import
|
||||
# itself using this condition (the level will be bigger or
|
||||
# if the same module is named as the package, it will be different
|
||||
# anyway).
|
||||
if isinstance(node, astroid.ImportFrom):
|
||||
if node.level and node.level > 0 and base == '__init__':
|
||||
return
|
||||
|
||||
try:
|
||||
importedmodname = get_module_part(importedmodname,
|
||||
module_file)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if context_name == importedmodname:
|
||||
self.add_message('import-self', node=node)
|
||||
elif not is_standard_module(importedmodname):
|
||||
# handle dependencies
|
||||
importedmodnames = self.stats['dependencies'].setdefault(
|
||||
importedmodname, set())
|
||||
if context_name not in importedmodnames:
|
||||
importedmodnames.add(context_name)
|
||||
|
||||
# update import graph
|
||||
self.import_graph[context_name].add(importedmodname)
|
||||
if not self.linter.is_message_enabled('cyclic-import', line=node.lineno):
|
||||
self._excluded_edges[context_name].add(importedmodname)
|
||||
|
||||
def _check_deprecated_module(self, node, mod_path):
|
||||
"""check if the module is deprecated"""
|
||||
for mod_name in self.config.deprecated_modules:
|
||||
if mod_path == mod_name or mod_path.startswith(mod_name + '.'):
|
||||
self.add_message('deprecated-module', node=node, args=mod_path)
|
||||
|
||||
def _check_reimport(self, node, basename=None, level=None):
|
||||
"""check if the import is necessary (i.e. not already done)"""
|
||||
if not self.linter.is_message_enabled('reimported'):
|
||||
return
|
||||
|
||||
frame = node.frame()
|
||||
root = node.root()
|
||||
contexts = [(frame, level)]
|
||||
if root is not frame:
|
||||
contexts.append((root, None))
|
||||
|
||||
for known_context, known_level in contexts:
|
||||
for name, alias in node.names:
|
||||
first = _get_first_import(
|
||||
node, known_context,
|
||||
name, basename,
|
||||
known_level, alias)
|
||||
if first is not None:
|
||||
self.add_message('reimported', node=node,
|
||||
args=(name, first.fromlineno))
|
||||
|
||||
def _report_external_dependencies(self, sect, _, _dummy):
|
||||
"""return a verbatim layout for displaying dependencies"""
|
||||
dep_info = _make_tree_defs(six.iteritems(self._external_dependencies_info()))
|
||||
if not dep_info:
|
||||
raise EmptyReportError()
|
||||
tree_str = _repr_tree_defs(dep_info)
|
||||
sect.append(VerbatimText(tree_str))
|
||||
|
||||
def _report_dependencies_graph(self, sect, _, _dummy):
|
||||
"""write dependencies as a dot (graphviz) file"""
|
||||
dep_info = self.stats['dependencies']
|
||||
if not dep_info or not (self.config.import_graph
|
||||
or self.config.ext_import_graph
|
||||
or self.config.int_import_graph):
|
||||
raise EmptyReportError()
|
||||
filename = self.config.import_graph
|
||||
if filename:
|
||||
_make_graph(filename, dep_info, sect, '')
|
||||
filename = self.config.ext_import_graph
|
||||
if filename:
|
||||
_make_graph(filename, self._external_dependencies_info(),
|
||||
sect, 'external ')
|
||||
filename = self.config.int_import_graph
|
||||
if filename:
|
||||
_make_graph(filename, self._internal_dependencies_info(),
|
||||
sect, 'internal ')
|
||||
|
||||
def _external_dependencies_info(self):
|
||||
"""return cached external dependencies information or build and
|
||||
cache them
|
||||
"""
|
||||
if self.__ext_dep_info is None:
|
||||
package = self.linter.current_name
|
||||
self.__ext_dep_info = result = {}
|
||||
for importee, importers in six.iteritems(self.stats['dependencies']):
|
||||
if not importee.startswith(package):
|
||||
result[importee] = importers
|
||||
return self.__ext_dep_info
|
||||
|
||||
def _internal_dependencies_info(self):
|
||||
"""return cached internal dependencies information or build and
|
||||
cache them
|
||||
"""
|
||||
if self.__int_dep_info is None:
|
||||
package = self.linter.current_name
|
||||
self.__int_dep_info = result = {}
|
||||
for importee, importers in six.iteritems(self.stats['dependencies']):
|
||||
if importee.startswith(package):
|
||||
result[importee] = importers
|
||||
return self.__int_dep_info
|
||||
|
||||
def _check_wildcard_imports(self, node, imported_module):
|
||||
wildcard_import_is_allowed = (
|
||||
self._wildcard_import_is_allowed(imported_module)
|
||||
)
|
||||
for name, _ in node.names:
|
||||
if name == '*' and not wildcard_import_is_allowed:
|
||||
self.add_message('wildcard-import', args=node.modname, node=node)
|
||||
|
||||
def _wildcard_import_is_allowed(self, imported_module):
|
||||
return (self.config.allow_wildcard_with_all
|
||||
and imported_module is not None
|
||||
and '__all__' in imported_module.locals)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(ImportsChecker(linter))
|
||||
@@ -0,0 +1,295 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2009, 2012, 2014 Google, Inc.
|
||||
# Copyright (c) 2012 Mike Bryant <leachim@leachim.info>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2018 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Chris Murray <chris@chrismurray.scot>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""checker for use of Python logging
|
||||
"""
|
||||
import string
|
||||
|
||||
import six
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import checkers
|
||||
from pylint import interfaces
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
|
||||
|
||||
MSGS = {
|
||||
'W1201': ('Specify string format arguments as logging function parameters',
|
||||
'logging-not-lazy',
|
||||
'Used when a logging statement has a call form of '
|
||||
'"logging.<logging method>(format_string % (format_args...))". '
|
||||
'Such calls should leave string interpolation to the logging '
|
||||
'method itself and be written '
|
||||
'"logging.<logging method>(format_string, format_args...)" '
|
||||
'so that the program may avoid incurring the cost of the '
|
||||
'interpolation in those cases in which no message will be '
|
||||
'logged. For more, see '
|
||||
'http://www.python.org/dev/peps/pep-0282/.'),
|
||||
'W1202': ('Use % formatting in logging functions and pass the % '
|
||||
'parameters as arguments',
|
||||
'logging-format-interpolation',
|
||||
'Used when a logging statement has a call form of '
|
||||
'"logging.<logging method>(format_string.format(format_args...))"'
|
||||
'. Such calls should use % formatting instead, but leave '
|
||||
'interpolation to the logging function by passing the parameters '
|
||||
'as arguments.'),
|
||||
'E1200': ('Unsupported logging format character %r (%#02x) at index %d',
|
||||
'logging-unsupported-format',
|
||||
'Used when an unsupported format character is used in a logging\
|
||||
statement format string.'),
|
||||
'E1201': ('Logging format string ends in middle of conversion specifier',
|
||||
'logging-format-truncated',
|
||||
'Used when a logging statement format string terminates before\
|
||||
the end of a conversion specifier.'),
|
||||
'E1205': ('Too many arguments for logging format string',
|
||||
'logging-too-many-args',
|
||||
'Used when a logging format string is given too many arguments.'),
|
||||
'E1206': ('Not enough arguments for logging format string',
|
||||
'logging-too-few-args',
|
||||
'Used when a logging format string is given too few arguments.'),
|
||||
}
|
||||
|
||||
|
||||
CHECKED_CONVENIENCE_FUNCTIONS = {
|
||||
'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn', 'warning'
|
||||
}
|
||||
|
||||
|
||||
def is_method_call(func, types=(), methods=()):
|
||||
"""Determines if a BoundMethod node represents a method call.
|
||||
|
||||
Args:
|
||||
func (astroid.BoundMethod): The BoundMethod AST node to check.
|
||||
types (Optional[String]): Optional sequence of caller type names to restrict check.
|
||||
methods (Optional[String]): Optional sequence of method names to restrict check.
|
||||
|
||||
Returns:
|
||||
bool: true if the node represents a method call for the given type and
|
||||
method names, False otherwise.
|
||||
"""
|
||||
return (isinstance(func, astroid.BoundMethod)
|
||||
and isinstance(func.bound, astroid.Instance)
|
||||
and (func.bound.name in types if types else True)
|
||||
and (func.name in methods if methods else True))
|
||||
|
||||
|
||||
class LoggingChecker(checkers.BaseChecker):
|
||||
"""Checks use of the logging module."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
name = 'logging'
|
||||
msgs = MSGS
|
||||
|
||||
options = (('logging-modules',
|
||||
{'default': ('logging',),
|
||||
'type': 'csv',
|
||||
'metavar': '<comma separated list>',
|
||||
'help': 'Logging modules to check that the string format '
|
||||
'arguments are in logging function parameter format'}
|
||||
),
|
||||
)
|
||||
|
||||
def visit_module(self, node): # pylint: disable=unused-argument
|
||||
"""Clears any state left in this checker from last module checked."""
|
||||
# The code being checked can just as easily "import logging as foo",
|
||||
# so it is necessary to process the imports and store in this field
|
||||
# what name the logging module is actually given.
|
||||
self._logging_names = set()
|
||||
logging_mods = self.config.logging_modules
|
||||
|
||||
self._logging_modules = set(logging_mods)
|
||||
self._from_imports = {}
|
||||
for logging_mod in logging_mods:
|
||||
parts = logging_mod.rsplit('.', 1)
|
||||
if len(parts) > 1:
|
||||
self._from_imports[parts[0]] = parts[1]
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""Checks to see if a module uses a non-Python logging module."""
|
||||
try:
|
||||
logging_name = self._from_imports[node.modname]
|
||||
for module, as_name in node.names:
|
||||
if module == logging_name:
|
||||
self._logging_names.add(as_name or module)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def visit_import(self, node):
|
||||
"""Checks to see if this module uses Python's built-in logging."""
|
||||
for module, as_name in node.names:
|
||||
if module in self._logging_modules:
|
||||
self._logging_names.add(as_name or module)
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_call(self, node):
|
||||
"""Checks calls to logging methods."""
|
||||
def is_logging_name():
|
||||
return (isinstance(node.func, astroid.Attribute) and
|
||||
isinstance(node.func.expr, astroid.Name) and
|
||||
node.func.expr.name in self._logging_names)
|
||||
|
||||
def is_logger_class():
|
||||
try:
|
||||
for inferred in node.func.infer():
|
||||
if isinstance(inferred, astroid.BoundMethod):
|
||||
parent = inferred._proxied.parent
|
||||
if (isinstance(parent, astroid.ClassDef) and
|
||||
(parent.qname() == 'logging.Logger' or
|
||||
any(ancestor.qname() == 'logging.Logger'
|
||||
for ancestor in parent.ancestors()))):
|
||||
return True, inferred._proxied.name
|
||||
except astroid.exceptions.InferenceError:
|
||||
pass
|
||||
return False, None
|
||||
|
||||
if is_logging_name():
|
||||
name = node.func.attrname
|
||||
else:
|
||||
result, name = is_logger_class()
|
||||
if not result:
|
||||
return
|
||||
self._check_log_method(node, name)
|
||||
|
||||
def _check_log_method(self, node, name):
|
||||
"""Checks calls to logging.log(level, format, *format_args)."""
|
||||
if name == 'log':
|
||||
if node.starargs or node.kwargs or len(node.args) < 2:
|
||||
# Either a malformed call, star args, or double-star args. Beyond
|
||||
# the scope of this checker.
|
||||
return
|
||||
format_pos = 1
|
||||
elif name in CHECKED_CONVENIENCE_FUNCTIONS:
|
||||
if node.starargs or node.kwargs or not node.args:
|
||||
# Either no args, star args, or double-star args. Beyond the
|
||||
# scope of this checker.
|
||||
return
|
||||
format_pos = 0
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(node.args[format_pos], astroid.BinOp):
|
||||
binop = node.args[format_pos]
|
||||
if (binop.op == '%' or binop.op == '+' and
|
||||
len([_operand for _operand in (binop.left, binop.right)
|
||||
if self._is_operand_literal_str(_operand)]) == 1):
|
||||
self.add_message('logging-not-lazy', node=node)
|
||||
elif isinstance(node.args[format_pos], astroid.Call):
|
||||
self._check_call_func(node.args[format_pos])
|
||||
elif isinstance(node.args[format_pos], astroid.Const):
|
||||
self._check_format_string(node, format_pos)
|
||||
|
||||
@staticmethod
|
||||
def _is_operand_literal_str(operand):
|
||||
"""
|
||||
Return True if the operand in argument is a literal string
|
||||
"""
|
||||
return isinstance(operand, astroid.Const) and operand.name == 'str'
|
||||
|
||||
def _check_call_func(self, node):
|
||||
"""Checks that function call is not format_string.format().
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.Call):
|
||||
Call AST node to be checked.
|
||||
"""
|
||||
func = utils.safe_infer(node.func)
|
||||
types = ('str', 'unicode')
|
||||
methods = ('format',)
|
||||
if is_method_call(func, types, methods) and not is_complex_format_str(func.bound):
|
||||
self.add_message('logging-format-interpolation', node=node)
|
||||
|
||||
def _check_format_string(self, node, format_arg):
|
||||
"""Checks that format string tokens match the supplied arguments.
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.NodeNG): AST node to be checked.
|
||||
format_arg (int): Index of the format string in the node arguments.
|
||||
"""
|
||||
num_args = _count_supplied_tokens(node.args[format_arg + 1:])
|
||||
if not num_args:
|
||||
# If no args were supplied, then all format strings are valid -
|
||||
# don't check any further.
|
||||
return
|
||||
format_string = node.args[format_arg].value
|
||||
if not isinstance(format_string, six.string_types):
|
||||
# If the log format is constant non-string (e.g. logging.debug(5)),
|
||||
# ensure there are no arguments.
|
||||
required_num_args = 0
|
||||
else:
|
||||
try:
|
||||
keyword_args, required_num_args = \
|
||||
utils.parse_format_string(format_string)
|
||||
if keyword_args:
|
||||
# Keyword checking on logging strings is complicated by
|
||||
# special keywords - out of scope.
|
||||
return
|
||||
except utils.UnsupportedFormatCharacter as ex:
|
||||
char = format_string[ex.index]
|
||||
self.add_message('logging-unsupported-format', node=node,
|
||||
args=(char, ord(char), ex.index))
|
||||
return
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message('logging-format-truncated', node=node)
|
||||
return
|
||||
if num_args > required_num_args:
|
||||
self.add_message('logging-too-many-args', node=node)
|
||||
elif num_args < required_num_args:
|
||||
self.add_message('logging-too-few-args', node=node)
|
||||
|
||||
|
||||
def is_complex_format_str(node):
|
||||
"""Checks if node represents a string with complex formatting specs.
|
||||
|
||||
Args:
|
||||
node (astroid.node_classes.NodeNG): AST node to check
|
||||
Returns:
|
||||
bool: True if inferred string uses complex formatting, False otherwise
|
||||
"""
|
||||
inferred = utils.safe_infer(node)
|
||||
if inferred is None or not isinstance(inferred.value, six.string_types):
|
||||
return True
|
||||
try:
|
||||
parsed = list(string.Formatter().parse(inferred.value))
|
||||
except ValueError:
|
||||
# This format string is invalid
|
||||
return False
|
||||
for _, _, format_spec, _ in parsed:
|
||||
if format_spec:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _count_supplied_tokens(args):
|
||||
"""Counts the number of tokens in an args list.
|
||||
|
||||
The Python log functions allow for special keyword arguments: func,
|
||||
exc_info and extra. To handle these cases correctly, we only count
|
||||
arguments that aren't keywords.
|
||||
|
||||
Args:
|
||||
args (list): AST nodes that are arguments for a log format string.
|
||||
|
||||
Returns:
|
||||
int: Number of AST nodes that aren't keywords.
|
||||
"""
|
||||
return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto-register this checker."""
|
||||
linter.register_checker(LoggingChecker(linter))
|
||||
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2009-2013 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Alexandru Coman <fcoman@bitdefender.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 glegoux <gilles.legoux@gmail.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
|
||||
"""Check source code is ascii only or has an encoding declaration (PEP 263)"""
|
||||
|
||||
# pylint: disable=W0511
|
||||
|
||||
import re
|
||||
|
||||
import six
|
||||
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.utils import OPTION_RGX
|
||||
|
||||
|
||||
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
|
||||
* encoding issues.
|
||||
"""
|
||||
__implements__ = IRawChecker
|
||||
|
||||
# configuration section name
|
||||
name = 'miscellaneous'
|
||||
msgs = MSGS
|
||||
|
||||
options = (('notes',
|
||||
{'type': 'csv', 'metavar': '<comma separated values>',
|
||||
'default': ('FIXME', 'XXX', 'TODO'),
|
||||
'help': ('List of note tags to take in consideration, '
|
||||
'separated by a comma.')}),)
|
||||
|
||||
def _check_note(self, notes, lineno, line, module_last_lineno):
|
||||
"""
|
||||
Add the message 'fixme' in case a note is found in the line.
|
||||
|
||||
:param notes: regular expression object matching any notes
|
||||
(XXX, TODO, FIXME) behind a '#'
|
||||
:type notes: re.pattern object
|
||||
:param lineno: line number
|
||||
:type lineno: int
|
||||
:param line: line to be checked
|
||||
:type line: str
|
||||
:param module_last_lineno: last line number of the module as parsed by astroid
|
||||
(may be different from real last line number in case
|
||||
commented lines exist at the end of the module)
|
||||
:type module_last_lineno: int
|
||||
"""
|
||||
# First, simply check if the notes are in the line at all. This is an
|
||||
# optimisation to prevent using the regular expression on every line,
|
||||
# but rather only on lines which may actually contain one of the notes.
|
||||
# This prevents a pathological problem with lines that are hundreds
|
||||
# of thousands of characters long.
|
||||
for note in self.config.notes:
|
||||
if note in line:
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
match = notes.search(line)
|
||||
if not match:
|
||||
return
|
||||
# In case the module ends with commented lines, the astroid parser
|
||||
# don't take into account those lines, then:
|
||||
# - the line number of those lines is greater than the
|
||||
# module last line number (module.tolineno)
|
||||
# - astroid module object can't inform pylint
|
||||
# of disabled messages in those extra lines.
|
||||
if lineno > module_last_lineno:
|
||||
disable_option_match = OPTION_RGX.search(line)
|
||||
if disable_option_match:
|
||||
try:
|
||||
_, value = disable_option_match.group(1).split('=', 1)
|
||||
values = [_val.strip().upper() for _val in value.split(',')]
|
||||
if set(values) & set(self.config.notes):
|
||||
return
|
||||
except ValueError:
|
||||
self.add_message('bad-inline-option',
|
||||
args=disable_option_match.group(1).strip(), line=line)
|
||||
return
|
||||
self.add_message('fixme', args=line[match.start(1):].rstrip(), line=lineno)
|
||||
|
||||
def _check_encoding(self, lineno, line, file_encoding):
|
||||
try:
|
||||
return six.text_type(line, file_encoding)
|
||||
except UnicodeDecodeError as ex:
|
||||
self.add_message('invalid-encoded-data', line=lineno,
|
||||
args=(file_encoding, ex.args[2]))
|
||||
except LookupError as ex:
|
||||
if (line.startswith('#') and
|
||||
"coding" in line and file_encoding in line):
|
||||
self.add_message('syntax-error',
|
||||
line=lineno,
|
||||
args='Cannot decode using encoding "{}",'
|
||||
' bad encoding'.format(file_encoding))
|
||||
|
||||
def process_module(self, module):
|
||||
"""inspect the source file to find encoding problem or fixmes like
|
||||
notes
|
||||
"""
|
||||
if self.config.notes:
|
||||
notes = re.compile(
|
||||
r'.*?#\s*(%s)(:*\s*.*)' % "|".join(self.config.notes))
|
||||
else:
|
||||
notes = None
|
||||
if module.file_encoding:
|
||||
encoding = module.file_encoding
|
||||
else:
|
||||
encoding = 'ascii'
|
||||
|
||||
with module.stream() as stream:
|
||||
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, module.tolineno)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(EncodingChecker(linter))
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) 2006, 2008-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""check for new / old style related problems
|
||||
"""
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
check_messages,
|
||||
node_frame_class,
|
||||
has_known_bases
|
||||
)
|
||||
|
||||
MSGS = {
|
||||
'E1001': ('Use of __slots__ on an old style class',
|
||||
'slots-on-old-class',
|
||||
'Used when an old style class uses the __slots__ attribute.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'E1002': ('Use of super on an old style class',
|
||||
'super-on-old-class',
|
||||
'Used when an old style class uses the super builtin.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'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.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'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.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'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".',
|
||||
{'maxversion': (3, 0)})
|
||||
}
|
||||
|
||||
|
||||
class NewStyleConflictChecker(BaseChecker):
|
||||
"""checks for usage of new style capabilities on old style classes and
|
||||
other new/old styles conflicts problems
|
||||
* use of property, __slots__, super
|
||||
* "super" usage
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'newstyle'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
# configuration options
|
||||
options = ()
|
||||
|
||||
@check_messages('slots-on-old-class', 'old-style-class')
|
||||
def visit_classdef(self, node):
|
||||
""" Check __slots__ in old style classes and old
|
||||
style class definition.
|
||||
"""
|
||||
if '__slots__' in node and not node.newstyle:
|
||||
confidence = (INFERENCE if has_known_bases(node)
|
||||
else INFERENCE_FAILURE)
|
||||
self.add_message('slots-on-old-class', node=node,
|
||||
confidence=confidence)
|
||||
# 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' and not node.metaclass():
|
||||
# We use confidence HIGH here because this message should only ever
|
||||
# be emitted for classes at the root of the inheritance hierarchyself.
|
||||
self.add_message('old-style-class', node=node, confidence=HIGH)
|
||||
|
||||
@check_messages('property-on-old-class')
|
||||
def visit_call(self, node):
|
||||
"""check property usage"""
|
||||
parent = node.parent.frame()
|
||||
if (isinstance(parent, astroid.ClassDef) and
|
||||
not parent.newstyle and
|
||||
isinstance(node.func, astroid.Name)):
|
||||
confidence = (INFERENCE if has_known_bases(parent)
|
||||
else INFERENCE_FAILURE)
|
||||
name = node.func.name
|
||||
if name == 'property':
|
||||
self.add_message('property-on-old-class', node=node,
|
||||
confidence=confidence)
|
||||
|
||||
@check_messages('super-on-old-class', 'bad-super-call', 'missing-super-argument')
|
||||
def visit_functiondef(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(astroid.Call):
|
||||
if node_frame_class(stmt) != node_frame_class(node):
|
||||
# Don't look down in other scopes.
|
||||
continue
|
||||
|
||||
expr = stmt.func
|
||||
if not isinstance(expr, astroid.Attribute):
|
||||
continue
|
||||
|
||||
call = expr.expr
|
||||
# skip the test if using super
|
||||
if not (isinstance(call, astroid.Call) and
|
||||
isinstance(call.func, astroid.Name) and
|
||||
call.func.name == 'super'):
|
||||
continue
|
||||
|
||||
if not klass.newstyle and has_known_bases(klass):
|
||||
# super should not be used on an old style class
|
||||
self.add_message('super-on-old-class', node=node)
|
||||
else:
|
||||
# super first arg should be the class
|
||||
if not call.args:
|
||||
if sys.version_info[0] == 3:
|
||||
# unless Python 3
|
||||
continue
|
||||
else:
|
||||
self.add_message('missing-super-argument', node=call)
|
||||
continue
|
||||
|
||||
# calling super(type(self), self) can lead to recursion loop
|
||||
# in derived classes
|
||||
arg0 = call.args[0]
|
||||
if isinstance(arg0, astroid.Call) and \
|
||||
isinstance(arg0.func, astroid.Name) and \
|
||||
arg0.func.name == 'type':
|
||||
self.add_message('bad-super-call', node=call, args=('type', ))
|
||||
continue
|
||||
|
||||
# calling super(self.__class__, self) can lead to recursion loop
|
||||
# in derived classes
|
||||
if len(call.args) >= 2 and \
|
||||
isinstance(call.args[1], astroid.Name) and \
|
||||
call.args[1].name == 'self' and \
|
||||
isinstance(arg0, astroid.Attribute) and \
|
||||
arg0.attrname == '__class__':
|
||||
self.add_message('bad-super-call', node=call, args=('self.__class__', ))
|
||||
continue
|
||||
|
||||
try:
|
||||
supcls = call.args and next(call.args[0].infer(), None)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
if klass is not supcls:
|
||||
name = None
|
||||
# if supcls is not YES, then supcls was infered
|
||||
# and use its name. Otherwise, try to look
|
||||
# for call.args[0].name
|
||||
if supcls:
|
||||
name = supcls.name
|
||||
elif call.args and hasattr(call.args[0], 'name'):
|
||||
name = call.args[0].name
|
||||
if name:
|
||||
self.add_message('bad-super-call', node=call, args=(name, ))
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(NewStyleConflictChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
# Copyright (c) 2007, 2010, 2013, 2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mike Frysinger <vapier@gentoo.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
""" Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
|
||||
http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
|
||||
Raw metrics checker
|
||||
"""
|
||||
|
||||
import tokenize
|
||||
|
||||
from pylint.interfaces import ITokenChecker
|
||||
from pylint.exceptions import EmptyReportError
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.reporters import diff_string
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
|
||||
|
||||
def report_raw_stats(sect, stats, old_stats):
|
||||
"""calculate percentage of code / doc / comment / empty
|
||||
"""
|
||||
total_lines = stats['total_lines']
|
||||
if not total_lines:
|
||||
raise EmptyReportError()
|
||||
sect.description = '%s lines have been analyzed' % total_lines
|
||||
lines = ('type', 'number', '%', 'previous', 'difference')
|
||||
for node_type in ('code', 'docstring', 'comment', 'empty'):
|
||||
key = node_type + '_lines'
|
||||
total = stats[key]
|
||||
percent = float(total * 100) / total_lines
|
||||
old = old_stats.get(key, None)
|
||||
if old is not None:
|
||||
diff_str = diff_string(old, total)
|
||||
else:
|
||||
old, diff_str = 'NC', 'NC'
|
||||
lines += (node_type, str(total), '%.2f' % percent,
|
||||
str(old), diff_str)
|
||||
sect.append(Table(children=lines, cols=5, rheaders=1))
|
||||
|
||||
|
||||
class RawMetricsChecker(BaseTokenChecker):
|
||||
"""does not check anything but gives some raw metrics :
|
||||
* total number of lines
|
||||
* total number of code lines
|
||||
* total number of docstring lines
|
||||
* total number of comments lines
|
||||
* total number of empty lines
|
||||
"""
|
||||
|
||||
__implements__ = (ITokenChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'metrics'
|
||||
# configuration options
|
||||
options = ()
|
||||
# messages
|
||||
msgs = {}
|
||||
# reports
|
||||
reports = (('RP0701', 'Raw metrics', report_raw_stats),)
|
||||
|
||||
def __init__(self, linter):
|
||||
BaseTokenChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
|
||||
def open(self):
|
||||
"""init statistics"""
|
||||
self.stats = self.linter.add_stats(total_lines=0, code_lines=0,
|
||||
empty_lines=0, docstring_lines=0,
|
||||
comment_lines=0)
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""update stats"""
|
||||
i = 0
|
||||
tokens = list(tokens)
|
||||
while i < len(tokens):
|
||||
i, lines_number, line_type = get_type(tokens, i)
|
||||
self.stats['total_lines'] += lines_number
|
||||
self.stats[line_type] += lines_number
|
||||
|
||||
|
||||
JUNK = (tokenize.NL, tokenize.INDENT, tokenize.NEWLINE, tokenize.ENDMARKER)
|
||||
|
||||
def get_type(tokens, start_index):
|
||||
"""return the line type : docstring, comment, code, empty"""
|
||||
i = start_index
|
||||
tok_type = tokens[i][0]
|
||||
start = tokens[i][2]
|
||||
pos = start
|
||||
line_type = None
|
||||
while i < len(tokens) and tokens[i][2][0] == start[0]:
|
||||
tok_type = tokens[i][0]
|
||||
pos = tokens[i][3]
|
||||
if line_type is None:
|
||||
if tok_type == tokenize.STRING:
|
||||
line_type = 'docstring_lines'
|
||||
elif tok_type == tokenize.COMMENT:
|
||||
line_type = 'comment_lines'
|
||||
elif tok_type in JUNK:
|
||||
pass
|
||||
else:
|
||||
line_type = 'code_lines'
|
||||
i += 1
|
||||
if line_type is None:
|
||||
line_type = 'empty_lines'
|
||||
elif i < len(tokens) and tokens[i][0] == tokenize.NEWLINE:
|
||||
i += 1
|
||||
return i, pos[0] - start[0] + 1, line_type
|
||||
|
||||
|
||||
def register(linter):
|
||||
""" required method to auto register this checker """
|
||||
linter.register_checker(RawMetricsChecker(linter))
|
||||
@@ -0,0 +1,924 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017-2018 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Hugo <hugovk@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Sznuk <ls@rdprojekt.pl>
|
||||
# Copyright (c) 2017 Alex Hearn <alex.d.hearn@gmail.com>
|
||||
# Copyright (c) 2017 Antonio Ossa <aaossa@uc.cl>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for code which can be refactored."""
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import tokenize
|
||||
|
||||
import astroid
|
||||
from astroid import decorators
|
||||
import six
|
||||
|
||||
from pylint import interfaces
|
||||
from pylint import checkers
|
||||
from pylint import utils as lint_utils
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _all_elements_are_true(gen):
|
||||
values = list(gen)
|
||||
return values and all(values)
|
||||
|
||||
|
||||
def _if_statement_is_always_returning(if_node):
|
||||
def _has_return_node(elems, scope):
|
||||
for node in elems:
|
||||
if isinstance(node, astroid.If):
|
||||
yield _if_statement_is_always_returning(node)
|
||||
elif isinstance(node, astroid.Return):
|
||||
yield node.scope() is scope
|
||||
|
||||
scope = if_node.scope()
|
||||
body_returns = _all_elements_are_true(
|
||||
_has_return_node(if_node.body, scope=scope)
|
||||
)
|
||||
if if_node.orelse:
|
||||
orelse_returns = _all_elements_are_true(
|
||||
_has_return_node(if_node.orelse, scope=scope)
|
||||
)
|
||||
else:
|
||||
orelse_returns = False
|
||||
|
||||
return body_returns and orelse_returns
|
||||
|
||||
|
||||
class RefactoringChecker(checkers.BaseTokenChecker):
|
||||
"""Looks for code which can be refactored
|
||||
|
||||
This checker also mixes the astroid and the token approaches
|
||||
in order to create knowledge about whether a "else if" node
|
||||
is a true "else if" node, or a "elif" node.
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.ITokenChecker, interfaces.IAstroidChecker)
|
||||
|
||||
name = 'refactoring'
|
||||
|
||||
msgs = {
|
||||
'R1701': ("Consider merging these isinstance calls to isinstance(%s, (%s))",
|
||||
"consider-merging-isinstance",
|
||||
"Used when multiple consecutive isinstance calls can be merged into one."),
|
||||
'R1706': ("Consider using ternary (%s)",
|
||||
"consider-using-ternary",
|
||||
"Used when one of known pre-python 2.5 ternary syntax is used.",),
|
||||
'R1709': ("Boolean expression may be simplified to %s",
|
||||
"simplify-boolean-expression",
|
||||
"Emitted when redundant pre-python 2.5 ternary syntax is used.",),
|
||||
'R1702': ('Too many nested blocks (%s/%s)',
|
||||
'too-many-nested-blocks',
|
||||
'Used when a function or a method has too many nested '
|
||||
'blocks. This makes the code less understandable and '
|
||||
'maintainable.',
|
||||
{'old_names': [('R0101', 'too-many-nested-blocks')]}),
|
||||
'R1703': ('The if statement can be replaced with %s',
|
||||
'simplifiable-if-statement',
|
||||
'Used when an if statement can be replaced with '
|
||||
'\'bool(test)\'. ',
|
||||
{'old_names': [('R0102', 'simplifiable-if-statement')]}),
|
||||
'R1704': ('Redefining argument with the local name %r',
|
||||
'redefined-argument-from-local',
|
||||
'Used when a local name is redefining an argument, which might '
|
||||
'suggest a potential error. This is taken in account only for '
|
||||
'a handful of name binding operations, such as for iteration, '
|
||||
'with statement assignment and exception handler assignment.'
|
||||
),
|
||||
'R1705': ('Unnecessary "else" after "return"',
|
||||
'no-else-return',
|
||||
'Used in order to highlight an unnecessary block of '
|
||||
'code following an if containing a return statement. '
|
||||
'As such, it will warn when it encounters an else '
|
||||
'following a chain of ifs, all of them containing a '
|
||||
'return statement.'
|
||||
),
|
||||
'R1707': ('Disallow trailing comma tuple',
|
||||
'trailing-comma-tuple',
|
||||
'In Python, a tuple is actually created by the comma symbol, '
|
||||
'not by the parentheses. Unfortunately, one can actually create a '
|
||||
'tuple by misplacing a trailing comma, which can lead to potential '
|
||||
'weird bugs in your code. You should always use parentheses '
|
||||
'explicitly for creating a tuple.',
|
||||
{'minversion': (3, 0)}),
|
||||
'R1708': ('Do not raise StopIteration in generator, use return statement instead',
|
||||
'stop-iteration-return',
|
||||
'According to PEP479, the raise of StopIteration to end the loop of '
|
||||
'a generator may lead to hard to find bugs. This PEP specify that '
|
||||
'raise StopIteration has to be replaced by a simple return statement',
|
||||
{'minversion': (3, 0)}),
|
||||
'R1710': ('Either all return statements in a function should return an expression, '
|
||||
'or none of them should.',
|
||||
'inconsistent-return-statements',
|
||||
'According to PEP8, if any return statement returns an expression, '
|
||||
'any return statements where no value is returned should explicitly '
|
||||
'state this as return None, and an explicit return statement '
|
||||
'should be present at the end of the function (if reachable)'
|
||||
),
|
||||
}
|
||||
options = (('max-nested-blocks',
|
||||
{'default': 5, 'type': 'int', 'metavar': '<int>',
|
||||
'help': 'Maximum number of nested blocks for function / '
|
||||
'method body'}
|
||||
),
|
||||
('never-returning-functions',
|
||||
{'default': ('optparse.Values', 'sys.exit',),
|
||||
'type': 'csv',
|
||||
'help': 'Complete name of functions that never returns. When checking '
|
||||
'for inconsistent-return-statements if a never returning function is '
|
||||
'called then it will be considered as an explicit return statement '
|
||||
'and no message will be printed.'}
|
||||
),)
|
||||
|
||||
priority = 0
|
||||
|
||||
def __init__(self, linter=None):
|
||||
checkers.BaseTokenChecker.__init__(self, linter)
|
||||
self._return_nodes = {}
|
||||
self._init()
|
||||
self._never_returning_functions = None
|
||||
|
||||
def _init(self):
|
||||
self._nested_blocks = []
|
||||
self._elifs = []
|
||||
self._nested_blocks_msg = None
|
||||
|
||||
def open(self):
|
||||
# do this in open since config not fully initialized in __init__
|
||||
self._never_returning_functions = set(self.config.never_returning_functions)
|
||||
|
||||
@decorators.cachedproperty
|
||||
def _dummy_rgx(self):
|
||||
return lint_utils.get_global_option(
|
||||
self, 'dummy-variables-rgx', default=None)
|
||||
|
||||
@staticmethod
|
||||
def _is_bool_const(node):
|
||||
return (isinstance(node.value, astroid.Const)
|
||||
and isinstance(node.value.value, bool))
|
||||
|
||||
def _is_actual_elif(self, node):
|
||||
"""Check if the given node is an actual elif
|
||||
|
||||
This is a problem we're having with the builtin ast module,
|
||||
which splits `elif` branches into a separate if statement.
|
||||
Unfortunately we need to know the exact type in certain
|
||||
cases.
|
||||
"""
|
||||
|
||||
if isinstance(node.parent, astroid.If):
|
||||
orelse = node.parent.orelse
|
||||
# current if node must directly follow a "else"
|
||||
if orelse and orelse == [node]:
|
||||
if (node.lineno, node.col_offset) in self._elifs:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_simplifiable_if(self, node):
|
||||
"""Check if the given if node can be simplified.
|
||||
|
||||
The if statement can be reduced to a boolean expression
|
||||
in some cases. For instance, if there are two branches
|
||||
and both of them return a boolean value that depends on
|
||||
the result of the statement's test, then this can be reduced
|
||||
to `bool(test)` without losing any functionality.
|
||||
"""
|
||||
|
||||
if self._is_actual_elif(node):
|
||||
# Not interested in if statements with multiple branches.
|
||||
return
|
||||
if len(node.orelse) != 1 or len(node.body) != 1:
|
||||
return
|
||||
|
||||
# Check if both branches can be reduced.
|
||||
first_branch = node.body[0]
|
||||
else_branch = node.orelse[0]
|
||||
if isinstance(first_branch, astroid.Return):
|
||||
if not isinstance(else_branch, astroid.Return):
|
||||
return
|
||||
first_branch_is_bool = self._is_bool_const(first_branch)
|
||||
else_branch_is_bool = self._is_bool_const(else_branch)
|
||||
reduced_to = "'return bool(test)'"
|
||||
elif isinstance(first_branch, astroid.Assign):
|
||||
if not isinstance(else_branch, astroid.Assign):
|
||||
return
|
||||
first_branch_is_bool = self._is_bool_const(first_branch)
|
||||
else_branch_is_bool = self._is_bool_const(else_branch)
|
||||
reduced_to = "'var = bool(test)'"
|
||||
else:
|
||||
return
|
||||
|
||||
if not first_branch_is_bool or not else_branch_is_bool:
|
||||
return
|
||||
if not first_branch.value.value:
|
||||
# This is a case that can't be easily simplified and
|
||||
# if it can be simplified, it will usually result in a
|
||||
# code that's harder to understand and comprehend.
|
||||
# Let's take for instance `arg and arg <= 3`. This could theoretically be
|
||||
# reduced to `not arg or arg > 3`, but the net result is that now the
|
||||
# condition is harder to understand, because it requires understanding of
|
||||
# an extra clause:
|
||||
# * first, there is the negation of truthness with `not arg`
|
||||
# * the second clause is `arg > 3`, which occurs when arg has a
|
||||
# a truth value, but it implies that `arg > 3` is equivalent
|
||||
# with `arg and arg > 3`, which means that the user must
|
||||
# think about this assumption when evaluating `arg > 3`.
|
||||
# The original form is easier to grasp.
|
||||
return
|
||||
|
||||
self.add_message('simplifiable-if-statement', node=node,
|
||||
args=(reduced_to,))
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
# Process tokens and look for 'if' or 'elif'
|
||||
for index, token in enumerate(tokens):
|
||||
token_string = token[1]
|
||||
if token_string == 'elif':
|
||||
# AST exists by the time process_tokens is called, so
|
||||
# it's safe to assume tokens[index+1]
|
||||
# exists. tokens[index+1][2] is the elif's position as
|
||||
# reported by CPython and PyPy,
|
||||
# tokens[index][2] is the actual position and also is
|
||||
# reported by IronPython.
|
||||
self._elifs.extend([tokens[index][2], tokens[index+1][2]])
|
||||
elif six.PY3 and is_trailing_comma(tokens, index):
|
||||
if self.linter.is_message_enabled('trailing-comma-tuple'):
|
||||
self.add_message('trailing-comma-tuple',
|
||||
line=token.start[0])
|
||||
|
||||
def leave_module(self, _):
|
||||
self._init()
|
||||
|
||||
@utils.check_messages('too-many-nested-blocks')
|
||||
def visit_tryexcept(self, node):
|
||||
self._check_nested_blocks(node)
|
||||
|
||||
visit_tryfinally = visit_tryexcept
|
||||
visit_while = visit_tryexcept
|
||||
|
||||
def _check_redefined_argument_from_local(self, name_node):
|
||||
if self._dummy_rgx and self._dummy_rgx.match(name_node.name):
|
||||
return
|
||||
if not name_node.lineno:
|
||||
# Unknown position, maybe it is a manually built AST?
|
||||
return
|
||||
|
||||
scope = name_node.scope()
|
||||
if not isinstance(scope, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
for defined_argument in scope.args.nodes_of_class(astroid.AssignName):
|
||||
if defined_argument.name == name_node.name:
|
||||
self.add_message('redefined-argument-from-local',
|
||||
node=name_node,
|
||||
args=(name_node.name, ))
|
||||
|
||||
@utils.check_messages('redefined-argument-from-local',
|
||||
'too-many-nested-blocks')
|
||||
def visit_for(self, node):
|
||||
self._check_nested_blocks(node)
|
||||
|
||||
for name in node.target.nodes_of_class(astroid.AssignName):
|
||||
self._check_redefined_argument_from_local(name)
|
||||
|
||||
@utils.check_messages('redefined-argument-from-local')
|
||||
def visit_excepthandler(self, node):
|
||||
if node.name and isinstance(node.name, astroid.AssignName):
|
||||
self._check_redefined_argument_from_local(node.name)
|
||||
|
||||
@utils.check_messages('redefined-argument-from-local')
|
||||
def visit_with(self, node):
|
||||
for _, names in node.items:
|
||||
if not names:
|
||||
continue
|
||||
for name in names.nodes_of_class(astroid.AssignName):
|
||||
self._check_redefined_argument_from_local(name)
|
||||
|
||||
def _check_superfluous_else_return(self, node):
|
||||
if not node.orelse:
|
||||
# Not interested in if statements without else.
|
||||
return
|
||||
|
||||
if _if_statement_is_always_returning(node) and not self._is_actual_elif(node):
|
||||
self.add_message('no-else-return', node=node)
|
||||
|
||||
@utils.check_messages('too-many-nested-blocks', 'simplifiable-if-statement',
|
||||
'no-else-return',)
|
||||
def visit_if(self, node):
|
||||
self._check_simplifiable_if(node)
|
||||
self._check_nested_blocks(node)
|
||||
self._check_superfluous_else_return(node)
|
||||
|
||||
@utils.check_messages('too-many-nested-blocks', 'inconsistent-return-statements')
|
||||
def leave_functiondef(self, node):
|
||||
# check left-over nested blocks stack
|
||||
self._emit_nested_blocks_message_if_needed(self._nested_blocks)
|
||||
# new scope = reinitialize the stack of nested blocks
|
||||
self._nested_blocks = []
|
||||
# check consistent return statements
|
||||
self._check_consistent_returns(node)
|
||||
self._return_nodes[node.name] = []
|
||||
|
||||
@utils.check_messages('stop-iteration-return')
|
||||
def visit_raise(self, node):
|
||||
self._check_stop_iteration_inside_generator(node)
|
||||
|
||||
def _check_stop_iteration_inside_generator(self, node):
|
||||
"""Check if an exception of type StopIteration is raised inside a generator"""
|
||||
frame = node.frame()
|
||||
if not isinstance(frame, astroid.FunctionDef) or not frame.is_generator():
|
||||
return
|
||||
if utils.node_ignores_exception(node, StopIteration):
|
||||
return
|
||||
if not node.exc:
|
||||
return
|
||||
exc = utils.safe_infer(node.exc)
|
||||
if exc is None or exc is astroid.Uninferable:
|
||||
return
|
||||
if self._check_exception_inherit_from_stopiteration(exc):
|
||||
self.add_message('stop-iteration-return', node=node)
|
||||
|
||||
@staticmethod
|
||||
def _check_exception_inherit_from_stopiteration(exc):
|
||||
"""Return True if the exception node in argument inherit from StopIteration"""
|
||||
stopiteration_qname = '{}.StopIteration'.format(utils.EXCEPTIONS_MODULE)
|
||||
return any(_class.qname() == stopiteration_qname for _class in exc.mro())
|
||||
|
||||
@utils.check_messages('stop-iteration-return')
|
||||
def visit_call(self, node):
|
||||
self._check_raising_stopiteration_in_generator_next_call(node)
|
||||
|
||||
def _check_raising_stopiteration_in_generator_next_call(self, node):
|
||||
"""Check if a StopIteration exception is raised by the call to next function"""
|
||||
inferred = utils.safe_infer(node.func)
|
||||
if getattr(inferred, 'name', '') == 'next':
|
||||
frame = node.frame()
|
||||
if (isinstance(frame, astroid.FunctionDef) and frame.is_generator()
|
||||
and not utils.node_ignores_exception(node, StopIteration)):
|
||||
self.add_message('stop-iteration-return', node=node)
|
||||
|
||||
def _check_nested_blocks(self, node):
|
||||
"""Update and check the number of nested blocks
|
||||
"""
|
||||
# only check block levels inside functions or methods
|
||||
if not isinstance(node.scope(), astroid.FunctionDef):
|
||||
return
|
||||
# messages are triggered on leaving the nested block. Here we save the
|
||||
# stack in case the current node isn't nested in the previous one
|
||||
nested_blocks = self._nested_blocks[:]
|
||||
if node.parent == node.scope():
|
||||
self._nested_blocks = [node]
|
||||
else:
|
||||
# go through ancestors from the most nested to the less
|
||||
for ancestor_node in reversed(self._nested_blocks):
|
||||
if ancestor_node == node.parent:
|
||||
break
|
||||
self._nested_blocks.pop()
|
||||
# if the node is a elif, this should not be another nesting level
|
||||
if isinstance(node, astroid.If) and self._is_actual_elif(node):
|
||||
if self._nested_blocks:
|
||||
self._nested_blocks.pop()
|
||||
self._nested_blocks.append(node)
|
||||
|
||||
# send message only once per group of nested blocks
|
||||
if len(nested_blocks) > len(self._nested_blocks):
|
||||
self._emit_nested_blocks_message_if_needed(nested_blocks)
|
||||
|
||||
def _emit_nested_blocks_message_if_needed(self, nested_blocks):
|
||||
if len(nested_blocks) > self.config.max_nested_blocks:
|
||||
self.add_message('too-many-nested-blocks', node=nested_blocks[0],
|
||||
args=(len(nested_blocks), self.config.max_nested_blocks))
|
||||
|
||||
@staticmethod
|
||||
def _duplicated_isinstance_types(node):
|
||||
"""Get the duplicated types from the underlying isinstance calls.
|
||||
|
||||
:param astroid.BoolOp node: Node which should contain a bunch of isinstance calls.
|
||||
:returns: Dictionary of the comparison objects from the isinstance calls,
|
||||
to duplicate values from consecutive calls.
|
||||
:rtype: dict
|
||||
"""
|
||||
duplicated_objects = set()
|
||||
all_types = collections.defaultdict(set)
|
||||
|
||||
for call in node.values:
|
||||
if not isinstance(call, astroid.Call) or len(call.args) != 2:
|
||||
continue
|
||||
|
||||
inferred = utils.safe_infer(call.func)
|
||||
if not inferred or not utils.is_builtin_object(inferred):
|
||||
continue
|
||||
|
||||
if inferred.name != 'isinstance':
|
||||
continue
|
||||
|
||||
isinstance_object = call.args[0].as_string()
|
||||
isinstance_types = call.args[1]
|
||||
|
||||
if isinstance_object in all_types:
|
||||
duplicated_objects.add(isinstance_object)
|
||||
|
||||
if isinstance(isinstance_types, astroid.Tuple):
|
||||
elems = [class_type.as_string() for class_type in isinstance_types.itered()]
|
||||
else:
|
||||
elems = [isinstance_types.as_string()]
|
||||
all_types[isinstance_object].update(elems)
|
||||
|
||||
# Remove all keys which not duplicated
|
||||
return {key: value for key, value in all_types.items()
|
||||
if key in duplicated_objects}
|
||||
|
||||
@utils.check_messages('consider-merging-isinstance')
|
||||
def visit_boolop(self, node):
|
||||
'''Check isinstance calls which can be merged together.'''
|
||||
if node.op != 'or':
|
||||
return
|
||||
|
||||
first_args = self._duplicated_isinstance_types(node)
|
||||
for duplicated_name, class_names in first_args.items():
|
||||
names = sorted(name for name in class_names)
|
||||
self.add_message('consider-merging-isinstance',
|
||||
node=node,
|
||||
args=(duplicated_name, ', '.join(names)))
|
||||
|
||||
@utils.check_messages('simplify-boolean-expression', 'consider-using-ternary')
|
||||
def visit_assign(self, node):
|
||||
if self._is_and_or_ternary(node.value):
|
||||
cond, truth_value, false_value = self._and_or_ternary_arguments(node.value)
|
||||
elif self._is_seq_based_ternary(node.value):
|
||||
cond, truth_value, false_value = self._seq_based_ternary_params(node.value)
|
||||
else:
|
||||
return
|
||||
|
||||
if truth_value.bool_value() is False:
|
||||
message = 'simplify-boolean-expression'
|
||||
suggestion = false_value.as_string()
|
||||
else:
|
||||
message = 'consider-using-ternary'
|
||||
suggestion = '{truth} if {cond} else {false}'.format(
|
||||
truth=truth_value.as_string(),
|
||||
cond=cond.as_string(),
|
||||
false=false_value.as_string()
|
||||
)
|
||||
self.add_message(message, node=node, args=(suggestion,))
|
||||
|
||||
visit_return = visit_assign
|
||||
|
||||
@staticmethod
|
||||
def _is_and_or_ternary(node):
|
||||
"""
|
||||
Returns true if node is 'condition and true_value else false_value' form.
|
||||
|
||||
All of: condition, true_value and false_value should not be a complex boolean expression
|
||||
"""
|
||||
return (isinstance(node, astroid.BoolOp)
|
||||
and node.op == 'or' and len(node.values) == 2
|
||||
and isinstance(node.values[0], astroid.BoolOp)
|
||||
and not isinstance(node.values[1], astroid.BoolOp)
|
||||
and node.values[0].op == 'and'
|
||||
and not isinstance(node.values[0].values[1], astroid.BoolOp)
|
||||
and len(node.values[0].values) == 2)
|
||||
|
||||
@staticmethod
|
||||
def _and_or_ternary_arguments(node):
|
||||
false_value = node.values[1]
|
||||
condition, true_value = node.values[0].values
|
||||
return condition, true_value, false_value
|
||||
|
||||
@staticmethod
|
||||
def _is_seq_based_ternary(node):
|
||||
"""Returns true if node is '[false_value,true_value][condition]' form"""
|
||||
return (isinstance(node, astroid.Subscript)
|
||||
and isinstance(node.value, (astroid.Tuple, astroid.List))
|
||||
and len(node.value.elts) == 2 and isinstance(node.slice, astroid.Index))
|
||||
|
||||
@staticmethod
|
||||
def _seq_based_ternary_params(node):
|
||||
false_value, true_value = node.value.elts
|
||||
condition = node.slice.value
|
||||
return condition, true_value, false_value
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
self._return_nodes[node.name] = []
|
||||
return_nodes = node.nodes_of_class(astroid.Return)
|
||||
self._return_nodes[node.name] = [_rnode for _rnode in return_nodes
|
||||
if _rnode.frame() == node.frame()]
|
||||
|
||||
def _check_consistent_returns(self, node):
|
||||
"""Check that all return statements inside a function are consistent.
|
||||
|
||||
Return statements are consistent if:
|
||||
- all returns are explicit and if there is no implicit return;
|
||||
- all returns are empty and if there is, possibly, an implicit return.
|
||||
|
||||
Args:
|
||||
node (astroid.FunctionDef): the function holding the return statements.
|
||||
|
||||
"""
|
||||
# explicit return statements are those with a not None value
|
||||
explicit_returns = [_node for _node in self._return_nodes[node.name]
|
||||
if _node.value is not None]
|
||||
if not explicit_returns:
|
||||
return
|
||||
if (len(explicit_returns) == len(self._return_nodes[node.name])
|
||||
and self._is_node_return_ended(node)):
|
||||
return
|
||||
self.add_message('inconsistent-return-statements', node=node)
|
||||
|
||||
def _is_node_return_ended(self, node):
|
||||
"""Check if the node ends with an explicit return statement.
|
||||
|
||||
Args:
|
||||
node (astroid.NodeNG): node to be checked.
|
||||
|
||||
Returns:
|
||||
bool: True if the node ends with an explicit statement, False otherwise.
|
||||
|
||||
"""
|
||||
# Recursion base case
|
||||
if isinstance(node, astroid.Return):
|
||||
return True
|
||||
if isinstance(node, astroid.Call):
|
||||
try:
|
||||
funcdef_node = node.func.infered()[0]
|
||||
if self._is_function_def_never_returning(funcdef_node):
|
||||
return True
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
# Avoid the check inside while loop as we don't know
|
||||
# if they will be completed
|
||||
if isinstance(node, astroid.While):
|
||||
return True
|
||||
if isinstance(node, astroid.Raise):
|
||||
# a Raise statement doesn't need to end with a return statement
|
||||
# but if the exception raised is handled, then the handler has to
|
||||
# ends with a return statement
|
||||
if not node.exc:
|
||||
# Ignore bare raises
|
||||
return True
|
||||
if not utils.is_node_inside_try_except(node):
|
||||
# If the raise statement is not inside a try/except statement
|
||||
# then the exception is raised and cannot be caught. No need
|
||||
# to infer it.
|
||||
return True
|
||||
exc = utils.safe_infer(node.exc)
|
||||
if exc is None or exc is astroid.Uninferable:
|
||||
return False
|
||||
exc_name = exc.pytype().split('.')[-1]
|
||||
handlers = utils.get_exception_handlers(node, exc_name)
|
||||
handlers = list(handlers) if handlers is not None else []
|
||||
if handlers:
|
||||
# among all the handlers handling the exception at least one
|
||||
# must end with a return statement
|
||||
return any(self._is_node_return_ended(_handler) for _handler in handlers)
|
||||
# if no handlers handle the exception then it's ok
|
||||
return True
|
||||
if isinstance(node, astroid.If):
|
||||
# if statement is returning if there are exactly two return statements in its
|
||||
# children : one for the body part, the other for the orelse part
|
||||
# Do not check if inner function definition are return ended.
|
||||
return_stmts = [self._is_node_return_ended(_child) for _child in node.get_children()
|
||||
if not isinstance(_child, astroid.FunctionDef)]
|
||||
return sum(return_stmts) == 2
|
||||
# recurses on the children of the node except for those which are except handler
|
||||
# because one cannot be sure that the handler will really be used
|
||||
return any(self._is_node_return_ended(_child) for _child in node.get_children()
|
||||
if not isinstance(_child, astroid.ExceptHandler))
|
||||
|
||||
def _is_function_def_never_returning(self, node):
|
||||
"""Return True if the function never returns. False otherwise.
|
||||
|
||||
Args:
|
||||
node (astroid.FunctionDef): function definition node to be analyzed.
|
||||
|
||||
Returns:
|
||||
bool: True if the function never returns, False otherwise.
|
||||
"""
|
||||
try:
|
||||
return node.qname() in self._never_returning_functions
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
class RecommandationChecker(checkers.BaseChecker):
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
name = 'refactoring'
|
||||
msgs = {'C0200': ('Consider using enumerate instead of iterating with range and len',
|
||||
'consider-using-enumerate',
|
||||
'Emitted when code that iterates with range and len is '
|
||||
'encountered. Such code can be simplified by using the '
|
||||
'enumerate builtin.'),
|
||||
'C0201': ('Consider iterating the dictionary directly instead of calling .keys()',
|
||||
'consider-iterating-dictionary',
|
||||
'Emitted when the keys of a dictionary are iterated through the .keys() '
|
||||
'method. It is enough to just iterate through the dictionary itself, as '
|
||||
'in "for key in dictionary".'),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_builtin(node, function):
|
||||
inferred = utils.safe_infer(node)
|
||||
if not inferred:
|
||||
return False
|
||||
return utils.is_builtin_object(inferred) and inferred.name == function
|
||||
|
||||
@utils.check_messages('consider-iterating-dictionary')
|
||||
def visit_call(self, node):
|
||||
inferred = utils.safe_infer(node.func)
|
||||
if not inferred:
|
||||
return
|
||||
if not isinstance(inferred, astroid.BoundMethod):
|
||||
return
|
||||
if not isinstance(inferred.bound, astroid.Dict) or inferred.name != 'keys':
|
||||
return
|
||||
|
||||
if isinstance(node.parent, (astroid.For, astroid.Comprehension)):
|
||||
self.add_message('consider-iterating-dictionary', node=node)
|
||||
|
||||
@utils.check_messages('consider-using-enumerate')
|
||||
def visit_for(self, node):
|
||||
"""Emit a convention whenever range and len are used for indexing."""
|
||||
# Verify that we have a `range([start], len(...), [stop])` call and
|
||||
# that the object which is iterated is used as a subscript in the
|
||||
# body of the for.
|
||||
|
||||
# Is it a proper range call?
|
||||
if not isinstance(node.iter, astroid.Call):
|
||||
return
|
||||
if not self._is_builtin(node.iter.func, 'range'):
|
||||
return
|
||||
if len(node.iter.args) == 2 and not _is_constant_zero(node.iter.args[0]):
|
||||
return
|
||||
if len(node.iter.args) > 2:
|
||||
return
|
||||
|
||||
# Is it a proper len call?
|
||||
if not isinstance(node.iter.args[-1], astroid.Call):
|
||||
return
|
||||
second_func = node.iter.args[-1].func
|
||||
if not self._is_builtin(second_func, 'len'):
|
||||
return
|
||||
len_args = node.iter.args[-1].args
|
||||
if not len_args or len(len_args) != 1:
|
||||
return
|
||||
iterating_object = len_args[0]
|
||||
if not isinstance(iterating_object, astroid.Name):
|
||||
return
|
||||
|
||||
# Verify that the body of the for loop uses a subscript
|
||||
# with the object that was iterated. This uses some heuristics
|
||||
# in order to make sure that the same object is used in the
|
||||
# for body.
|
||||
for child in node.body:
|
||||
for subscript in child.nodes_of_class(astroid.Subscript):
|
||||
if not isinstance(subscript.value, astroid.Name):
|
||||
continue
|
||||
if not isinstance(subscript.slice, astroid.Index):
|
||||
continue
|
||||
if not isinstance(subscript.slice.value, astroid.Name):
|
||||
continue
|
||||
if subscript.slice.value.name != node.target.name:
|
||||
continue
|
||||
if iterating_object.name != subscript.value.name:
|
||||
continue
|
||||
if subscript.value.scope() != node.scope():
|
||||
# Ignore this subscript if it's not in the same
|
||||
# scope. This means that in the body of the for
|
||||
# loop, another scope was created, where the same
|
||||
# name for the iterating object was used.
|
||||
continue
|
||||
self.add_message('consider-using-enumerate', node=node)
|
||||
return
|
||||
|
||||
|
||||
class NotChecker(checkers.BaseChecker):
|
||||
"""checks for too many not in comparison expressions
|
||||
|
||||
- "not not" should trigger a warning
|
||||
- "not" followed by a comparison should trigger a warning
|
||||
"""
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
msgs = {'C0113': ('Consider changing "%s" to "%s"',
|
||||
'unneeded-not',
|
||||
'Used when a boolean expression contains an unneeded '
|
||||
'negation.'),
|
||||
}
|
||||
name = 'basic'
|
||||
reverse_op = {'<': '>=', '<=': '>', '>': '<=', '>=': '<', '==': '!=',
|
||||
'!=': '==', 'in': 'not in', 'is': 'is not'}
|
||||
# sets are not ordered, so for example "not set(LEFT_VALS) <= set(RIGHT_VALS)" is
|
||||
# not equivalent to "set(LEFT_VALS) > set(RIGHT_VALS)"
|
||||
skipped_nodes = (astroid.Set,)
|
||||
# 'builtins' py3, '__builtin__' py2
|
||||
skipped_classnames = ['%s.%s' % (six.moves.builtins.__name__, qname)
|
||||
for qname in ('set', 'frozenset')]
|
||||
|
||||
@utils.check_messages('unneeded-not')
|
||||
def visit_unaryop(self, node):
|
||||
if node.op != 'not':
|
||||
return
|
||||
operand = node.operand
|
||||
|
||||
if isinstance(operand, astroid.UnaryOp) and operand.op == 'not':
|
||||
self.add_message('unneeded-not', node=node,
|
||||
args=(node.as_string(),
|
||||
operand.operand.as_string()))
|
||||
elif isinstance(operand, astroid.Compare):
|
||||
left = operand.left
|
||||
# ignore multiple comparisons
|
||||
if len(operand.ops) > 1:
|
||||
return
|
||||
operator, right = operand.ops[0]
|
||||
if operator not in self.reverse_op:
|
||||
return
|
||||
# Ignore __ne__ as function of __eq__
|
||||
frame = node.frame()
|
||||
if frame.name == '__ne__' and operator == '==':
|
||||
return
|
||||
for _type in (utils.node_type(left), utils.node_type(right)):
|
||||
if not _type:
|
||||
return
|
||||
if isinstance(_type, self.skipped_nodes):
|
||||
return
|
||||
if (isinstance(_type, astroid.Instance) and
|
||||
_type.qname() in self.skipped_classnames):
|
||||
return
|
||||
suggestion = '%s %s %s' % (left.as_string(),
|
||||
self.reverse_op[operator],
|
||||
right.as_string())
|
||||
self.add_message('unneeded-not', node=node,
|
||||
args=(node.as_string(), suggestion))
|
||||
|
||||
|
||||
def _is_len_call(node):
|
||||
"""Checks if node is len(SOMETHING)."""
|
||||
return (isinstance(node, astroid.Call) and isinstance(node.func, astroid.Name) and
|
||||
node.func.name == 'len')
|
||||
|
||||
def _is_constant_zero(node):
|
||||
return isinstance(node, astroid.Const) and node.value == 0
|
||||
|
||||
def _node_is_test_condition(node):
|
||||
""" Checks if node is an if, while, assert or if expression statement."""
|
||||
return isinstance(node, (astroid.If, astroid.While, astroid.Assert, astroid.IfExp))
|
||||
|
||||
|
||||
class LenChecker(checkers.BaseChecker):
|
||||
"""Checks for incorrect usage of len() inside conditions.
|
||||
Pep8 states:
|
||||
For sequences, (strings, lists, tuples), use the fact that empty sequences are false.
|
||||
|
||||
Yes: if not seq:
|
||||
if seq:
|
||||
|
||||
No: if len(seq):
|
||||
if not len(seq):
|
||||
|
||||
Problems detected:
|
||||
* if len(sequence):
|
||||
* if not len(sequence):
|
||||
* if len(sequence) == 0:
|
||||
* if len(sequence) != 0:
|
||||
* if len(sequence) > 0:
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'len'
|
||||
msgs = {'C1801': ('Do not use `len(SEQUENCE)` to determine if a sequence is empty',
|
||||
'len-as-condition',
|
||||
'Used when Pylint detects that len(sequence) is being used inside '
|
||||
'a condition to determine if a sequence is empty. Instead of '
|
||||
'comparing the length to 0, rely on the fact that empty sequences '
|
||||
'are false.'),
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages('len-as-condition')
|
||||
def visit_call(self, node):
|
||||
# a len(S) call is used inside a test condition
|
||||
# could be if, while, assert or if expression statement
|
||||
# e.g. `if len(S):`
|
||||
if _is_len_call(node):
|
||||
# the len() call could also be nested together with other
|
||||
# boolean operations, e.g. `if z or len(x):`
|
||||
parent = node.parent
|
||||
while isinstance(parent, astroid.BoolOp):
|
||||
parent = parent.parent
|
||||
|
||||
# we're finally out of any nested boolean operations so check if
|
||||
# this len() call is part of a test condition
|
||||
if not _node_is_test_condition(parent):
|
||||
return
|
||||
if not (node is parent.test or parent.test.parent_of(node)):
|
||||
return
|
||||
self.add_message('len-as-condition', node=node)
|
||||
|
||||
@utils.check_messages('len-as-condition')
|
||||
def visit_unaryop(self, node):
|
||||
"""`not len(S)` must become `not S` regardless if the parent block
|
||||
is a test condition or something else (boolean expression)
|
||||
e.g. `if not len(S):`"""
|
||||
if isinstance(node, astroid.UnaryOp) and node.op == 'not' and _is_len_call(node.operand):
|
||||
self.add_message('len-as-condition', node=node)
|
||||
|
||||
@utils.check_messages('len-as-condition')
|
||||
def visit_compare(self, node):
|
||||
# compare nodes are trickier because the len(S) expression
|
||||
# may be somewhere in the middle of the node
|
||||
|
||||
# note: astroid.Compare has the left most operand in node.left
|
||||
# while the rest are a list of tuples in node.ops
|
||||
# the format of the tuple is ('compare operator sign', node)
|
||||
# here we squash everything into `ops` to make it easier for processing later
|
||||
ops = [('', node.left)]
|
||||
ops.extend(node.ops)
|
||||
ops = list(itertools.chain(*ops))
|
||||
|
||||
for ops_idx in range(len(ops) - 2):
|
||||
op_1 = ops[ops_idx]
|
||||
op_2 = ops[ops_idx + 1]
|
||||
op_3 = ops[ops_idx + 2]
|
||||
error_detected = False
|
||||
|
||||
# 0 ?? len()
|
||||
if _is_constant_zero(op_1) and op_2 in ['==', '!=', '<'] and _is_len_call(op_3):
|
||||
error_detected = True
|
||||
# len() ?? 0
|
||||
elif _is_len_call(op_1) and op_2 in ['==', '!=', '>'] and _is_constant_zero(op_3):
|
||||
error_detected = True
|
||||
|
||||
if error_detected:
|
||||
parent = node.parent
|
||||
# traverse the AST to figure out if this comparison was part of
|
||||
# a test condition
|
||||
while parent and not _node_is_test_condition(parent):
|
||||
parent = parent.parent
|
||||
|
||||
# report only if this len() comparison is part of a test condition
|
||||
# for example: return len() > 0 should not report anything
|
||||
if _node_is_test_condition(parent):
|
||||
self.add_message('len-as-condition', node=node)
|
||||
|
||||
|
||||
def is_trailing_comma(tokens, index):
|
||||
"""Check if the given token is a trailing comma
|
||||
|
||||
:param tokens: Sequence of modules tokens
|
||||
:type tokens: list[tokenize.TokenInfo]
|
||||
:param int index: Index of token under check in tokens
|
||||
:returns: True if the token is a comma which trails an expression
|
||||
:rtype: bool
|
||||
"""
|
||||
token = tokens[index]
|
||||
if token.exact_type != tokenize.COMMA:
|
||||
return False
|
||||
# Must have remaining tokens on the same line such as NEWLINE
|
||||
left_tokens = itertools.islice(tokens, index + 1, None)
|
||||
same_line_remaining_tokens = list(itertools.takewhile(
|
||||
lambda other_token, _token=token: other_token.start[0] == _token.start[0],
|
||||
left_tokens
|
||||
))
|
||||
# Note: If the newline is tokenize.NEWLINE and not tokenize.NL
|
||||
# then the newline denotes the end of expression
|
||||
is_last_element = all(
|
||||
other_token.type in (tokenize.NEWLINE, tokenize.COMMENT)
|
||||
for other_token in same_line_remaining_tokens
|
||||
)
|
||||
if not same_line_remaining_tokens or not is_last_element:
|
||||
return False
|
||||
def get_curline_index_start():
|
||||
"""Get the index denoting the start of the current line"""
|
||||
for subindex, token in enumerate(reversed(tokens[:index])):
|
||||
# See Lib/tokenize.py and Lib/token.py in cpython for more info
|
||||
if token.type in (tokenize.NEWLINE, tokenize.NL):
|
||||
return index - subindex
|
||||
return 0
|
||||
curline_start = get_curline_index_start()
|
||||
for prevtoken in tokens[curline_start:index]:
|
||||
if '=' in prevtoken.string:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(RefactoringChecker(linter))
|
||||
linter.register_checker(NotChecker(linter))
|
||||
linter.register_checker(RecommandationChecker(linter))
|
||||
linter.register_checker(LenChecker(linter))
|
||||
@@ -0,0 +1,372 @@
|
||||
# Copyright (c) 2006, 2008-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012 Ry4an Brase <ry4an-hg@ry4an.org>
|
||||
# Copyright (c) 2012 Google, Inc.
|
||||
# Copyright (c) 2012 Anthony VEREZ <anthony.verez.external@cassidian.com>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 Anthony Sottile <asottile@umich.edu>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
# pylint: disable=W0622
|
||||
"""a similarities / code duplication command line tool and pylint checker
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import six
|
||||
from six.moves import zip
|
||||
|
||||
from pylint.utils import decoding_stream
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.checkers import BaseChecker, table_lines_from_stats
|
||||
from pylint.reporters.ureports.nodes import Table
|
||||
|
||||
|
||||
class Similar(object):
|
||||
"""finds copy-pasted lines of code in a project"""
|
||||
|
||||
def __init__(self, min_lines=4, ignore_comments=False,
|
||||
ignore_docstrings=False, ignore_imports=False):
|
||||
self.min_lines = min_lines
|
||||
self.ignore_comments = ignore_comments
|
||||
self.ignore_docstrings = ignore_docstrings
|
||||
self.ignore_imports = ignore_imports
|
||||
self.linesets = []
|
||||
|
||||
def append_stream(self, streamid, stream, encoding=None):
|
||||
"""append a file to search for similarities"""
|
||||
if encoding is None:
|
||||
readlines = stream.readlines
|
||||
else:
|
||||
readlines = decoding_stream(stream, encoding).readlines
|
||||
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"""
|
||||
self._display_sims(self._compute_sims())
|
||||
|
||||
def _compute_sims(self):
|
||||
"""compute similarities in appended files"""
|
||||
no_duplicates = defaultdict(list)
|
||||
for num, lineset1, idx1, lineset2, idx2 in self._iter_sims():
|
||||
duplicate = no_duplicates[num]
|
||||
for couples in duplicate:
|
||||
if (lineset1, idx1) in couples or (lineset2, idx2) in couples:
|
||||
couples.add((lineset1, idx1))
|
||||
couples.add((lineset2, idx2))
|
||||
break
|
||||
else:
|
||||
duplicate.append(set([(lineset1, idx1), (lineset2, idx2)]))
|
||||
sims = []
|
||||
for num, ensembles in six.iteritems(no_duplicates):
|
||||
for couples in ensembles:
|
||||
sims.append((num, couples))
|
||||
sims.sort()
|
||||
sims.reverse()
|
||||
return sims
|
||||
|
||||
def _display_sims(self, sims):
|
||||
"""display computed similarities on stdout"""
|
||||
nb_lignes_dupliquees = 0
|
||||
for num, couples in sims:
|
||||
print()
|
||||
print(num, "similar lines in", len(couples), "files")
|
||||
couples = sorted(couples)
|
||||
for lineset, idx in couples:
|
||||
print("==%s:%s" % (lineset.name, idx))
|
||||
# pylint: disable=W0631
|
||||
for line in lineset._real_lines[idx:idx+num]:
|
||||
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" \
|
||||
% (nb_total_lignes, nb_lignes_dupliquees,
|
||||
nb_lignes_dupliquees*100. / nb_total_lignes))
|
||||
|
||||
def _find_common(self, lineset1, lineset2):
|
||||
"""find similarities in the two given linesets"""
|
||||
lines1 = lineset1.enumerate_stripped
|
||||
lines2 = lineset2.enumerate_stripped
|
||||
find = lineset2.find
|
||||
index1 = 0
|
||||
min_lines = self.min_lines
|
||||
while index1 < len(lineset1):
|
||||
skip = 1
|
||||
num = 0
|
||||
for index2 in find(lineset1[index1]):
|
||||
non_blank = 0
|
||||
for num, ((_, line1), (_, line2)) in enumerate(
|
||||
zip(lines1(index1), lines2(index2))):
|
||||
if line1 != line2:
|
||||
if non_blank > min_lines:
|
||||
yield num, lineset1, index1, lineset2, index2
|
||||
skip = max(skip, num)
|
||||
break
|
||||
if line1:
|
||||
non_blank += 1
|
||||
else:
|
||||
# we may have reach the end
|
||||
num += 1
|
||||
if non_blank > min_lines:
|
||||
yield num, lineset1, index1, lineset2, index2
|
||||
skip = max(skip, num)
|
||||
index1 += skip
|
||||
|
||||
def _iter_sims(self):
|
||||
"""iterate on similarities among all files, by making a cartesian
|
||||
product
|
||||
"""
|
||||
for idx, lineset in enumerate(self.linesets[:-1]):
|
||||
for lineset2 in self.linesets[idx+1:]:
|
||||
for sim in self._find_common(lineset, lineset2):
|
||||
yield sim
|
||||
|
||||
def stripped_lines(lines, ignore_comments, ignore_docstrings, ignore_imports):
|
||||
"""return lines with leading/trailing whitespace and any ignored code
|
||||
features removed
|
||||
"""
|
||||
|
||||
strippedlines = []
|
||||
docstring = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if ignore_docstrings:
|
||||
if not docstring and \
|
||||
(line.startswith('"""') or line.startswith("'''")):
|
||||
docstring = line[:3]
|
||||
line = line[3:]
|
||||
if docstring:
|
||||
if line.endswith(docstring):
|
||||
docstring = None
|
||||
line = ''
|
||||
if ignore_imports:
|
||||
if line.startswith("import ") or line.startswith("from "):
|
||||
line = ''
|
||||
if ignore_comments:
|
||||
# XXX should use regex in checkers/format to avoid cutting
|
||||
# at a "#" in a string
|
||||
line = line.split('#', 1)[0].strip()
|
||||
strippedlines.append(line)
|
||||
return strippedlines
|
||||
|
||||
|
||||
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):
|
||||
self.name = name
|
||||
self._real_lines = lines
|
||||
self._stripped_lines = stripped_lines(lines, ignore_comments,
|
||||
ignore_docstrings,
|
||||
ignore_imports)
|
||||
self._index = self._mk_index()
|
||||
|
||||
def __str__(self):
|
||||
return '<Lineset for %s>' % self.name
|
||||
|
||||
def __len__(self):
|
||||
return len(self._real_lines)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._stripped_lines[index]
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.name < other.name
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def enumerate_stripped(self, start_at=0):
|
||||
"""return an iterator on stripped lines, starting from a given index
|
||||
if specified, else 0
|
||||
"""
|
||||
idx = start_at
|
||||
if start_at:
|
||||
lines = self._stripped_lines[start_at:]
|
||||
else:
|
||||
lines = self._stripped_lines
|
||||
for line in lines:
|
||||
#if line:
|
||||
yield idx, line
|
||||
idx += 1
|
||||
|
||||
def find(self, stripped_line):
|
||||
"""return positions of the given stripped line in this set"""
|
||||
return self._index.get(stripped_line, ())
|
||||
|
||||
def _mk_index(self):
|
||||
"""create the index for this set"""
|
||||
index = defaultdict(list)
|
||||
for line_no, line in enumerate(self._stripped_lines):
|
||||
if line:
|
||||
index[line].append(line_no)
|
||||
return index
|
||||
|
||||
|
||||
MSGS = {'R0801': ('Similar lines in %s files\n%s',
|
||||
'duplicate-code',
|
||||
'Indicates that a set of similar lines has been detected \
|
||||
among multiple file. This usually means that the code should \
|
||||
be refactored to avoid this duplication.')}
|
||||
|
||||
def report_similarities(sect, stats, old_stats):
|
||||
"""make a layout with some stats about duplication"""
|
||||
lines = ['', 'now', 'previous', 'difference']
|
||||
lines += table_lines_from_stats(stats, old_stats,
|
||||
('nb_duplicated_lines',
|
||||
'percent_duplicated_lines'))
|
||||
sect.append(Table(children=lines, cols=4, rheaders=1, cheaders=1))
|
||||
|
||||
|
||||
# wrapper to get a pylint checker from the similar class
|
||||
class SimilarChecker(BaseChecker, Similar):
|
||||
"""checks for similarities and duplicated code. This computation may be
|
||||
memory / CPU intensive, so you should disable it if you experiment some
|
||||
problems.
|
||||
"""
|
||||
|
||||
__implements__ = (IRawChecker,)
|
||||
# configuration section name
|
||||
name = 'similarities'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
# configuration options
|
||||
# for available dict keys/values see the optik parser 'add_option' method
|
||||
options = (('min-similarity-lines',
|
||||
{'default' : 4, 'type' : "int", 'metavar' : '<int>',
|
||||
'help' : 'Minimum lines number of a similarity.'}),
|
||||
('ignore-comments',
|
||||
{'default' : True, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Ignore comments when computing similarities.'}
|
||||
),
|
||||
('ignore-docstrings',
|
||||
{'default' : True, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Ignore docstrings when computing similarities.'}
|
||||
),
|
||||
('ignore-imports',
|
||||
{'default' : False, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Ignore imports when computing similarities.'}
|
||||
),
|
||||
)
|
||||
# reports
|
||||
reports = (('RP0801', 'Duplication', report_similarities),)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
Similar.__init__(self, min_lines=4,
|
||||
ignore_comments=True, ignore_docstrings=True)
|
||||
self.stats = None
|
||||
|
||||
def set_option(self, optname, value, action=None, optdict=None):
|
||||
"""method called to set an option (registered in the options list)
|
||||
|
||||
overridden to report options setting to Similar
|
||||
"""
|
||||
BaseChecker.set_option(self, optname, value, action, optdict)
|
||||
if optname == 'min-similarity-lines':
|
||||
self.min_lines = self.config.min_similarity_lines
|
||||
elif optname == 'ignore-comments':
|
||||
self.ignore_comments = self.config.ignore_comments
|
||||
elif optname == 'ignore-docstrings':
|
||||
self.ignore_docstrings = self.config.ignore_docstrings
|
||||
elif optname == 'ignore-imports':
|
||||
self.ignore_imports = self.config.ignore_imports
|
||||
|
||||
def open(self):
|
||||
"""init the checkers: reset linesets and statistics information"""
|
||||
self.linesets = []
|
||||
self.stats = self.linter.add_stats(nb_duplicated_lines=0,
|
||||
percent_duplicated_lines=0)
|
||||
|
||||
def process_module(self, node):
|
||||
"""process a module
|
||||
|
||||
the module's content is accessible via the stream object
|
||||
|
||||
stream must implement the readlines method
|
||||
"""
|
||||
with node.stream() as stream:
|
||||
self.append_stream(self.linter.current_name,
|
||||
stream,
|
||||
node.file_encoding)
|
||||
|
||||
def close(self):
|
||||
"""compute and display similarities on closing (i.e. end of parsing)"""
|
||||
total = sum(len(lineset) for lineset in self.linesets)
|
||||
duplicated = 0
|
||||
stats = self.stats
|
||||
for num, couples in self._compute_sims():
|
||||
msg = []
|
||||
for lineset, idx in couples:
|
||||
msg.append("==%s:%s" % (lineset.name, idx))
|
||||
msg.sort()
|
||||
# pylint: disable=W0631
|
||||
for line in lineset._real_lines[idx:idx+num]:
|
||||
msg.append(line.rstrip())
|
||||
self.add_message('R0801', args=(len(couples), '\n'.join(msg)))
|
||||
duplicated += num * (len(couples) - 1)
|
||||
stats['nb_duplicated_lines'] = duplicated
|
||||
stats['percent_duplicated_lines'] = total and duplicated * 100. / total
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(SimilarChecker(linter))
|
||||
|
||||
def usage(status=0):
|
||||
"""display command line usage information"""
|
||||
print("finds copy pasted blocks in a set of files")
|
||||
print()
|
||||
print('Usage: symilar [-d|--duplicates min_duplicated_lines] \
|
||||
[-i|--ignore-comments] [--ignore-docstrings] [--ignore-imports] file1...')
|
||||
sys.exit(status)
|
||||
|
||||
def Run(argv=None):
|
||||
"""standalone command line access point"""
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
from getopt import getopt
|
||||
s_opts = 'hdi'
|
||||
l_opts = ('help', 'duplicates=', 'ignore-comments', 'ignore-imports',
|
||||
'ignore-docstrings')
|
||||
min_lines = 4
|
||||
ignore_comments = False
|
||||
ignore_docstrings = False
|
||||
ignore_imports = False
|
||||
opts, args = getopt(argv, s_opts, l_opts)
|
||||
for opt, val in opts:
|
||||
if opt in ('-d', '--duplicates'):
|
||||
min_lines = int(val)
|
||||
elif opt in ('-h', '--help'):
|
||||
usage()
|
||||
elif opt in ('-i', '--ignore-comments'):
|
||||
ignore_comments = True
|
||||
elif opt in ('--ignore-docstrings',):
|
||||
ignore_docstrings = True
|
||||
elif opt in ('--ignore-imports',):
|
||||
ignore_imports = True
|
||||
if not args:
|
||||
usage(1)
|
||||
sim = Similar(min_lines, ignore_comments, ignore_docstrings, ignore_imports)
|
||||
for filename in args:
|
||||
with open(filename) as stream:
|
||||
sim.append_stream(filename, stream)
|
||||
sim.run()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
Run()
|
||||
@@ -0,0 +1,357 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2015 Pavel Roskin <proski@gnu.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016-2017 Pedro Algarvio <pedro@algarvio.me>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Mikhail Fesenko <proggga@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for spelling errors in comments and docstrings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tokenize
|
||||
import re
|
||||
|
||||
try:
|
||||
import enchant
|
||||
from enchant.tokenize import (get_tokenizer,
|
||||
Chunker,
|
||||
Filter,
|
||||
EmailFilter,
|
||||
URLFilter,
|
||||
WikiWordFilter)
|
||||
except ImportError:
|
||||
enchant = None
|
||||
# pylint: disable=old-style-class,no-init
|
||||
class Filter:
|
||||
def _skip(self, word):
|
||||
raise NotImplementedError
|
||||
|
||||
class Chunker:
|
||||
pass
|
||||
|
||||
|
||||
import six
|
||||
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.utils import safe_decode
|
||||
|
||||
if enchant is not None:
|
||||
br = enchant.Broker()
|
||||
dicts = br.list_dicts()
|
||||
dict_choices = [''] + [d[0] for d in dicts]
|
||||
dicts = ["%s (%s)" % (d[0], d[1].name) for d in dicts]
|
||||
dicts = ", ".join(dicts)
|
||||
instr = ""
|
||||
else:
|
||||
dicts = "none"
|
||||
dict_choices = ['']
|
||||
instr = " To make it working install python-enchant package."
|
||||
|
||||
|
||||
class WordsWithDigigtsFilter(Filter):
|
||||
"""Skips words with digits.
|
||||
"""
|
||||
|
||||
def _skip(self, word):
|
||||
for char in word:
|
||||
if char.isdigit():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class WordsWithUnderscores(Filter):
|
||||
"""Skips words with underscores.
|
||||
|
||||
They are probably function parameter names.
|
||||
"""
|
||||
def _skip(self, word):
|
||||
return '_' in word
|
||||
|
||||
|
||||
class CamelCasedWord(Filter):
|
||||
r"""Filter skipping over camelCasedWords.
|
||||
This filter skips any words matching the following regular expression:
|
||||
|
||||
^([a-z]\w+[A-Z]+\w+)
|
||||
|
||||
That is, any words that are camelCasedWords.
|
||||
"""
|
||||
_pattern = re.compile(r"^([a-z]+([\d]|[A-Z])(?:\w+)?)")
|
||||
|
||||
def _skip(self, word):
|
||||
return bool(self._pattern.match(word))
|
||||
|
||||
|
||||
class SphinxDirectives(Filter):
|
||||
r"""Filter skipping over Sphinx Directives.
|
||||
This filter skips any words matching the following regular expression:
|
||||
|
||||
^:([a-z]+):`([^`]+)(`)?
|
||||
|
||||
That is, for example, :class:`BaseQuery`
|
||||
"""
|
||||
# The final ` in the pattern is optional because enchant strips it out
|
||||
_pattern = re.compile(r"^:([a-z]+):`([^`]+)(`)?")
|
||||
|
||||
def _skip(self, word):
|
||||
return bool(self._pattern.match(word))
|
||||
|
||||
|
||||
class ForwardSlashChunkder(Chunker):
|
||||
'''
|
||||
This chunker allows splitting words like 'before/after' into 'before' and 'after'
|
||||
'''
|
||||
def next(self):
|
||||
while True:
|
||||
if not self._text:
|
||||
raise StopIteration()
|
||||
if '/' not in self._text:
|
||||
text = self._text
|
||||
self._offset = 0
|
||||
self._text = ''
|
||||
return (text, 0)
|
||||
pre_text, post_text = self._text.split('/', 1)
|
||||
self._text = post_text
|
||||
self._offset = 0
|
||||
if not pre_text or not post_text or \
|
||||
not pre_text[-1].isalpha() or not post_text[0].isalpha():
|
||||
self._text = ''
|
||||
self._offset = 0
|
||||
return (pre_text + '/' + post_text, 0)
|
||||
return (pre_text, 0)
|
||||
|
||||
def _next(self):
|
||||
while True:
|
||||
if '/' not in self._text:
|
||||
return (self._text, 0)
|
||||
pre_text, post_text = self._text.split('/', 1)
|
||||
if not pre_text or not post_text:
|
||||
break
|
||||
if not pre_text[-1].isalpha() or not post_text[0].isalpha():
|
||||
raise StopIteration()
|
||||
self._text = pre_text + ' ' + post_text
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
class SpellingChecker(BaseTokenChecker):
|
||||
"""Check spelling in comments and docstrings"""
|
||||
__implements__ = (ITokenChecker, IAstroidChecker)
|
||||
name = 'spelling'
|
||||
msgs = {
|
||||
'C0401': ('Wrong spelling of a word \'%s\' in a comment:\n%s\n'
|
||||
'%s\nDid you mean: \'%s\'?',
|
||||
'wrong-spelling-in-comment',
|
||||
'Used when a word in comment is not spelled correctly.'),
|
||||
'C0402': ('Wrong spelling of a word \'%s\' in a docstring:\n%s\n'
|
||||
'%s\nDid you mean: \'%s\'?',
|
||||
'wrong-spelling-in-docstring',
|
||||
'Used when a word in docstring is not spelled correctly.'),
|
||||
'C0403': ('Invalid characters %r in a docstring',
|
||||
'invalid-characters-in-docstring',
|
||||
'Used when a word in docstring cannot be checked by enchant.'),
|
||||
}
|
||||
options = (('spelling-dict',
|
||||
{'default' : '', 'type' : 'choice', 'metavar' : '<dict name>',
|
||||
'choices': dict_choices,
|
||||
'help' : 'Spelling dictionary name. '
|
||||
'Available dictionaries: %s.%s' % (dicts, instr)}),
|
||||
('spelling-ignore-words',
|
||||
{'default' : '',
|
||||
'type' : 'string',
|
||||
'metavar' : '<comma separated words>',
|
||||
'help' : 'List of comma separated words that '
|
||||
'should not be checked.'}),
|
||||
('spelling-private-dict-file',
|
||||
{'default' : '',
|
||||
'type' : 'string',
|
||||
'metavar' : '<path to file>',
|
||||
'help' : 'A path to a file that contains private '
|
||||
'dictionary; one word per line.'}),
|
||||
('spelling-store-unknown-words',
|
||||
{'default' : 'n', 'type' : 'yn', 'metavar' : '<y_or_n>',
|
||||
'help' : 'Tells whether to store unknown words to '
|
||||
'indicated private dictionary in '
|
||||
'--spelling-private-dict-file option instead of '
|
||||
'raising a message.'}),
|
||||
('max-spelling-suggestions',
|
||||
{'default': 4, 'type': 'int', 'metavar': 'N',
|
||||
'help': 'Limits count of emitted suggestions for '
|
||||
'spelling mistakes'}),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
self.initialized = False
|
||||
self.private_dict_file = None
|
||||
|
||||
if enchant is None:
|
||||
return
|
||||
dict_name = self.config.spelling_dict
|
||||
if not dict_name:
|
||||
return
|
||||
|
||||
self.ignore_list = [w.strip() for w in self.config.spelling_ignore_words.split(",")]
|
||||
# "param" appears in docstring in param description and
|
||||
# "pylint" appears in comments in pylint pragmas.
|
||||
self.ignore_list.extend(["param", "pylint"])
|
||||
|
||||
# Expand tilde to allow e.g. spelling-private-dict-file = ~/.pylintdict
|
||||
if self.config.spelling_private_dict_file:
|
||||
self.config.spelling_private_dict_file = os.path.expanduser(
|
||||
self.config.spelling_private_dict_file)
|
||||
|
||||
if self.config.spelling_private_dict_file:
|
||||
self.spelling_dict = enchant.DictWithPWL(
|
||||
dict_name, self.config.spelling_private_dict_file)
|
||||
self.private_dict_file = open(
|
||||
self.config.spelling_private_dict_file, "a")
|
||||
else:
|
||||
self.spelling_dict = enchant.Dict(dict_name)
|
||||
|
||||
if self.config.spelling_store_unknown_words:
|
||||
self.unknown_words = set()
|
||||
|
||||
self.tokenizer = get_tokenizer(dict_name,
|
||||
chunkers=[ForwardSlashChunkder],
|
||||
filters=[EmailFilter,
|
||||
URLFilter,
|
||||
WikiWordFilter,
|
||||
WordsWithDigigtsFilter,
|
||||
WordsWithUnderscores,
|
||||
CamelCasedWord,
|
||||
SphinxDirectives])
|
||||
self.initialized = True
|
||||
|
||||
def close(self):
|
||||
if self.private_dict_file:
|
||||
self.private_dict_file.close()
|
||||
|
||||
def _check_spelling(self, msgid, line, line_num):
|
||||
original_line = line
|
||||
if line.strip().startswith('#'):
|
||||
line = line.strip()[1:]
|
||||
starts_with_comment = True
|
||||
else:
|
||||
starts_with_comment = False
|
||||
for word, _ in self.tokenizer(line.strip()):
|
||||
if six.PY2:
|
||||
lower_cased_word = word.lower()
|
||||
else:
|
||||
lower_cased_word = word.casefold()
|
||||
|
||||
# Skip words from ignore list.
|
||||
if word in self.ignore_list or lower_cased_word in self.ignore_list:
|
||||
continue
|
||||
|
||||
# Strip starting u' from unicode literals and r' from raw strings.
|
||||
if word.startswith(("u'", 'u"', "r'", 'r"')) and len(word) > 2:
|
||||
word = word[2:]
|
||||
lower_cased_word = lower_cased_word[2:]
|
||||
|
||||
# If it is a known word, then continue.
|
||||
try:
|
||||
if self.spelling_dict.check(lower_cased_word):
|
||||
# The lower cased version of word passed spell checking
|
||||
continue
|
||||
|
||||
# If we reached this far, it means there was a spelling mistake.
|
||||
# Let's retry with the original work because 'unicode' is a
|
||||
# spelling mistake but 'Unicode' is not
|
||||
if self.spelling_dict.check(word):
|
||||
continue
|
||||
except enchant.errors.Error:
|
||||
self.add_message('invalid-characters-in-docstring',
|
||||
line=line_num, args=(word,))
|
||||
continue
|
||||
|
||||
# Store word to private dict or raise a message.
|
||||
if self.config.spelling_store_unknown_words:
|
||||
if lower_cased_word not in self.unknown_words:
|
||||
self.private_dict_file.write("%s\n" % lower_cased_word)
|
||||
self.unknown_words.add(lower_cased_word)
|
||||
else:
|
||||
# Present up to N suggestions.
|
||||
suggestions = self.spelling_dict.suggest(word)
|
||||
del suggestions[self.config.max_spelling_suggestions:]
|
||||
|
||||
m = re.search(r"(\W|^)(%s)(\W|$)" % word, line)
|
||||
if m:
|
||||
# Start position of second group in regex.
|
||||
col = m.regs[2][0]
|
||||
else:
|
||||
col = line.index(word)
|
||||
|
||||
if starts_with_comment:
|
||||
col += 1
|
||||
indicator = (" " * col) + ("^" * len(word))
|
||||
|
||||
self.add_message(msgid, line=line_num,
|
||||
args=(word, original_line,
|
||||
indicator,
|
||||
"'{0}'".format("' or '".join(suggestions))))
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
if not self.initialized:
|
||||
return
|
||||
|
||||
# Process tokens and look for comments.
|
||||
for (tok_type, token, (start_row, _), _, _) in tokens:
|
||||
if tok_type == tokenize.COMMENT:
|
||||
if start_row == 1 and token.startswith('#!/'):
|
||||
# Skip shebang lines
|
||||
continue
|
||||
if token.startswith('# pylint:'):
|
||||
# Skip pylint enable/disable comments
|
||||
continue
|
||||
self._check_spelling('wrong-spelling-in-comment',
|
||||
token, start_row)
|
||||
|
||||
@check_messages('wrong-spelling-in-docstring')
|
||||
def visit_module(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
@check_messages('wrong-spelling-in-docstring')
|
||||
def visit_classdef(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
@check_messages('wrong-spelling-in-docstring')
|
||||
def visit_functiondef(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
def _check_docstring(self, node):
|
||||
"""check the node has any spelling errors"""
|
||||
docstring = node.doc
|
||||
if not docstring:
|
||||
return
|
||||
|
||||
start_line = node.lineno + 1
|
||||
if six.PY2:
|
||||
encoding = node.root().file_encoding
|
||||
docstring = safe_decode(docstring, encoding, 'replace')
|
||||
|
||||
# Go through lines of docstring
|
||||
for idx, line in enumerate(docstring.splitlines()):
|
||||
self._check_spelling('wrong-spelling-in-docstring',
|
||||
line, start_line + idx)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(SpellingChecker(linter))
|
||||
@@ -0,0 +1,320 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Cosmin Poieana <cmin@ropython.org>
|
||||
# Copyright (c) 2014 Vlad Temian <vladtemian@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Cezar <celnazli@bitdefender.com>
|
||||
# Copyright (c) 2015 Chris Rebert <code@rebertia.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Jared Garst <cultofjared@gmail.com>
|
||||
# Copyright (c) 2017 Martin <MartinBasti@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Christopher Zurcher <zurcher@users.noreply.github.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checkers for various standard library functions."""
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import astroid
|
||||
from astroid.bases import Instance
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
OPEN_FILES = {'open', 'file'}
|
||||
UNITTEST_CASE = 'unittest.case'
|
||||
THREADING_THREAD = 'threading.Thread'
|
||||
COPY_COPY = 'copy.copy'
|
||||
OS_ENVIRON = 'os._Environ'
|
||||
if sys.version_info >= (3, 0):
|
||||
OPEN_MODULE = '_io'
|
||||
else:
|
||||
OPEN_MODULE = '__builtin__'
|
||||
|
||||
|
||||
def _check_mode_str(mode):
|
||||
# check type
|
||||
if not isinstance(mode, six.string_types):
|
||||
return False
|
||||
# check syntax
|
||||
modes = set(mode)
|
||||
_mode = "rwatb+U"
|
||||
creating = False
|
||||
if six.PY3:
|
||||
_mode += "x"
|
||||
creating = "x" in modes
|
||||
if modes - set(_mode) or len(mode) > len(modes):
|
||||
return False
|
||||
# check logic
|
||||
reading = "r" in modes
|
||||
writing = "w" in modes
|
||||
appending = "a" in modes
|
||||
text = "t" in modes
|
||||
binary = "b" in modes
|
||||
if "U" in modes:
|
||||
if writing or appending or creating and six.PY3:
|
||||
return False
|
||||
reading = True
|
||||
if not six.PY3:
|
||||
binary = True
|
||||
if text and binary:
|
||||
return False
|
||||
total = reading + writing + appending + (creating if six.PY3 else 0)
|
||||
if total > 1:
|
||||
return False
|
||||
if not (reading or writing or appending or creating and six.PY3):
|
||||
return False
|
||||
# other 2.x constraints
|
||||
if not six.PY3:
|
||||
if "U" in mode:
|
||||
mode = mode.replace("U", "")
|
||||
if "r" not in mode:
|
||||
mode = "r" + mode
|
||||
return mode[0] in ("r", "w", "a", "U")
|
||||
return True
|
||||
|
||||
|
||||
class StdlibChecker(BaseChecker):
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = 'stdlib'
|
||||
|
||||
msgs = {
|
||||
'W1501': ('"%s" is not a valid mode for open.',
|
||||
'bad-open-mode',
|
||||
'Python supports: r, w, a[, x] modes with b, +, '
|
||||
'and U (only with r) options. '
|
||||
'See http://docs.python.org/2/library/functions.html#open'),
|
||||
'W1502': ('Using datetime.time in a boolean context.',
|
||||
'boolean-datetime',
|
||||
'Using datetime.time in a boolean context can hide '
|
||||
'subtle bugs when the time they represent matches '
|
||||
'midnight UTC. This behaviour was fixed in Python 3.5. '
|
||||
'See http://bugs.python.org/issue13936 for reference.',
|
||||
{'maxversion': (3, 5)}),
|
||||
'W1503': ('Redundant use of %s with constant '
|
||||
'value %r',
|
||||
'redundant-unittest-assert',
|
||||
'The first argument of assertTrue and assertFalse is '
|
||||
'a condition. If a constant is passed as parameter, that '
|
||||
'condition will be always true. In this case a warning '
|
||||
'should be emitted.'),
|
||||
'W1505': ('Using deprecated method %s()',
|
||||
'deprecated-method',
|
||||
'The method is marked as deprecated and will be removed in '
|
||||
'a future version of Python. Consider looking for an '
|
||||
'alternative in the documentation.'),
|
||||
'W1506': ('threading.Thread needs the target function',
|
||||
'bad-thread-instantiation',
|
||||
'The warning is emitted when a threading.Thread class '
|
||||
'is instantiated without the target function being passed. '
|
||||
'By default, the first parameter is the group param, not the target param. '),
|
||||
'W1507': ('Using copy.copy(os.environ). Use os.environ.copy() '
|
||||
'instead. ',
|
||||
'shallow-copy-environ',
|
||||
'os.environ is not a dict object but proxy object, so '
|
||||
'shallow copy has still effects on original object. '
|
||||
'See https://bugs.python.org/issue15373 for reference. '),
|
||||
}
|
||||
|
||||
deprecated = {
|
||||
0: [
|
||||
'cgi.parse_qs', 'cgi.parse_qsl',
|
||||
'ctypes.c_buffer',
|
||||
'distutils.command.register.register.check_metadata',
|
||||
'distutils.command.sdist.sdist.check_metadata',
|
||||
'tkinter.Misc.tk_menuBar',
|
||||
'tkinter.Menu.tk_bindForTraversal',
|
||||
],
|
||||
2: {
|
||||
(2, 6, 0): [
|
||||
'commands.getstatus',
|
||||
'os.popen2',
|
||||
'os.popen3',
|
||||
'os.popen4',
|
||||
'macostools.touched',
|
||||
],
|
||||
(2, 7, 0): [
|
||||
'unittest.case.TestCase.assertEquals',
|
||||
'unittest.case.TestCase.assertNotEquals',
|
||||
'unittest.case.TestCase.assertAlmostEquals',
|
||||
'unittest.case.TestCase.assertNotAlmostEquals',
|
||||
'unittest.case.TestCase.assert_',
|
||||
'xml.etree.ElementTree.Element.getchildren',
|
||||
'xml.etree.ElementTree.Element.getiterator',
|
||||
'xml.etree.ElementTree.XMLParser.getiterator',
|
||||
'xml.etree.ElementTree.XMLParser.doctype',
|
||||
],
|
||||
},
|
||||
3: {
|
||||
(3, 0, 0): [
|
||||
'inspect.getargspec',
|
||||
'unittest.case.TestCase._deprecate.deprecated_func',
|
||||
],
|
||||
(3, 1, 0): [
|
||||
'base64.encodestring', 'base64.decodestring',
|
||||
'ntpath.splitunc',
|
||||
],
|
||||
(3, 2, 0): [
|
||||
'cgi.escape',
|
||||
'configparser.RawConfigParser.readfp',
|
||||
'xml.etree.ElementTree.Element.getchildren',
|
||||
'xml.etree.ElementTree.Element.getiterator',
|
||||
'xml.etree.ElementTree.XMLParser.getiterator',
|
||||
'xml.etree.ElementTree.XMLParser.doctype',
|
||||
],
|
||||
(3, 3, 0): [
|
||||
'inspect.getmoduleinfo',
|
||||
'logging.warn', 'logging.Logger.warn',
|
||||
'logging.LoggerAdapter.warn',
|
||||
'nntplib._NNTPBase.xpath',
|
||||
'platform.popen',
|
||||
],
|
||||
(3, 4, 0): [
|
||||
'importlib.find_loader',
|
||||
'plistlib.readPlist', 'plistlib.writePlist',
|
||||
'plistlib.readPlistFromBytes',
|
||||
'plistlib.writePlistToBytes',
|
||||
],
|
||||
(3, 4, 4): [
|
||||
'asyncio.tasks.async',
|
||||
],
|
||||
(3, 5, 0): [
|
||||
'fractions.gcd',
|
||||
'inspect.getargvalues',
|
||||
'inspect.formatargspec', 'inspect.formatargvalues',
|
||||
'inspect.getcallargs',
|
||||
'platform.linux_distribution', 'platform.dist',
|
||||
],
|
||||
(3, 6, 0): [
|
||||
'importlib._bootstrap_external.FileLoader.load_module',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
def _check_bad_thread_instantiation(self, node):
|
||||
if not node.kwargs and node.args:
|
||||
self.add_message('bad-thread-instantiation', node=node)
|
||||
|
||||
def _check_shallow_copy_environ(self, node):
|
||||
arg = utils.get_argument_from_call(node, position=0)
|
||||
for inferred in arg.inferred():
|
||||
if inferred.qname() == OS_ENVIRON:
|
||||
self.add_message('shallow-copy-environ', node=node)
|
||||
break
|
||||
|
||||
@utils.check_messages('bad-open-mode', 'redundant-unittest-assert',
|
||||
'deprecated-method',
|
||||
'bad-thread-instantiation',
|
||||
'shallow-copy-environ')
|
||||
def visit_call(self, node):
|
||||
"""Visit a Call node."""
|
||||
try:
|
||||
for inferred in node.func.infer():
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
if inferred.root().name == OPEN_MODULE:
|
||||
if getattr(node.func, 'name', None) in OPEN_FILES:
|
||||
self._check_open_mode(node)
|
||||
if inferred.root().name == UNITTEST_CASE:
|
||||
self._check_redundant_assert(node, inferred)
|
||||
if isinstance(inferred, astroid.ClassDef) and inferred.qname() == THREADING_THREAD:
|
||||
self._check_bad_thread_instantiation(node)
|
||||
if isinstance(inferred, astroid.FunctionDef) and inferred.qname() == COPY_COPY:
|
||||
self._check_shallow_copy_environ(node)
|
||||
self._check_deprecated_method(node, inferred)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
|
||||
@utils.check_messages('boolean-datetime')
|
||||
def visit_unaryop(self, node):
|
||||
if node.op == 'not':
|
||||
self._check_datetime(node.operand)
|
||||
|
||||
@utils.check_messages('boolean-datetime')
|
||||
def visit_if(self, node):
|
||||
self._check_datetime(node.test)
|
||||
|
||||
@utils.check_messages('boolean-datetime')
|
||||
def visit_ifexp(self, node):
|
||||
self._check_datetime(node.test)
|
||||
|
||||
@utils.check_messages('boolean-datetime')
|
||||
def visit_boolop(self, node):
|
||||
for value in node.values:
|
||||
self._check_datetime(value)
|
||||
|
||||
def _check_deprecated_method(self, node, inferred):
|
||||
py_vers = sys.version_info[0]
|
||||
|
||||
if isinstance(node.func, astroid.Attribute):
|
||||
func_name = node.func.attrname
|
||||
elif isinstance(node.func, astroid.Name):
|
||||
func_name = node.func.name
|
||||
else:
|
||||
# Not interested in other nodes.
|
||||
return
|
||||
|
||||
# Reject nodes which aren't of interest to us.
|
||||
acceptable_nodes = (astroid.BoundMethod,
|
||||
astroid.UnboundMethod,
|
||||
astroid.FunctionDef)
|
||||
if not isinstance(inferred, acceptable_nodes):
|
||||
return
|
||||
|
||||
qname = inferred.qname()
|
||||
if qname in self.deprecated[0]:
|
||||
self.add_message('deprecated-method', node=node,
|
||||
args=(func_name, ))
|
||||
else:
|
||||
for since_vers, func_list in self.deprecated[py_vers].items():
|
||||
if since_vers <= sys.version_info and qname in func_list:
|
||||
self.add_message('deprecated-method', node=node,
|
||||
args=(func_name, ))
|
||||
break
|
||||
|
||||
def _check_redundant_assert(self, node, infer):
|
||||
if (isinstance(infer, astroid.BoundMethod) and
|
||||
node.args and isinstance(node.args[0], astroid.Const) and
|
||||
infer.name in ['assertTrue', 'assertFalse']):
|
||||
self.add_message('redundant-unittest-assert',
|
||||
args=(infer.name, node.args[0].value, ),
|
||||
node=node)
|
||||
|
||||
def _check_datetime(self, node):
|
||||
""" Check that a datetime was infered.
|
||||
If so, emit boolean-datetime warning.
|
||||
"""
|
||||
try:
|
||||
infered = next(node.infer())
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
if (isinstance(infered, Instance) and
|
||||
infered.qname() == 'datetime.time'):
|
||||
self.add_message('boolean-datetime', node=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')
|
||||
except utils.NoSuchArgumentError:
|
||||
return
|
||||
if mode_arg:
|
||||
mode_arg = utils.safe_infer(mode_arg)
|
||||
if (isinstance(mode_arg, astroid.Const)
|
||||
and not _check_mode_str(mode_arg.value)):
|
||||
self.add_message('bad-open-mode', node=node,
|
||||
args=mode_arg.value)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(StdlibChecker(linter))
|
||||
@@ -0,0 +1,632 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2009 Charles Hebert <charles.hebert@logilab.fr>
|
||||
# Copyright (c) 2010-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2010 Daniel Harding <dharding@gmail.com>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2013-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Rene Zhang <rz99@cornell.edu>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Peter Dawyndt <Peter.Dawyndt@UGent.be>
|
||||
# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for string formatting operations.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
import string
|
||||
import numbers
|
||||
|
||||
import six
|
||||
|
||||
import astroid
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker
|
||||
from pylint.checkers import BaseChecker, BaseTokenChecker
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
|
||||
_PY3K = sys.version_info[:2] >= (3, 0)
|
||||
_PY27 = sys.version_info[:2] == (2, 7)
|
||||
|
||||
MSGS = {
|
||||
'E1300': ("Unsupported format character %r (%#02x) at index %d",
|
||||
"bad-format-character",
|
||||
"Used when a unsupported format character is used in a format\
|
||||
string."),
|
||||
'E1301': ("Format string ends in middle of conversion specifier",
|
||||
"truncated-format-string",
|
||||
"Used when a format string terminates before the end of a \
|
||||
conversion specifier."),
|
||||
'E1302': ("Mixing named and unnamed conversion specifiers in format string",
|
||||
"mixed-format-string",
|
||||
"Used when a format string contains both named (e.g. '%(foo)d') \
|
||||
and unnamed (e.g. '%d') conversion specifiers. This is also \
|
||||
used when a named conversion specifier contains * for the \
|
||||
minimum field width and/or precision."),
|
||||
'E1303': ("Expected mapping for format string, not %s",
|
||||
"format-needs-mapping",
|
||||
"Used when a format string that uses named conversion specifiers \
|
||||
is used with an argument that is not a mapping."),
|
||||
'W1300': ("Format string dictionary key should be a string, not %s",
|
||||
"bad-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers \
|
||||
is used with a dictionary whose keys are not all strings."),
|
||||
'W1301': ("Unused key %r in format string dictionary",
|
||||
"unused-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers \
|
||||
is used with a dictionary that contains keys not required by the \
|
||||
format string."),
|
||||
'E1304': ("Missing key %r in format string dictionary",
|
||||
"missing-format-string-key",
|
||||
"Used when a format string that uses named conversion specifiers \
|
||||
is used with a dictionary that doesn't contain all the keys \
|
||||
required by the format string."),
|
||||
'E1305': ("Too many arguments for format string",
|
||||
"too-many-format-args",
|
||||
"Used when a format string that uses unnamed conversion \
|
||||
specifiers is given too many arguments."),
|
||||
'E1306': ("Not enough arguments for format string",
|
||||
"too-few-format-args",
|
||||
"Used when a format string that uses unnamed conversion \
|
||||
specifiers is given too few arguments"),
|
||||
'E1310': ("Suspicious argument in %s.%s call",
|
||||
"bad-str-strip-call",
|
||||
"The argument to a str.{l,r,}strip call contains a"
|
||||
" duplicate character, "),
|
||||
'W1302': ("Invalid format string",
|
||||
"bad-format-string",
|
||||
"Used when a PEP 3101 format string is invalid.",
|
||||
{'minversion': (2, 7)}),
|
||||
'W1303': ("Missing keyword argument %r for format string",
|
||||
"missing-format-argument-key",
|
||||
"Used when a PEP 3101 format string that uses named fields "
|
||||
"doesn't receive one or more required keywords.",
|
||||
{'minversion': (2, 7)}),
|
||||
'W1304': ("Unused format argument %r",
|
||||
"unused-format-string-argument",
|
||||
"Used when a PEP 3101 format string that uses named "
|
||||
"fields is used with an argument that "
|
||||
"is not required by the format string.",
|
||||
{'minversion': (2, 7)}),
|
||||
'W1305': ("Format string contains both automatic field numbering "
|
||||
"and manual field specification",
|
||||
"format-combined-specification",
|
||||
"Used when a PEP 3101 format string contains both automatic "
|
||||
"field numbering (e.g. '{}') and manual field "
|
||||
"specification (e.g. '{0}').",
|
||||
{'minversion': (2, 7)}),
|
||||
'W1306': ("Missing format attribute %r in format specifier %r",
|
||||
"missing-format-attribute",
|
||||
"Used when a PEP 3101 format string uses an "
|
||||
"attribute specifier ({0.length}), but the argument "
|
||||
"passed for formatting doesn't have that attribute.",
|
||||
{'minversion': (2, 7)}),
|
||||
'W1307': ("Using invalid lookup key %r in format specifier %r",
|
||||
"invalid-format-index",
|
||||
"Used when a PEP 3101 format string uses a lookup specifier "
|
||||
"({a[1]}), but the argument passed for formatting "
|
||||
"doesn't contain or doesn't have that key as an attribute.",
|
||||
{'minversion': (2, 7)})
|
||||
}
|
||||
|
||||
OTHER_NODES = (astroid.Const, astroid.List, astroid.Repr,
|
||||
astroid.Lambda, astroid.FunctionDef,
|
||||
astroid.ListComp, astroid.SetComp, astroid.GeneratorExp)
|
||||
|
||||
if _PY3K:
|
||||
import _string # pylint: disable=wrong-import-position, wrong-import-order
|
||||
|
||||
def split_format_field_names(format_string):
|
||||
return _string.formatter_field_name_split(format_string)
|
||||
else:
|
||||
def _field_iterator_convertor(iterator):
|
||||
for is_attr, key in iterator:
|
||||
if isinstance(key, numbers.Number):
|
||||
yield is_attr, int(key)
|
||||
else:
|
||||
yield is_attr, key
|
||||
|
||||
def split_format_field_names(format_string):
|
||||
try:
|
||||
keyname, fielditerator = format_string._formatter_field_name_split()
|
||||
except ValueError:
|
||||
raise utils.IncompleteFormatString
|
||||
# it will return longs, instead of ints, which will complicate
|
||||
# the output
|
||||
return keyname, _field_iterator_convertor(fielditerator)
|
||||
|
||||
|
||||
def collect_string_fields(format_string):
|
||||
""" Given a format string, return an iterator
|
||||
of all the valid format fields. It handles nested fields
|
||||
as well.
|
||||
"""
|
||||
|
||||
formatter = string.Formatter()
|
||||
try:
|
||||
parseiterator = formatter.parse(format_string)
|
||||
for result in parseiterator:
|
||||
if all(item is None for item in result[1:]):
|
||||
# not a replacement format
|
||||
continue
|
||||
name = result[1]
|
||||
nested = result[2]
|
||||
yield name
|
||||
if nested:
|
||||
for field in collect_string_fields(nested):
|
||||
yield field
|
||||
except ValueError as exc:
|
||||
# Probably the format string is invalid.
|
||||
if exc.args[0].startswith("cannot switch from manual"):
|
||||
# On Jython, parsing a string with both manual
|
||||
# and automatic positions will fail with a ValueError,
|
||||
# while on CPython it will simply return the fields,
|
||||
# the validation being done in the interpreter (?).
|
||||
# We're just returning two mixed fields in order
|
||||
# to trigger the format-combined-specification check.
|
||||
yield ""
|
||||
yield "1"
|
||||
return
|
||||
raise utils.IncompleteFormatString(format_string)
|
||||
|
||||
def parse_format_method_string(format_string):
|
||||
"""
|
||||
Parses a PEP 3101 format string, returning a tuple of
|
||||
(keys, num_args, manual_pos_arg),
|
||||
where keys is the set of mapping keys in the format string, num_args
|
||||
is the number of arguments required by the format string and
|
||||
manual_pos_arg is the number of arguments passed with the position.
|
||||
"""
|
||||
keys = []
|
||||
num_args = 0
|
||||
manual_pos_arg = set()
|
||||
for name in collect_string_fields(format_string):
|
||||
if name and str(name).isdigit():
|
||||
manual_pos_arg.add(str(name))
|
||||
elif name:
|
||||
keyname, fielditerator = split_format_field_names(name)
|
||||
if isinstance(keyname, numbers.Number):
|
||||
# In Python 2 it will return long which will lead
|
||||
# to different output between 2 and 3
|
||||
manual_pos_arg.add(str(keyname))
|
||||
keyname = int(keyname)
|
||||
try:
|
||||
keys.append((keyname, list(fielditerator)))
|
||||
except ValueError:
|
||||
raise utils.IncompleteFormatString()
|
||||
else:
|
||||
num_args += 1
|
||||
return keys, num_args, len(manual_pos_arg)
|
||||
|
||||
def get_args(call):
|
||||
"""Get the arguments from the given `Call` node.
|
||||
|
||||
Return a tuple, where the first element is the
|
||||
number of positional arguments and the second element
|
||||
is the keyword arguments in a dict.
|
||||
"""
|
||||
if call.keywords:
|
||||
named = {arg.arg: utils.safe_infer(arg.value)
|
||||
for arg in call.keywords}
|
||||
else:
|
||||
named = {}
|
||||
positional = len(call.args)
|
||||
return positional, named
|
||||
|
||||
def get_access_path(key, parts):
|
||||
""" Given a list of format specifiers, returns
|
||||
the final access path (e.g. a.b.c[0][1]).
|
||||
"""
|
||||
path = []
|
||||
for is_attribute, specifier in parts:
|
||||
if is_attribute:
|
||||
path.append(".{}".format(specifier))
|
||||
else:
|
||||
path.append("[{!r}]".format(specifier))
|
||||
return str(key) + "".join(path)
|
||||
|
||||
|
||||
class StringFormatChecker(BaseChecker):
|
||||
"""Checks string formatting operations to ensure that the format string
|
||||
is valid and the arguments match the format string.
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = 'string'
|
||||
msgs = MSGS
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_binop(self, node):
|
||||
if node.op != '%':
|
||||
return
|
||||
left = node.left
|
||||
args = node.right
|
||||
|
||||
if not (isinstance(left, astroid.Const)
|
||||
and isinstance(left.value, six.string_types)):
|
||||
return
|
||||
format_string = left.value
|
||||
try:
|
||||
required_keys, required_num_args = \
|
||||
utils.parse_format_string(format_string)
|
||||
except utils.UnsupportedFormatCharacter as e:
|
||||
c = format_string[e.index]
|
||||
self.add_message('bad-format-character',
|
||||
node=node, args=(c, ord(c), e.index))
|
||||
return
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message('truncated-format-string', node=node)
|
||||
return
|
||||
if required_keys and required_num_args:
|
||||
# The format string uses both named and unnamed format
|
||||
# specifiers.
|
||||
self.add_message('mixed-format-string', node=node)
|
||||
elif required_keys:
|
||||
# The format string uses only named format specifiers.
|
||||
# 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, astroid.Dict):
|
||||
keys = set()
|
||||
unknown_keys = False
|
||||
for k, _ in args.items:
|
||||
if isinstance(k, astroid.Const):
|
||||
key = k.value
|
||||
if isinstance(key, six.string_types):
|
||||
keys.add(key)
|
||||
else:
|
||||
self.add_message('bad-format-string-key',
|
||||
node=node, args=key)
|
||||
else:
|
||||
# One of the keys was something other than a
|
||||
# constant. Since we can't tell what it is,
|
||||
# suppress checks for missing keys in the
|
||||
# dictionary.
|
||||
unknown_keys = True
|
||||
if not unknown_keys:
|
||||
for key in required_keys:
|
||||
if key not in keys:
|
||||
self.add_message('missing-format-string-key',
|
||||
node=node, args=key)
|
||||
for key in keys:
|
||||
if key not in required_keys:
|
||||
self.add_message('unused-format-string-key',
|
||||
node=node, args=key)
|
||||
elif isinstance(args, OTHER_NODES + (astroid.Tuple,)):
|
||||
type_name = type(args).__name__
|
||||
self.add_message('format-needs-mapping',
|
||||
node=node, args=type_name)
|
||||
# else:
|
||||
# The RHS of the format specifier is a name or
|
||||
# expression. It may be a mapping object, so
|
||||
# there's nothing we can check.
|
||||
else:
|
||||
# The format string uses only unnamed format specifiers.
|
||||
# Check that the number of arguments passed to the RHS of
|
||||
# the % operator matches the number required by the format
|
||||
# string.
|
||||
if isinstance(args, astroid.Tuple):
|
||||
rhs_tuple = utils.safe_infer(args)
|
||||
num_args = None
|
||||
if rhs_tuple not in (None, astroid.Uninferable):
|
||||
num_args = len(rhs_tuple.elts)
|
||||
elif isinstance(args, OTHER_NODES + (astroid.Dict, astroid.DictComp)):
|
||||
num_args = 1
|
||||
else:
|
||||
# The RHS of the format specifier is a name or
|
||||
# expression. It could be a tuple of unknown size, so
|
||||
# there's nothing we can check.
|
||||
num_args = None
|
||||
if num_args is not None:
|
||||
if num_args > required_num_args:
|
||||
self.add_message('too-many-format-args', node=node)
|
||||
elif num_args < required_num_args:
|
||||
self.add_message('too-few-format-args', node=node)
|
||||
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_call(self, node):
|
||||
func = utils.safe_infer(node.func)
|
||||
if (isinstance(func, astroid.BoundMethod)
|
||||
and isinstance(func.bound, astroid.Instance)
|
||||
and func.bound.name in ('str', 'unicode', 'bytes')):
|
||||
if func.name in ('strip', 'lstrip', 'rstrip') and node.args:
|
||||
arg = utils.safe_infer(node.args[0])
|
||||
if not isinstance(arg, astroid.Const):
|
||||
return
|
||||
if len(arg.value) != len(set(arg.value)):
|
||||
self.add_message('bad-str-strip-call', node=node,
|
||||
args=(func.bound.name, func.name))
|
||||
elif func.name == 'format':
|
||||
if _PY27 or _PY3K:
|
||||
self._check_new_format(node, func)
|
||||
|
||||
def _check_new_format(self, node, func):
|
||||
""" Check the new string formatting. """
|
||||
# TODO: skip (for now) format nodes which don't have
|
||||
# an explicit string on the left side of the format operation.
|
||||
# We do this because our inference engine can't properly handle
|
||||
# redefinitions of the original string.
|
||||
# For more details, see issue 287.
|
||||
#
|
||||
# Note that there may not be any left side at all, if the format method
|
||||
# has been assigned to another variable. See issue 351. For example:
|
||||
#
|
||||
# fmt = 'some string {}'.format
|
||||
# fmt('arg')
|
||||
if (isinstance(node.func, astroid.Attribute)
|
||||
and not isinstance(node.func.expr, astroid.Const)):
|
||||
return
|
||||
try:
|
||||
strnode = next(func.bound.infer())
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
if not isinstance(strnode, astroid.Const):
|
||||
return
|
||||
if not isinstance(strnode.value, six.string_types):
|
||||
return
|
||||
|
||||
if node.starargs or node.kwargs:
|
||||
return
|
||||
try:
|
||||
positional, named = get_args(node)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
try:
|
||||
fields, num_args, manual_pos = parse_format_method_string(strnode.value)
|
||||
except utils.IncompleteFormatString:
|
||||
self.add_message('bad-format-string', node=node)
|
||||
return
|
||||
|
||||
named_fields = set(field[0] for field in fields
|
||||
if isinstance(field[0], six.string_types))
|
||||
if num_args and manual_pos:
|
||||
self.add_message('format-combined-specification',
|
||||
node=node)
|
||||
return
|
||||
|
||||
check_args = False
|
||||
# Consider "{[0]} {[1]}" as num_args.
|
||||
num_args += sum(1 for field in named_fields
|
||||
if field == '')
|
||||
if named_fields:
|
||||
for field in named_fields:
|
||||
if field not in named and field:
|
||||
self.add_message('missing-format-argument-key',
|
||||
node=node,
|
||||
args=(field, ))
|
||||
for field in named:
|
||||
if field not in named_fields:
|
||||
self.add_message('unused-format-string-argument',
|
||||
node=node,
|
||||
args=(field, ))
|
||||
# num_args can be 0 if manual_pos is not.
|
||||
num_args = num_args or manual_pos
|
||||
if positional or num_args:
|
||||
empty = any(True for field in named_fields
|
||||
if field == '')
|
||||
if named or empty:
|
||||
# Verify the required number of positional arguments
|
||||
# only if the .format got at least one keyword argument.
|
||||
# This means that the format strings accepts both
|
||||
# positional and named fields and we should warn
|
||||
# when one of the them is missing or is extra.
|
||||
check_args = True
|
||||
else:
|
||||
check_args = True
|
||||
if check_args:
|
||||
# num_args can be 0 if manual_pos is not.
|
||||
num_args = num_args or manual_pos
|
||||
if positional > num_args:
|
||||
self.add_message('too-many-format-args', node=node)
|
||||
elif positional < num_args:
|
||||
self.add_message('too-few-format-args', node=node)
|
||||
|
||||
self._check_new_format_specifiers(node, fields, named)
|
||||
|
||||
def _check_new_format_specifiers(self, node, fields, named):
|
||||
"""
|
||||
Check attribute and index access in the format
|
||||
string ("{0.a}" and "{0[a]}").
|
||||
"""
|
||||
for key, specifiers in fields:
|
||||
# Obtain the argument. If it can't be obtained
|
||||
# or infered, skip this check.
|
||||
if key == '':
|
||||
# {[0]} will have an unnamed argument, defaulting
|
||||
# to 0. It will not be present in `named`, so use the value
|
||||
# 0 for it.
|
||||
key = 0
|
||||
if isinstance(key, numbers.Number):
|
||||
try:
|
||||
argname = utils.get_argument_from_call(node, key)
|
||||
except utils.NoSuchArgumentError:
|
||||
continue
|
||||
else:
|
||||
if key not in named:
|
||||
continue
|
||||
argname = named[key]
|
||||
if argname in (astroid.YES, None):
|
||||
continue
|
||||
try:
|
||||
argument = next(argname.infer())
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
if not specifiers or argument is astroid.Uninferable:
|
||||
# No need to check this key if it doesn't
|
||||
# use attribute / item access
|
||||
continue
|
||||
if argument.parent and isinstance(argument.parent, astroid.Arguments):
|
||||
# Ignore any object coming from an argument,
|
||||
# because we can't infer its value properly.
|
||||
continue
|
||||
previous = argument
|
||||
parsed = []
|
||||
for is_attribute, specifier in specifiers:
|
||||
if previous is astroid.Uninferable:
|
||||
break
|
||||
parsed.append((is_attribute, specifier))
|
||||
if is_attribute:
|
||||
try:
|
||||
previous = previous.getattr(specifier)[0]
|
||||
except astroid.NotFoundError:
|
||||
if (hasattr(previous, 'has_dynamic_getattr') and
|
||||
previous.has_dynamic_getattr()):
|
||||
# Don't warn if the object has a custom __getattr__
|
||||
break
|
||||
path = get_access_path(key, parsed)
|
||||
self.add_message('missing-format-attribute',
|
||||
args=(specifier, path),
|
||||
node=node)
|
||||
break
|
||||
else:
|
||||
warn_error = False
|
||||
if hasattr(previous, 'getitem'):
|
||||
try:
|
||||
previous = previous.getitem(astroid.Const(specifier))
|
||||
except (astroid.AstroidIndexError,
|
||||
astroid.AstroidTypeError,
|
||||
astroid.AttributeInferenceError):
|
||||
warn_error = True
|
||||
except astroid.InferenceError:
|
||||
break
|
||||
if previous is astroid.Uninferable:
|
||||
break
|
||||
else:
|
||||
try:
|
||||
# Lookup __getitem__ in the current node,
|
||||
# but skip further checks, because we can't
|
||||
# retrieve the looked object
|
||||
previous.getattr('__getitem__')
|
||||
break
|
||||
except astroid.NotFoundError:
|
||||
warn_error = True
|
||||
if warn_error:
|
||||
path = get_access_path(key, parsed)
|
||||
self.add_message('invalid-format-index',
|
||||
args=(specifier, path),
|
||||
node=node)
|
||||
break
|
||||
|
||||
try:
|
||||
previous = next(previous.infer())
|
||||
except astroid.InferenceError:
|
||||
# can't check further if we can't infer it
|
||||
break
|
||||
|
||||
|
||||
class StringConstantChecker(BaseTokenChecker):
|
||||
"""Check string literals"""
|
||||
__implements__ = (ITokenChecker, IRawChecker)
|
||||
name = 'string_constant'
|
||||
msgs = {
|
||||
'W1401': ('Anomalous backslash in string: \'%s\'. '
|
||||
'String constant might be missing an r prefix.',
|
||||
'anomalous-backslash-in-string',
|
||||
'Used when a backslash is in a literal string but not as an '
|
||||
'escape.'),
|
||||
'W1402': ('Anomalous Unicode escape in byte string: \'%s\'. '
|
||||
'String constant might be missing an r or u prefix.',
|
||||
'anomalous-unicode-escape-in-string',
|
||||
'Used when an escape like \\u is encountered in a byte '
|
||||
'string where it has no effect.'),
|
||||
}
|
||||
|
||||
# Characters that have a special meaning after a backslash in either
|
||||
# Unicode or byte strings.
|
||||
ESCAPE_CHARACTERS = 'abfnrtvx\n\r\t\\\'\"01234567'
|
||||
|
||||
# TODO(mbp): Octal characters are quite an edge case today; people may
|
||||
# prefer a separate warning where they occur. \0 should be allowed.
|
||||
|
||||
# Characters that have a special meaning after a backslash but only in
|
||||
# Unicode strings.
|
||||
UNICODE_ESCAPE_CHARACTERS = 'uUN'
|
||||
|
||||
def process_module(self, module):
|
||||
self._unicode_literals = 'unicode_literals' in module.future_imports
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
for (tok_type, token, (start_row, _), _, _) in tokens:
|
||||
if tok_type == tokenize.STRING:
|
||||
# 'token' is the whole un-parsed token; we can look at the start
|
||||
# of it to see whether it's a raw or unicode string etc.
|
||||
self.process_string_token(token, start_row)
|
||||
|
||||
def process_string_token(self, token, start_row):
|
||||
for i, c in enumerate(token):
|
||||
if c in '\'\"':
|
||||
quote_char = c
|
||||
break
|
||||
# pylint: disable=undefined-loop-variable
|
||||
prefix = token[:i].lower() # markers like u, b, r.
|
||||
after_prefix = token[i:]
|
||||
if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char:
|
||||
string_body = after_prefix[3:-3]
|
||||
else:
|
||||
string_body = after_prefix[1:-1] # Chop off quotes
|
||||
# No special checks on raw strings at the moment.
|
||||
if 'r' not in prefix:
|
||||
self.process_non_raw_string_token(prefix, string_body, start_row)
|
||||
|
||||
def process_non_raw_string_token(self, prefix, string_body, start_row):
|
||||
"""check for bad escapes in a non-raw string.
|
||||
|
||||
prefix: lowercase string of eg 'ur' string prefix markers.
|
||||
string_body: the un-parsed body of the string, not including the quote
|
||||
marks.
|
||||
start_row: integer line number in the source.
|
||||
"""
|
||||
# Walk through the string; if we see a backslash then escape the next
|
||||
# character, and skip over it. If we see a non-escaped character,
|
||||
# alert, and continue.
|
||||
#
|
||||
# Accept a backslash when it escapes a backslash, or a quote, or
|
||||
# end-of-line, or one of the letters that introduce a special escape
|
||||
# sequence <http://docs.python.org/reference/lexical_analysis.html>
|
||||
#
|
||||
# TODO(mbp): Maybe give a separate warning about the rarely-used
|
||||
# \a \b \v \f?
|
||||
#
|
||||
# TODO(mbp): We could give the column of the problem character, but
|
||||
# add_message doesn't seem to have a way to pass it through at present.
|
||||
i = 0
|
||||
while True:
|
||||
i = string_body.find('\\', i)
|
||||
if i == -1:
|
||||
break
|
||||
# There must be a next character; having a backslash at the end
|
||||
# of the string would be a SyntaxError.
|
||||
next_char = string_body[i+1]
|
||||
match = string_body[i:i+2]
|
||||
if next_char in self.UNICODE_ESCAPE_CHARACTERS:
|
||||
if 'u' in prefix:
|
||||
pass
|
||||
elif (_PY3K or self._unicode_literals) and 'b' not in prefix:
|
||||
pass # unicode by default
|
||||
else:
|
||||
self.add_message('anomalous-unicode-escape-in-string',
|
||||
line=start_row, args=(match, ))
|
||||
elif next_char not in self.ESCAPE_CHARACTERS:
|
||||
self.add_message('anomalous-backslash-in-string',
|
||||
line=start_row, args=(match, ))
|
||||
# Whether it was a valid escape or not, backslash followed by
|
||||
# another character can always be consumed whole: the second
|
||||
# character can never be the start of a new backslash escape.
|
||||
i += 2
|
||||
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(StringFormatChecker(linter))
|
||||
linter.register_checker(StringConstantChecker(linter))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,968 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2007, 2009-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2009 Mads Kiilerich <mads@kiilerich.com>
|
||||
# Copyright (c) 2010 Daniel Harding <dharding@gmail.com>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
|
||||
# Copyright (c) 2013-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Dmitry Pribysh <dmand@yandex.ru>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Radu Ciorba <radu@devrandom.ro>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016, 2018 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016-2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016-2017 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2016 Brian C. Lane <bcl@redhat.com>
|
||||
# Copyright (c) 2017-2018 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 ttenhoeve-aa <ttenhoeve@appannie.com>
|
||||
# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
|
||||
# Copyright (c) 2018 Brian Shaginaw <brian.shaginaw@warbyparker.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
# pylint: disable=W0611
|
||||
"""some functions that may be useful for various checkers
|
||||
"""
|
||||
import collections
|
||||
import functools
|
||||
try:
|
||||
from functools import singledispatch as singledispatch
|
||||
except ImportError:
|
||||
# pylint: disable=import-error
|
||||
from singledispatch import singledispatch as singledispatch
|
||||
try:
|
||||
from functools import lru_cache
|
||||
except ImportError:
|
||||
from backports.functools_lru_cache import lru_cache
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
import string
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from six.moves import map, builtins # pylint: disable=redefined-builtin
|
||||
|
||||
import astroid
|
||||
from astroid import bases as _bases
|
||||
from astroid import scoped_nodes
|
||||
|
||||
|
||||
BUILTINS_NAME = builtins.__name__
|
||||
COMP_NODE_TYPES = (astroid.ListComp, astroid.SetComp,
|
||||
astroid.DictComp, astroid.GeneratorExp)
|
||||
PY3K = sys.version_info[0] == 3
|
||||
|
||||
if not PY3K:
|
||||
EXCEPTIONS_MODULE = "exceptions"
|
||||
else:
|
||||
EXCEPTIONS_MODULE = "builtins"
|
||||
ABC_METHODS = set(('abc.abstractproperty', 'abc.abstractmethod',
|
||||
'abc.abstractclassmethod', 'abc.abstractstaticmethod'))
|
||||
ITER_METHOD = '__iter__'
|
||||
NEXT_METHOD = 'next' if six.PY2 else '__next__'
|
||||
GETITEM_METHOD = '__getitem__'
|
||||
SETITEM_METHOD = '__setitem__'
|
||||
DELITEM_METHOD = '__delitem__'
|
||||
CONTAINS_METHOD = '__contains__'
|
||||
KEYS_METHOD = 'keys'
|
||||
|
||||
# Dictionary which maps the number of expected parameters a
|
||||
# special method can have to a set of special methods.
|
||||
# The following keys are used to denote the parameters restrictions:
|
||||
#
|
||||
# * None: variable number of parameters
|
||||
# * number: exactly that number of parameters
|
||||
# * tuple: this are the odd ones. Basically it means that the function
|
||||
# can work with any number of arguments from that tuple,
|
||||
# although it's best to implement it in order to accept
|
||||
# all of them.
|
||||
_SPECIAL_METHODS_PARAMS = {
|
||||
None: ('__new__', '__init__', '__call__'),
|
||||
|
||||
0: ('__del__', '__repr__', '__str__', '__bytes__', '__hash__', '__bool__',
|
||||
'__dir__', '__len__', '__length_hint__', '__iter__', '__reversed__',
|
||||
'__neg__', '__pos__', '__abs__', '__invert__', '__complex__', '__int__',
|
||||
'__float__', '__neg__', '__pos__', '__abs__', '__complex__', '__int__',
|
||||
'__float__', '__index__', '__enter__', '__aenter__', '__getnewargs_ex__',
|
||||
'__getnewargs__', '__getstate__', '__reduce__', '__copy__',
|
||||
'__unicode__', '__nonzero__', '__await__', '__aiter__', '__anext__',
|
||||
'__fspath__'),
|
||||
|
||||
1: ('__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__',
|
||||
'__ge__', '__getattr__', '__getattribute__', '__delattr__',
|
||||
'__delete__', '__instancecheck__', '__subclasscheck__',
|
||||
'__getitem__', '__missing__', '__delitem__', '__contains__',
|
||||
'__add__', '__sub__', '__mul__', '__truediv__', '__floordiv__',
|
||||
'__mod__', '__divmod__', '__lshift__', '__rshift__', '__and__',
|
||||
'__xor__', '__or__', '__radd__', '__rsub__', '__rmul__', '__rtruediv__',
|
||||
'__rmod__', '__rdivmod__', '__rpow__', '__rlshift__', '__rrshift__',
|
||||
'__rand__', '__rxor__', '__ror__', '__iadd__', '__isub__', '__imul__',
|
||||
'__itruediv__', '__ifloordiv__', '__imod__', '__ilshift__',
|
||||
'__irshift__', '__iand__', '__ixor__', '__ior__', '__ipow__',
|
||||
'__setstate__', '__reduce_ex__', '__deepcopy__', '__cmp__',
|
||||
'__matmul__', '__rmatmul__', '__div__'),
|
||||
|
||||
2: ('__setattr__', '__get__', '__set__', '__setitem__', '__set_name__'),
|
||||
|
||||
3: ('__exit__', '__aexit__'),
|
||||
|
||||
(0, 1): ('__round__', ),
|
||||
}
|
||||
|
||||
SPECIAL_METHODS_PARAMS = {
|
||||
name: params
|
||||
for params, methods in _SPECIAL_METHODS_PARAMS.items()
|
||||
for name in methods
|
||||
}
|
||||
PYMETHODS = set(SPECIAL_METHODS_PARAMS)
|
||||
|
||||
|
||||
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, astroid.ExceptHandler):
|
||||
current = current.parent
|
||||
|
||||
return current and current is current.parent.name
|
||||
|
||||
|
||||
def get_all_elements(node):
|
||||
"""Recursively returns all atoms in nested lists and tuples."""
|
||||
if isinstance(node, (astroid.Tuple, astroid.List)):
|
||||
for child in node.elts:
|
||||
for e in get_all_elements(child):
|
||||
yield e
|
||||
else:
|
||||
yield node
|
||||
|
||||
|
||||
def clobber_in_except(node):
|
||||
"""Checks if an assignment node in an except handler clobbers an existing
|
||||
variable.
|
||||
|
||||
Returns (True, args for W0623) if assignment clobbers an existing variable,
|
||||
(False, None) otherwise.
|
||||
"""
|
||||
if isinstance(node, astroid.AssignAttr):
|
||||
return (True, (node.attrname, 'object %r' % (node.expr.as_string(),)))
|
||||
elif isinstance(node, astroid.AssignName):
|
||||
name = node.name
|
||||
if is_builtin(name):
|
||||
return (True, (name, 'builtins'))
|
||||
else:
|
||||
stmts = node.lookup(name)[1]
|
||||
if (stmts and not isinstance(stmts[0].assign_type(),
|
||||
(astroid.Assign, astroid.AugAssign,
|
||||
astroid.ExceptHandler))):
|
||||
return (True, (name, 'outer scope (line %s)' % stmts[0].fromlineno))
|
||||
return (False, None)
|
||||
|
||||
|
||||
def is_super(node):
|
||||
"""return True if the node is referencing the "super" builtin function
|
||||
"""
|
||||
if getattr(node, 'name', None) == 'super' and \
|
||||
node.root().name == BUILTINS_NAME:
|
||||
return True
|
||||
return False
|
||||
|
||||
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, 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, astroid.Raise):
|
||||
return True
|
||||
return False
|
||||
|
||||
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 == BUILTINS_NAME
|
||||
|
||||
def is_builtin(name):
|
||||
"""return true if <name> could be considered as a builtin defined by python
|
||||
"""
|
||||
return name in builtins or name in SPECIAL_BUILTINS
|
||||
|
||||
def is_defined_before(var_node):
|
||||
"""return True if the variable node is defined by a parent node (list,
|
||||
set, dict, or generator comprehension, lambda) or in a previous sibling
|
||||
node on the same line (statement_defining ; statement_using)
|
||||
"""
|
||||
varname = var_node.name
|
||||
_node = var_node.parent
|
||||
while _node:
|
||||
if isinstance(_node, COMP_NODE_TYPES):
|
||||
for ass_node in _node.nodes_of_class(astroid.AssignName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
elif isinstance(_node, astroid.For):
|
||||
for ass_node in _node.target.nodes_of_class(astroid.AssignName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
elif isinstance(_node, astroid.With):
|
||||
for expr, ids in _node.items:
|
||||
if expr.parent_of(var_node):
|
||||
break
|
||||
if (ids and
|
||||
isinstance(ids, astroid.AssignName) and
|
||||
ids.name == varname):
|
||||
return True
|
||||
elif isinstance(_node, (astroid.Lambda, astroid.FunctionDef)):
|
||||
if _node.args.is_argument(varname):
|
||||
# If the name is found inside a default value
|
||||
# of a function, then let the search continue
|
||||
# in the parent's tree.
|
||||
if _node.args.parent_of(var_node):
|
||||
try:
|
||||
_node.args.default_value(varname)
|
||||
_node = _node.parent
|
||||
continue
|
||||
except astroid.NoDefault:
|
||||
pass
|
||||
return True
|
||||
if getattr(_node, 'name', None) == varname:
|
||||
return True
|
||||
break
|
||||
elif isinstance(_node, astroid.ExceptHandler):
|
||||
if isinstance(_node.name, astroid.AssignName):
|
||||
ass_node = _node.name
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
_node = _node.parent
|
||||
# possibly multiple statements on the same line using semi colon separator
|
||||
stmt = var_node.statement()
|
||||
_node = stmt.previous_sibling()
|
||||
lineno = stmt.fromlineno
|
||||
while _node and _node.fromlineno == lineno:
|
||||
for ass_node in _node.nodes_of_class(astroid.AssignName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
for imp_node in _node.nodes_of_class((astroid.ImportFrom, astroid.Import)):
|
||||
if varname in [name[1] or name[0] for name in imp_node.names]:
|
||||
return True
|
||||
_node = _node.previous_sibling()
|
||||
return False
|
||||
|
||||
def is_func_default(node):
|
||||
"""return true if the given Name node is used in function default argument's
|
||||
value
|
||||
"""
|
||||
parent = node.scope()
|
||||
if isinstance(parent, astroid.FunctionDef):
|
||||
for default_node in parent.args.defaults:
|
||||
for default_name_node in default_node.nodes_of_class(astroid.Name):
|
||||
if default_name_node is node:
|
||||
return True
|
||||
return False
|
||||
|
||||
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, astroid.Decorators):
|
||||
return True
|
||||
if (parent.is_statement or
|
||||
isinstance(parent, (astroid.Lambda,
|
||||
scoped_nodes.ComprehensionScope,
|
||||
scoped_nodes.ListComp))):
|
||||
break
|
||||
parent = parent.parent
|
||||
return False
|
||||
|
||||
def is_ancestor_name(frame, node):
|
||||
"""return True if `frame` is a astroid.Class node with `node` in the
|
||||
subtree of its bases attribute
|
||||
"""
|
||||
try:
|
||||
bases = frame.bases
|
||||
except AttributeError:
|
||||
return False
|
||||
for base in bases:
|
||||
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 AssignName, Tuple or List node
|
||||
"""
|
||||
while node and isinstance(node, (astroid.AssignName,
|
||||
astroid.Tuple,
|
||||
astroid.List)):
|
||||
node = node.parent
|
||||
return node
|
||||
|
||||
|
||||
def overrides_a_method(class_node, name):
|
||||
"""return True if <name> is a method overridden from an ancestor"""
|
||||
for ancestor in class_node.ancestors():
|
||||
if name in ancestor and isinstance(ancestor[name], astroid.FunctionDef):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_messages(*messages):
|
||||
"""decorator to store messages that are handled by a checker method"""
|
||||
|
||||
def store_messages(func):
|
||||
func.checks_msgs = messages
|
||||
return func
|
||||
return store_messages
|
||||
|
||||
class IncompleteFormatString(Exception):
|
||||
"""A format string ended in the middle of a format specifier."""
|
||||
pass
|
||||
|
||||
class UnsupportedFormatCharacter(Exception):
|
||||
"""A format character in a format string is not one of the supported
|
||||
format characters."""
|
||||
def __init__(self, index):
|
||||
Exception.__init__(self, index)
|
||||
self.index = index
|
||||
|
||||
def parse_format_string(format_string):
|
||||
"""Parses a format string, returning a tuple of (keys, num_args), where keys
|
||||
is the set of mapping keys in the format string, and num_args is the number
|
||||
of arguments required by the format string. Raises
|
||||
IncompleteFormatString or UnsupportedFormatCharacter if a
|
||||
parse error occurs."""
|
||||
keys = set()
|
||||
num_args = 0
|
||||
def next_char(i):
|
||||
i += 1
|
||||
if i == len(format_string):
|
||||
raise IncompleteFormatString
|
||||
return (i, format_string[i])
|
||||
i = 0
|
||||
while i < len(format_string):
|
||||
char = format_string[i]
|
||||
if char == '%':
|
||||
i, char = next_char(i)
|
||||
# Parse the mapping key (optional).
|
||||
key = None
|
||||
if char == '(':
|
||||
depth = 1
|
||||
i, char = next_char(i)
|
||||
key_start = i
|
||||
while depth != 0:
|
||||
if char == '(':
|
||||
depth += 1
|
||||
elif char == ')':
|
||||
depth -= 1
|
||||
i, char = next_char(i)
|
||||
key_end = i - 1
|
||||
key = format_string[key_start:key_end]
|
||||
|
||||
# Parse the conversion flags (optional).
|
||||
while char in '#0- +':
|
||||
i, char = next_char(i)
|
||||
# Parse the minimum field width (optional).
|
||||
if char == '*':
|
||||
num_args += 1
|
||||
i, char = next_char(i)
|
||||
else:
|
||||
while char in string.digits:
|
||||
i, char = next_char(i)
|
||||
# Parse the precision (optional).
|
||||
if char == '.':
|
||||
i, char = next_char(i)
|
||||
if char == '*':
|
||||
num_args += 1
|
||||
i, char = next_char(i)
|
||||
else:
|
||||
while char in string.digits:
|
||||
i, char = next_char(i)
|
||||
# Parse the length modifier (optional).
|
||||
if char in 'hlL':
|
||||
i, char = next_char(i)
|
||||
# Parse the conversion type (mandatory).
|
||||
if PY3K:
|
||||
flags = 'diouxXeEfFgGcrs%a'
|
||||
else:
|
||||
flags = 'diouxXeEfFgGcrs%'
|
||||
if char not in flags:
|
||||
raise UnsupportedFormatCharacter(i)
|
||||
if key:
|
||||
keys.add(key)
|
||||
elif char != '%':
|
||||
num_args += 1
|
||||
i += 1
|
||||
return keys, num_args
|
||||
|
||||
|
||||
def is_attr_protected(attrname):
|
||||
"""return True if attribute name is protected (start with _ and some other
|
||||
details), False otherwise.
|
||||
"""
|
||||
return attrname[0] == '_' and attrname != '_' and not (
|
||||
attrname.startswith('__') and attrname.endswith('__'))
|
||||
|
||||
def node_frame_class(node):
|
||||
"""return klass node for a method node (or a staticmethod or a
|
||||
classmethod), return null otherwise
|
||||
"""
|
||||
klass = node.frame()
|
||||
|
||||
while klass is not None and not isinstance(klass, astroid.ClassDef):
|
||||
if klass.parent is None:
|
||||
klass = None
|
||||
else:
|
||||
klass = klass.parent.frame()
|
||||
|
||||
return klass
|
||||
|
||||
|
||||
def is_attr_private(attrname):
|
||||
"""Check that attribute name is private (at least two leading underscores,
|
||||
at most one trailing underscore)
|
||||
"""
|
||||
regex = re.compile('^_{2,}.*[^_]+_?$')
|
||||
return regex.match(attrname)
|
||||
|
||||
def get_argument_from_call(call_node, position=None, keyword=None):
|
||||
"""Returns the specified argument from a function call.
|
||||
|
||||
:param astroid.Call call_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.
|
||||
:rtype: astroid.Name
|
||||
:raises ValueError: if both position and keyword are None.
|
||||
:raises NoSuchArgumentError: if no argument at the provided position or with
|
||||
the provided keyword.
|
||||
"""
|
||||
if position is None and keyword is None:
|
||||
raise ValueError('Must specify at least one of: position or keyword.')
|
||||
if position is not None:
|
||||
try:
|
||||
return call_node.args[position]
|
||||
except IndexError:
|
||||
pass
|
||||
if keyword and call_node.keywords:
|
||||
for arg in call_node.keywords:
|
||||
if arg.arg == keyword:
|
||||
return arg.value
|
||||
|
||||
raise NoSuchArgumentError
|
||||
|
||||
def inherit_from_std_ex(node):
|
||||
"""
|
||||
Return true if the given class node is subclass of
|
||||
exceptions.Exception.
|
||||
"""
|
||||
if node.name in ('Exception', 'BaseException') \
|
||||
and node.root().name == EXCEPTIONS_MODULE:
|
||||
return True
|
||||
if not hasattr(node, 'ancestors'):
|
||||
return False
|
||||
return any(inherit_from_std_ex(parent)
|
||||
for parent in node.ancestors(recurs=True))
|
||||
|
||||
def error_of_type(handler, error_type):
|
||||
"""
|
||||
Check if the given exception handler catches
|
||||
the given error_type.
|
||||
|
||||
The *handler* parameter is a node, representing an ExceptHandler node.
|
||||
The *error_type* can be an exception, such as AttributeError,
|
||||
the name of an exception, or it can be a tuple of errors.
|
||||
The function will return True if the handler catches any of the
|
||||
given errors.
|
||||
"""
|
||||
def stringify_error(error):
|
||||
if not isinstance(error, six.string_types):
|
||||
return error.__name__
|
||||
return error
|
||||
|
||||
if not isinstance(error_type, tuple):
|
||||
error_type = (error_type, )
|
||||
expected_errors = {stringify_error(error) for error in error_type}
|
||||
if not handler.type:
|
||||
# bare except. While this indeed catches anything, if the desired errors
|
||||
# aren't specified directly, then we just ignore it.
|
||||
return False
|
||||
return handler.catch(expected_errors)
|
||||
|
||||
|
||||
def decorated_with_property(node):
|
||||
""" Detect if the given function node is decorated with a property. """
|
||||
if not node.decorators:
|
||||
return False
|
||||
for decorator in node.decorators.nodes:
|
||||
if not isinstance(decorator, astroid.Name):
|
||||
continue
|
||||
try:
|
||||
if _is_property_decorator(decorator):
|
||||
return True
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _is_property_decorator(decorator):
|
||||
for infered in decorator.infer():
|
||||
if isinstance(infered, astroid.ClassDef):
|
||||
if infered.root().name == BUILTINS_NAME and infered.name == 'property':
|
||||
return True
|
||||
for ancestor in infered.ancestors():
|
||||
if ancestor.name == 'property' and ancestor.root().name == BUILTINS_NAME:
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def decorated_with(func, qnames):
|
||||
"""Determine if the `func` node has a decorator with the qualified name `qname`."""
|
||||
decorators = func.decorators.nodes if func.decorators else []
|
||||
for decorator_node in decorators:
|
||||
try:
|
||||
if any(i is not None and i.qname() in qnames for i in decorator_node.infer()):
|
||||
return True
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def unimplemented_abstract_methods(node, is_abstract_cb=None):
|
||||
"""
|
||||
Get the unimplemented abstract methods for the given *node*.
|
||||
|
||||
A method can be considered abstract if the callback *is_abstract_cb*
|
||||
returns a ``True`` value. The check defaults to verifying that
|
||||
a method is decorated with abstract methods.
|
||||
The function will work only for new-style classes. For old-style
|
||||
classes, it will simply return an empty dictionary.
|
||||
For the rest of them, it will return a dictionary of abstract method
|
||||
names and their inferred objects.
|
||||
"""
|
||||
if is_abstract_cb is None:
|
||||
is_abstract_cb = functools.partial(
|
||||
decorated_with, qnames=ABC_METHODS)
|
||||
visited = {}
|
||||
try:
|
||||
mro = reversed(node.mro())
|
||||
except NotImplementedError:
|
||||
# Old style class, it will not have a mro.
|
||||
return {}
|
||||
except astroid.ResolveError:
|
||||
# Probably inconsistent hierarchy, don'try
|
||||
# to figure this out here.
|
||||
return {}
|
||||
for ancestor in mro:
|
||||
for obj in ancestor.values():
|
||||
infered = obj
|
||||
if isinstance(obj, astroid.AssignName):
|
||||
infered = safe_infer(obj)
|
||||
if not infered:
|
||||
# Might be an abstract function,
|
||||
# but since we don't have enough information
|
||||
# in order to take this decision, we're taking
|
||||
# the *safe* decision instead.
|
||||
if obj.name in visited:
|
||||
del visited[obj.name]
|
||||
continue
|
||||
if not isinstance(infered, astroid.FunctionDef):
|
||||
if obj.name in visited:
|
||||
del visited[obj.name]
|
||||
if isinstance(infered, astroid.FunctionDef):
|
||||
# It's critical to use the original name,
|
||||
# since after inferring, an object can be something
|
||||
# else than expected, as in the case of the
|
||||
# following assignment.
|
||||
#
|
||||
# class A:
|
||||
# def keys(self): pass
|
||||
# __iter__ = keys
|
||||
abstract = is_abstract_cb(infered)
|
||||
if abstract:
|
||||
visited[obj.name] = infered
|
||||
elif not abstract and obj.name in visited:
|
||||
del visited[obj.name]
|
||||
return visited
|
||||
|
||||
|
||||
def _import_node_context(node):
|
||||
"""Return the ExceptHandler or the TryExcept node in which the node is."""
|
||||
current = node
|
||||
ignores = (astroid.ExceptHandler, astroid.TryExcept)
|
||||
while current and not isinstance(current.parent, ignores):
|
||||
current = current.parent
|
||||
|
||||
if current and isinstance(current.parent, ignores):
|
||||
return current.parent
|
||||
return None
|
||||
|
||||
|
||||
def is_from_fallback_block(node):
|
||||
"""Check if the given node is from a fallback import block."""
|
||||
context = _import_node_context(node)
|
||||
if not context:
|
||||
return False
|
||||
|
||||
if isinstance(context, astroid.ExceptHandler):
|
||||
other_body = context.parent.body
|
||||
handlers = context.parent.handlers
|
||||
else:
|
||||
other_body = itertools.chain.from_iterable(
|
||||
handler.body for handler in context.handlers)
|
||||
handlers = context.handlers
|
||||
|
||||
has_fallback_imports = any(isinstance(import_node, (astroid.ImportFrom, astroid.Import))
|
||||
for import_node in other_body)
|
||||
ignores_import_error = _except_handlers_ignores_exception(handlers, ImportError)
|
||||
return ignores_import_error or has_fallback_imports
|
||||
|
||||
|
||||
def _except_handlers_ignores_exception(handlers, exception):
|
||||
func = functools.partial(error_of_type, error_type=(exception, ))
|
||||
return any(map(func, handlers))
|
||||
|
||||
|
||||
def get_exception_handlers(node, exception):
|
||||
"""Return the collections of handlers handling the exception in arguments.
|
||||
|
||||
Args:
|
||||
node (astroid.Raise): the node raising the exception.
|
||||
exception (builtin.Exception or str): exception or name of the exception.
|
||||
|
||||
Returns:
|
||||
generator: the collection of handlers that are handling the exception or None.
|
||||
|
||||
"""
|
||||
context = _import_node_context(node)
|
||||
if isinstance(context, astroid.TryExcept):
|
||||
return (_handler for _handler in context.handlers
|
||||
if error_of_type(_handler, exception))
|
||||
return None
|
||||
|
||||
|
||||
def is_node_inside_try_except(node):
|
||||
"""Check if the node is directly under a Try/Except statement.
|
||||
(but not under an ExceptHandler!)
|
||||
|
||||
Args:
|
||||
node (astroid.Raise): the node raising the exception.
|
||||
|
||||
Returns:
|
||||
bool: True if the node is inside a try/except statement, False otherwise.
|
||||
"""
|
||||
context = _import_node_context(node)
|
||||
return isinstance(context, astroid.TryExcept)
|
||||
|
||||
|
||||
def node_ignores_exception(node, exception):
|
||||
"""Check if the node is in a TryExcept which handles the given exception."""
|
||||
managing_handlers = get_exception_handlers(node, exception)
|
||||
if not managing_handlers:
|
||||
return False
|
||||
return any(managing_handlers)
|
||||
|
||||
|
||||
def class_is_abstract(node):
|
||||
"""return true if the given class node should be considered as an abstract
|
||||
class
|
||||
"""
|
||||
for method in node.methods():
|
||||
if method.parent.frame() is node:
|
||||
if method.is_abstract(pass_is_abstract=False):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _supports_protocol_method(value, attr):
|
||||
try:
|
||||
attributes = value.getattr(attr)
|
||||
except astroid.NotFoundError:
|
||||
return False
|
||||
|
||||
first = attributes[0]
|
||||
if isinstance(first, astroid.AssignName):
|
||||
if isinstance(first.parent.value, astroid.Const):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_comprehension(node):
|
||||
comprehensions = (astroid.ListComp,
|
||||
astroid.SetComp,
|
||||
astroid.DictComp,
|
||||
astroid.GeneratorExp)
|
||||
return isinstance(node, comprehensions)
|
||||
|
||||
|
||||
def _supports_mapping_protocol(value):
|
||||
return (
|
||||
_supports_protocol_method(value, GETITEM_METHOD)
|
||||
and _supports_protocol_method(value, KEYS_METHOD)
|
||||
)
|
||||
|
||||
|
||||
def _supports_membership_test_protocol(value):
|
||||
return _supports_protocol_method(value, CONTAINS_METHOD)
|
||||
|
||||
|
||||
def _supports_iteration_protocol(value):
|
||||
return (
|
||||
_supports_protocol_method(value, ITER_METHOD)
|
||||
or _supports_protocol_method(value, GETITEM_METHOD)
|
||||
)
|
||||
|
||||
|
||||
def _supports_getitem_protocol(value):
|
||||
return _supports_protocol_method(value, GETITEM_METHOD)
|
||||
|
||||
|
||||
def _supports_setitem_protocol(value):
|
||||
return _supports_protocol_method(value, SETITEM_METHOD)
|
||||
|
||||
|
||||
def _supports_delitem_protocol(value):
|
||||
return _supports_protocol_method(value, DELITEM_METHOD)
|
||||
|
||||
|
||||
def _is_abstract_class_name(name):
|
||||
lname = name.lower()
|
||||
is_mixin = lname.endswith('mixin')
|
||||
is_abstract = lname.startswith('abstract')
|
||||
is_base = lname.startswith('base') or lname.endswith('base')
|
||||
return is_mixin or is_abstract or is_base
|
||||
|
||||
|
||||
def is_inside_abstract_class(node):
|
||||
while node is not None:
|
||||
if isinstance(node, astroid.ClassDef):
|
||||
if class_is_abstract(node):
|
||||
return True
|
||||
name = getattr(node, 'name', None)
|
||||
if name is not None and _is_abstract_class_name(name):
|
||||
return True
|
||||
node = node.parent
|
||||
return False
|
||||
|
||||
|
||||
def _supports_protocol(value, protocol_callback):
|
||||
if isinstance(value, astroid.ClassDef):
|
||||
if not has_known_bases(value):
|
||||
return True
|
||||
# classobj can only be iterable if it has an iterable metaclass
|
||||
meta = value.metaclass()
|
||||
if meta is not None:
|
||||
if protocol_callback(meta):
|
||||
return True
|
||||
if isinstance(value, astroid.BaseInstance):
|
||||
if not has_known_bases(value):
|
||||
return True
|
||||
if value.has_dynamic_getattr():
|
||||
return True
|
||||
if protocol_callback(value):
|
||||
return True
|
||||
|
||||
# TODO: this is not needed in astroid 2.0, where we can
|
||||
# check the type using a virtual base class instead.
|
||||
if (isinstance(value, _bases.Proxy)
|
||||
and isinstance(value._proxied, astroid.BaseInstance)
|
||||
and has_known_bases(value._proxied)):
|
||||
value = value._proxied
|
||||
return protocol_callback(value)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_iterable(value):
|
||||
return _supports_protocol(value, _supports_iteration_protocol)
|
||||
|
||||
|
||||
def is_mapping(value):
|
||||
return _supports_protocol(value, _supports_mapping_protocol)
|
||||
|
||||
|
||||
def supports_membership_test(value):
|
||||
supported = _supports_protocol(value, _supports_membership_test_protocol)
|
||||
return supported or is_iterable(value)
|
||||
|
||||
|
||||
def supports_getitem(value):
|
||||
return _supports_protocol(value, _supports_getitem_protocol)
|
||||
|
||||
|
||||
def supports_setitem(value):
|
||||
return _supports_protocol(value, _supports_setitem_protocol)
|
||||
|
||||
|
||||
def supports_delitem(value):
|
||||
return _supports_protocol(value, _supports_delitem_protocol)
|
||||
|
||||
|
||||
# TODO(cpopa): deprecate these or leave them as aliases?
|
||||
@lru_cache(maxsize=1024)
|
||||
def safe_infer(node, context=None):
|
||||
"""Return the inferred value for the given node.
|
||||
|
||||
Return None if inference failed or if there is some ambiguity (more than
|
||||
one node has been inferred).
|
||||
"""
|
||||
try:
|
||||
inferit = node.infer(context=context)
|
||||
value = next(inferit)
|
||||
except astroid.InferenceError:
|
||||
return None
|
||||
try:
|
||||
next(inferit)
|
||||
return None # None if there is ambiguity on the inferred node
|
||||
except astroid.InferenceError:
|
||||
return None # there is some kind of ambiguity
|
||||
except StopIteration:
|
||||
return value
|
||||
|
||||
|
||||
def has_known_bases(klass, context=None):
|
||||
"""Return true if all base classes of a class could be inferred."""
|
||||
try:
|
||||
return klass._all_bases_known
|
||||
except AttributeError:
|
||||
pass
|
||||
for base in klass.bases:
|
||||
result = safe_infer(base, context=context)
|
||||
# TODO: check for A->B->A->B pattern in class structure too?
|
||||
if (not isinstance(result, astroid.ClassDef) or
|
||||
result is klass or
|
||||
not has_known_bases(result, context=context)):
|
||||
klass._all_bases_known = False
|
||||
return False
|
||||
klass._all_bases_known = True
|
||||
return True
|
||||
|
||||
|
||||
def is_none(node):
|
||||
return (node is None or
|
||||
(isinstance(node, astroid.Const) and node.value is None) or
|
||||
(isinstance(node, astroid.Name) and node.name == 'None')
|
||||
)
|
||||
|
||||
|
||||
def node_type(node):
|
||||
"""Return the inferred type for `node`
|
||||
|
||||
If there is more than one possible type, or if inferred type is YES or None,
|
||||
return None
|
||||
"""
|
||||
# check there is only one possible type for the assign node. Else we
|
||||
# don't handle it for now
|
||||
types = set()
|
||||
try:
|
||||
for var_type in node.infer():
|
||||
if var_type == astroid.Uninferable or is_none(var_type):
|
||||
continue
|
||||
types.add(var_type)
|
||||
if len(types) > 1:
|
||||
return None
|
||||
except astroid.InferenceError:
|
||||
return None
|
||||
return types.pop() if types else None
|
||||
|
||||
|
||||
def is_registered_in_singledispatch_function(node):
|
||||
"""Check if the given function node is a singledispatch function."""
|
||||
|
||||
singledispatch_qnames = (
|
||||
'functools.singledispatch',
|
||||
'singledispatch.singledispatch'
|
||||
)
|
||||
|
||||
if not isinstance(node, astroid.FunctionDef):
|
||||
return False
|
||||
|
||||
decorators = node.decorators.nodes if node.decorators else []
|
||||
for decorator in decorators:
|
||||
# func.register are function calls
|
||||
if not isinstance(decorator, astroid.Call):
|
||||
continue
|
||||
|
||||
func = decorator.func
|
||||
if not isinstance(func, astroid.Attribute) or func.attrname != 'register':
|
||||
continue
|
||||
|
||||
try:
|
||||
func_def = next(func.expr.infer())
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
if isinstance(func_def, astroid.FunctionDef):
|
||||
return decorated_with(func_def, singledispatch_qnames)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_node_last_lineno(node):
|
||||
"""
|
||||
Get the last lineno of the given node. For a simple statement this will just be node.lineno,
|
||||
but for a node that has child statements (e.g. a method) this will be the lineno of the last
|
||||
child statement recursively.
|
||||
"""
|
||||
# 'finalbody' is always the last clause in a try statement, if present
|
||||
if getattr(node, 'finalbody', False):
|
||||
return get_node_last_lineno(node.finalbody[-1])
|
||||
# For if, while, and for statements 'orelse' is always the last clause.
|
||||
# For try statements 'orelse' is the last in the absence of a 'finalbody'
|
||||
if getattr(node, 'orelse', False):
|
||||
return get_node_last_lineno(node.orelse[-1])
|
||||
# try statements have the 'handlers' last if there is no 'orelse' or 'finalbody'
|
||||
if getattr(node, 'handlers', False):
|
||||
return get_node_last_lineno(node.handlers[-1])
|
||||
# All compound statements have a 'body'
|
||||
if getattr(node, 'body', False):
|
||||
return get_node_last_lineno(node.body[-1])
|
||||
# Not a compound statement
|
||||
return node.lineno
|
||||
|
||||
|
||||
def in_comprehension(node):
|
||||
"""Return True if the given node is in a comprehension"""
|
||||
curnode = node
|
||||
while curnode.parent:
|
||||
curnode = curnode.parent
|
||||
if is_comprehension(curnode):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_enum_class(node):
|
||||
"""Check if a class definition defines an Enum class.
|
||||
|
||||
:param node: The class node to check.
|
||||
:type node: astroid.ClassDef
|
||||
|
||||
:returns: True if the given node represents an Enum class. False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
for base in node.bases:
|
||||
try:
|
||||
inferred_bases = base.inferred()
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
for ancestor in inferred_bases:
|
||||
if not isinstance(ancestor, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
if ancestor.name == 'Enum' and ancestor.root().name == 'enum':
|
||||
return True
|
||||
|
||||
return False
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,855 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2008 pyves@crater.logilab.fr <pyves@crater.logilab.fr>
|
||||
# Copyright (c) 2010 Julien Jehannet <julien.jehannet@logilab.fr>
|
||||
# Copyright (c) 2013 Google, Inc.
|
||||
# Copyright (c) 2013 John McGehee <jmcgehee@altera.com>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Aru Sahni <arusahni@gmail.com>
|
||||
# Copyright (c) 2015 John Kirkham <jakirkham@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Erik <erik.eriksson@yahoo.com>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 ahirnish <ahirnish@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities for Pylint configuration :
|
||||
|
||||
* pylintrc
|
||||
* pylint.d (PYLINTHOME)
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
# TODO(cpopa): this module contains the logic for the
|
||||
# configuration parser and for the command line parser,
|
||||
# but it's really coupled to optparse's internals.
|
||||
# The code was copied almost verbatim from logilab.common,
|
||||
# in order to not depend on it anymore and it will definitely
|
||||
# need a cleanup. It could be completely reengineered as well.
|
||||
|
||||
import contextlib
|
||||
import collections
|
||||
import copy
|
||||
import io
|
||||
import optparse
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import configparser
|
||||
from six.moves import range
|
||||
|
||||
from pylint import utils
|
||||
|
||||
|
||||
USER_HOME = os.path.expanduser('~')
|
||||
if 'PYLINTHOME' in os.environ:
|
||||
PYLINT_HOME = os.environ['PYLINTHOME']
|
||||
if USER_HOME == '~':
|
||||
USER_HOME = os.path.dirname(PYLINT_HOME)
|
||||
elif USER_HOME == '~':
|
||||
PYLINT_HOME = ".pylint.d"
|
||||
else:
|
||||
PYLINT_HOME = os.path.join(USER_HOME, '.pylint.d')
|
||||
|
||||
|
||||
def _get_pdata_path(base_name, recurs):
|
||||
base_name = base_name.replace(os.sep, '_')
|
||||
return os.path.join(PYLINT_HOME, "%s%s%s"%(base_name, recurs, '.stats'))
|
||||
|
||||
|
||||
def load_results(base):
|
||||
data_file = _get_pdata_path(base, 1)
|
||||
try:
|
||||
with open(data_file, _PICK_LOAD) as stream:
|
||||
return pickle.load(stream)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return {}
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
_PICK_DUMP, _PICK_LOAD = 'w', 'r'
|
||||
else:
|
||||
_PICK_DUMP, _PICK_LOAD = 'wb', 'rb'
|
||||
|
||||
def save_results(results, base):
|
||||
if not os.path.exists(PYLINT_HOME):
|
||||
try:
|
||||
os.mkdir(PYLINT_HOME)
|
||||
except OSError:
|
||||
print('Unable to create directory %s' % PYLINT_HOME, file=sys.stderr)
|
||||
data_file = _get_pdata_path(base, 1)
|
||||
try:
|
||||
with open(data_file, _PICK_DUMP) as stream:
|
||||
pickle.dump(results, stream)
|
||||
except (IOError, OSError) as ex:
|
||||
print('Unable to create file %s: %s' % (data_file, ex), file=sys.stderr)
|
||||
|
||||
|
||||
def find_pylintrc():
|
||||
"""search the pylint rc file and return its path if it find it, else None
|
||||
"""
|
||||
# is there a pylint rc file in the current directory ?
|
||||
if os.path.exists('pylintrc'):
|
||||
return os.path.abspath('pylintrc')
|
||||
if os.path.exists('.pylintrc'):
|
||||
return os.path.abspath('.pylintrc')
|
||||
if os.path.isfile('__init__.py'):
|
||||
curdir = os.path.abspath(os.getcwd())
|
||||
while os.path.isfile(os.path.join(curdir, '__init__.py')):
|
||||
curdir = os.path.abspath(os.path.join(curdir, '..'))
|
||||
if os.path.isfile(os.path.join(curdir, 'pylintrc')):
|
||||
return os.path.join(curdir, 'pylintrc')
|
||||
if os.path.isfile(os.path.join(curdir, '.pylintrc')):
|
||||
return os.path.join(curdir, '.pylintrc')
|
||||
if 'PYLINTRC' in os.environ and os.path.exists(os.environ['PYLINTRC']):
|
||||
pylintrc = os.environ['PYLINTRC']
|
||||
else:
|
||||
user_home = os.path.expanduser('~')
|
||||
if user_home == '~' or user_home == '/root':
|
||||
pylintrc = ".pylintrc"
|
||||
else:
|
||||
pylintrc = os.path.join(user_home, '.pylintrc')
|
||||
if not os.path.isfile(pylintrc):
|
||||
pylintrc = os.path.join(user_home, '.config', 'pylintrc')
|
||||
if not os.path.isfile(pylintrc):
|
||||
if os.path.isfile('/etc/pylintrc'):
|
||||
pylintrc = '/etc/pylintrc'
|
||||
else:
|
||||
pylintrc = None
|
||||
return pylintrc
|
||||
|
||||
PYLINTRC = find_pylintrc()
|
||||
|
||||
ENV_HELP = '''
|
||||
The following environment variables are used:
|
||||
* PYLINTHOME
|
||||
Path to the directory where the persistent for the 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. See the documentation for the method used
|
||||
to search for configuration file.
|
||||
''' % globals()
|
||||
|
||||
|
||||
class UnsupportedAction(Exception):
|
||||
"""raised by set_option when it doesn't know what to do for an action"""
|
||||
|
||||
|
||||
def _multiple_choice_validator(choices, name, value):
|
||||
values = utils._check_csv(value)
|
||||
for csv_value in values:
|
||||
if csv_value not in choices:
|
||||
msg = "option %s: invalid value: %r, should be in %s"
|
||||
raise optparse.OptionValueError(msg % (name, csv_value, choices))
|
||||
return values
|
||||
|
||||
|
||||
def _choice_validator(choices, name, value):
|
||||
if value not in choices:
|
||||
msg = "option %s: invalid value: %r, should be in %s"
|
||||
raise optparse.OptionValueError(msg % (name, value, choices))
|
||||
return value
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _csv_validator(_, name, value):
|
||||
return utils._check_csv(value)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _regexp_validator(_, name, value):
|
||||
if hasattr(value, 'pattern'):
|
||||
return value
|
||||
return re.compile(value)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _regexp_csv_validator(_, name, value):
|
||||
return [_regexp_validator(_, name, val) for val in _csv_validator(_, name, value)]
|
||||
|
||||
def _yn_validator(opt, _, value):
|
||||
if isinstance(value, int):
|
||||
return bool(value)
|
||||
if value in ('y', 'yes'):
|
||||
return True
|
||||
if value in ('n', 'no'):
|
||||
return False
|
||||
msg = "option %s: invalid yn value %r, should be in (y, yes, n, no)"
|
||||
raise optparse.OptionValueError(msg % (opt, value))
|
||||
|
||||
|
||||
def _non_empty_string_validator(opt, _, value):
|
||||
if not value:
|
||||
msg = "indent string can't be empty."
|
||||
raise optparse.OptionValueError(msg)
|
||||
return utils._unquote(value)
|
||||
|
||||
|
||||
VALIDATORS = {
|
||||
'string': utils._unquote,
|
||||
'int': int,
|
||||
'regexp': re.compile,
|
||||
'regexp_csv': _regexp_csv_validator,
|
||||
'csv': _csv_validator,
|
||||
'yn': _yn_validator,
|
||||
'choice': lambda opt, name, value: _choice_validator(opt['choices'], name, value),
|
||||
'multiple_choice': lambda opt, name, value: _multiple_choice_validator(opt['choices'],
|
||||
name, value),
|
||||
'non_empty_string': _non_empty_string_validator,
|
||||
}
|
||||
|
||||
def _call_validator(opttype, optdict, option, value):
|
||||
if opttype not in VALIDATORS:
|
||||
raise Exception('Unsupported type "%s"' % opttype)
|
||||
try:
|
||||
return VALIDATORS[opttype](optdict, option, value)
|
||||
except TypeError:
|
||||
try:
|
||||
return VALIDATORS[opttype](value)
|
||||
except Exception:
|
||||
raise optparse.OptionValueError('%s value (%r) should be of type %s' %
|
||||
(option, value, opttype))
|
||||
|
||||
|
||||
def _validate(value, optdict, name=''):
|
||||
"""return a validated value for an option according to its type
|
||||
|
||||
optional argument name is only used for error message formatting
|
||||
"""
|
||||
try:
|
||||
_type = optdict['type']
|
||||
except KeyError:
|
||||
# FIXME
|
||||
return value
|
||||
return _call_validator(_type, optdict, name, value)
|
||||
|
||||
|
||||
def _level_options(group, outputlevel):
|
||||
return [option for option in group.option_list
|
||||
if (getattr(option, 'level', 0) or 0) <= outputlevel
|
||||
and option.help is not optparse.SUPPRESS_HELP]
|
||||
|
||||
|
||||
def _expand_default(self, option):
|
||||
"""Patch OptionParser.expand_default with custom behaviour
|
||||
|
||||
This will handle defaults to avoid overriding values in the
|
||||
configuration file.
|
||||
"""
|
||||
if self.parser is None or not self.default_tag:
|
||||
return option.help
|
||||
optname = option._long_opts[0][2:]
|
||||
try:
|
||||
provider = self.parser.options_manager._all_options[optname]
|
||||
except KeyError:
|
||||
value = None
|
||||
else:
|
||||
optdict = provider.get_option_def(optname)
|
||||
optname = provider.option_attrname(optname, optdict)
|
||||
value = getattr(provider.config, optname, optdict)
|
||||
value = utils._format_option_value(optdict, value)
|
||||
if value is optparse.NO_DEFAULT or not value:
|
||||
value = self.NO_DEFAULT_VALUE
|
||||
return option.help.replace(self.default_tag, str(value))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_optparse():
|
||||
orig_default = optparse.HelpFormatter
|
||||
try:
|
||||
optparse.HelpFormatter.expand_default = _expand_default
|
||||
yield
|
||||
finally:
|
||||
optparse.HelpFormatter.expand_default = orig_default
|
||||
|
||||
|
||||
def _multiple_choices_validating_option(opt, name, value):
|
||||
return _multiple_choice_validator(opt.choices, name, value)
|
||||
|
||||
|
||||
class Option(optparse.Option):
|
||||
TYPES = optparse.Option.TYPES + ('regexp', 'regexp_csv', 'csv', 'yn',
|
||||
'multiple_choice',
|
||||
'non_empty_string')
|
||||
ATTRS = optparse.Option.ATTRS + ['hide', 'level']
|
||||
TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
|
||||
TYPE_CHECKER['regexp'] = _regexp_validator
|
||||
TYPE_CHECKER['regexp_csv'] = _regexp_csv_validator
|
||||
TYPE_CHECKER['csv'] = _csv_validator
|
||||
TYPE_CHECKER['yn'] = _yn_validator
|
||||
TYPE_CHECKER['multiple_choice'] = _multiple_choices_validating_option
|
||||
TYPE_CHECKER['non_empty_string'] = _non_empty_string_validator
|
||||
|
||||
def __init__(self, *opts, **attrs):
|
||||
optparse.Option.__init__(self, *opts, **attrs)
|
||||
if hasattr(self, "hide") and self.hide:
|
||||
self.help = optparse.SUPPRESS_HELP
|
||||
|
||||
def _check_choice(self):
|
||||
if self.type in ("choice", "multiple_choice"):
|
||||
if self.choices is None:
|
||||
raise optparse.OptionError(
|
||||
"must supply a list of choices for type 'choice'", self)
|
||||
elif not isinstance(self.choices, (tuple, list)):
|
||||
raise optparse.OptionError(
|
||||
"choices must be a list of strings ('%s' supplied)"
|
||||
% str(type(self.choices)).split("'")[1], self)
|
||||
elif self.choices is not None:
|
||||
raise optparse.OptionError(
|
||||
"must not supply choices for type %r" % self.type, self)
|
||||
optparse.Option.CHECK_METHODS[2] = _check_choice
|
||||
|
||||
def process(self, opt, value, values, parser):
|
||||
# First, convert the value(s) to the right type. Howl if any
|
||||
# value(s) are bogus.
|
||||
value = self.convert_value(opt, value)
|
||||
if self.type == 'named':
|
||||
existent = getattr(values, self.dest)
|
||||
if existent:
|
||||
existent.update(value)
|
||||
value = existent
|
||||
# And then take whatever action is expected of us.
|
||||
# This is a separate method to make life easier for
|
||||
# subclasses to add new actions.
|
||||
return self.take_action(
|
||||
self.action, self.dest, opt, value, values, parser)
|
||||
|
||||
|
||||
class OptionParser(optparse.OptionParser):
|
||||
|
||||
def __init__(self, option_class, *args, **kwargs):
|
||||
optparse.OptionParser.__init__(self, option_class=Option, *args, **kwargs)
|
||||
|
||||
def format_option_help(self, formatter=None):
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
outputlevel = getattr(formatter, 'output_level', 0)
|
||||
formatter.store_option_strings(self)
|
||||
result = []
|
||||
result.append(formatter.format_heading("Options"))
|
||||
formatter.indent()
|
||||
if self.option_list:
|
||||
result.append(optparse.OptionContainer.format_option_help(self, formatter))
|
||||
result.append("\n")
|
||||
for group in self.option_groups:
|
||||
if group.level <= outputlevel and (
|
||||
group.description or _level_options(group, outputlevel)):
|
||||
result.append(group.format_help(formatter))
|
||||
result.append("\n")
|
||||
formatter.dedent()
|
||||
# Drop the last "\n", or the header if no options or option groups:
|
||||
return "".join(result[:-1])
|
||||
|
||||
def _match_long_opt(self, opt):
|
||||
"""Disable abbreviations."""
|
||||
if opt not in self._long_opt:
|
||||
raise optparse.BadOptionError(opt)
|
||||
return opt
|
||||
|
||||
|
||||
# pylint: disable=abstract-method; by design?
|
||||
class _ManHelpFormatter(optparse.HelpFormatter):
|
||||
|
||||
def __init__(self, indent_increment=0, max_help_position=24,
|
||||
width=79, short_first=0):
|
||||
optparse.HelpFormatter.__init__(
|
||||
self, indent_increment, max_help_position, width, short_first)
|
||||
|
||||
def format_heading(self, heading):
|
||||
return '.SH %s\n' % heading.upper()
|
||||
|
||||
def format_description(self, description):
|
||||
return description
|
||||
|
||||
def format_option(self, option):
|
||||
try:
|
||||
optstring = option.option_strings
|
||||
except AttributeError:
|
||||
optstring = self.format_option_strings(option)
|
||||
if option.help:
|
||||
help_text = self.expand_default(option)
|
||||
help = ' '.join([l.strip() for l in help_text.splitlines()])
|
||||
else:
|
||||
help = ''
|
||||
return '''.IP "%s"
|
||||
%s
|
||||
''' % (optstring, help)
|
||||
|
||||
def format_head(self, optparser, pkginfo, section=1):
|
||||
long_desc = ""
|
||||
try:
|
||||
pgm = optparser._get_prog_name()
|
||||
except AttributeError:
|
||||
# py >= 2.4.X (dunno which X exactly, at least 2)
|
||||
pgm = optparser.get_prog_name()
|
||||
short_desc = self.format_short_description(pgm, pkginfo.description)
|
||||
if hasattr(pkginfo, "long_desc"):
|
||||
long_desc = self.format_long_description(pgm, pkginfo.long_desc)
|
||||
return '%s\n%s\n%s\n%s' % (self.format_title(pgm, section),
|
||||
short_desc, self.format_synopsis(pgm),
|
||||
long_desc)
|
||||
|
||||
@staticmethod
|
||||
def format_title(pgm, section):
|
||||
date = '-'.join(str(num) for num in time.localtime()[:3])
|
||||
return '.TH %s %s "%s" %s' % (pgm, section, date, pgm)
|
||||
|
||||
@staticmethod
|
||||
def format_short_description(pgm, short_desc):
|
||||
return '''.SH NAME
|
||||
.B %s
|
||||
\\- %s
|
||||
''' % (pgm, short_desc.strip())
|
||||
|
||||
@staticmethod
|
||||
def format_synopsis(pgm):
|
||||
return '''.SH SYNOPSIS
|
||||
.B %s
|
||||
[
|
||||
.I OPTIONS
|
||||
] [
|
||||
.I <arguments>
|
||||
]
|
||||
''' % pgm
|
||||
|
||||
@staticmethod
|
||||
def format_long_description(pgm, long_desc):
|
||||
long_desc = '\n'.join(line.lstrip()
|
||||
for line in long_desc.splitlines())
|
||||
long_desc = long_desc.replace('\n.\n', '\n\n')
|
||||
if long_desc.lower().startswith(pgm):
|
||||
long_desc = long_desc[len(pgm):]
|
||||
return '''.SH DESCRIPTION
|
||||
.B %s
|
||||
%s
|
||||
''' % (pgm, long_desc.strip())
|
||||
|
||||
@staticmethod
|
||||
def format_tail(pkginfo):
|
||||
tail = '''.SH SEE ALSO
|
||||
/usr/share/doc/pythonX.Y-%s/
|
||||
|
||||
.SH BUGS
|
||||
Please report bugs on the project\'s mailing list:
|
||||
%s
|
||||
|
||||
.SH AUTHOR
|
||||
%s <%s>
|
||||
''' % (getattr(pkginfo, 'debian_name', pkginfo.modname),
|
||||
pkginfo.mailinglist, pkginfo.author, pkginfo.author_email)
|
||||
|
||||
if hasattr(pkginfo, "copyright"):
|
||||
tail += '''
|
||||
.SH COPYRIGHT
|
||||
%s
|
||||
''' % pkginfo.copyright
|
||||
|
||||
return tail
|
||||
|
||||
|
||||
class OptionsManagerMixIn(object):
|
||||
"""Handle configuration from both a configuration file and command line options"""
|
||||
|
||||
def __init__(self, usage, config_file=None, version=None, quiet=0):
|
||||
self.config_file = config_file
|
||||
self.reset_parsers(usage, version=version)
|
||||
# list of registered options providers
|
||||
self.options_providers = []
|
||||
# dictionary associating option name to checker
|
||||
self._all_options = collections.OrderedDict()
|
||||
self._short_options = {}
|
||||
self._nocallback_options = {}
|
||||
self._mygroups = {}
|
||||
# verbosity
|
||||
self.quiet = quiet
|
||||
self._maxlevel = 0
|
||||
|
||||
def reset_parsers(self, usage='', version=None):
|
||||
# configuration file parser
|
||||
self.cfgfile_parser = configparser.ConfigParser(inline_comment_prefixes=('#', ';'))
|
||||
# command line parser
|
||||
self.cmdline_parser = OptionParser(Option, usage=usage, version=version)
|
||||
self.cmdline_parser.options_manager = self
|
||||
self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS)
|
||||
|
||||
def register_options_provider(self, provider, own_group=True):
|
||||
"""register an options provider"""
|
||||
assert provider.priority <= 0, "provider's priority can't be >= 0"
|
||||
for i in range(len(self.options_providers)):
|
||||
if provider.priority > self.options_providers[i].priority:
|
||||
self.options_providers.insert(i, provider)
|
||||
break
|
||||
else:
|
||||
self.options_providers.append(provider)
|
||||
non_group_spec_options = [option for option in provider.options
|
||||
if 'group' not in option[1]]
|
||||
groups = getattr(provider, 'option_groups', ())
|
||||
if own_group and non_group_spec_options:
|
||||
self.add_option_group(provider.name.upper(), provider.__doc__,
|
||||
non_group_spec_options, provider)
|
||||
else:
|
||||
for opt, optdict in non_group_spec_options:
|
||||
self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
|
||||
for gname, gdoc in groups:
|
||||
gname = gname.upper()
|
||||
goptions = [option for option in provider.options
|
||||
if option[1].get('group', '').upper() == gname]
|
||||
self.add_option_group(gname, gdoc, goptions, provider)
|
||||
|
||||
def add_option_group(self, group_name, _, options, provider):
|
||||
# add option group to the command line parser
|
||||
if group_name in self._mygroups:
|
||||
group = self._mygroups[group_name]
|
||||
else:
|
||||
group = optparse.OptionGroup(self.cmdline_parser,
|
||||
title=group_name.capitalize())
|
||||
self.cmdline_parser.add_option_group(group)
|
||||
group.level = provider.level
|
||||
self._mygroups[group_name] = group
|
||||
# add section to the config file
|
||||
if group_name != "DEFAULT" and \
|
||||
group_name not in self.cfgfile_parser._sections:
|
||||
self.cfgfile_parser.add_section(group_name)
|
||||
# add provider's specific options
|
||||
for opt, optdict in options:
|
||||
self.add_optik_option(provider, group, opt, optdict)
|
||||
|
||||
def add_optik_option(self, provider, optikcontainer, opt, optdict):
|
||||
args, optdict = self.optik_option(provider, opt, optdict)
|
||||
option = optikcontainer.add_option(*args, **optdict)
|
||||
self._all_options[opt] = provider
|
||||
self._maxlevel = max(self._maxlevel, option.level or 0)
|
||||
|
||||
def optik_option(self, provider, opt, optdict):
|
||||
"""get our personal option definition and return a suitable form for
|
||||
use with optik/optparse
|
||||
"""
|
||||
optdict = copy.copy(optdict)
|
||||
if 'action' in optdict:
|
||||
self._nocallback_options[provider] = opt
|
||||
else:
|
||||
optdict['action'] = 'callback'
|
||||
optdict['callback'] = self.cb_set_provider_option
|
||||
# default is handled here and *must not* be given to optik if you
|
||||
# want the whole machinery to work
|
||||
if 'default' in optdict:
|
||||
if ('help' in optdict
|
||||
and optdict.get('default') is not None
|
||||
and optdict['action'] not in ('store_true', 'store_false')):
|
||||
optdict['help'] += ' [current: %default]'
|
||||
del optdict['default']
|
||||
args = ['--' + str(opt)]
|
||||
if 'short' in optdict:
|
||||
self._short_options[optdict['short']] = opt
|
||||
args.append('-' + optdict['short'])
|
||||
del optdict['short']
|
||||
# cleanup option definition dict before giving it to optik
|
||||
for key in list(optdict.keys()):
|
||||
if key not in self._optik_option_attrs:
|
||||
optdict.pop(key)
|
||||
return args, optdict
|
||||
|
||||
def cb_set_provider_option(self, option, opt, value, parser):
|
||||
"""optik callback for option setting"""
|
||||
if opt.startswith('--'):
|
||||
# remove -- on long option
|
||||
opt = opt[2:]
|
||||
else:
|
||||
# short option, get its long equivalent
|
||||
opt = self._short_options[opt[1:]]
|
||||
# trick since we can't set action='store_true' on options
|
||||
if value is None:
|
||||
value = 1
|
||||
self.global_set_option(opt, value)
|
||||
|
||||
def global_set_option(self, opt, value):
|
||||
"""set option on the correct option provider"""
|
||||
self._all_options[opt].set_option(opt, value)
|
||||
|
||||
def generate_config(self, stream=None, skipsections=(), encoding=None):
|
||||
"""write a configuration file according to the current configuration
|
||||
into the given stream or stdout
|
||||
"""
|
||||
options_by_section = {}
|
||||
sections = []
|
||||
for provider in self.options_providers:
|
||||
for section, options in provider.options_by_section():
|
||||
if section is None:
|
||||
section = provider.name
|
||||
if section in skipsections:
|
||||
continue
|
||||
options = [(n, d, v) for (n, d, v) in options
|
||||
if d.get('type') is not None
|
||||
and not d.get('deprecated')]
|
||||
if not options:
|
||||
continue
|
||||
if section not in sections:
|
||||
sections.append(section)
|
||||
alloptions = options_by_section.setdefault(section, [])
|
||||
alloptions += options
|
||||
stream = stream or sys.stdout
|
||||
encoding = utils._get_encoding(encoding, stream)
|
||||
printed = False
|
||||
for section in sections:
|
||||
if printed:
|
||||
print('\n', file=stream)
|
||||
utils.format_section(stream, section.upper(),
|
||||
sorted(options_by_section[section]),
|
||||
encoding)
|
||||
printed = True
|
||||
|
||||
def generate_manpage(self, pkginfo, section=1, stream=None):
|
||||
with _patch_optparse():
|
||||
_generate_manpage(self.cmdline_parser, pkginfo,
|
||||
section, stream=stream or sys.stdout,
|
||||
level=self._maxlevel)
|
||||
|
||||
def load_provider_defaults(self):
|
||||
"""initialize configuration using default values"""
|
||||
for provider in self.options_providers:
|
||||
provider.load_defaults()
|
||||
|
||||
def read_config_file(self, config_file=None):
|
||||
"""read the configuration file but do not load it (i.e. dispatching
|
||||
values to each options provider)
|
||||
"""
|
||||
helplevel = 1
|
||||
while helplevel <= self._maxlevel:
|
||||
opt = '-'.join(['long'] * helplevel) + '-help'
|
||||
if opt in self._all_options:
|
||||
break # already processed
|
||||
# pylint: disable=unused-argument
|
||||
def helpfunc(option, opt, val, p, level=helplevel):
|
||||
print(self.help(level))
|
||||
sys.exit(0)
|
||||
helpmsg = '%s verbose help.' % ' '.join(['more'] * helplevel)
|
||||
optdict = {'action': 'callback', 'callback': helpfunc,
|
||||
'help': helpmsg}
|
||||
provider = self.options_providers[0]
|
||||
self.add_optik_option(provider, self.cmdline_parser, opt, optdict)
|
||||
provider.options += ((opt, optdict),)
|
||||
helplevel += 1
|
||||
if config_file is None:
|
||||
config_file = self.config_file
|
||||
if config_file is not None:
|
||||
config_file = os.path.expanduser(config_file)
|
||||
if not os.path.exists(config_file):
|
||||
raise IOError("The config file {:s} doesn't exist!".format(config_file))
|
||||
|
||||
use_config_file = config_file and os.path.exists(config_file)
|
||||
if use_config_file:
|
||||
parser = self.cfgfile_parser
|
||||
|
||||
# Use this encoding in order to strip the BOM marker, if any.
|
||||
with io.open(config_file, 'r', encoding='utf_8_sig') as fp:
|
||||
parser.read_file(fp)
|
||||
|
||||
# normalize sections'title
|
||||
for sect, values in list(parser._sections.items()):
|
||||
if not sect.isupper() and values:
|
||||
parser._sections[sect.upper()] = values
|
||||
|
||||
if self.quiet:
|
||||
return
|
||||
|
||||
if use_config_file:
|
||||
msg = 'Using config file {0}'.format(os.path.abspath(config_file))
|
||||
else:
|
||||
msg = 'No config file found, using default configuration'
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
def load_config_file(self):
|
||||
"""dispatch values previously read from a configuration file to each
|
||||
options provider)
|
||||
"""
|
||||
parser = self.cfgfile_parser
|
||||
for section in parser.sections():
|
||||
for option, value in parser.items(section):
|
||||
try:
|
||||
self.global_set_option(option, value)
|
||||
except (KeyError, optparse.OptionError):
|
||||
# TODO handle here undeclared options appearing in the config file
|
||||
continue
|
||||
|
||||
def load_configuration(self, **kwargs):
|
||||
"""override configuration according to given parameters"""
|
||||
return self.load_configuration_from_config(kwargs)
|
||||
|
||||
def load_configuration_from_config(self, config):
|
||||
for opt, opt_value in config.items():
|
||||
opt = opt.replace('_', '-')
|
||||
provider = self._all_options[opt]
|
||||
provider.set_option(opt, opt_value)
|
||||
|
||||
def load_command_line_configuration(self, args=None):
|
||||
"""Override configuration according to command line parameters
|
||||
|
||||
return additional arguments
|
||||
"""
|
||||
with _patch_optparse():
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
else:
|
||||
args = list(args)
|
||||
(options, args) = self.cmdline_parser.parse_args(args=args)
|
||||
for provider in self._nocallback_options:
|
||||
config = provider.config
|
||||
for attr in config.__dict__.keys():
|
||||
value = getattr(options, attr, None)
|
||||
if value is None:
|
||||
continue
|
||||
setattr(config, attr, value)
|
||||
return args
|
||||
|
||||
def add_help_section(self, title, description, level=0):
|
||||
"""add a dummy option section for help purpose """
|
||||
group = optparse.OptionGroup(self.cmdline_parser,
|
||||
title=title.capitalize(),
|
||||
description=description)
|
||||
group.level = level
|
||||
self._maxlevel = max(self._maxlevel, level)
|
||||
self.cmdline_parser.add_option_group(group)
|
||||
|
||||
def help(self, level=0):
|
||||
"""return the usage string for available options """
|
||||
self.cmdline_parser.formatter.output_level = level
|
||||
with _patch_optparse():
|
||||
return self.cmdline_parser.format_help()
|
||||
|
||||
|
||||
class OptionsProviderMixIn(object):
|
||||
"""Mixin to provide options to an OptionsManager"""
|
||||
|
||||
# those attributes should be overridden
|
||||
priority = -1
|
||||
name = 'default'
|
||||
options = ()
|
||||
level = 0
|
||||
|
||||
def __init__(self):
|
||||
self.config = optparse.Values()
|
||||
self.load_defaults()
|
||||
|
||||
def load_defaults(self):
|
||||
"""initialize the provider using default values"""
|
||||
for opt, optdict in self.options:
|
||||
action = optdict.get('action')
|
||||
if action != 'callback':
|
||||
# callback action have no default
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(opt)
|
||||
default = optdict.get('default')
|
||||
self.set_option(opt, default, action, optdict)
|
||||
|
||||
def option_attrname(self, opt, optdict=None):
|
||||
"""get the config attribute corresponding to opt"""
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(opt)
|
||||
return optdict.get('dest', opt.replace('-', '_'))
|
||||
|
||||
def option_value(self, opt):
|
||||
"""get the current value for the given option"""
|
||||
return getattr(self.config, self.option_attrname(opt), None)
|
||||
|
||||
def set_option(self, optname, value, action=None, optdict=None):
|
||||
"""method called to set an option (registered in the options list)"""
|
||||
if optdict is None:
|
||||
optdict = self.get_option_def(optname)
|
||||
if value is not None:
|
||||
value = _validate(value, optdict, optname)
|
||||
if action is None:
|
||||
action = optdict.get('action', 'store')
|
||||
if action == 'store':
|
||||
setattr(self.config, self.option_attrname(optname, optdict), value)
|
||||
elif action in ('store_true', 'count'):
|
||||
setattr(self.config, self.option_attrname(optname, optdict), 0)
|
||||
elif action == 'store_false':
|
||||
setattr(self.config, self.option_attrname(optname, optdict), 1)
|
||||
elif action == 'append':
|
||||
optname = self.option_attrname(optname, optdict)
|
||||
_list = getattr(self.config, optname, None)
|
||||
if _list is None:
|
||||
if isinstance(value, (list, tuple)):
|
||||
_list = value
|
||||
elif value is not None:
|
||||
_list = []
|
||||
_list.append(value)
|
||||
setattr(self.config, optname, _list)
|
||||
elif isinstance(_list, tuple):
|
||||
setattr(self.config, optname, _list + (value,))
|
||||
else:
|
||||
_list.append(value)
|
||||
elif action == 'callback':
|
||||
optdict['callback'](None, optname, value, None)
|
||||
else:
|
||||
raise UnsupportedAction(action)
|
||||
|
||||
def get_option_def(self, opt):
|
||||
"""return the dictionary defining an option given its name"""
|
||||
assert self.options
|
||||
for option in self.options:
|
||||
if option[0] == opt:
|
||||
return option[1]
|
||||
raise optparse.OptionError('no such option %s in section %r'
|
||||
% (opt, self.name), opt)
|
||||
|
||||
def options_by_section(self):
|
||||
"""return an iterator on options grouped by section
|
||||
|
||||
(section, [list of (optname, optdict, optvalue)])
|
||||
"""
|
||||
sections = {}
|
||||
for optname, optdict in self.options:
|
||||
sections.setdefault(optdict.get('group'), []).append(
|
||||
(optname, optdict, self.option_value(optname)))
|
||||
if None in sections:
|
||||
yield None, sections.pop(None)
|
||||
for section, options in sorted(sections.items()):
|
||||
yield section.upper(), options
|
||||
|
||||
def options_and_values(self, options=None):
|
||||
if options is None:
|
||||
options = self.options
|
||||
for optname, optdict in options:
|
||||
yield (optname, optdict, self.option_value(optname))
|
||||
|
||||
|
||||
class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn):
|
||||
"""basic mixin for simple configurations which don't need the
|
||||
manager / providers model
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args:
|
||||
kwargs.setdefault('usage', '')
|
||||
kwargs.setdefault('quiet', 1)
|
||||
OptionsManagerMixIn.__init__(self, *args, **kwargs)
|
||||
OptionsProviderMixIn.__init__(self)
|
||||
if not getattr(self, 'option_groups', None):
|
||||
self.option_groups = []
|
||||
for _, optdict in self.options:
|
||||
try:
|
||||
gdef = (optdict['group'].upper(), '')
|
||||
except KeyError:
|
||||
continue
|
||||
if gdef not in self.option_groups:
|
||||
self.option_groups.append(gdef)
|
||||
self.register_options_provider(self, own_group=False)
|
||||
|
||||
|
||||
def _generate_manpage(optparser, pkginfo, section=1,
|
||||
stream=sys.stdout, level=0):
|
||||
formatter = _ManHelpFormatter()
|
||||
formatter.output_level = level
|
||||
formatter.parser = optparser
|
||||
print(formatter.format_head(optparser, pkginfo, section), file=stream)
|
||||
print(optparser.format_option_help(formatter), file=stream)
|
||||
print(formatter.format_tail(pkginfo), file=stream)
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8;
|
||||
# mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4
|
||||
# -*- vim:fenc=utf-8:ft=python:et:sw=4:ts=4:sts=4
|
||||
|
||||
# Copyright (c) 2008-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Jakob Normark <jakobnormark@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Manuel Vázquez Acosta <mva.led@gmail.com>
|
||||
# Copyright (c) 2014 Derek Harland <derek.harland@finq.co.nz>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mihai Balint <balint.mihai@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Daniela Plascencia <daplascen@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Emacs and Flymake compatible Pylint.
|
||||
|
||||
This script is for integration with emacs and is compatible with flymake mode.
|
||||
|
||||
epylint walks out of python packages before invoking pylint. This avoids
|
||||
reporting import errors that occur when a module within a package uses the
|
||||
absolute import path to get another module within this package.
|
||||
|
||||
For example:
|
||||
- Suppose a package is structured as
|
||||
|
||||
a/__init__.py
|
||||
a/b/x.py
|
||||
a/c/y.py
|
||||
|
||||
- Then if y.py imports x as "from a.b import x" the following produces pylint
|
||||
errors
|
||||
|
||||
cd a/c; pylint y.py
|
||||
|
||||
- The following obviously doesn't
|
||||
|
||||
pylint a/c/y.py
|
||||
|
||||
- As this script will be invoked by emacs within the directory of the file
|
||||
we are checking we need to go out of it to avoid these false positives.
|
||||
|
||||
|
||||
You may also use py_run to run pylint with desired options and get back (or not)
|
||||
its output.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import os.path as osp
|
||||
import sys
|
||||
import shlex
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def _get_env():
|
||||
'''Extracts the environment PYTHONPATH and appends the current sys.path to
|
||||
those.'''
|
||||
env = dict(os.environ)
|
||||
env['PYTHONPATH'] = os.pathsep.join(sys.path)
|
||||
return env
|
||||
|
||||
def lint(filename, options=None):
|
||||
"""Pylint the given file.
|
||||
|
||||
When run from emacs we will be in the directory of a file, and passed its
|
||||
filename. If this file is part of a package and is trying to import other
|
||||
modules from within its own package or another package rooted in a directory
|
||||
below it, pylint will classify it as a failed import.
|
||||
|
||||
To get around this, we traverse down the directory tree to find the root of
|
||||
the package this module is in. We then invoke pylint from this directory.
|
||||
|
||||
Finally, we must correct the filenames in the output generated by pylint so
|
||||
Emacs doesn't become confused (it will expect just the original filename,
|
||||
while pylint may extend it with extra directories if we've traversed down
|
||||
the tree)
|
||||
"""
|
||||
# traverse downwards until we are out of a python package
|
||||
full_path = osp.abspath(filename)
|
||||
parent_path = osp.dirname(full_path)
|
||||
child_path = osp.basename(full_path)
|
||||
|
||||
while parent_path != "/" and osp.exists(osp.join(parent_path, '__init__.py')):
|
||||
child_path = osp.join(osp.basename(parent_path), child_path)
|
||||
parent_path = osp.dirname(parent_path)
|
||||
|
||||
# Start pylint
|
||||
# Ensure we use the python and pylint associated with the running epylint
|
||||
run_cmd = "import sys; from pylint.lint import Run; Run(sys.argv[1:])"
|
||||
options = options or ['--disable=C,R,I']
|
||||
cmd = [sys.executable, "-c", run_cmd] + [
|
||||
'--msg-template', '{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}',
|
||||
'-r', 'n', child_path] + options
|
||||
process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=_get_env(),
|
||||
universal_newlines=True)
|
||||
|
||||
for line in process.stdout:
|
||||
# remove pylintrc warning
|
||||
if line.startswith("No config file found"):
|
||||
continue
|
||||
|
||||
# modify the file name thats output to reverse the path traversal we made
|
||||
parts = line.split(":")
|
||||
if parts and parts[0] == child_path:
|
||||
line = ":".join([filename] + parts[1:])
|
||||
print(line, end=' ')
|
||||
|
||||
process.wait()
|
||||
return process.returncode
|
||||
|
||||
|
||||
def py_run(command_options='', return_std=False, stdout=None, stderr=None):
|
||||
"""Run pylint from python
|
||||
|
||||
``command_options`` is a string containing ``pylint`` command line options;
|
||||
``return_std`` (boolean) indicates return of created standard output
|
||||
and error (see below);
|
||||
``stdout`` and ``stderr`` are 'file-like' objects in which standard output
|
||||
could be written.
|
||||
|
||||
Calling agent is responsible for stdout/err management (creation, close).
|
||||
Default standard output and error are those from sys,
|
||||
or standalone ones (``subprocess.PIPE``) are used
|
||||
if they are not set and ``return_std``.
|
||||
|
||||
If ``return_std`` is set to ``True``, this function returns a 2-uple
|
||||
containing standard output and error related to created process,
|
||||
as follows: ``(stdout, stderr)``.
|
||||
|
||||
A trivial usage could be as follows:
|
||||
>>> py_run( '--version')
|
||||
No config file found, using default configuration
|
||||
pylint 0.18.1,
|
||||
...
|
||||
|
||||
To silently run Pylint on a module, and get its standard output and error:
|
||||
>>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True)
|
||||
"""
|
||||
# Create command line to call pylint
|
||||
epylint_part = [sys.executable, "-c", "from pylint import epylint;epylint.Run()"]
|
||||
options = shlex.split(command_options)
|
||||
cli = epylint_part + options
|
||||
|
||||
# Providing standard output and/or error if not set
|
||||
if stdout is None:
|
||||
if return_std:
|
||||
stdout = PIPE
|
||||
else:
|
||||
stdout = sys.stdout
|
||||
if stderr is None:
|
||||
if return_std:
|
||||
stderr = PIPE
|
||||
else:
|
||||
stderr = sys.stderr
|
||||
# Call pylint in a subprocess
|
||||
process = Popen(cli, shell=False, stdout=stdout, stderr=stderr,
|
||||
env=_get_env(), universal_newlines=True)
|
||||
proc_stdout, proc_stderr = process.communicate()
|
||||
# Return standard output and error
|
||||
if return_std:
|
||||
return six.moves.StringIO(proc_stdout), six.moves.StringIO(proc_stderr)
|
||||
return None
|
||||
|
||||
|
||||
def Run():
|
||||
if len(sys.argv) == 1:
|
||||
print("Usage: %s <filename> [options]" % sys.argv[0])
|
||||
sys.exit(1)
|
||||
elif not osp.exists(sys.argv[1]):
|
||||
print("%s does not exist" % sys.argv[1])
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(lint(sys.argv[1], sys.argv[2:]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Run()
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Exception classes raised by various operations within pylint."""
|
||||
|
||||
|
||||
class InvalidMessageError(Exception):
|
||||
"""raised when a message creation, registration or addition is rejected"""
|
||||
|
||||
class UnknownMessageError(Exception):
|
||||
"""raised when a unregistered message id is encountered"""
|
||||
|
||||
class EmptyReportError(Exception):
|
||||
"""raised when a report is empty and so should not be displayed"""
|
||||
|
||||
class InvalidReporterError(Exception):
|
||||
"""raised when selected reporter is invalid (e.g. not found)"""
|
||||
@@ -0,0 +1,705 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Yuri Bochkarev <baltazar.bz@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
# Copyright (c) 2017 Mitar <mitar.github@tnode.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Utility methods for docstring checking."""
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import re
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def space_indentation(s):
|
||||
"""The number of leading spaces in a string
|
||||
|
||||
:param str s: input string
|
||||
|
||||
:rtype: int
|
||||
:return: number of leading spaces
|
||||
"""
|
||||
return len(s) - len(s.lstrip(' '))
|
||||
|
||||
|
||||
def get_setters_property_name(node):
|
||||
"""Get the name of the property that the given node is a setter for.
|
||||
|
||||
:param node: The node to get the property name for.
|
||||
:type node: str
|
||||
|
||||
:rtype: str or None
|
||||
:returns: The name of the property that the node is a setter for,
|
||||
or None if one could not be found.
|
||||
"""
|
||||
decorators = node.decorators.nodes if node.decorators else []
|
||||
for decorator in decorators:
|
||||
if (isinstance(decorator, astroid.Attribute) and
|
||||
decorator.attrname == "setter" and
|
||||
isinstance(decorator.expr, astroid.Name)):
|
||||
return decorator.expr.name
|
||||
return None
|
||||
|
||||
|
||||
def get_setters_property(node):
|
||||
"""Get the property node for the given setter node.
|
||||
|
||||
:param node: The node to get the property for.
|
||||
:type node: astroid.FunctionDef
|
||||
|
||||
:rtype: astroid.FunctionDef or None
|
||||
:returns: The node relating to the property of the given setter node,
|
||||
or None if one could not be found.
|
||||
"""
|
||||
property_ = None
|
||||
|
||||
property_name = get_setters_property_name(node)
|
||||
class_node = utils.node_frame_class(node)
|
||||
if property_name and class_node:
|
||||
class_attrs = class_node.getattr(node.name)
|
||||
for attr in class_attrs:
|
||||
if utils.decorated_with_property(attr):
|
||||
property_ = attr
|
||||
break
|
||||
|
||||
return property_
|
||||
|
||||
|
||||
def returns_something(return_node):
|
||||
"""Check if a return node returns a value other than None.
|
||||
|
||||
:param return_node: The return node to check.
|
||||
:type return_node: astroid.Return
|
||||
|
||||
:rtype: bool
|
||||
:return: True if the return node returns a value other than None,
|
||||
False otherwise.
|
||||
"""
|
||||
returns = return_node.value
|
||||
|
||||
if returns is None:
|
||||
return False
|
||||
|
||||
return not (isinstance(returns, astroid.Const) and returns.value is None)
|
||||
|
||||
|
||||
def possible_exc_types(node):
|
||||
"""
|
||||
Gets all of the possible raised exception types for the given raise node.
|
||||
|
||||
.. note::
|
||||
|
||||
Caught exception types are ignored.
|
||||
|
||||
|
||||
:param node: The raise node to find exception types for.
|
||||
:type node: astroid.node_classes.NodeNG
|
||||
|
||||
:returns: A list of exception types possibly raised by :param:`node`.
|
||||
:rtype: set(str)
|
||||
"""
|
||||
excs = []
|
||||
if isinstance(node.exc, astroid.Name):
|
||||
inferred = utils.safe_infer(node.exc)
|
||||
if inferred:
|
||||
excs = [inferred.name]
|
||||
elif (isinstance(node.exc, astroid.Call) and
|
||||
isinstance(node.exc.func, astroid.Name)):
|
||||
target = utils.safe_infer(node.exc.func)
|
||||
if isinstance(target, astroid.ClassDef):
|
||||
excs = [target.name]
|
||||
elif isinstance(target, astroid.FunctionDef):
|
||||
for ret in target.nodes_of_class(astroid.Return):
|
||||
if ret.frame() != target:
|
||||
# return from inner function - ignore it
|
||||
continue
|
||||
|
||||
val = utils.safe_infer(ret.value)
|
||||
if (val and isinstance(val, (astroid.Instance, astroid.ClassDef))
|
||||
and utils.inherit_from_std_ex(val)):
|
||||
excs.append(val.name)
|
||||
elif node.exc is None:
|
||||
handler = node.parent
|
||||
while handler and not isinstance(handler, astroid.ExceptHandler):
|
||||
handler = handler.parent
|
||||
|
||||
if handler and handler.type:
|
||||
inferred_excs = astroid.unpack_infer(handler.type)
|
||||
excs = (exc.name for exc in inferred_excs
|
||||
if exc is not astroid.Uninferable)
|
||||
|
||||
|
||||
try:
|
||||
return set(exc for exc in excs if not utils.node_ignores_exception(node, exc))
|
||||
except astroid.InferenceError:
|
||||
return set()
|
||||
|
||||
|
||||
def docstringify(docstring):
|
||||
for docstring_type in [SphinxDocstring, EpytextDocstring,
|
||||
GoogleDocstring, NumpyDocstring]:
|
||||
instance = docstring_type(docstring)
|
||||
if instance.is_valid():
|
||||
return instance
|
||||
|
||||
return Docstring(docstring)
|
||||
|
||||
|
||||
class Docstring(object):
|
||||
re_for_parameters_see = re.compile(r"""
|
||||
For\s+the\s+(other)?\s*parameters\s*,\s+see
|
||||
""", re.X | re.S)
|
||||
|
||||
supports_yields = None
|
||||
"""True if the docstring supports a "yield" section.
|
||||
|
||||
False if the docstring uses the returns section to document generators.
|
||||
"""
|
||||
|
||||
# These methods are designed to be overridden
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, doc):
|
||||
doc = doc or ""
|
||||
self.doc = doc.expandtabs()
|
||||
|
||||
def is_valid(self):
|
||||
return False
|
||||
|
||||
def exceptions(self):
|
||||
return set()
|
||||
|
||||
def has_params(self):
|
||||
return False
|
||||
|
||||
def has_returns(self):
|
||||
return False
|
||||
|
||||
def has_rtype(self):
|
||||
return False
|
||||
|
||||
def has_property_returns(self):
|
||||
return False
|
||||
|
||||
def has_property_type(self):
|
||||
return False
|
||||
|
||||
def has_yields(self):
|
||||
return False
|
||||
|
||||
def has_yields_type(self):
|
||||
return False
|
||||
|
||||
def match_param_docs(self):
|
||||
return set(), set()
|
||||
|
||||
def params_documented_elsewhere(self):
|
||||
return self.re_for_parameters_see.search(self.doc) is not None
|
||||
|
||||
|
||||
class SphinxDocstring(Docstring):
|
||||
re_type = r"[\w\.]+"
|
||||
|
||||
re_simple_container_type = r"""
|
||||
{type} # a container type
|
||||
[\(\[] [^\n\s]+ [\)\]] # with the contents of the container
|
||||
""".format(type=re_type)
|
||||
|
||||
re_xref = r"""
|
||||
(?::\w+:)? # optional tag
|
||||
`{0}` # what to reference
|
||||
""".format(re_type)
|
||||
|
||||
re_param_raw = r"""
|
||||
: # initial colon
|
||||
(?: # Sphinx keywords
|
||||
param|parameter|
|
||||
arg|argument|
|
||||
key|keyword
|
||||
)
|
||||
\s+ # whitespace
|
||||
|
||||
(?: # optional type declaration
|
||||
({type}|{container_type})
|
||||
\s+
|
||||
)?
|
||||
|
||||
(\w+) # Parameter name
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(type=re_type, container_type=re_simple_container_type)
|
||||
re_param_in_docstring = re.compile(re_param_raw, re.X | re.S)
|
||||
|
||||
re_type_raw = r"""
|
||||
:type # Sphinx keyword
|
||||
\s+ # whitespace
|
||||
({type}) # Parameter name
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(type=re_type)
|
||||
re_type_in_docstring = re.compile(re_type_raw, re.X | re.S)
|
||||
|
||||
re_property_type_raw = r"""
|
||||
:type: # Sphinx keyword
|
||||
\s+ # whitespace
|
||||
{type} # type declaration
|
||||
""".format(type=re_type)
|
||||
re_property_type_in_docstring = re.compile(
|
||||
re_property_type_raw, re.X | re.S
|
||||
)
|
||||
|
||||
re_raise_raw = r"""
|
||||
: # initial colon
|
||||
(?: # Sphinx keyword
|
||||
raises?|
|
||||
except|exception
|
||||
)
|
||||
\s+ # whitespace
|
||||
|
||||
(?: # type declaration
|
||||
({type})
|
||||
\s+
|
||||
)?
|
||||
|
||||
(\w+) # Parameter name
|
||||
\s* # whitespace
|
||||
: # final colon
|
||||
""".format(type=re_type)
|
||||
re_raise_in_docstring = re.compile(re_raise_raw, re.X | re.S)
|
||||
|
||||
re_rtype_in_docstring = re.compile(r":rtype:")
|
||||
|
||||
re_returns_in_docstring = re.compile(r":returns?:")
|
||||
|
||||
supports_yields = False
|
||||
|
||||
def is_valid(self):
|
||||
return bool(self.re_param_in_docstring.search(self.doc) or
|
||||
self.re_raise_in_docstring.search(self.doc) or
|
||||
self.re_rtype_in_docstring.search(self.doc) or
|
||||
self.re_returns_in_docstring.search(self.doc) or
|
||||
self.re_property_type_in_docstring.search(self.doc))
|
||||
|
||||
def exceptions(self):
|
||||
types = set()
|
||||
|
||||
for match in re.finditer(self.re_raise_in_docstring, self.doc):
|
||||
raise_type = match.group(2)
|
||||
types.add(raise_type)
|
||||
|
||||
return types
|
||||
|
||||
def has_params(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return self.re_param_in_docstring.search(self.doc) is not None
|
||||
|
||||
def has_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_returns_in_docstring.search(self.doc))
|
||||
|
||||
def has_rtype(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_rtype_in_docstring.search(self.doc))
|
||||
|
||||
def has_property_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
return not self.doc.lstrip().startswith(':')
|
||||
|
||||
def has_property_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_property_type_in_docstring.search(self.doc))
|
||||
|
||||
def match_param_docs(self):
|
||||
params_with_doc = set()
|
||||
params_with_type = set()
|
||||
|
||||
for match in re.finditer(self.re_param_in_docstring, self.doc):
|
||||
name = match.group(2)
|
||||
params_with_doc.add(name)
|
||||
param_type = match.group(1)
|
||||
if param_type is not None:
|
||||
params_with_type.add(name)
|
||||
|
||||
params_with_type.update(re.findall(self.re_type_in_docstring, self.doc))
|
||||
return params_with_doc, params_with_type
|
||||
|
||||
|
||||
class EpytextDocstring(SphinxDocstring):
|
||||
"""
|
||||
Epytext is similar to Sphinx. See the docs:
|
||||
http://epydoc.sourceforge.net/epytext.html
|
||||
http://epydoc.sourceforge.net/fields.html#fields
|
||||
|
||||
It's used in PyCharm:
|
||||
https://www.jetbrains.com/help/pycharm/2016.1/creating-documentation-comments.html#d848203e314
|
||||
https://www.jetbrains.com/help/pycharm/2016.1/using-docstrings-to-specify-types.html
|
||||
"""
|
||||
re_param_in_docstring = re.compile(
|
||||
SphinxDocstring.re_param_raw.replace(':', '@', 1),
|
||||
re.X | re.S)
|
||||
|
||||
re_type_in_docstring = re.compile(
|
||||
SphinxDocstring.re_type_raw.replace(':', '@', 1),
|
||||
re.X | re.S)
|
||||
|
||||
re_property_type_in_docstring = re.compile(
|
||||
SphinxDocstring.re_property_type_raw.replace(':', '@', 1),
|
||||
re.X | re.S)
|
||||
|
||||
re_raise_in_docstring = re.compile(
|
||||
SphinxDocstring.re_raise_raw.replace(':', '@', 1),
|
||||
re.X | re.S)
|
||||
|
||||
re_rtype_in_docstring = re.compile(r"""
|
||||
@ # initial "at" symbol
|
||||
(?: # Epytext keyword
|
||||
rtype|returntype
|
||||
)
|
||||
: # final colon
|
||||
""", re.X | re.S)
|
||||
|
||||
re_returns_in_docstring = re.compile(r"@returns?:")
|
||||
|
||||
def has_property_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
# If this is a property docstring, the summary is the return doc.
|
||||
if self.has_property_type():
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
return not self.doc.lstrip().startswith('@')
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class GoogleDocstring(Docstring):
|
||||
re_type = SphinxDocstring.re_type
|
||||
|
||||
re_xref = SphinxDocstring.re_xref
|
||||
|
||||
re_container_type = r"""
|
||||
(?:{type}|{xref}) # a container type
|
||||
[\(\[] [^\n]+ [\)\]] # with the contents of the container
|
||||
""".format(type=re_type, xref=re_xref)
|
||||
|
||||
re_multiple_type = r"""
|
||||
(?:{container_type}|{type}|{xref})
|
||||
(?:\s+or\s+(?:{container_type}|{type}|{xref}))*
|
||||
""".format(type=re_type, xref=re_xref, container_type=re_container_type)
|
||||
|
||||
_re_section_template = r"""
|
||||
^([ ]*) {0} \s*: \s*$ # Google parameter header
|
||||
( .* ) # section
|
||||
"""
|
||||
|
||||
re_param_section = re.compile(
|
||||
_re_section_template.format(r"(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_keyword_param_section = re.compile(
|
||||
_re_section_template.format(r"Keyword\s(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_param_line = re.compile(r"""
|
||||
\s* \*{{0,2}}(\w+) # identifier potentially with asterisks
|
||||
\s* ( [(]
|
||||
{type}
|
||||
[)] )? \s* : # optional type declaration
|
||||
\s* (.*) # beginning of optional description
|
||||
""".format(
|
||||
type=re_multiple_type,
|
||||
), re.X | re.S | re.M)
|
||||
|
||||
re_raise_section = re.compile(
|
||||
_re_section_template.format(r"Raises"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_raise_line = re.compile(r"""
|
||||
\s* ({type}) \s* : # identifier
|
||||
\s* (.*) # beginning of optional description
|
||||
""".format(type=re_type), re.X | re.S | re.M)
|
||||
|
||||
re_returns_section = re.compile(
|
||||
_re_section_template.format(r"Returns?"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_returns_line = re.compile(r"""
|
||||
\s* ({type}:)? # identifier
|
||||
\s* (.*) # beginning of description
|
||||
""".format(
|
||||
type=re_multiple_type,
|
||||
), re.X | re.S | re.M)
|
||||
|
||||
re_property_returns_line = re.compile(r"""
|
||||
^{type}: # indentifier
|
||||
\s* (.*) # Summary line / description
|
||||
""".format(
|
||||
type=re_multiple_type,
|
||||
), re.X | re.S | re.M)
|
||||
|
||||
re_yields_section = re.compile(
|
||||
_re_section_template.format(r"Yields?"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_yields_line = re_returns_line
|
||||
|
||||
supports_yields = True
|
||||
|
||||
def is_valid(self):
|
||||
return bool(self.re_param_section.search(self.doc) or
|
||||
self.re_raise_section.search(self.doc) or
|
||||
self.re_returns_section.search(self.doc) or
|
||||
self.re_yields_section.search(self.doc) or
|
||||
self.re_property_returns_line.search(self._first_line()))
|
||||
|
||||
def has_params(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return self.re_param_section.search(self.doc) is not None
|
||||
|
||||
def has_returns(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_returns_section)
|
||||
for entry in entries:
|
||||
match = self.re_returns_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
return_desc = match.group(2)
|
||||
if return_desc:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_rtype(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_returns_section)
|
||||
for entry in entries:
|
||||
match = self.re_returns_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
return_type = match.group(1)
|
||||
if return_type:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_property_returns(self):
|
||||
# The summary line is the return doc,
|
||||
# so the first line must not be a known directive.
|
||||
first_line = self._first_line()
|
||||
return not bool(self.re_param_section.search(first_line) or
|
||||
self.re_raise_section.search(first_line) or
|
||||
self.re_returns_section.search(first_line) or
|
||||
self.re_yields_section.search(first_line))
|
||||
|
||||
def has_property_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
return bool(self.re_property_returns_line.match(self._first_line()))
|
||||
|
||||
def has_yields(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_yields_section)
|
||||
for entry in entries:
|
||||
match = self.re_yields_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
yield_desc = match.group(2)
|
||||
if yield_desc:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_yields_type(self):
|
||||
if not self.doc:
|
||||
return False
|
||||
|
||||
entries = self._parse_section(self.re_yields_section)
|
||||
for entry in entries:
|
||||
match = self.re_yields_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
yield_type = match.group(1)
|
||||
if yield_type:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def exceptions(self):
|
||||
types = set()
|
||||
|
||||
entries = self._parse_section(self.re_raise_section)
|
||||
for entry in entries:
|
||||
match = self.re_raise_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
exc_type = match.group(1)
|
||||
exc_desc = match.group(2)
|
||||
if exc_desc:
|
||||
types.add(exc_type)
|
||||
|
||||
return types
|
||||
|
||||
def match_param_docs(self):
|
||||
params_with_doc = set()
|
||||
params_with_type = set()
|
||||
|
||||
entries = self._parse_section(self.re_param_section)
|
||||
entries.extend(self._parse_section(self.re_keyword_param_section))
|
||||
for entry in entries:
|
||||
match = self.re_param_line.match(entry)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
param_name = match.group(1)
|
||||
param_type = match.group(2)
|
||||
param_desc = match.group(3)
|
||||
if param_type:
|
||||
params_with_type.add(param_name)
|
||||
|
||||
if param_desc:
|
||||
params_with_doc.add(param_name)
|
||||
|
||||
return params_with_doc, params_with_type
|
||||
|
||||
def _first_line(self):
|
||||
return self.doc.lstrip().split('\n', 1)[0]
|
||||
|
||||
@staticmethod
|
||||
def min_section_indent(section_match):
|
||||
return len(section_match.group(1)) + 1
|
||||
|
||||
def _parse_section(self, section_re):
|
||||
section_match = section_re.search(self.doc)
|
||||
if section_match is None:
|
||||
return []
|
||||
|
||||
min_indentation = self.min_section_indent(section_match)
|
||||
|
||||
entries = []
|
||||
entry = []
|
||||
is_first = True
|
||||
for line in section_match.group(2).splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
indentation = space_indentation(line)
|
||||
if indentation < min_indentation:
|
||||
break
|
||||
|
||||
# The first line after the header defines the minimum
|
||||
# indentation.
|
||||
if is_first:
|
||||
min_indentation = indentation
|
||||
is_first = False
|
||||
|
||||
if indentation == min_indentation:
|
||||
# Lines with minimum indentation must contain the beginning
|
||||
# of a new parameter documentation.
|
||||
if entry:
|
||||
entries.append("\n".join(entry))
|
||||
entry = []
|
||||
|
||||
entry.append(line)
|
||||
|
||||
if entry:
|
||||
entries.append("\n".join(entry))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
class NumpyDocstring(GoogleDocstring):
|
||||
_re_section_template = r"""
|
||||
^([ ]*) {0} \s*?$ # Numpy parameters header
|
||||
\s* [-=]+ \s*?$ # underline
|
||||
( .* ) # section
|
||||
"""
|
||||
|
||||
re_param_section = re.compile(
|
||||
_re_section_template.format(r"(?:Args|Arguments|Parameters)"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_param_line = re.compile(r"""
|
||||
\s* (\w+) # identifier
|
||||
\s* :
|
||||
\s* (?:({type})(?:,\s+optional)?)? # optional type declaration
|
||||
\n # description starts on a new line
|
||||
\s* (.*) # description
|
||||
""".format(
|
||||
type=GoogleDocstring.re_multiple_type,
|
||||
), re.X | re.S)
|
||||
|
||||
re_raise_section = re.compile(
|
||||
_re_section_template.format(r"Raises"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_raise_line = re.compile(r"""
|
||||
\s* ({type})$ # type declaration
|
||||
\s* (.*) # optional description
|
||||
""".format(type=GoogleDocstring.re_type), re.X | re.S | re.M)
|
||||
|
||||
re_returns_section = re.compile(
|
||||
_re_section_template.format(r"Returns?"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_returns_line = re.compile(r"""
|
||||
\s* ({type})$ # type declaration
|
||||
\s* (.*) # optional description
|
||||
""".format(
|
||||
type=GoogleDocstring.re_multiple_type,
|
||||
), re.X | re.S | re.M)
|
||||
|
||||
re_yields_section = re.compile(
|
||||
_re_section_template.format(r"Yields?"),
|
||||
re.X | re.S | re.M
|
||||
)
|
||||
|
||||
re_yields_line = re_returns_line
|
||||
|
||||
supports_yields = True
|
||||
|
||||
@staticmethod
|
||||
def min_section_indent(section_match):
|
||||
return len(section_match.group(1))
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) 2016 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Checker for deprecated builtins."""
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
|
||||
BAD_FUNCTIONS = ['map', 'filter']
|
||||
if sys.version_info < (3, 0):
|
||||
BAD_FUNCTIONS.append('input')
|
||||
# Some hints regarding the use of bad builtins.
|
||||
BUILTIN_HINTS = {
|
||||
'map': 'Using a list comprehension can be clearer.',
|
||||
}
|
||||
BUILTIN_HINTS['filter'] = BUILTIN_HINTS['map']
|
||||
|
||||
|
||||
class BadBuiltinChecker(BaseChecker):
|
||||
|
||||
__implements__ = (IAstroidChecker, )
|
||||
name = 'deprecated_builtins'
|
||||
msgs = {'W0141': ('Used builtin function %s',
|
||||
'bad-builtin',
|
||||
'Used when a black listed builtin function is used (see the '
|
||||
'bad-function option). Usual black listed functions are the ones '
|
||||
'like map, or filter , where Python offers now some cleaner '
|
||||
'alternative like list comprehension.'),
|
||||
}
|
||||
|
||||
options = (('bad-functions',
|
||||
{'default' : BAD_FUNCTIONS,
|
||||
'type' :'csv', 'metavar' : '<builtin function names>',
|
||||
'help' : 'List of builtins function names that should not be '
|
||||
'used, separated by a comma'}
|
||||
),
|
||||
)
|
||||
|
||||
@check_messages('bad-builtin')
|
||||
def visit_call(self, node):
|
||||
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 in self.config.bad_functions:
|
||||
hint = BUILTIN_HINTS.get(name)
|
||||
if hint:
|
||||
args = "%r. %s" % (name, hint)
|
||||
else:
|
||||
args = repr(name)
|
||||
self.add_message('bad-builtin', node=node, args=args)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(BadBuiltinChecker(linter))
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <bruno.daniel@blue-yonder.com>
|
||||
# Copyright (c) 2015-2016 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import warnings
|
||||
|
||||
from pylint.extensions import docparams
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
warnings.warn("This plugin is deprecated, use pylint.extensions.docparams instead.",
|
||||
DeprecationWarning)
|
||||
linter.register_checker(docparams.DocstringParameterChecker(linter))
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker
|
||||
|
||||
|
||||
class ElseifUsedChecker(BaseTokenChecker):
|
||||
"""Checks for use of "else if" when a "elif" could be used
|
||||
"""
|
||||
|
||||
__implements__ = (ITokenChecker, IAstroidChecker)
|
||||
name = 'else_if_used'
|
||||
msgs = {'R5501': ('Consider using "elif" instead of "else if"',
|
||||
'else-if-used',
|
||||
'Used when an else statement is immediately followed by '
|
||||
'an if statement and does not contain statements that '
|
||||
'would be unrelated to it.'),
|
||||
}
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseTokenChecker.__init__(self, linter)
|
||||
self._init()
|
||||
|
||||
def _init(self):
|
||||
self._elifs = []
|
||||
self._if_counter = 0
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
# Process tokens and look for 'if' or 'elif'
|
||||
for _, token, _, _, _ in tokens:
|
||||
if token == 'elif':
|
||||
self._elifs.append(True)
|
||||
elif token == 'if':
|
||||
self._elifs.append(False)
|
||||
|
||||
def leave_module(self, _):
|
||||
self._init()
|
||||
|
||||
def visit_ifexp(self, _):
|
||||
self._if_counter += 1
|
||||
|
||||
def visit_comprehension(self, node):
|
||||
self._if_counter += len(node.ifs)
|
||||
|
||||
@check_messages('else-if-used')
|
||||
def visit_if(self, node):
|
||||
if isinstance(node.parent, astroid.If):
|
||||
orelse = node.parent.orelse
|
||||
# current if node must directly follow a "else"
|
||||
if orelse and orelse == [node]:
|
||||
if not self._elifs[self._if_counter]:
|
||||
self.add_message('else-if-used', node=node)
|
||||
self._if_counter += 1
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(ElseifUsedChecker(linter))
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for comparisons to empty string."""
|
||||
|
||||
import itertools
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import interfaces
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _is_constant_zero(node):
|
||||
return isinstance(node, astroid.Const) and node.value == 0
|
||||
|
||||
|
||||
class CompareToZeroChecker(checkers.BaseChecker):
|
||||
"""Checks for comparisons to zero.
|
||||
Most of the times you should use the fact that integers with a value of 0 are false.
|
||||
An exception to this rule is when 0 is allowed in the program and has a
|
||||
different meaning than None!
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'compare-to-zero'
|
||||
msgs = {'C2001': ('Avoid comparisons to zero',
|
||||
'compare-to-zero',
|
||||
'Used when Pylint detects comparison to a 0 constant.'),
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages('compare-to-zero')
|
||||
def visit_compare(self, node):
|
||||
_operators = ['!=', '==', 'is not', 'is']
|
||||
# note: astroid.Compare has the left most operand in node.left
|
||||
# while the rest are a list of tuples in node.ops
|
||||
# the format of the tuple is ('compare operator sign', node)
|
||||
# here we squash everything into `ops` to make it easier for processing later
|
||||
ops = [('', node.left)]
|
||||
ops.extend(node.ops)
|
||||
ops = list(itertools.chain(*ops))
|
||||
|
||||
for ops_idx in range(len(ops) - 2):
|
||||
op_1 = ops[ops_idx]
|
||||
op_2 = ops[ops_idx + 1]
|
||||
op_3 = ops[ops_idx + 2]
|
||||
error_detected = False
|
||||
|
||||
# 0 ?? X
|
||||
if _is_constant_zero(op_1) and op_2 in _operators + ['<']:
|
||||
error_detected = True
|
||||
# X ?? 0
|
||||
elif op_2 in _operators + ['>'] and _is_constant_zero(op_3):
|
||||
error_detected = True
|
||||
|
||||
if error_detected:
|
||||
self.add_message('compare-to-zero', node=node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(CompareToZeroChecker(linter))
|
||||
@@ -0,0 +1,442 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014-2015 Bruno Daniel <bruno.daniel@blue-yonder.com>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2018 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 Ville Skyttä <ville.skytta@iki.fi>
|
||||
# Copyright (c) 2017 John Paraskevopoulos <io.paraskev@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Pylint plugin for checking in Sphinx, Google, or Numpy style docstrings
|
||||
"""
|
||||
from __future__ import print_function, division, absolute_import
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils as checker_utils
|
||||
import pylint.extensions._check_docs_utils as utils
|
||||
|
||||
|
||||
class DocstringParameterChecker(BaseChecker):
|
||||
"""Checker for Sphinx, Google, or Numpy style docstrings
|
||||
|
||||
* Check that all function, method and constructor parameters are mentioned
|
||||
in the params and types part of the docstring. Constructor parameters
|
||||
can be documented in either the class docstring or ``__init__`` docstring,
|
||||
but not both.
|
||||
* Check that there are no naming inconsistencies between the signature and
|
||||
the documentation, i.e. also report documented parameters that are missing
|
||||
in the signature. This is important to find cases where parameters are
|
||||
renamed only in the code, not in the documentation.
|
||||
* Check that all explicitly raised exceptions in a function are documented
|
||||
in the function docstring. Caught exceptions are ignored.
|
||||
|
||||
Activate this checker by adding the line::
|
||||
|
||||
load-plugins=pylint.extensions.docparams
|
||||
|
||||
to the ``MASTER`` section of your ``.pylintrc``.
|
||||
|
||||
:param linter: linter object
|
||||
:type linter: :class:`pylint.lint.PyLinter`
|
||||
"""
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = 'parameter_documentation'
|
||||
msgs = {
|
||||
'W9005': ('"%s" has constructor parameters documented in class and __init__',
|
||||
'multiple-constructor-doc',
|
||||
'Please remove parameter declarations in the class or constructor.'),
|
||||
'W9006': ('"%s" not documented as being raised',
|
||||
'missing-raises-doc',
|
||||
'Please document exceptions for all raised exception types.'),
|
||||
'W9008': ('Redundant returns documentation',
|
||||
'redundant-returns-doc',
|
||||
'Please remove the return/rtype documentation from this method.'),
|
||||
'W9010': ('Redundant yields documentation',
|
||||
'redundant-yields-doc',
|
||||
'Please remove the yields documentation from this method.'),
|
||||
'W9011': ('Missing return documentation',
|
||||
'missing-return-doc',
|
||||
'Please add documentation about what this method returns.',
|
||||
{'old_names': [('W9007', 'missing-returns-doc')]}),
|
||||
'W9012': ('Missing return type documentation',
|
||||
'missing-return-type-doc',
|
||||
'Please document the type returned by this method.',
|
||||
# we can't use the same old_name for two different warnings
|
||||
# {'old_names': [('W9007', 'missing-returns-doc')]},
|
||||
),
|
||||
'W9013': ('Missing yield documentation',
|
||||
'missing-yield-doc',
|
||||
'Please add documentation about what this generator yields.',
|
||||
{'old_names': [('W9009', 'missing-yields-doc')]}),
|
||||
'W9014': ('Missing yield type documentation',
|
||||
'missing-yield-type-doc',
|
||||
'Please document the type yielded by this method.',
|
||||
# we can't use the same old_name for two different warnings
|
||||
# {'old_names': [('W9009', 'missing-yields-doc')]},
|
||||
),
|
||||
'W9015': ('"%s" missing in parameter documentation',
|
||||
'missing-param-doc',
|
||||
'Please add parameter declarations for all parameters.',
|
||||
{'old_names': [('W9003', 'missing-param-doc')]}),
|
||||
'W9016': ('"%s" missing in parameter type documentation',
|
||||
'missing-type-doc',
|
||||
'Please add parameter type declarations for all parameters.',
|
||||
{'old_names': [('W9004', 'missing-type-doc')]}),
|
||||
'W9017': ('"%s" differing in parameter documentation',
|
||||
'differing-param-doc',
|
||||
'Please check parameter names in declarations.',
|
||||
),
|
||||
'W9018': ('"%s" differing in parameter type documentation',
|
||||
'differing-type-doc',
|
||||
'Please check parameter names in type declarations.',
|
||||
),
|
||||
}
|
||||
|
||||
options = (('accept-no-param-doc',
|
||||
{'default': True, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Whether to accept totally missing parameter '
|
||||
'documentation in the docstring of a function that has '
|
||||
'parameters.'
|
||||
}),
|
||||
('accept-no-raise-doc',
|
||||
{'default': True, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Whether to accept totally missing raises '
|
||||
'documentation in the docstring of a function that '
|
||||
'raises an exception.'
|
||||
}),
|
||||
('accept-no-return-doc',
|
||||
{'default': True, 'type' : 'yn', 'metavar' : '<y or n>',
|
||||
'help': 'Whether to accept totally missing return '
|
||||
'documentation in the docstring of a function that '
|
||||
'returns a statement.'
|
||||
}),
|
||||
('accept-no-yields-doc',
|
||||
{'default': True, 'type' : 'yn', 'metavar': '<y or n>',
|
||||
'help': 'Whether to accept totally missing yields '
|
||||
'documentation in the docstring of a generator.'
|
||||
}),
|
||||
)
|
||||
|
||||
priority = -2
|
||||
|
||||
constructor_names = {'__init__', '__new__'}
|
||||
not_needed_param_in_docstring = {'self', 'cls'}
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
"""Called for function and method definitions (def).
|
||||
|
||||
:param node: Node for a function or method definition in the AST
|
||||
:type node: :class:`astroid.scoped_nodes.Function`
|
||||
"""
|
||||
node_doc = utils.docstringify(node.doc)
|
||||
self.check_functiondef_params(node, node_doc)
|
||||
self.check_functiondef_returns(node, node_doc)
|
||||
self.check_functiondef_yields(node, node_doc)
|
||||
|
||||
def check_functiondef_params(self, node, node_doc):
|
||||
node_allow_no_param = None
|
||||
if node.name in self.constructor_names:
|
||||
class_node = checker_utils.node_frame_class(node)
|
||||
if class_node is not None:
|
||||
class_doc = utils.docstringify(class_node.doc)
|
||||
self.check_single_constructor_params(class_doc, node_doc, class_node)
|
||||
|
||||
# __init__ or class docstrings can have no parameters documented
|
||||
# as long as the other documents them.
|
||||
node_allow_no_param = (
|
||||
class_doc.has_params() or
|
||||
class_doc.params_documented_elsewhere() or
|
||||
None
|
||||
)
|
||||
class_allow_no_param = (
|
||||
node_doc.has_params() or
|
||||
node_doc.params_documented_elsewhere() or
|
||||
None
|
||||
)
|
||||
|
||||
self.check_arguments_in_docstring(
|
||||
class_doc, node.args, class_node, class_allow_no_param)
|
||||
|
||||
self.check_arguments_in_docstring(
|
||||
node_doc, node.args, node, node_allow_no_param)
|
||||
|
||||
def check_functiondef_returns(self, node, node_doc):
|
||||
if ((not node_doc.supports_yields and node.is_generator())
|
||||
or node.is_abstract()):
|
||||
return
|
||||
|
||||
return_nodes = node.nodes_of_class(astroid.Return)
|
||||
if ((node_doc.has_returns() or node_doc.has_rtype()) and
|
||||
not any(utils.returns_something(ret_node) for ret_node in return_nodes)):
|
||||
self.add_message(
|
||||
'redundant-returns-doc',
|
||||
node=node)
|
||||
|
||||
def check_functiondef_yields(self, node, node_doc):
|
||||
if not node_doc.supports_yields or node.is_abstract():
|
||||
return
|
||||
|
||||
if ((node_doc.has_yields() or node_doc.has_yields_type()) and
|
||||
not node.is_generator()):
|
||||
self.add_message(
|
||||
'redundant-yields-doc',
|
||||
node=node)
|
||||
|
||||
def visit_raise(self, node):
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
expected_excs = utils.possible_exc_types(node)
|
||||
if not expected_excs:
|
||||
return
|
||||
|
||||
if not func_node.doc:
|
||||
# If this is a property setter,
|
||||
# the property should have the docstring instead.
|
||||
property_ = utils.get_setters_property(func_node)
|
||||
if property_:
|
||||
func_node = property_
|
||||
|
||||
doc = utils.docstringify(func_node.doc)
|
||||
if not doc.is_valid():
|
||||
if doc.doc:
|
||||
self._handle_no_raise_doc(expected_excs, func_node)
|
||||
return
|
||||
|
||||
found_excs = doc.exceptions()
|
||||
missing_excs = expected_excs - found_excs
|
||||
self._add_raise_message(missing_excs, func_node)
|
||||
|
||||
def visit_return(self, node):
|
||||
if not utils.returns_something(node):
|
||||
return
|
||||
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
doc = utils.docstringify(func_node.doc)
|
||||
if not doc.is_valid() and self.config.accept_no_return_doc:
|
||||
return
|
||||
|
||||
is_property = checker_utils.decorated_with_property(func_node)
|
||||
|
||||
if not (doc.has_returns() or
|
||||
(doc.has_property_returns() and is_property)):
|
||||
self.add_message(
|
||||
'missing-return-doc',
|
||||
node=func_node
|
||||
)
|
||||
|
||||
if not (doc.has_rtype() or
|
||||
(doc.has_property_type() and is_property)):
|
||||
self.add_message(
|
||||
'missing-return-type-doc',
|
||||
node=func_node
|
||||
)
|
||||
|
||||
def visit_yield(self, node):
|
||||
func_node = node.frame()
|
||||
if not isinstance(func_node, astroid.FunctionDef):
|
||||
return
|
||||
|
||||
doc = utils.docstringify(func_node.doc)
|
||||
if not doc.is_valid() and self.config.accept_no_yields_doc:
|
||||
return
|
||||
|
||||
if doc.supports_yields:
|
||||
doc_has_yields = doc.has_yields()
|
||||
doc_has_yields_type = doc.has_yields_type()
|
||||
else:
|
||||
doc_has_yields = doc.has_returns()
|
||||
doc_has_yields_type = doc.has_rtype()
|
||||
|
||||
if not doc_has_yields:
|
||||
self.add_message(
|
||||
'missing-yield-doc',
|
||||
node=func_node
|
||||
)
|
||||
|
||||
if not doc_has_yields_type:
|
||||
self.add_message(
|
||||
'missing-yield-type-doc',
|
||||
node=func_node
|
||||
)
|
||||
|
||||
def visit_yieldfrom(self, node):
|
||||
self.visit_yield(node)
|
||||
|
||||
def check_arguments_in_docstring(self, doc, arguments_node, warning_node,
|
||||
accept_no_param_doc=None):
|
||||
"""Check that all parameters in a function, method or class constructor
|
||||
on the one hand and the parameters mentioned in the parameter
|
||||
documentation (e.g. the Sphinx tags 'param' and 'type') on the other
|
||||
hand are consistent with each other.
|
||||
|
||||
* Undocumented parameters except 'self' are noticed.
|
||||
* Undocumented parameter types except for 'self' and the ``*<args>``
|
||||
and ``**<kwargs>`` parameters are noticed.
|
||||
* Parameters mentioned in the parameter documentation that don't or no
|
||||
longer exist in the function parameter list are noticed.
|
||||
* If the text "For the parameters, see" or "For the other parameters,
|
||||
see" (ignoring additional whitespace) is mentioned in the docstring,
|
||||
missing parameter documentation is tolerated.
|
||||
* If there's no Sphinx style, Google style or NumPy style parameter
|
||||
documentation at all, i.e. ``:param`` is never mentioned etc., the
|
||||
checker assumes that the parameters are documented in another format
|
||||
and the absence is tolerated.
|
||||
|
||||
:param doc: Docstring for the function, method or class.
|
||||
:type doc: str
|
||||
|
||||
:param arguments_node: Arguments node for the function, method or
|
||||
class constructor.
|
||||
:type arguments_node: :class:`astroid.scoped_nodes.Arguments`
|
||||
|
||||
:param warning_node: The node to assign the warnings to
|
||||
:type warning_node: :class:`astroid.scoped_nodes.Node`
|
||||
|
||||
:param accept_no_param_doc: Whether or not to allow no parameters
|
||||
to be documented.
|
||||
If None then this value is read from the configuration.
|
||||
:type accept_no_param_doc: bool or None
|
||||
"""
|
||||
# Tolerate missing param or type declarations if there is a link to
|
||||
# another method carrying the same name.
|
||||
if not doc.doc:
|
||||
return
|
||||
|
||||
if accept_no_param_doc is None:
|
||||
accept_no_param_doc = self.config.accept_no_param_doc
|
||||
tolerate_missing_params = doc.params_documented_elsewhere()
|
||||
|
||||
# Collect the function arguments.
|
||||
expected_argument_names = set(arg.name for arg in arguments_node.args)
|
||||
expected_argument_names.update(arg.name for arg in arguments_node.kwonlyargs)
|
||||
not_needed_type_in_docstring = (
|
||||
self.not_needed_param_in_docstring.copy())
|
||||
|
||||
if arguments_node.vararg is not None:
|
||||
expected_argument_names.add(arguments_node.vararg)
|
||||
not_needed_type_in_docstring.add(arguments_node.vararg)
|
||||
if arguments_node.kwarg is not None:
|
||||
expected_argument_names.add(arguments_node.kwarg)
|
||||
not_needed_type_in_docstring.add(arguments_node.kwarg)
|
||||
params_with_doc, params_with_type = doc.match_param_docs()
|
||||
|
||||
# Tolerate no parameter documentation at all.
|
||||
if (not params_with_doc and not params_with_type
|
||||
and accept_no_param_doc):
|
||||
tolerate_missing_params = True
|
||||
|
||||
def _compare_missing_args(found_argument_names, message_id,
|
||||
not_needed_names):
|
||||
"""Compare the found argument names with the expected ones and
|
||||
generate a message if there are arguments missing.
|
||||
|
||||
:param set found_argument_names: argument names found in the
|
||||
docstring
|
||||
|
||||
:param str message_id: pylint message id
|
||||
|
||||
:param not_needed_names: names that may be omitted
|
||||
:type not_needed_names: set of str
|
||||
"""
|
||||
if not tolerate_missing_params:
|
||||
missing_argument_names = (
|
||||
(expected_argument_names - found_argument_names)
|
||||
- not_needed_names)
|
||||
if missing_argument_names:
|
||||
self.add_message(
|
||||
message_id,
|
||||
args=(', '.join(
|
||||
sorted(missing_argument_names)),),
|
||||
node=warning_node)
|
||||
|
||||
def _compare_different_args(found_argument_names, message_id,
|
||||
not_needed_names):
|
||||
"""Compare the found argument names with the expected ones and
|
||||
generate a message if there are extra arguments found.
|
||||
|
||||
:param set found_argument_names: argument names found in the
|
||||
docstring
|
||||
|
||||
:param str message_id: pylint message id
|
||||
|
||||
:param not_needed_names: names that may be omitted
|
||||
:type not_needed_names: set of str
|
||||
"""
|
||||
differing_argument_names = (
|
||||
(expected_argument_names ^ found_argument_names)
|
||||
- not_needed_names - expected_argument_names)
|
||||
|
||||
if differing_argument_names:
|
||||
self.add_message(
|
||||
message_id,
|
||||
args=(', '.join(
|
||||
sorted(differing_argument_names)),),
|
||||
node=warning_node)
|
||||
|
||||
_compare_missing_args(params_with_doc, 'missing-param-doc',
|
||||
self.not_needed_param_in_docstring)
|
||||
_compare_missing_args(params_with_type, 'missing-type-doc',
|
||||
not_needed_type_in_docstring)
|
||||
|
||||
_compare_different_args(params_with_doc, 'differing-param-doc',
|
||||
self.not_needed_param_in_docstring)
|
||||
_compare_different_args(params_with_type, 'differing-type-doc',
|
||||
not_needed_type_in_docstring)
|
||||
|
||||
def check_single_constructor_params(self, class_doc, init_doc, class_node):
|
||||
if class_doc.has_params() and init_doc.has_params():
|
||||
self.add_message(
|
||||
'multiple-constructor-doc',
|
||||
args=(class_node.name,),
|
||||
node=class_node)
|
||||
|
||||
def _handle_no_raise_doc(self, excs, node):
|
||||
if self.config.accept_no_raise_doc:
|
||||
return
|
||||
|
||||
self._add_raise_message(excs, node)
|
||||
|
||||
def _add_raise_message(self, missing_excs, node):
|
||||
"""
|
||||
Adds a message on :param:`node` for the missing exception type.
|
||||
|
||||
:param missing_excs: A list of missing exception types.
|
||||
:type missing_excs: set(str)
|
||||
|
||||
:param node: The node show the message on.
|
||||
:type node: astroid.node_classes.NodeNG
|
||||
"""
|
||||
if node.is_abstract():
|
||||
try:
|
||||
missing_excs.remove('NotImplementedError')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not missing_excs:
|
||||
return
|
||||
|
||||
self.add_message(
|
||||
'missing-raises-doc',
|
||||
args=(', '.join(sorted(missing_excs)),),
|
||||
node=node)
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(DocstringParameterChecker(linter))
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Luis Escobar <lescobar@vauxoo.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import linecache
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.interfaces import IAstroidChecker, HIGH
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
|
||||
class DocStringStyleChecker(checkers.BaseChecker):
|
||||
"""Checks format of docstrings based on PEP 0257"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
name = 'docstyle'
|
||||
|
||||
msgs = {
|
||||
'C0198': ('Bad docstring quotes in %s, expected """, given %s',
|
||||
'bad-docstring-quotes',
|
||||
'Used when a docstring does not have triple double quotes.'),
|
||||
'C0199': ('First line empty in %s docstring',
|
||||
'docstring-first-line-empty',
|
||||
'Used when a blank line is found at the beginning of a docstring.'),
|
||||
}
|
||||
|
||||
@check_messages('docstring-first-line-empty', 'bad-docstring-quotes')
|
||||
def visit_module(self, node):
|
||||
self._check_docstring('module', node)
|
||||
|
||||
def visit_classdef(self, node):
|
||||
self._check_docstring('class', node)
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
ftype = 'method' if node.is_method() else 'function'
|
||||
self._check_docstring(ftype, node)
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
def _check_docstring(self, node_type, node):
|
||||
docstring = node.doc
|
||||
if docstring and docstring[0] == '\n':
|
||||
self.add_message('docstring-first-line-empty', node=node,
|
||||
args=(node_type,), confidence=HIGH)
|
||||
|
||||
# Use "linecache", instead of node.as_string(), because the latter
|
||||
# looses the original form of the docstrings.
|
||||
|
||||
if docstring:
|
||||
lineno = node.fromlineno + 1
|
||||
line = linecache.getline(node.root().file, lineno).lstrip()
|
||||
if line and line.find('"""') == 0:
|
||||
return
|
||||
if line and '\'\'\'' in line:
|
||||
quotes = '\'\'\''
|
||||
elif line and line[0] == '"':
|
||||
quotes = '"'
|
||||
elif line and line[0] == '\'':
|
||||
quotes = '\''
|
||||
else:
|
||||
quotes = False
|
||||
if quotes:
|
||||
self.add_message('bad-docstring-quotes', node=node,
|
||||
args=(node_type, quotes), confidence=HIGH)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(DocStringStyleChecker(linter))
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for comparisons to empty string."""
|
||||
|
||||
import itertools
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import interfaces
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
def _is_constant_empty_str(node):
|
||||
return isinstance(node, astroid.Const) and node.value == ''
|
||||
|
||||
|
||||
class CompareToEmptyStringChecker(checkers.BaseChecker):
|
||||
"""Checks for comparisons to empty string.
|
||||
Most of the times you should use the fact that empty strings are false.
|
||||
An exception to this rule is when an empty string value is allowed in the program
|
||||
and has a different meaning than None!
|
||||
"""
|
||||
|
||||
__implements__ = (interfaces.IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'compare-to-empty-string'
|
||||
msgs = {'C1901': ('Avoid comparisons to empty string',
|
||||
'compare-to-empty-string',
|
||||
'Used when Pylint detects comparison to an empty string constant.'),
|
||||
}
|
||||
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages('compare-to-empty-string')
|
||||
def visit_compare(self, node):
|
||||
_operators = ['!=', '==', 'is not', 'is']
|
||||
# note: astroid.Compare has the left most operand in node.left
|
||||
# while the rest are a list of tuples in node.ops
|
||||
# the format of the tuple is ('compare operator sign', node)
|
||||
# here we squash everything into `ops` to make it easier for processing later
|
||||
ops = [('', node.left)]
|
||||
ops.extend(node.ops)
|
||||
ops = list(itertools.chain(*ops))
|
||||
|
||||
for ops_idx in range(len(ops) - 2):
|
||||
op_1 = ops[ops_idx]
|
||||
op_2 = ops[ops_idx + 1]
|
||||
op_3 = ops[ops_idx + 2]
|
||||
error_detected = False
|
||||
|
||||
# x ?? ""
|
||||
if _is_constant_empty_str(op_1) and op_2 in _operators:
|
||||
error_detected = True
|
||||
# '' ?? X
|
||||
elif op_2 in _operators and _is_constant_empty_str(op_3):
|
||||
error_detected = True
|
||||
|
||||
if error_detected:
|
||||
self.add_message('compare-to-empty-string', node=node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(CompareToEmptyStringChecker(linter))
|
||||
@@ -0,0 +1,172 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Module to add McCabe checker class for pylint. """
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from mccabe import PathGraph as Mccabe_PathGraph, \
|
||||
PathGraphingAstVisitor as Mccabe_PathGraphingAstVisitor
|
||||
from pylint import checkers
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.interfaces import HIGH, IAstroidChecker
|
||||
|
||||
|
||||
class PathGraph(Mccabe_PathGraph):
|
||||
def __init__(self, node):
|
||||
super(PathGraph, self).__init__(name='', entity='', lineno=1)
|
||||
self.root = node
|
||||
|
||||
|
||||
class PathGraphingAstVisitor(Mccabe_PathGraphingAstVisitor):
|
||||
def __init__(self):
|
||||
super(PathGraphingAstVisitor, self).__init__()
|
||||
self._bottom_counter = 0
|
||||
|
||||
def default(self, node, *args):
|
||||
for child in node.get_children():
|
||||
self.dispatch(child, *args)
|
||||
|
||||
def dispatch(self, node, *args):
|
||||
self.node = node
|
||||
klass = node.__class__
|
||||
meth = self._cache.get(klass)
|
||||
if meth is None:
|
||||
className = klass.__name__
|
||||
meth = getattr(self.visitor, 'visit' + className, self.default)
|
||||
self._cache[klass] = meth
|
||||
return meth(node, *args)
|
||||
|
||||
def visitFunctionDef(self, node):
|
||||
if self.graph is not None:
|
||||
# closure
|
||||
pathnode = self._append_node(node)
|
||||
self.tail = pathnode
|
||||
self.dispatch_list(node.body)
|
||||
bottom = "%s" % self._bottom_counter
|
||||
self._bottom_counter += 1
|
||||
self.graph.connect(self.tail, bottom)
|
||||
self.graph.connect(node, bottom)
|
||||
self.tail = bottom
|
||||
else:
|
||||
self.graph = PathGraph(node)
|
||||
self.tail = node
|
||||
self.dispatch_list(node.body)
|
||||
self.graphs["%s%s" % (self.classname, node.name)] = self.graph
|
||||
self.reset()
|
||||
|
||||
visitAsyncFunctionDef = visitFunctionDef
|
||||
|
||||
def visitSimpleStatement(self, node):
|
||||
self._append_node(node)
|
||||
|
||||
visitAssert = visitAssign = visitAugAssign = visitDelete = visitPrint = \
|
||||
visitRaise = visitYield = visitImport = visitCall = visitSubscript = \
|
||||
visitPass = visitContinue = visitBreak = visitGlobal = visitReturn = \
|
||||
visitExpr = visitAwait = visitSimpleStatement
|
||||
|
||||
def visitWith(self, node):
|
||||
self._append_node(node)
|
||||
self.dispatch_list(node.body)
|
||||
|
||||
visitAsyncWith = visitWith
|
||||
|
||||
def _append_node(self, node):
|
||||
if not self.tail:
|
||||
return None
|
||||
self.graph.connect(self.tail, node)
|
||||
self.tail = node
|
||||
return node
|
||||
|
||||
def _subgraph(self, node, name, extra_blocks=()):
|
||||
"""create the subgraphs representing any `if` and `for` statements"""
|
||||
if self.graph is None:
|
||||
# global loop
|
||||
self.graph = PathGraph(node)
|
||||
self._subgraph_parse(node, node, extra_blocks)
|
||||
self.graphs["%s%s" % (self.classname, name)] = self.graph
|
||||
self.reset()
|
||||
else:
|
||||
self._append_node(node)
|
||||
self._subgraph_parse(node, node, extra_blocks)
|
||||
|
||||
def _subgraph_parse(self, node, pathnode, extra_blocks): # pylint: disable=unused-argument
|
||||
"""parse the body and any `else` block of `if` and `for` statements"""
|
||||
loose_ends = []
|
||||
self.tail = node
|
||||
self.dispatch_list(node.body)
|
||||
loose_ends.append(self.tail)
|
||||
for extra in extra_blocks:
|
||||
self.tail = node
|
||||
self.dispatch_list(extra.body)
|
||||
loose_ends.append(self.tail)
|
||||
if node.orelse:
|
||||
self.tail = node
|
||||
self.dispatch_list(node.orelse)
|
||||
loose_ends.append(self.tail)
|
||||
else:
|
||||
loose_ends.append(node)
|
||||
if node:
|
||||
bottom = "%s" % self._bottom_counter
|
||||
self._bottom_counter += 1
|
||||
for le in loose_ends:
|
||||
self.graph.connect(le, bottom)
|
||||
self.tail = bottom
|
||||
|
||||
|
||||
class McCabeMethodChecker(checkers.BaseChecker):
|
||||
"""Checks McCabe complexity cyclomatic threshold in methods and functions
|
||||
to validate a too complex code.
|
||||
"""
|
||||
|
||||
__implements__ = IAstroidChecker
|
||||
name = 'design'
|
||||
|
||||
msgs = {
|
||||
'R1260': (
|
||||
"%s is too complex. The McCabe rating is %d",
|
||||
'too-complex',
|
||||
'Used when a method or function is too complex based on '
|
||||
'McCabe Complexity Cyclomatic'),
|
||||
}
|
||||
options = (
|
||||
('max-complexity', {
|
||||
'default': 10,
|
||||
'type': 'int',
|
||||
'metavar': '<int>',
|
||||
'help': 'McCabe complexity cyclomatic threshold',
|
||||
}),
|
||||
)
|
||||
|
||||
@check_messages('too-complex')
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node to check too complex rating and
|
||||
add message if is greather than max_complexity stored from options"""
|
||||
visitor = PathGraphingAstVisitor()
|
||||
for child in node.body:
|
||||
visitor.preorder(child, visitor)
|
||||
for graph in visitor.graphs.values():
|
||||
complexity = graph.complexity()
|
||||
node = graph.root
|
||||
if hasattr(node, 'name'):
|
||||
node_name = "'%s'" % node.name
|
||||
else:
|
||||
node_name = "This '%s'" % node.__class__.__name__.lower()
|
||||
if complexity <= self.config.max_complexity:
|
||||
continue
|
||||
self.add_message(
|
||||
'too-complex', node=node, confidence=HIGH,
|
||||
args=(node_name, complexity))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(McCabeMethodChecker(linter))
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Looks for overlapping exceptions."""
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint import interfaces
|
||||
from pylint import checkers
|
||||
from pylint.checkers import utils
|
||||
|
||||
from pylint.checkers.exceptions import _annotated_unpack_infer
|
||||
|
||||
|
||||
class OverlappingExceptionsChecker(checkers.BaseChecker):
|
||||
"""Checks for two or more exceptions in the same exception handler
|
||||
clause that are identical or parts of the same inheritance hierarchy
|
||||
(i.e. overlapping)."""
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
|
||||
name = 'overlap-except'
|
||||
msgs = {'W0714': ('Overlapping exceptions (%s)',
|
||||
'overlapping-except',
|
||||
'Used when exceptions in handler overlap or are identical')}
|
||||
priority = -2
|
||||
options = ()
|
||||
|
||||
@utils.check_messages('overlapping-except')
|
||||
def visit_tryexcept(self, node):
|
||||
"""check for empty except"""
|
||||
for handler in node.handlers:
|
||||
if handler.type is None:
|
||||
continue
|
||||
if isinstance(handler.type, astroid.BoolOp):
|
||||
continue
|
||||
try:
|
||||
excs = list(_annotated_unpack_infer(handler.type))
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
handled_in_clause = []
|
||||
for part, exc in excs:
|
||||
if exc is astroid.YES:
|
||||
continue
|
||||
if (isinstance(exc, astroid.Instance) and
|
||||
utils.inherit_from_std_ex(exc)):
|
||||
# pylint: disable=protected-access
|
||||
exc = exc._proxied
|
||||
|
||||
if not isinstance(exc, astroid.ClassDef):
|
||||
continue
|
||||
|
||||
exc_ancestors = [anc for anc in exc.ancestors()
|
||||
if isinstance(anc, astroid.ClassDef)]
|
||||
|
||||
for prev_part, prev_exc in handled_in_clause:
|
||||
prev_exc_ancestors = [anc for anc in prev_exc.ancestors()
|
||||
if isinstance(anc, astroid.ClassDef)]
|
||||
if exc == prev_exc:
|
||||
self.add_message('overlapping-except',
|
||||
node=handler.type,
|
||||
args='%s and %s are the same' %
|
||||
(prev_part.as_string(),
|
||||
part.as_string()))
|
||||
elif (prev_exc in exc_ancestors or
|
||||
exc in prev_exc_ancestors):
|
||||
ancestor = part if exc in prev_exc_ancestors else prev_part
|
||||
descendant = part if prev_exc in exc_ancestors else prev_part
|
||||
self.add_message('overlapping-except',
|
||||
node=handler.type,
|
||||
args='%s is an ancestor class of %s' %
|
||||
(ancestor.as_string(), descendant.as_string()))
|
||||
handled_in_clause += [(part, exc)]
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker."""
|
||||
linter.register_checker(OverlappingExceptionsChecker(linter))
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glmatthe@cisco.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import six
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages, is_none, node_type
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
|
||||
|
||||
BUILTINS = six.moves.builtins.__name__
|
||||
|
||||
|
||||
class MultipleTypesChecker(BaseChecker):
|
||||
"""Checks for variable type redefinitions (NoneType excepted)
|
||||
|
||||
At a function, method, class or module scope
|
||||
|
||||
This rule could be improved:
|
||||
|
||||
- Currently, if an attribute is set to different types in 2 methods of a
|
||||
same class, it won't be detected (see functional test)
|
||||
- One could improve the support for inference on assignment with tuples,
|
||||
ifexpr, etc. Also it would be great to have support for inference on
|
||||
str.split()
|
||||
"""
|
||||
__implements__ = IAstroidChecker
|
||||
|
||||
name = 'multiple_types'
|
||||
msgs = {'R0204': ('Redefinition of %s type from %s to %s',
|
||||
'redefined-variable-type',
|
||||
'Used when the type of a variable changes inside a '
|
||||
'method or a function.'
|
||||
),
|
||||
}
|
||||
|
||||
def visit_classdef(self, _):
|
||||
self._assigns.append({})
|
||||
|
||||
@check_messages('redefined-variable-type')
|
||||
def leave_classdef(self, _):
|
||||
self._check_and_add_messages()
|
||||
|
||||
visit_functiondef = visit_classdef
|
||||
leave_functiondef = leave_module = leave_classdef
|
||||
|
||||
def visit_module(self, _):
|
||||
self._assigns = [{}]
|
||||
|
||||
def _check_and_add_messages(self):
|
||||
assigns = self._assigns.pop()
|
||||
for name, args in assigns.items():
|
||||
if len(args) <= 1:
|
||||
continue
|
||||
orig_node, orig_type = args[0]
|
||||
# Check if there is a type in the following nodes that would be
|
||||
# different from orig_type.
|
||||
for redef_node, redef_type in args[1:]:
|
||||
if redef_type == orig_type:
|
||||
continue
|
||||
# if a variable is defined to several types in a if node,
|
||||
# this is not actually redefining.
|
||||
orig_parent = orig_node.parent
|
||||
redef_parent = redef_node.parent
|
||||
if isinstance(orig_parent, astroid.If):
|
||||
if orig_parent == redef_parent:
|
||||
if (redef_node in orig_parent.orelse and
|
||||
orig_node not in orig_parent.orelse):
|
||||
orig_node, orig_type = redef_node, redef_type
|
||||
continue
|
||||
elif (isinstance(redef_parent, astroid.If) and
|
||||
redef_parent in orig_parent.nodes_of_class(astroid.If)):
|
||||
orig_node, orig_type = redef_node, redef_type
|
||||
continue
|
||||
orig_type = orig_type.replace(BUILTINS + ".", '')
|
||||
redef_type = redef_type.replace(BUILTINS + ".", '')
|
||||
self.add_message('redefined-variable-type', node=redef_node,
|
||||
args=(name, orig_type, redef_type))
|
||||
break
|
||||
|
||||
def visit_assign(self, node):
|
||||
# we don't handle multiple assignment nor slice assignment
|
||||
target = node.targets[0]
|
||||
if isinstance(target, (astroid.Tuple, astroid.Subscript)):
|
||||
return
|
||||
# ignore NoneType
|
||||
if is_none(node):
|
||||
return
|
||||
_type = node_type(node.value)
|
||||
if _type:
|
||||
self._assigns[-1].setdefault(target.as_string(), []).append(
|
||||
(node, _type.pytype()))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Required method to auto register this checker.
|
||||
|
||||
:param linter: Main interface object for Pylint plugins
|
||||
:type linter: Pylint object
|
||||
"""
|
||||
linter.register_checker(MultipleTypesChecker(linter))
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Graph manipulation utilities.
|
||||
|
||||
(dot generation adapted from pypy/translator/tool/make_dot.py)
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import codecs
|
||||
|
||||
def target_info_from_filename(filename):
|
||||
"""Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
|
||||
basename = osp.basename(filename)
|
||||
storedir = osp.dirname(osp.abspath(filename))
|
||||
target = filename.split('.')[-1]
|
||||
return storedir, basename, target
|
||||
|
||||
|
||||
class DotBackend(object):
|
||||
"""Dot File backend."""
|
||||
def __init__(self, graphname, rankdir=None, size=None, ratio=None,
|
||||
charset='utf-8', renderer='dot', additional_param=None):
|
||||
if additional_param is None:
|
||||
additional_param = {}
|
||||
self.graphname = graphname
|
||||
self.renderer = renderer
|
||||
self.lines = []
|
||||
self._source = None
|
||||
self.emit("digraph %s {" % normalize_node_id(graphname))
|
||||
if rankdir:
|
||||
self.emit('rankdir=%s' % rankdir)
|
||||
if ratio:
|
||||
self.emit('ratio=%s' % ratio)
|
||||
if size:
|
||||
self.emit('size="%s"' % size)
|
||||
if charset:
|
||||
assert charset.lower() in ('utf-8', 'iso-8859-1', 'latin1'), \
|
||||
'unsupported charset %s' % charset
|
||||
self.emit('charset="%s"' % charset)
|
||||
for param in additional_param.items():
|
||||
self.emit('='.join(param))
|
||||
|
||||
def get_source(self):
|
||||
"""returns self._source"""
|
||||
if self._source is None:
|
||||
self.emit("}\n")
|
||||
self._source = '\n'.join(self.lines)
|
||||
del self.lines
|
||||
return self._source
|
||||
|
||||
source = property(get_source)
|
||||
|
||||
def generate(self, outputfile=None, dotfile=None, mapfile=None):
|
||||
"""Generates a graph file.
|
||||
|
||||
:param str outputfile: filename and path [defaults to graphname.png]
|
||||
:param str dotfile: filename and path [defaults to graphname.dot]
|
||||
:param str mapfile: filename and path
|
||||
|
||||
:rtype: str
|
||||
:return: a path to the generated file
|
||||
"""
|
||||
import subprocess # introduced in py 2.4
|
||||
name = self.graphname
|
||||
if not dotfile:
|
||||
# if 'outputfile' is a dot file use it as 'dotfile'
|
||||
if outputfile and outputfile.endswith(".dot"):
|
||||
dotfile = outputfile
|
||||
else:
|
||||
dotfile = '%s.dot' % name
|
||||
if outputfile is not None:
|
||||
storedir, _, target = target_info_from_filename(outputfile)
|
||||
if target != "dot":
|
||||
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
|
||||
os.close(pdot)
|
||||
else:
|
||||
dot_sourcepath = osp.join(storedir, dotfile)
|
||||
else:
|
||||
target = 'png'
|
||||
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
|
||||
ppng, outputfile = tempfile.mkstemp(".png", name)
|
||||
os.close(pdot)
|
||||
os.close(ppng)
|
||||
pdot = codecs.open(dot_sourcepath, 'w', encoding='utf8')
|
||||
pdot.write(self.source)
|
||||
pdot.close()
|
||||
if target != 'dot':
|
||||
use_shell = sys.platform == 'win32'
|
||||
if mapfile:
|
||||
subprocess.call([self.renderer, '-Tcmapx', '-o',
|
||||
mapfile, '-T', target, dot_sourcepath,
|
||||
'-o', outputfile],
|
||||
shell=use_shell)
|
||||
else:
|
||||
subprocess.call([self.renderer, '-T', target,
|
||||
dot_sourcepath, '-o', outputfile],
|
||||
shell=use_shell)
|
||||
os.unlink(dot_sourcepath)
|
||||
return outputfile
|
||||
|
||||
def emit(self, line):
|
||||
"""Adds <line> to final output."""
|
||||
self.lines.append(line)
|
||||
|
||||
def emit_edge(self, name1, name2, **props):
|
||||
"""emit an edge from <name1> to <name2>.
|
||||
edge properties: see http://www.graphviz.org/doc/info/attrs.html
|
||||
"""
|
||||
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
|
||||
n_from, n_to = normalize_node_id(name1), normalize_node_id(name2)
|
||||
self.emit('%s -> %s [%s];' % (n_from, n_to, ', '.join(sorted(attrs))))
|
||||
|
||||
def emit_node(self, name, **props):
|
||||
"""emit a node with given properties.
|
||||
node properties: see http://www.graphviz.org/doc/info/attrs.html
|
||||
"""
|
||||
attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()]
|
||||
self.emit('%s [%s];' % (normalize_node_id(name), ', '.join(sorted(attrs))))
|
||||
|
||||
def normalize_node_id(nid):
|
||||
"""Returns a suitable DOT node id for `nid`."""
|
||||
return '"%s"' % nid
|
||||
|
||||
def get_cycles(graph_dict, vertices=None):
|
||||
'''given a dictionary representing an ordered graph (i.e. key are vertices
|
||||
and values is a list of destination vertices representing edges), return a
|
||||
list of detected cycles
|
||||
'''
|
||||
if not graph_dict:
|
||||
return ()
|
||||
result = []
|
||||
if vertices is None:
|
||||
vertices = graph_dict.keys()
|
||||
for vertice in vertices:
|
||||
_get_cycles(graph_dict, [], set(), result, vertice)
|
||||
return result
|
||||
|
||||
def _get_cycles(graph_dict, path, visited, result, vertice):
|
||||
"""recursive function doing the real work for get_cycles"""
|
||||
if vertice in path:
|
||||
cycle = [vertice]
|
||||
for node in path[::-1]:
|
||||
if node == vertice:
|
||||
break
|
||||
cycle.insert(0, node)
|
||||
# make a canonical representation
|
||||
start_from = min(cycle)
|
||||
index = cycle.index(start_from)
|
||||
cycle = cycle[index:] + cycle[0:index]
|
||||
# append it to result if not already in
|
||||
if cycle not in result:
|
||||
result.append(cycle)
|
||||
return
|
||||
path.append(vertice)
|
||||
try:
|
||||
for node in graph_dict[vertice]:
|
||||
# don't check already visited nodes again
|
||||
if node not in visited:
|
||||
_get_cycles(graph_dict, path, visited, result, node)
|
||||
visited.add(node)
|
||||
except KeyError:
|
||||
pass
|
||||
path.pop()
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2009-2010, 2012-2013 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2013-2014 Google, Inc.
|
||||
# Copyright (c) 2014 Michal Nowikowski <godfryd@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Interfaces for Pylint objects"""
|
||||
from collections import namedtuple
|
||||
|
||||
Confidence = namedtuple('Confidence', ['name', 'description'])
|
||||
# Warning Certainties
|
||||
HIGH = Confidence('HIGH', 'No false positive possible.')
|
||||
INFERENCE = Confidence('INFERENCE', 'Warning based on inference result.')
|
||||
INFERENCE_FAILURE = Confidence('INFERENCE_FAILURE',
|
||||
'Warning based on inference with failures.')
|
||||
UNDEFINED = Confidence('UNDEFINED',
|
||||
'Warning without any associated confidence level.')
|
||||
|
||||
CONFIDENCE_LEVELS = [HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED]
|
||||
|
||||
|
||||
class Interface(object):
|
||||
"""Base class for interfaces."""
|
||||
@classmethod
|
||||
def is_implemented_by(cls, instance):
|
||||
return implements(instance, cls)
|
||||
|
||||
|
||||
def implements(obj, interface):
|
||||
"""Return true if the give object (maybe an instance or class) implements
|
||||
the interface.
|
||||
"""
|
||||
kimplements = getattr(obj, '__implements__', ())
|
||||
if not isinstance(kimplements, (list, tuple)):
|
||||
kimplements = (kimplements,)
|
||||
for implementedinterface in kimplements:
|
||||
if issubclass(implementedinterface, interface):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class IChecker(Interface):
|
||||
"""This is an base interface, not designed to be used elsewhere than for
|
||||
sub interfaces definition.
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
|
||||
def close(self):
|
||||
"""called after visiting project (i.e set of modules)"""
|
||||
|
||||
|
||||
class IRawChecker(IChecker):
|
||||
"""interface for checker which need to parse the raw file
|
||||
"""
|
||||
|
||||
def process_module(self, astroid):
|
||||
""" process a module
|
||||
|
||||
the module's content is accessible via astroid.stream
|
||||
"""
|
||||
|
||||
|
||||
class ITokenChecker(IChecker):
|
||||
"""Interface for checkers that need access to the token list."""
|
||||
def process_tokens(self, tokens):
|
||||
"""Process a module.
|
||||
|
||||
tokens is a list of all source code tokens in the file.
|
||||
"""
|
||||
|
||||
|
||||
class IAstroidChecker(IChecker):
|
||||
""" interface for checker which prefers receive events according to
|
||||
statement type
|
||||
"""
|
||||
|
||||
|
||||
class IReporter(Interface):
|
||||
""" reporter collect messages and display results encapsulated in a layout
|
||||
"""
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Handle the given message object."""
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""display results encapsulated in the layout tree
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ('IRawChecker', 'IAstroidChecker', 'ITokenChecker', 'IReporter')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
pyreverse.extensions
|
||||
"""
|
||||
|
||||
__revision__ = "$Id $"
|
||||
@@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""handle diagram generation options for class diagram or default diagrams
|
||||
"""
|
||||
|
||||
from six.moves import builtins
|
||||
|
||||
import astroid
|
||||
|
||||
from pylint.pyreverse.diagrams import PackageDiagram, ClassDiagram
|
||||
from pylint.pyreverse.utils import LocalsVisitor
|
||||
|
||||
BUILTINS_NAME = builtins.__name__
|
||||
|
||||
# diagram generators ##########################################################
|
||||
|
||||
class DiaDefGenerator(object):
|
||||
"""handle diagram generation options"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
"""common Diagram Handler initialization"""
|
||||
self.config = handler.config
|
||||
self._set_default_options()
|
||||
self.linker = linker
|
||||
self.classdiagram = None # defined by subclasses
|
||||
|
||||
def get_title(self, node):
|
||||
"""get title for objects"""
|
||||
title = node.name
|
||||
if self.module_names:
|
||||
title = '%s.%s' % (node.root().name, title)
|
||||
return title
|
||||
|
||||
def _set_option(self, option):
|
||||
"""activate some options if not explicitly deactivated"""
|
||||
# if we have a class diagram, we want more information by default;
|
||||
# so if the option is None, we return True
|
||||
if option is None:
|
||||
return bool(self.config.classes)
|
||||
return option
|
||||
|
||||
def _set_default_options(self):
|
||||
"""set different default options with _default dictionary"""
|
||||
self.module_names = self._set_option(self.config.module_names)
|
||||
all_ancestors = self._set_option(self.config.all_ancestors)
|
||||
all_associated = self._set_option(self.config.all_associated)
|
||||
anc_level, ass_level = (0, 0)
|
||||
if all_ancestors:
|
||||
anc_level = -1
|
||||
if all_associated:
|
||||
ass_level = -1
|
||||
if self.config.show_ancestors is not None:
|
||||
anc_level = self.config.show_ancestors
|
||||
if self.config.show_associated is not None:
|
||||
ass_level = self.config.show_associated
|
||||
self.anc_level, self.ass_level = anc_level, ass_level
|
||||
|
||||
def _get_levels(self):
|
||||
"""help function for search levels"""
|
||||
return self.anc_level, self.ass_level
|
||||
|
||||
def show_node(self, node):
|
||||
"""true if builtins and not show_builtins"""
|
||||
if self.config.show_builtin:
|
||||
return True
|
||||
return node.root().name != BUILTINS_NAME
|
||||
|
||||
def add_class(self, node):
|
||||
"""visit one class and add it to diagram"""
|
||||
self.linker.visit(node)
|
||||
self.classdiagram.add_object(self.get_title(node), node)
|
||||
|
||||
def get_ancestors(self, node, level):
|
||||
"""return ancestor nodes of a class node"""
|
||||
if level == 0:
|
||||
return
|
||||
for ancestor in node.ancestors(recurs=False):
|
||||
if not self.show_node(ancestor):
|
||||
continue
|
||||
yield ancestor
|
||||
|
||||
def get_associated(self, klass_node, level):
|
||||
"""return associated nodes of a class node"""
|
||||
if level == 0:
|
||||
return
|
||||
for ass_nodes in list(klass_node.instance_attrs_type.values()) + \
|
||||
list(klass_node.locals_type.values()):
|
||||
for ass_node in ass_nodes:
|
||||
if isinstance(ass_node, astroid.Instance):
|
||||
ass_node = ass_node._proxied
|
||||
if not (isinstance(ass_node, astroid.ClassDef)
|
||||
and self.show_node(ass_node)):
|
||||
continue
|
||||
yield ass_node
|
||||
|
||||
def extract_classes(self, klass_node, anc_level, ass_level):
|
||||
"""extract recursively classes related to klass_node"""
|
||||
if self.classdiagram.has_node(klass_node) or not self.show_node(klass_node):
|
||||
return
|
||||
self.add_class(klass_node)
|
||||
|
||||
for ancestor in self.get_ancestors(klass_node, anc_level):
|
||||
self.extract_classes(ancestor, anc_level-1, ass_level)
|
||||
|
||||
for ass_node in self.get_associated(klass_node, ass_level):
|
||||
self.extract_classes(ass_node, anc_level, ass_level-1)
|
||||
|
||||
|
||||
class DefaultDiadefGenerator(LocalsVisitor, DiaDefGenerator):
|
||||
"""generate minimum diagram definition for the project :
|
||||
|
||||
* a package diagram including project's modules
|
||||
* a class diagram including project's classes
|
||||
"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
DiaDefGenerator.__init__(self, linker, handler)
|
||||
LocalsVisitor.__init__(self)
|
||||
|
||||
def visit_project(self, node):
|
||||
"""visit an pyreverse.utils.Project node
|
||||
|
||||
create a diagram definition for packages
|
||||
"""
|
||||
mode = self.config.mode
|
||||
if len(node.modules) > 1:
|
||||
self.pkgdiagram = PackageDiagram('packages %s' % node.name, mode)
|
||||
else:
|
||||
self.pkgdiagram = None
|
||||
self.classdiagram = ClassDiagram('classes %s' % node.name, mode)
|
||||
|
||||
def leave_project(self, node): # pylint: disable=unused-argument
|
||||
"""leave the pyreverse.utils.Project node
|
||||
|
||||
return the generated diagram definition
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
return self.pkgdiagram, self.classdiagram
|
||||
return self.classdiagram,
|
||||
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node
|
||||
|
||||
add this class to the package diagram definition
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
self.linker.visit(node)
|
||||
self.pkgdiagram.add_object(node.name, node)
|
||||
|
||||
def visit_classdef(self, node):
|
||||
"""visit an astroid.Class node
|
||||
|
||||
add this class to the class diagram definition
|
||||
"""
|
||||
anc_level, ass_level = self._get_levels()
|
||||
self.extract_classes(node, anc_level, ass_level)
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""visit astroid.ImportFrom and catch modules for package diagram
|
||||
"""
|
||||
if self.pkgdiagram:
|
||||
self.pkgdiagram.add_from_depend(node, node.modname)
|
||||
|
||||
|
||||
class ClassDiadefGenerator(DiaDefGenerator):
|
||||
"""generate a class diagram definition including all classes related to a
|
||||
given class
|
||||
"""
|
||||
|
||||
def __init__(self, linker, handler):
|
||||
DiaDefGenerator.__init__(self, linker, handler)
|
||||
|
||||
def class_diagram(self, project, klass):
|
||||
"""return a class diagram definition for the given klass and its
|
||||
related klasses
|
||||
"""
|
||||
|
||||
self.classdiagram = ClassDiagram(klass, self.config.mode)
|
||||
if len(project.modules) > 1:
|
||||
module, klass = klass.rsplit('.', 1)
|
||||
module = project.get_module(module)
|
||||
else:
|
||||
module = project.modules[0]
|
||||
klass = klass.split('.')[-1]
|
||||
klass = next(module.ilookup(klass))
|
||||
|
||||
anc_level, ass_level = self._get_levels()
|
||||
self.extract_classes(klass, anc_level, ass_level)
|
||||
return self.classdiagram
|
||||
|
||||
# diagram handler #############################################################
|
||||
|
||||
class DiadefsHandler(object):
|
||||
"""handle diagram definitions :
|
||||
|
||||
get it from user (i.e. xml files) or generate them
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
def get_diadefs(self, project, linker):
|
||||
"""Get the diagrams configuration data
|
||||
|
||||
:param project:The pyreverse project
|
||||
:type project: pyreverse.utils.Project
|
||||
:param linker: The linker
|
||||
:type linker: pyreverse.inspector.Linker(IdGeneratorMixIn, LocalsVisitor)
|
||||
|
||||
:returns: The list of diagram definitions
|
||||
:rtype: list(:class:`pylint.pyreverse.diagrams.ClassDiagram`)
|
||||
"""
|
||||
|
||||
# read and interpret diagram definitions (Diadefs)
|
||||
diagrams = []
|
||||
generator = ClassDiadefGenerator(linker, self)
|
||||
for klass in self.config.classes:
|
||||
diagrams.append(generator.class_diagram(project, klass))
|
||||
if not diagrams:
|
||||
diagrams = DefaultDiadefGenerator(linker, self).visit(project)
|
||||
for diagram in diagrams:
|
||||
diagram.extract_relationships()
|
||||
return diagrams
|
||||
@@ -0,0 +1,252 @@
|
||||
# Copyright (c) 2006, 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""diagram objects
|
||||
"""
|
||||
|
||||
import astroid
|
||||
from pylint.pyreverse.utils import is_interface, FilterMixIn
|
||||
from pylint.checkers.utils import decorated_with_property
|
||||
|
||||
|
||||
class Figure(object):
|
||||
"""base class for counter handling"""
|
||||
|
||||
|
||||
class Relationship(Figure):
|
||||
"""a relation ship from an object in the diagram to another
|
||||
"""
|
||||
def __init__(self, from_object, to_object, relation_type, name=None):
|
||||
Figure.__init__(self)
|
||||
self.from_object = from_object
|
||||
self.to_object = to_object
|
||||
self.type = relation_type
|
||||
self.name = name
|
||||
|
||||
|
||||
class DiagramEntity(Figure):
|
||||
"""a diagram object, i.e. a label associated to an astroid node
|
||||
"""
|
||||
def __init__(self, title='No name', node=None):
|
||||
Figure.__init__(self)
|
||||
self.title = title
|
||||
self.node = node
|
||||
|
||||
|
||||
class ClassDiagram(Figure, FilterMixIn):
|
||||
"""main class diagram handling
|
||||
"""
|
||||
TYPE = 'class'
|
||||
def __init__(self, title, mode):
|
||||
FilterMixIn.__init__(self, mode)
|
||||
Figure.__init__(self)
|
||||
self.title = title
|
||||
self.objects = []
|
||||
self.relationships = {}
|
||||
self._nodes = {}
|
||||
self.depends = []
|
||||
|
||||
def get_relationships(self, role):
|
||||
# sorted to get predictable (hence testable) results
|
||||
return sorted(self.relationships.get(role, ()),
|
||||
key=lambda x: (x.from_object.fig_id, x.to_object.fig_id))
|
||||
|
||||
def add_relationship(self, from_object, to_object,
|
||||
relation_type, name=None):
|
||||
"""create a relation ship
|
||||
"""
|
||||
rel = Relationship(from_object, to_object, relation_type, name)
|
||||
self.relationships.setdefault(relation_type, []).append(rel)
|
||||
|
||||
def get_relationship(self, from_object, relation_type):
|
||||
"""return a relation ship or None
|
||||
"""
|
||||
for rel in self.relationships.get(relation_type, ()):
|
||||
if rel.from_object is from_object:
|
||||
return rel
|
||||
raise KeyError(relation_type)
|
||||
|
||||
def get_attrs(self, node):
|
||||
"""return visible attributes, possibly with class name"""
|
||||
attrs = []
|
||||
properties = [
|
||||
(n, m) for n, m in node.items()
|
||||
if isinstance(m, astroid.FunctionDef)
|
||||
and decorated_with_property(m)
|
||||
]
|
||||
for node_name, ass_nodes in list(node.instance_attrs_type.items()) + \
|
||||
list(node.locals_type.items()) + properties:
|
||||
if not self.show_attr(node_name):
|
||||
continue
|
||||
names = self.class_names(ass_nodes)
|
||||
if names:
|
||||
node_name = "%s : %s" % (node_name, ", ".join(names))
|
||||
attrs.append(node_name)
|
||||
return sorted(attrs)
|
||||
|
||||
def get_methods(self, node):
|
||||
"""return visible methods"""
|
||||
methods = [
|
||||
m for m in node.values()
|
||||
if isinstance(m, astroid.FunctionDef)
|
||||
and not decorated_with_property(m)
|
||||
and self.show_attr(m.name)
|
||||
]
|
||||
return sorted(methods, key=lambda n: n.name)
|
||||
|
||||
def add_object(self, title, node):
|
||||
"""create a diagram object
|
||||
"""
|
||||
assert node not in self._nodes
|
||||
ent = DiagramEntity(title, node)
|
||||
self._nodes[node] = ent
|
||||
self.objects.append(ent)
|
||||
|
||||
def class_names(self, nodes):
|
||||
"""return class names if needed in diagram"""
|
||||
names = []
|
||||
for ass_node in nodes:
|
||||
if isinstance(ass_node, astroid.Instance):
|
||||
ass_node = ass_node._proxied
|
||||
if isinstance(ass_node, astroid.ClassDef) \
|
||||
and hasattr(ass_node, "name") and not self.has_node(ass_node):
|
||||
if ass_node.name not in names:
|
||||
ass_name = ass_node.name
|
||||
names.append(ass_name)
|
||||
return names
|
||||
|
||||
def nodes(self):
|
||||
"""return the list of underlying nodes
|
||||
"""
|
||||
return self._nodes.keys()
|
||||
|
||||
def has_node(self, node):
|
||||
"""return true if the given node is included in the diagram
|
||||
"""
|
||||
return node in self._nodes
|
||||
|
||||
def object_from_node(self, node):
|
||||
"""return the diagram object mapped to node
|
||||
"""
|
||||
return self._nodes[node]
|
||||
|
||||
def classes(self):
|
||||
"""return all class nodes in the diagram"""
|
||||
return [o for o in self.objects if isinstance(o.node, astroid.ClassDef)]
|
||||
|
||||
def classe(self, name):
|
||||
"""return a class by its name, raise KeyError if not found
|
||||
"""
|
||||
for klass in self.classes():
|
||||
if klass.node.name == name:
|
||||
return klass
|
||||
raise KeyError(name)
|
||||
|
||||
def extract_relationships(self):
|
||||
"""extract relation ships between nodes in the diagram
|
||||
"""
|
||||
for obj in self.classes():
|
||||
node = obj.node
|
||||
obj.attrs = self.get_attrs(node)
|
||||
obj.methods = self.get_methods(node)
|
||||
# shape
|
||||
if is_interface(node):
|
||||
obj.shape = 'interface'
|
||||
else:
|
||||
obj.shape = 'class'
|
||||
# inheritance link
|
||||
for par_node in node.ancestors(recurs=False):
|
||||
try:
|
||||
par_obj = self.object_from_node(par_node)
|
||||
self.add_relationship(obj, par_obj, 'specialization')
|
||||
except KeyError:
|
||||
continue
|
||||
# implements link
|
||||
for impl_node in node.implements:
|
||||
try:
|
||||
impl_obj = self.object_from_node(impl_node)
|
||||
self.add_relationship(obj, impl_obj, 'implements')
|
||||
except KeyError:
|
||||
continue
|
||||
# associations link
|
||||
for name, values in list(node.instance_attrs_type.items()) + \
|
||||
list(node.locals_type.items()):
|
||||
for value in values:
|
||||
if value is astroid.YES:
|
||||
continue
|
||||
if isinstance(value, astroid.Instance):
|
||||
value = value._proxied
|
||||
try:
|
||||
ass_obj = self.object_from_node(value)
|
||||
self.add_relationship(ass_obj, obj, 'association', name)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
|
||||
class PackageDiagram(ClassDiagram):
|
||||
"""package diagram handling
|
||||
"""
|
||||
TYPE = 'package'
|
||||
|
||||
def modules(self):
|
||||
"""return all module nodes in the diagram"""
|
||||
return [o for o in self.objects if isinstance(o.node, astroid.Module)]
|
||||
|
||||
def module(self, name):
|
||||
"""return a module by its name, raise KeyError if not found
|
||||
"""
|
||||
for mod in self.modules():
|
||||
if mod.node.name == name:
|
||||
return mod
|
||||
raise KeyError(name)
|
||||
|
||||
def get_module(self, name, node):
|
||||
"""return a module by its name, looking also for relative imports;
|
||||
raise KeyError if not found
|
||||
"""
|
||||
for mod in self.modules():
|
||||
mod_name = mod.node.name
|
||||
if mod_name == name:
|
||||
return mod
|
||||
#search for fullname of relative import modules
|
||||
package = node.root().name
|
||||
if mod_name == "%s.%s" % (package, name):
|
||||
return mod
|
||||
if mod_name == "%s.%s" % (package.rsplit('.', 1)[0], name):
|
||||
return mod
|
||||
raise KeyError(name)
|
||||
|
||||
def add_from_depend(self, node, from_module):
|
||||
"""add dependencies created by from-imports
|
||||
"""
|
||||
mod_name = node.root().name
|
||||
obj = self.module(mod_name)
|
||||
if from_module not in obj.node.depends:
|
||||
obj.node.depends.append(from_module)
|
||||
|
||||
def extract_relationships(self):
|
||||
"""extract relation ships between nodes in the diagram
|
||||
"""
|
||||
ClassDiagram.extract_relationships(self)
|
||||
for obj in self.classes():
|
||||
# ownership
|
||||
try:
|
||||
mod = self.object_from_node(obj.node.root())
|
||||
self.add_relationship(obj, mod, 'ownership')
|
||||
except KeyError:
|
||||
continue
|
||||
for obj in self.modules():
|
||||
obj.shape = 'package'
|
||||
# dependencies
|
||||
for dep_name in obj.node.depends:
|
||||
try:
|
||||
dep = self.get_module(dep_name, obj.node)
|
||||
except KeyError:
|
||||
continue
|
||||
self.add_relationship(obj, dep, 'depends')
|
||||
@@ -0,0 +1,363 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2017 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
Visitor doing some postprocessing on the astroid tree.
|
||||
Try to resolve definitions (namespace) dictionary, relationship...
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import astroid
|
||||
from astroid import bases
|
||||
from astroid import exceptions
|
||||
from astroid import manager
|
||||
from astroid import modutils
|
||||
from astroid import node_classes
|
||||
|
||||
|
||||
from pylint.pyreverse import utils
|
||||
|
||||
|
||||
def _iface_hdlr(_):
|
||||
"""Handler used by interfaces to handle suspicious interface nodes."""
|
||||
return True
|
||||
|
||||
|
||||
def _astroid_wrapper(func, modname):
|
||||
print('parsing %s...' % modname)
|
||||
try:
|
||||
return func(modname)
|
||||
except exceptions.AstroidBuildingException as exc:
|
||||
print(exc)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def interfaces(node, herited=True, handler_func=_iface_hdlr):
|
||||
"""Return an iterator on interfaces implemented by the given class node."""
|
||||
# FIXME: what if __implements__ = (MyIFace, MyParent.__implements__)...
|
||||
try:
|
||||
implements = bases.Instance(node).getattr('__implements__')[0]
|
||||
except exceptions.NotFoundError:
|
||||
return
|
||||
if not herited and implements.frame() is not node:
|
||||
return
|
||||
found = set()
|
||||
missing = False
|
||||
for iface in node_classes.unpack_infer(implements):
|
||||
if iface is astroid.YES:
|
||||
missing = True
|
||||
continue
|
||||
if iface not in found and handler_func(iface):
|
||||
found.add(iface)
|
||||
yield iface
|
||||
if missing:
|
||||
raise exceptions.InferenceError()
|
||||
|
||||
|
||||
class IdGeneratorMixIn(object):
|
||||
"""Mixin adding the ability to generate integer uid."""
|
||||
|
||||
def __init__(self, start_value=0):
|
||||
self.id_count = start_value
|
||||
|
||||
def init_counter(self, start_value=0):
|
||||
"""init the id counter
|
||||
"""
|
||||
self.id_count = start_value
|
||||
|
||||
def generate_id(self):
|
||||
"""generate a new identifier
|
||||
"""
|
||||
self.id_count += 1
|
||||
return self.id_count
|
||||
|
||||
|
||||
class Linker(IdGeneratorMixIn, utils.LocalsVisitor):
|
||||
"""Walk on the project tree and resolve relationships.
|
||||
|
||||
According to options the following attributes may be
|
||||
added to visited nodes:
|
||||
|
||||
* uid,
|
||||
a unique identifier for the node (on astroid.Project, astroid.Module,
|
||||
astroid.Class and astroid.locals_type). Only if the linker
|
||||
has been instantiated with tag=True parameter (False by default).
|
||||
|
||||
* Function
|
||||
a mapping from locals names to their bounded value, which may be a
|
||||
constant like a string or an integer, or an astroid node
|
||||
(on astroid.Module, astroid.Class and astroid.Function).
|
||||
|
||||
* instance_attrs_type
|
||||
as locals_type but for klass member attributes (only on astroid.Class)
|
||||
|
||||
* implements,
|
||||
list of implemented interface _objects_ (only on astroid.Class nodes)
|
||||
"""
|
||||
|
||||
def __init__(self, project, inherited_interfaces=0, tag=False):
|
||||
IdGeneratorMixIn.__init__(self)
|
||||
utils.LocalsVisitor.__init__(self)
|
||||
# take inherited interface in consideration or not
|
||||
self.inherited_interfaces = inherited_interfaces
|
||||
# tag nodes or not
|
||||
self.tag = tag
|
||||
# visited project
|
||||
self.project = project
|
||||
|
||||
def visit_project(self, node):
|
||||
"""visit an pyreverse.utils.Project node
|
||||
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
for module in node.modules:
|
||||
self.visit(module)
|
||||
|
||||
def visit_package(self, node):
|
||||
"""visit an astroid.Package node
|
||||
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
for subelmt in node.values():
|
||||
self.visit(subelmt)
|
||||
|
||||
def visit_module(self, node):
|
||||
"""visit an astroid.Module node
|
||||
|
||||
* set the locals_type mapping
|
||||
* set the depends mapping
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, 'locals_type'):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
node.depends = []
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
|
||||
def visit_classdef(self, node):
|
||||
"""visit an astroid.Class node
|
||||
|
||||
* set the locals_type and instance_attrs_type mappings
|
||||
* set the implements list and build it
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, 'locals_type'):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
# resolve ancestors
|
||||
for baseobj in node.ancestors(recurs=False):
|
||||
specializations = getattr(baseobj, 'specializations', [])
|
||||
specializations.append(node)
|
||||
baseobj.specializations = specializations
|
||||
# resolve instance attributes
|
||||
node.instance_attrs_type = collections.defaultdict(list)
|
||||
for assignattrs in node.instance_attrs.values():
|
||||
for assignattr in assignattrs:
|
||||
self.handle_assignattr_type(assignattr, node)
|
||||
# resolve implemented interface
|
||||
try:
|
||||
node.implements = list(interfaces(node, self.inherited_interfaces))
|
||||
except astroid.InferenceError:
|
||||
node.implements = ()
|
||||
|
||||
def visit_functiondef(self, node):
|
||||
"""visit an astroid.Function node
|
||||
|
||||
* set the locals_type mapping
|
||||
* optionally tag the node with a unique id
|
||||
"""
|
||||
if hasattr(node, 'locals_type'):
|
||||
return
|
||||
node.locals_type = collections.defaultdict(list)
|
||||
if self.tag:
|
||||
node.uid = self.generate_id()
|
||||
|
||||
link_project = visit_project
|
||||
link_module = visit_module
|
||||
link_class = visit_classdef
|
||||
link_function = visit_functiondef
|
||||
|
||||
def visit_assignname(self, node):
|
||||
"""visit an astroid.AssignName node
|
||||
|
||||
handle locals_type
|
||||
"""
|
||||
# avoid double parsing done by different Linkers.visit
|
||||
# running over the same project:
|
||||
if hasattr(node, '_handled'):
|
||||
return
|
||||
node._handled = True
|
||||
if node.name in node.frame():
|
||||
frame = node.frame()
|
||||
else:
|
||||
# the name has been defined as 'global' in the frame and belongs
|
||||
# there.
|
||||
frame = node.root()
|
||||
try:
|
||||
if not hasattr(frame, 'locals_type'):
|
||||
# If the frame doesn't have a locals_type yet,
|
||||
# it means it wasn't yet visited. Visit it now
|
||||
# to add what's missing from it.
|
||||
if isinstance(frame, astroid.ClassDef):
|
||||
self.visit_classdef(frame)
|
||||
elif isinstance(frame, astroid.FunctionDef):
|
||||
self.visit_functiondef(frame)
|
||||
else:
|
||||
self.visit_module(frame)
|
||||
|
||||
current = frame.locals_type[node.name]
|
||||
values = set(node.infer())
|
||||
frame.locals_type[node.name] = list(set(current) | values)
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def handle_assignattr_type(node, parent):
|
||||
"""handle an astroid.assignattr node
|
||||
|
||||
handle instance_attrs_type
|
||||
"""
|
||||
try:
|
||||
values = set(node.infer())
|
||||
current = set(parent.instance_attrs_type[node.attrname])
|
||||
parent.instance_attrs_type[node.attrname] = list(current | values)
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
|
||||
def visit_import(self, node):
|
||||
"""visit an astroid.Import node
|
||||
|
||||
resolve module dependencies
|
||||
"""
|
||||
context_file = node.root().file
|
||||
for name in node.names:
|
||||
relative = modutils.is_relative(name[0], context_file)
|
||||
self._imported_module(node, name[0], relative)
|
||||
|
||||
def visit_importfrom(self, node):
|
||||
"""visit an astroid.ImportFrom node
|
||||
|
||||
resolve module dependencies
|
||||
"""
|
||||
basename = node.modname
|
||||
context_file = node.root().file
|
||||
if context_file is not None:
|
||||
relative = modutils.is_relative(basename, context_file)
|
||||
else:
|
||||
relative = False
|
||||
for name in node.names:
|
||||
if name[0] == '*':
|
||||
continue
|
||||
# analyze dependencies
|
||||
fullname = '%s.%s' % (basename, name[0])
|
||||
if fullname.find('.') > -1:
|
||||
try:
|
||||
# TODO: don't use get_module_part,
|
||||
# missing package precedence
|
||||
fullname = modutils.get_module_part(fullname,
|
||||
context_file)
|
||||
except ImportError:
|
||||
continue
|
||||
if fullname != basename:
|
||||
self._imported_module(node, fullname, relative)
|
||||
|
||||
def compute_module(self, context_name, mod_path):
|
||||
"""return true if the module should be added to dependencies"""
|
||||
package_dir = os.path.dirname(self.project.path)
|
||||
if context_name == mod_path:
|
||||
return 0
|
||||
elif modutils.is_standard_module(mod_path, (package_dir,)):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def _imported_module(self, node, mod_path, relative):
|
||||
"""Notify an imported module, used to analyze dependencies"""
|
||||
module = node.root()
|
||||
context_name = module.name
|
||||
if relative:
|
||||
mod_path = '%s.%s' % ('.'.join(context_name.split('.')[:-1]),
|
||||
mod_path)
|
||||
if self.compute_module(context_name, mod_path):
|
||||
# handle dependencies
|
||||
if not hasattr(module, 'depends'):
|
||||
module.depends = []
|
||||
mod_paths = module.depends
|
||||
if mod_path not in mod_paths:
|
||||
mod_paths.append(mod_path)
|
||||
|
||||
|
||||
class Project(object):
|
||||
"""a project handle a set of modules / packages"""
|
||||
def __init__(self, name=''):
|
||||
self.name = name
|
||||
self.path = None
|
||||
self.modules = []
|
||||
self.locals = {}
|
||||
self.__getitem__ = self.locals.__getitem__
|
||||
self.__iter__ = self.locals.__iter__
|
||||
self.values = self.locals.values
|
||||
self.keys = self.locals.keys
|
||||
self.items = self.locals.items
|
||||
|
||||
def add_module(self, node):
|
||||
self.locals[node.name] = node
|
||||
self.modules.append(node)
|
||||
|
||||
def get_module(self, name):
|
||||
return self.locals[name]
|
||||
|
||||
def get_children(self):
|
||||
return self.modules
|
||||
|
||||
def __repr__(self):
|
||||
return '<Project %r at %s (%s modules)>' % (self.name, id(self),
|
||||
len(self.modules))
|
||||
|
||||
|
||||
def project_from_files(files, func_wrapper=_astroid_wrapper,
|
||||
project_name="no name",
|
||||
black_list=('CVS',)):
|
||||
"""return a Project from a list of files or modules"""
|
||||
# build the project representation
|
||||
astroid_manager = manager.AstroidManager()
|
||||
project = Project(project_name)
|
||||
for something in files:
|
||||
if not os.path.exists(something):
|
||||
fpath = modutils.file_from_modpath(something.split('.'))
|
||||
elif os.path.isdir(something):
|
||||
fpath = os.path.join(something, '__init__.py')
|
||||
else:
|
||||
fpath = something
|
||||
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
|
||||
if ast is None:
|
||||
continue
|
||||
# XXX why is first file defining the project.path ?
|
||||
project.path = project.path or ast.file
|
||||
project.add_module(ast)
|
||||
base_name = ast.name
|
||||
# recurse in package except if __init__ was explicitly given
|
||||
if ast.package and something.find('__init__') == -1:
|
||||
# recurse on others packages / modules if this is a package
|
||||
for fpath in modutils.get_module_files(os.path.dirname(ast.file),
|
||||
black_list):
|
||||
ast = func_wrapper(astroid_manager.ast_from_file, fpath)
|
||||
if ast is None or ast.name == base_name:
|
||||
continue
|
||||
project.add_module(ast)
|
||||
return project
|
||||
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) 2008-2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 Alexander Pervakov <frost.nzcr4@jagmort.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
%prog [options] <packages>
|
||||
|
||||
create UML diagrams for classes and modules in <packages>
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pylint.config import ConfigurationMixIn
|
||||
from pylint.pyreverse.inspector import Linker, project_from_files
|
||||
from pylint.pyreverse.diadefslib import DiadefsHandler
|
||||
from pylint.pyreverse import writer
|
||||
from pylint.pyreverse.utils import insert_default_options
|
||||
|
||||
OPTIONS = (
|
||||
("filter-mode",
|
||||
dict(short='f', default='PUB_ONLY', dest='mode', type='string',
|
||||
action='store', metavar='<mode>',
|
||||
help="""filter attributes and functions according to
|
||||
<mode>. Correct modes are :
|
||||
'PUB_ONLY' filter all non public attributes
|
||||
[DEFAULT], equivalent to PRIVATE+SPECIAL_A
|
||||
'ALL' no filter
|
||||
'SPECIAL' filter Python special functions
|
||||
except constructor
|
||||
'OTHER' filter protected and private
|
||||
attributes""")),
|
||||
|
||||
("class",
|
||||
dict(short='c', action="append", metavar="<class>", dest="classes", default=[],
|
||||
help="create a class diagram with all classes related to <class>;\
|
||||
this uses by default the options -ASmy")),
|
||||
|
||||
("show-ancestors",
|
||||
dict(short="a", action="store", metavar='<ancestor>', type='int',
|
||||
help='show <ancestor> generations of ancestor classes not in <projects>')),
|
||||
("all-ancestors",
|
||||
dict(short="A", default=None,
|
||||
help="show all ancestors off all classes in <projects>")),
|
||||
("show-associated",
|
||||
dict(short='s', action="store", metavar='<ass_level>', type='int',
|
||||
help='show <ass_level> levels of associated classes not in <projects>')),
|
||||
("all-associated",
|
||||
dict(short='S', default=None,
|
||||
help='show recursively all associated off all associated classes')),
|
||||
("show-builtin",
|
||||
dict(short="b", action="store_true", default=False,
|
||||
help='include builtin objects in representation of classes')),
|
||||
|
||||
("module-names",
|
||||
dict(short="m", default=None, type='yn', metavar='[yn]',
|
||||
help='include module name in representation of classes')),
|
||||
# TODO : generate dependencies like in pylint
|
||||
# ("package-dependencies",
|
||||
# dict(short="M", action="store", metavar='<package_depth>', type='int',
|
||||
# help='show <package_depth> module dependencies beyond modules in \
|
||||
# <projects> (for the package diagram)')),
|
||||
("only-classnames",
|
||||
dict(short='k', action="store_true", default=False,
|
||||
help="don't show attributes and methods in the class boxes; \
|
||||
this disables -f values")),
|
||||
("output", dict(short="o", dest="output_format", action="store",
|
||||
default="dot", metavar="<format>",
|
||||
help="create a *.<format> output file if format available.")),
|
||||
("ignore", {'type' : "csv", 'metavar' : "<file[,file...]>",
|
||||
'dest' : "black_list", "default" : ('CVS',),
|
||||
'help' : "Add files or directories to the blacklist. They "
|
||||
"should be base names, not paths."}),
|
||||
("project", {'default': "", 'type' : 'string', 'short': 'p',
|
||||
'metavar': '<project name>', 'help': 'set the project name.'}),
|
||||
)
|
||||
# FIXME : quiet mode
|
||||
#( ('quiet',
|
||||
#dict(help='run quietly', action='store_true', short='q')), )
|
||||
|
||||
def _check_graphviz_available(output_format):
|
||||
"""check if we need graphviz for different output format"""
|
||||
try:
|
||||
subprocess.call(['dot', '-V'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
except OSError:
|
||||
print("The output format '%s' is currently not available.\n"
|
||||
"Please install 'Graphviz' to have other output formats "
|
||||
"than 'dot' or 'vcg'." % output_format)
|
||||
sys.exit(32)
|
||||
|
||||
|
||||
|
||||
class Run(ConfigurationMixIn):
|
||||
"""base class providing common behaviour for pyreverse commands"""
|
||||
|
||||
options = OPTIONS
|
||||
|
||||
def __init__(self, args):
|
||||
ConfigurationMixIn.__init__(self, usage=__doc__)
|
||||
insert_default_options()
|
||||
args = self.load_command_line_configuration()
|
||||
if self.config.output_format not in ('dot', 'vcg'):
|
||||
_check_graphviz_available(self.config.output_format)
|
||||
|
||||
sys.exit(self.run(args))
|
||||
|
||||
def run(self, args):
|
||||
"""checking arguments and run project"""
|
||||
if not args:
|
||||
print(self.help())
|
||||
return 1
|
||||
# insert current working directory to the python path to recognize
|
||||
# dependencies to local modules even if cwd is not in the PYTHONPATH
|
||||
sys.path.insert(0, os.getcwd())
|
||||
try:
|
||||
project = project_from_files(args, project_name=self.config.project,
|
||||
black_list=self.config.black_list)
|
||||
linker = Linker(project, tag=True)
|
||||
handler = DiadefsHandler(self.config)
|
||||
diadefs = handler.get_diadefs(project, linker)
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
|
||||
if self.config.output_format == "vcg":
|
||||
writer.VCGWriter(self.config).write(diadefs)
|
||||
else:
|
||||
writer.DotWriter(self.config).write(diadefs)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Run(sys.argv[1:])
|
||||
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2006, 2008, 2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 hippo91 <guillaume.peillex@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""
|
||||
generic classes/functions for pyreverse core/extensions
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
########### pyreverse option utils ##############################
|
||||
|
||||
|
||||
RCFILE = '.pyreverserc'
|
||||
|
||||
def get_default_options():
|
||||
"""
|
||||
Read config file and return list of options
|
||||
"""
|
||||
options = []
|
||||
home = os.environ.get('HOME', '')
|
||||
if home:
|
||||
rcfile = os.path.join(home, RCFILE)
|
||||
try:
|
||||
options = open(rcfile).read().split()
|
||||
except IOError:
|
||||
pass # ignore if no config file found
|
||||
return options
|
||||
|
||||
def insert_default_options():
|
||||
"""insert default options to sys.argv
|
||||
"""
|
||||
options = get_default_options()
|
||||
options.reverse()
|
||||
for arg in options:
|
||||
sys.argv.insert(1, arg)
|
||||
|
||||
|
||||
|
||||
# astroid utilities ###########################################################
|
||||
|
||||
SPECIAL = re.compile('^__[A-Za-z0-9]+[A-Za-z0-9_]*__$')
|
||||
PRIVATE = re.compile('^__[_A-Za-z0-9]*[A-Za-z0-9]+_?$')
|
||||
PROTECTED = re.compile('^_[_A-Za-z0-9]*$')
|
||||
|
||||
def get_visibility(name):
|
||||
"""return the visibility from a name: public, protected, private or special
|
||||
"""
|
||||
if SPECIAL.match(name):
|
||||
visibility = 'special'
|
||||
elif PRIVATE.match(name):
|
||||
visibility = 'private'
|
||||
elif PROTECTED.match(name):
|
||||
visibility = 'protected'
|
||||
|
||||
else:
|
||||
visibility = 'public'
|
||||
return visibility
|
||||
|
||||
ABSTRACT = re.compile('^.*Abstract.*')
|
||||
FINAL = re.compile('^[A-Z_]*$')
|
||||
|
||||
def is_abstract(node):
|
||||
"""return true if the given class node correspond to an abstract class
|
||||
definition
|
||||
"""
|
||||
return ABSTRACT.match(node.name)
|
||||
|
||||
def is_final(node):
|
||||
"""return true if the given class/function node correspond to final
|
||||
definition
|
||||
"""
|
||||
return FINAL.match(node.name)
|
||||
|
||||
def is_interface(node):
|
||||
# bw compat
|
||||
return node.type == 'interface'
|
||||
|
||||
def is_exception(node):
|
||||
# bw compat
|
||||
return node.type == 'exception'
|
||||
|
||||
|
||||
# Helpers #####################################################################
|
||||
|
||||
_CONSTRUCTOR = 1
|
||||
_SPECIAL = 2
|
||||
_PROTECTED = 4
|
||||
_PRIVATE = 8
|
||||
MODES = {
|
||||
'ALL' : 0,
|
||||
'PUB_ONLY' : _SPECIAL + _PROTECTED + _PRIVATE,
|
||||
'SPECIAL' : _SPECIAL,
|
||||
'OTHER' : _PROTECTED + _PRIVATE,
|
||||
}
|
||||
VIS_MOD = {'special': _SPECIAL, 'protected': _PROTECTED,
|
||||
'private': _PRIVATE, 'public': 0}
|
||||
|
||||
|
||||
class FilterMixIn(object):
|
||||
"""filter nodes according to a mode and nodes' visibility
|
||||
"""
|
||||
def __init__(self, mode):
|
||||
"init filter modes"
|
||||
__mode = 0
|
||||
for nummod in mode.split('+'):
|
||||
try:
|
||||
__mode += MODES[nummod]
|
||||
except KeyError as ex:
|
||||
print('Unknown filter mode %s' % ex, file=sys.stderr)
|
||||
self.__mode = __mode
|
||||
|
||||
|
||||
def show_attr(self, node):
|
||||
"""return true if the node should be treated
|
||||
"""
|
||||
visibility = get_visibility(getattr(node, 'name', node))
|
||||
return not self.__mode & VIS_MOD[visibility]
|
||||
|
||||
|
||||
class ASTWalker(object):
|
||||
"""a walker visiting a tree in preorder, calling on the handler:
|
||||
|
||||
* visit_<class name> on entering a node, where class name is the class of
|
||||
the node in lower case
|
||||
|
||||
* leave_<class name> on leaving a node, where class name is the class of
|
||||
the node in lower case
|
||||
"""
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self._cache = {}
|
||||
|
||||
def walk(self, node, _done=None):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
if _done is None:
|
||||
_done = set()
|
||||
if node in _done:
|
||||
raise AssertionError((id(node), node, node.parent))
|
||||
_done.add(node)
|
||||
self.visit(node)
|
||||
for child_node in node.get_children():
|
||||
assert child_node is not node
|
||||
self.walk(child_node, _done)
|
||||
self.leave(node)
|
||||
assert node.parent is not node
|
||||
|
||||
def get_callbacks(self, node):
|
||||
"""get callbacks from handler for the visited node"""
|
||||
klass = node.__class__
|
||||
methods = self._cache.get(klass)
|
||||
if methods is None:
|
||||
handler = self.handler
|
||||
kid = klass.__name__.lower()
|
||||
e_method = getattr(handler, 'visit_%s' % kid,
|
||||
getattr(handler, 'visit_default', None))
|
||||
l_method = getattr(handler, 'leave_%s' % kid,
|
||||
getattr(handler, 'leave_default', None))
|
||||
self._cache[klass] = (e_method, l_method)
|
||||
else:
|
||||
e_method, l_method = methods
|
||||
return e_method, l_method
|
||||
|
||||
def visit(self, node):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
method = self.get_callbacks(node)[0]
|
||||
if method is not None:
|
||||
method(node)
|
||||
|
||||
def leave(self, node):
|
||||
"""walk on the tree from <node>, getting callbacks from handler"""
|
||||
method = self.get_callbacks(node)[1]
|
||||
if method is not None:
|
||||
method(node)
|
||||
|
||||
|
||||
class LocalsVisitor(ASTWalker):
|
||||
"""visit a project by traversing the locals dictionary"""
|
||||
def __init__(self):
|
||||
ASTWalker.__init__(self, self)
|
||||
self._visited = {}
|
||||
|
||||
def visit(self, node):
|
||||
"""launch the visit starting from the given node"""
|
||||
if node in self._visited:
|
||||
return None
|
||||
self._visited[node] = 1 # FIXME: use set ?
|
||||
methods = self.get_callbacks(node)
|
||||
if methods[0] is not None:
|
||||
methods[0](node)
|
||||
if hasattr(node, 'locals'): # skip Instance and other proxy
|
||||
for local_node in node.values():
|
||||
self.visit(local_node)
|
||||
if methods[1] is not None:
|
||||
return methods[1](node)
|
||||
return None
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Functions to generate files readable with Georg Sander's vcg
|
||||
(Visualization of Compiler Graphs).
|
||||
|
||||
You can download vcg at http://rw4.cs.uni-sb.de/~sander/html/gshome.html
|
||||
Note that vcg exists as a debian package.
|
||||
|
||||
See vcg's documentation for explanation about the different values that
|
||||
maybe used for the functions parameters.
|
||||
"""
|
||||
|
||||
ATTRS_VAL = {
|
||||
'algos': ('dfs', 'tree', 'minbackward',
|
||||
'left_to_right', 'right_to_left',
|
||||
'top_to_bottom', 'bottom_to_top',
|
||||
'maxdepth', 'maxdepthslow', 'mindepth', 'mindepthslow',
|
||||
'mindegree', 'minindegree', 'minoutdegree',
|
||||
'maxdegree', 'maxindegree', 'maxoutdegree'),
|
||||
'booleans': ('yes', 'no'),
|
||||
'colors': ('black', 'white', 'blue', 'red', 'green', 'yellow',
|
||||
'magenta', 'lightgrey',
|
||||
'cyan', 'darkgrey', 'darkblue', 'darkred', 'darkgreen',
|
||||
'darkyellow', 'darkmagenta', 'darkcyan', 'gold',
|
||||
'lightblue', 'lightred', 'lightgreen', 'lightyellow',
|
||||
'lightmagenta', 'lightcyan', 'lilac', 'turquoise',
|
||||
'aquamarine', 'khaki', 'purple', 'yellowgreen', 'pink',
|
||||
'orange', 'orchid'),
|
||||
'shapes': ('box', 'ellipse', 'rhomb', 'triangle'),
|
||||
'textmodes': ('center', 'left_justify', 'right_justify'),
|
||||
'arrowstyles': ('solid', 'line', 'none'),
|
||||
'linestyles': ('continuous', 'dashed', 'dotted', 'invisible'),
|
||||
}
|
||||
|
||||
# meaning of possible values:
|
||||
# O -> string
|
||||
# 1 -> int
|
||||
# list -> value in list
|
||||
GRAPH_ATTRS = {
|
||||
'title': 0,
|
||||
'label': 0,
|
||||
'color': ATTRS_VAL['colors'],
|
||||
'textcolor': ATTRS_VAL['colors'],
|
||||
'bordercolor': ATTRS_VAL['colors'],
|
||||
'width': 1,
|
||||
'height': 1,
|
||||
'borderwidth': 1,
|
||||
'textmode': ATTRS_VAL['textmodes'],
|
||||
'shape': ATTRS_VAL['shapes'],
|
||||
'shrink': 1,
|
||||
'stretch': 1,
|
||||
'orientation': ATTRS_VAL['algos'],
|
||||
'vertical_order': 1,
|
||||
'horizontal_order': 1,
|
||||
'xspace': 1,
|
||||
'yspace': 1,
|
||||
'layoutalgorithm': ATTRS_VAL['algos'],
|
||||
'late_edge_labels': ATTRS_VAL['booleans'],
|
||||
'display_edge_labels': ATTRS_VAL['booleans'],
|
||||
'dirty_edge_labels': ATTRS_VAL['booleans'],
|
||||
'finetuning': ATTRS_VAL['booleans'],
|
||||
'manhattan_edges': ATTRS_VAL['booleans'],
|
||||
'smanhattan_edges': ATTRS_VAL['booleans'],
|
||||
'port_sharing': ATTRS_VAL['booleans'],
|
||||
'edges': ATTRS_VAL['booleans'],
|
||||
'nodes': ATTRS_VAL['booleans'],
|
||||
'splines': ATTRS_VAL['booleans'],
|
||||
}
|
||||
NODE_ATTRS = {
|
||||
'title': 0,
|
||||
'label': 0,
|
||||
'color': ATTRS_VAL['colors'],
|
||||
'textcolor': ATTRS_VAL['colors'],
|
||||
'bordercolor': ATTRS_VAL['colors'],
|
||||
'width': 1,
|
||||
'height': 1,
|
||||
'borderwidth': 1,
|
||||
'textmode': ATTRS_VAL['textmodes'],
|
||||
'shape': ATTRS_VAL['shapes'],
|
||||
'shrink': 1,
|
||||
'stretch': 1,
|
||||
'vertical_order': 1,
|
||||
'horizontal_order': 1,
|
||||
}
|
||||
EDGE_ATTRS = {
|
||||
'sourcename': 0,
|
||||
'targetname': 0,
|
||||
'label': 0,
|
||||
'linestyle': ATTRS_VAL['linestyles'],
|
||||
'class': 1,
|
||||
'thickness': 0,
|
||||
'color': ATTRS_VAL['colors'],
|
||||
'textcolor': ATTRS_VAL['colors'],
|
||||
'arrowcolor': ATTRS_VAL['colors'],
|
||||
'backarrowcolor': ATTRS_VAL['colors'],
|
||||
'arrowsize': 1,
|
||||
'backarrowsize': 1,
|
||||
'arrowstyle': ATTRS_VAL['arrowstyles'],
|
||||
'backarrowstyle': ATTRS_VAL['arrowstyles'],
|
||||
'textmode': ATTRS_VAL['textmodes'],
|
||||
'priority': 1,
|
||||
'anchor': 1,
|
||||
'horizontal_order': 1,
|
||||
}
|
||||
|
||||
|
||||
# Misc utilities ###############################################################
|
||||
|
||||
class VCGPrinter(object):
|
||||
"""A vcg graph writer.
|
||||
"""
|
||||
|
||||
def __init__(self, output_stream):
|
||||
self._stream = output_stream
|
||||
self._indent = ''
|
||||
|
||||
def open_graph(self, **args):
|
||||
"""open a vcg graph
|
||||
"""
|
||||
self._stream.write('%sgraph:{\n'%self._indent)
|
||||
self._inc_indent()
|
||||
self._write_attributes(GRAPH_ATTRS, **args)
|
||||
|
||||
def close_graph(self):
|
||||
"""close a vcg graph
|
||||
"""
|
||||
self._dec_indent()
|
||||
self._stream.write('%s}\n'%self._indent)
|
||||
|
||||
|
||||
def node(self, title, **args):
|
||||
"""draw a node
|
||||
"""
|
||||
self._stream.write('%snode: {title:"%s"' % (self._indent, title))
|
||||
self._write_attributes(NODE_ATTRS, **args)
|
||||
self._stream.write('}\n')
|
||||
|
||||
|
||||
def edge(self, from_node, to_node, edge_type='', **args):
|
||||
"""draw an edge from a node to another.
|
||||
"""
|
||||
self._stream.write(
|
||||
'%s%sedge: {sourcename:"%s" targetname:"%s"' % (
|
||||
self._indent, edge_type, from_node, to_node))
|
||||
self._write_attributes(EDGE_ATTRS, **args)
|
||||
self._stream.write('}\n')
|
||||
|
||||
|
||||
# private ##################################################################
|
||||
|
||||
def _write_attributes(self, attributes_dict, **args):
|
||||
"""write graph, node or edge attributes
|
||||
"""
|
||||
for key, value in args.items():
|
||||
try:
|
||||
_type = attributes_dict[key]
|
||||
except KeyError:
|
||||
raise Exception('''no such attribute %s
|
||||
possible attributes are %s''' % (key, attributes_dict.keys()))
|
||||
|
||||
if not _type:
|
||||
self._stream.write('%s%s:"%s"\n' % (self._indent, key, value))
|
||||
elif _type == 1:
|
||||
self._stream.write('%s%s:%s\n' % (self._indent, key,
|
||||
int(value)))
|
||||
elif value in _type:
|
||||
self._stream.write('%s%s:%s\n' % (self._indent, key, value))
|
||||
else:
|
||||
raise Exception('''value %s isn\'t correct for attribute %s
|
||||
correct values are %s''' % (value, key, _type))
|
||||
|
||||
def _inc_indent(self):
|
||||
"""increment indentation
|
||||
"""
|
||||
self._indent = ' %s' % self._indent
|
||||
|
||||
def _dec_indent(self):
|
||||
"""decrement indentation
|
||||
"""
|
||||
self._indent = self._indent[:-2]
|
||||
@@ -0,0 +1,192 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Mike Frysinger <vapier@gentoo.org>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Utilities for creating VCG and Dot diagrams"""
|
||||
|
||||
from pylint.pyreverse.utils import is_exception
|
||||
from pylint.pyreverse.vcgutils import VCGPrinter
|
||||
from pylint.graph import DotBackend
|
||||
|
||||
class DiagramWriter(object):
|
||||
"""base class for writing project diagrams
|
||||
"""
|
||||
def __init__(self, config, styles):
|
||||
self.config = config
|
||||
self.pkg_edges, self.inh_edges, self.imp_edges, self.ass_edges = styles
|
||||
self.printer = None # defined in set_printer
|
||||
|
||||
def write(self, diadefs):
|
||||
"""write files for <project> according to <diadefs>
|
||||
"""
|
||||
for diagram in diadefs:
|
||||
basename = diagram.title.strip().replace(' ', '_')
|
||||
file_name = '%s.%s' % (basename, self.config.output_format)
|
||||
self.set_printer(file_name, basename)
|
||||
if diagram.TYPE == 'class':
|
||||
self.write_classes(diagram)
|
||||
else:
|
||||
self.write_packages(diagram)
|
||||
self.close_graph()
|
||||
|
||||
def write_packages(self, diagram):
|
||||
"""write a package diagram"""
|
||||
# sorted to get predictable (hence testable) results
|
||||
for i, obj in enumerate(sorted(diagram.modules(), key=lambda x: x.title)):
|
||||
self.printer.emit_node(i, label=self.get_title(obj), shape='box')
|
||||
obj.fig_id = i
|
||||
# package dependencies
|
||||
for rel in diagram.get_relationships('depends'):
|
||||
self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id,
|
||||
**self.pkg_edges)
|
||||
|
||||
def write_classes(self, diagram):
|
||||
"""write a class diagram"""
|
||||
# sorted to get predictable (hence testable) results
|
||||
for i, obj in enumerate(sorted(diagram.objects, key=lambda x: x.title)):
|
||||
self.printer.emit_node(i, **self.get_values(obj))
|
||||
obj.fig_id = i
|
||||
# inheritance links
|
||||
for rel in diagram.get_relationships('specialization'):
|
||||
self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id,
|
||||
**self.inh_edges)
|
||||
# implementation links
|
||||
for rel in diagram.get_relationships('implements'):
|
||||
self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id,
|
||||
**self.imp_edges)
|
||||
# generate associations
|
||||
for rel in diagram.get_relationships('association'):
|
||||
self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id,
|
||||
label=rel.name, **self.ass_edges)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""set printer"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close_graph(self):
|
||||
"""finalize the graph"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DotWriter(DiagramWriter):
|
||||
"""write dot graphs from a diagram definition and a project
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
styles = [dict(arrowtail='none', arrowhead="open"),
|
||||
dict(arrowtail='none', arrowhead='empty'),
|
||||
dict(arrowtail='node', arrowhead='empty', style='dashed'),
|
||||
dict(fontcolor='green', arrowtail='none',
|
||||
arrowhead='diamond', style='solid'),
|
||||
]
|
||||
DiagramWriter.__init__(self, config, styles)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""initialize DotWriter and add options for layout.
|
||||
"""
|
||||
layout = dict(rankdir="BT")
|
||||
self.printer = DotBackend(basename, additional_param=layout)
|
||||
self.file_name = file_name
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title"""
|
||||
return obj.title
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes.
|
||||
|
||||
The label contains all attributes and methods
|
||||
"""
|
||||
label = obj.title
|
||||
if obj.shape == 'interface':
|
||||
label = u'«interface»\\n%s' % label
|
||||
if not self.config.only_classnames:
|
||||
label = r'%s|%s\l|' % (label, r'\l'.join(obj.attrs))
|
||||
for func in obj.methods:
|
||||
label = r'%s%s()\l' % (label, func.name)
|
||||
label = '{%s}' % label
|
||||
if is_exception(obj.node):
|
||||
return dict(fontcolor='red', label=label, shape='record')
|
||||
return dict(label=label, shape='record')
|
||||
|
||||
def close_graph(self):
|
||||
"""print the dot graph into <file_name>"""
|
||||
self.printer.generate(self.file_name)
|
||||
|
||||
|
||||
class VCGWriter(DiagramWriter):
|
||||
"""write vcg graphs from a diagram definition and a project
|
||||
"""
|
||||
def __init__(self, config):
|
||||
styles = [dict(arrowstyle='solid', backarrowstyle='none',
|
||||
backarrowsize=0),
|
||||
dict(arrowstyle='solid', backarrowstyle='none',
|
||||
backarrowsize=10),
|
||||
dict(arrowstyle='solid', backarrowstyle='none',
|
||||
linestyle='dotted', backarrowsize=10),
|
||||
dict(arrowstyle='solid', backarrowstyle='none',
|
||||
textcolor='green'),
|
||||
]
|
||||
DiagramWriter.__init__(self, config, styles)
|
||||
|
||||
def set_printer(self, file_name, basename):
|
||||
"""initialize VCGWriter for a UML graph"""
|
||||
self.graph_file = open(file_name, 'w+')
|
||||
self.printer = VCGPrinter(self.graph_file)
|
||||
self.printer.open_graph(title=basename, layoutalgorithm='dfs',
|
||||
late_edge_labels='yes', port_sharing='no',
|
||||
manhattan_edges='yes')
|
||||
self.printer.emit_node = self.printer.node
|
||||
self.printer.emit_edge = self.printer.edge
|
||||
|
||||
def get_title(self, obj):
|
||||
"""get project title in vcg format"""
|
||||
return r'\fb%s\fn' % obj.title
|
||||
|
||||
def get_values(self, obj):
|
||||
"""get label and shape for classes.
|
||||
|
||||
The label contains all attributes and methods
|
||||
"""
|
||||
if is_exception(obj.node):
|
||||
label = r'\fb\f09%s\fn' % obj.title
|
||||
else:
|
||||
label = r'\fb%s\fn' % obj.title
|
||||
if obj.shape == 'interface':
|
||||
shape = 'ellipse'
|
||||
else:
|
||||
shape = 'box'
|
||||
if not self.config.only_classnames:
|
||||
attrs = obj.attrs
|
||||
methods = [func.name for func in obj.methods]
|
||||
# box width for UML like diagram
|
||||
maxlen = max(len(name) for name in [obj.title] + methods + attrs)
|
||||
line = '_' * (maxlen + 2)
|
||||
label = r'%s\n\f%s' % (label, line)
|
||||
for attr in attrs:
|
||||
label = r'%s\n\f08%s' % (label, attr)
|
||||
if attrs:
|
||||
label = r'%s\n\f%s' % (label, line)
|
||||
for func in methods:
|
||||
label = r'%s\n\f10%s()' % (label, func)
|
||||
return dict(label=label, shape=shape)
|
||||
|
||||
def close_graph(self):
|
||||
"""close graph and file"""
|
||||
self.printer.close_graph()
|
||||
self.graph_file.close()
|
||||
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2006, 2010, 2012-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2012 FELD Boris <lothiraldan@gmail.com>
|
||||
# Copyright (c) 2014-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Ricardo Gemignani <ricardo.gemignani@gmail.com>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015 Simu Toni <simutoni@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 Kári Tristan Helgason <kthelgason@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""utilities methods and classes for reporters"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import locale
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import six
|
||||
|
||||
CMPS = ['=', '-', '+']
|
||||
|
||||
# py3k has no more cmp builtin
|
||||
if sys.version_info >= (3, 0):
|
||||
def cmp(a, b): # pylint: disable=redefined-builtin
|
||||
return (a > b) - (a < b)
|
||||
|
||||
def diff_string(old, new):
|
||||
"""given a old and new int value, return a string representing the
|
||||
difference
|
||||
"""
|
||||
diff = abs(old - new)
|
||||
diff_str = "%s%s" % (CMPS[cmp(old, new)], diff and ('%.2f' % diff) or '')
|
||||
return diff_str
|
||||
|
||||
|
||||
class BaseReporter(object):
|
||||
"""base class for reporters
|
||||
|
||||
symbols: show short symbolic names for messages.
|
||||
"""
|
||||
|
||||
extension = ''
|
||||
|
||||
def __init__(self, output=None):
|
||||
self.linter = None
|
||||
self.section = 0
|
||||
self.out = None
|
||||
self.out_encoding = None
|
||||
self.set_output(output)
|
||||
# Build the path prefix to strip to get relative paths
|
||||
self.path_strip_prefix = os.getcwd() + os.sep
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Handle a new message triggered on the current file."""
|
||||
|
||||
def set_output(self, output=None):
|
||||
"""set output stream"""
|
||||
self.out = output or sys.stdout
|
||||
|
||||
if six.PY3:
|
||||
encode = lambda self, string: string
|
||||
else:
|
||||
def encode(self, string):
|
||||
if not isinstance(string, six.text_type):
|
||||
return string
|
||||
|
||||
encoding = (getattr(self.out, 'encoding', None) or
|
||||
locale.getpreferredencoding(do_setlocale=False) or
|
||||
sys.getdefaultencoding())
|
||||
# errors=replace, we don't want to crash when attempting to show
|
||||
# source code line that can't be encoded with the current locale
|
||||
# settings
|
||||
return string.encode(encoding, 'replace')
|
||||
|
||||
def writeln(self, string=''):
|
||||
"""write a line in the output buffer"""
|
||||
print(self.encode(string), file=self.out)
|
||||
|
||||
def display_reports(self, layout):
|
||||
"""display results encapsulated in the layout tree"""
|
||||
self.section = 0
|
||||
if hasattr(layout, 'report_id'):
|
||||
layout.children[0].children[0].data += ' (%s)' % layout.report_id
|
||||
self._display(layout)
|
||||
|
||||
def _display(self, layout):
|
||||
"""display the layout"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def display_messages(self, layout):
|
||||
"""Hook for displaying the messages of the reporter
|
||||
|
||||
This will be called whenever the underlying messages
|
||||
needs to be displayed. For some reporters, it probably
|
||||
doesn't make sense to display messages as soon as they
|
||||
are available, so some mechanism of storing them could be used.
|
||||
This method can be implemented to display them after they've
|
||||
been aggregated.
|
||||
"""
|
||||
|
||||
# Event callbacks
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
"""Hook called when a module starts to be analysed."""
|
||||
|
||||
def on_close(self, stats, previous_stats):
|
||||
"""Hook called when a module finished analyzing."""
|
||||
|
||||
|
||||
class CollectingReporter(BaseReporter):
|
||||
"""collects messages"""
|
||||
|
||||
name = 'collector'
|
||||
|
||||
def __init__(self):
|
||||
BaseReporter.__init__(self)
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
_display = None
|
||||
|
||||
|
||||
def initialize(linter):
|
||||
"""initialize linter with reporters in this package """
|
||||
from pylint import utils
|
||||
utils.register_plugins(linter, __path__[0])
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) 2014 Vlad Temian <vladtemian@gmail.com>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2017 guillaume2 <guillaume.peillex@gmail.col>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""JSON reporter"""
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import cgi
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
|
||||
|
||||
class JSONReporter(BaseReporter):
|
||||
"""Report messages and layouts in JSON."""
|
||||
|
||||
__implements__ = IReporter
|
||||
name = 'json'
|
||||
extension = 'json'
|
||||
|
||||
def __init__(self, output=sys.stdout):
|
||||
BaseReporter.__init__(self, output)
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""Manage message of different type and in the context of path."""
|
||||
self.messages.append({
|
||||
'type': msg.category,
|
||||
'module': msg.module,
|
||||
'obj': msg.obj,
|
||||
'line': msg.line,
|
||||
'column': msg.column,
|
||||
'path': msg.path,
|
||||
'symbol': msg.symbol,
|
||||
# pylint: disable=deprecated-method; deprecated since 3.2.
|
||||
'message': cgi.escape(msg.msg or ''),
|
||||
'message-id': msg.msg_id,
|
||||
})
|
||||
|
||||
def display_messages(self, layout):
|
||||
"""Launch layouts display"""
|
||||
if self.messages:
|
||||
print(json.dumps(self.messages, indent=4), file=self.out)
|
||||
|
||||
def display_reports(self, layout): # pylint: disable=arguments-differ
|
||||
"""Don't do nothing in this reporter."""
|
||||
|
||||
def _display(self, layout):
|
||||
"""Don't do nothing."""
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(JSONReporter)
|
||||
@@ -0,0 +1,239 @@
|
||||
# Copyright (c) 2006-2007, 2010-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2012-2014 Google, Inc.
|
||||
# Copyright (c) 2014 Brett Cannon <brett@python.org>
|
||||
# Copyright (c) 2014 Arun Persaud <arun@nubati.net>
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
|
||||
# Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro>
|
||||
# Copyright (c) 2016 y2kbugger <y2kbugger@users.noreply.github.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Plain text reporters:
|
||||
|
||||
:text: the default one grouping messages by module
|
||||
:colorized: an ANSI colorized text reporter
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
from pylint import utils
|
||||
from pylint.reporters.ureports.text_writer import TextWriter
|
||||
|
||||
|
||||
TITLE_UNDERLINES = ['', '=', '-', '.']
|
||||
|
||||
ANSI_PREFIX = '\033['
|
||||
ANSI_END = 'm'
|
||||
ANSI_RESET = '\033[0m'
|
||||
ANSI_STYLES = {
|
||||
'reset': "0",
|
||||
'bold': "1",
|
||||
'italic': "3",
|
||||
'underline': "4",
|
||||
'blink': "5",
|
||||
'inverse': "7",
|
||||
'strike': "9",
|
||||
}
|
||||
ANSI_COLORS = {
|
||||
'reset': "0",
|
||||
'black': "30",
|
||||
'red': "31",
|
||||
'green': "32",
|
||||
'yellow': "33",
|
||||
'blue': "34",
|
||||
'magenta': "35",
|
||||
'cyan': "36",
|
||||
'white': "37",
|
||||
}
|
||||
|
||||
def _get_ansi_code(color=None, style=None):
|
||||
"""return ansi escape code corresponding to color and style
|
||||
|
||||
:type color: str or None
|
||||
:param color:
|
||||
the color name (see `ANSI_COLORS` for available values)
|
||||
or the color number when 256 colors are available
|
||||
|
||||
:type style: str or None
|
||||
:param style:
|
||||
style string (see `ANSI_COLORS` for available values). To get
|
||||
several style effects at the same time, use a coma as separator.
|
||||
|
||||
:raise KeyError: if an unexistent color or style identifier is given
|
||||
|
||||
:rtype: str
|
||||
:return: the built escape code
|
||||
"""
|
||||
ansi_code = []
|
||||
if style:
|
||||
style_attrs = utils._splitstrip(style)
|
||||
for effect in style_attrs:
|
||||
ansi_code.append(ANSI_STYLES[effect])
|
||||
if color:
|
||||
if color.isdigit():
|
||||
ansi_code.extend(['38', '5'])
|
||||
ansi_code.append(color)
|
||||
else:
|
||||
ansi_code.append(ANSI_COLORS[color])
|
||||
if ansi_code:
|
||||
return ANSI_PREFIX + ';'.join(ansi_code) + ANSI_END
|
||||
return ''
|
||||
|
||||
def colorize_ansi(msg, color=None, style=None):
|
||||
"""colorize message by wrapping it with ansi escape codes
|
||||
|
||||
:type msg: str or unicode
|
||||
:param msg: the message string to colorize
|
||||
|
||||
:type color: str or None
|
||||
:param color:
|
||||
the color identifier (see `ANSI_COLORS` for available values)
|
||||
|
||||
:type style: str or None
|
||||
:param style:
|
||||
style string (see `ANSI_COLORS` for available values). To get
|
||||
several style effects at the same time, use a coma as separator.
|
||||
|
||||
:raise KeyError: if an unexistent color or style identifier is given
|
||||
|
||||
:rtype: str or unicode
|
||||
:return: the ansi escaped string
|
||||
"""
|
||||
# If both color and style are not defined, then leave the text as is
|
||||
if color is None and style is None:
|
||||
return msg
|
||||
escape_code = _get_ansi_code(color, style)
|
||||
# If invalid (or unknown) color, don't wrap msg with ansi codes
|
||||
if escape_code:
|
||||
return '%s%s%s' % (escape_code, msg, ANSI_RESET)
|
||||
return msg
|
||||
|
||||
|
||||
class TextReporter(BaseReporter):
|
||||
"""reports messages and layouts in plain text"""
|
||||
|
||||
__implements__ = IReporter
|
||||
name = 'text'
|
||||
extension = 'txt'
|
||||
line_format = '{C}:{line:3d},{column:2d}: {msg} ({symbol})'
|
||||
|
||||
def __init__(self, output=None):
|
||||
BaseReporter.__init__(self, output)
|
||||
self._modules = set()
|
||||
self._template = None
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
self._template = six.text_type(self.linter.config.msg_template or self.line_format)
|
||||
|
||||
def write_message(self, msg):
|
||||
"""Convenience method to write a formated message with class default template"""
|
||||
self.writeln(msg.format(self._template))
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different type and in the context of path"""
|
||||
if msg.module not in self._modules:
|
||||
if msg.module:
|
||||
self.writeln('************* Module %s' % msg.module)
|
||||
self._modules.add(msg.module)
|
||||
else:
|
||||
self.writeln('************* ')
|
||||
self.write_message(msg)
|
||||
|
||||
def _display(self, layout):
|
||||
"""launch layouts display"""
|
||||
print(file=self.out)
|
||||
TextWriter().format(layout, self.out)
|
||||
|
||||
|
||||
class ParseableTextReporter(TextReporter):
|
||||
"""a reporter very similar to TextReporter, but display messages in a form
|
||||
recognized by most text editors :
|
||||
|
||||
<filename>:<linenum>:<msg>
|
||||
"""
|
||||
name = 'parseable'
|
||||
line_format = '{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}'
|
||||
|
||||
def __init__(self, output=None):
|
||||
warnings.warn('%s output format is deprecated. This is equivalent '
|
||||
'to --msg-template=%s' % (self.name, self.line_format),
|
||||
DeprecationWarning)
|
||||
TextReporter.__init__(self, output)
|
||||
|
||||
|
||||
class VSTextReporter(ParseableTextReporter):
|
||||
"""Visual studio text reporter"""
|
||||
name = 'msvs'
|
||||
line_format = '{path}({line}): [{msg_id}({symbol}){obj}] {msg}'
|
||||
|
||||
|
||||
class ColorizedTextReporter(TextReporter):
|
||||
"""Simple TextReporter that colorizes text output"""
|
||||
|
||||
name = 'colorized'
|
||||
COLOR_MAPPING = {
|
||||
"I" : ("green", None),
|
||||
'C' : (None, "bold"),
|
||||
'R' : ("magenta", "bold, italic"),
|
||||
'W' : ("blue", None),
|
||||
'E' : ("red", "bold"),
|
||||
'F' : ("red", "bold, underline"),
|
||||
'S' : ("yellow", "inverse"), # S stands for module Separator
|
||||
}
|
||||
|
||||
def __init__(self, output=None, color_mapping=None):
|
||||
TextReporter.__init__(self, output)
|
||||
self.color_mapping = color_mapping or \
|
||||
dict(ColorizedTextReporter.COLOR_MAPPING)
|
||||
ansi_terms = ['xterm-16color', 'xterm-256color']
|
||||
if os.environ.get('TERM') not in ansi_terms:
|
||||
if sys.platform == 'win32':
|
||||
import colorama
|
||||
self.out = colorama.AnsiToWin32(self.out)
|
||||
|
||||
def _get_decoration(self, msg_id):
|
||||
"""Returns the tuple color, style associated with msg_id as defined
|
||||
in self.color_mapping
|
||||
"""
|
||||
try:
|
||||
return self.color_mapping[msg_id[0]]
|
||||
except KeyError:
|
||||
return None, None
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different types, and colorize output
|
||||
using ansi escape codes
|
||||
"""
|
||||
if msg.module not in self._modules:
|
||||
color, style = self._get_decoration('S')
|
||||
if msg.module:
|
||||
modsep = colorize_ansi('************* Module %s' % msg.module,
|
||||
color, style)
|
||||
else:
|
||||
modsep = colorize_ansi('************* %s' % msg.module,
|
||||
color, style)
|
||||
self.writeln(modsep)
|
||||
self._modules.add(msg.module)
|
||||
color, style = self._get_decoration(msg.C)
|
||||
|
||||
msg = msg._replace(
|
||||
**{attr: colorize_ansi(getattr(msg, attr), color, style)
|
||||
for attr in ('msg', 'symbol', 'category', 'C')})
|
||||
self.write_message(msg)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(TextReporter)
|
||||
linter.register_reporter(ParseableTextReporter)
|
||||
linter.register_reporter(VSTextReporter)
|
||||
linter.register_reporter(ColorizedTextReporter)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2015-2016 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Universal report objects and some formatting drivers.
|
||||
|
||||
A way to create simple reports using python objects, primarily designed to be
|
||||
formatted as text and html.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class BaseWriter(object):
|
||||
"""base class for ureport writers"""
|
||||
|
||||
def format(self, layout, stream=None, encoding=None):
|
||||
"""format and write the given layout into the stream object
|
||||
|
||||
unicode policy: unicode strings may be found in the layout;
|
||||
try to call stream.write with it, but give it back encoded using
|
||||
the given encoding if it fails
|
||||
"""
|
||||
if stream is None:
|
||||
stream = sys.stdout
|
||||
if not encoding:
|
||||
encoding = getattr(stream, 'encoding', 'UTF-8')
|
||||
self.encoding = encoding or 'UTF-8'
|
||||
self.out = stream
|
||||
self.begin_format()
|
||||
layout.accept(self)
|
||||
self.end_format()
|
||||
|
||||
def format_children(self, layout):
|
||||
"""recurse on the layout children and call their accept method
|
||||
(see the Visitor pattern)
|
||||
"""
|
||||
for child in getattr(layout, 'children', ()):
|
||||
child.accept(self)
|
||||
|
||||
def writeln(self, string=u''):
|
||||
"""write a line in the output buffer"""
|
||||
self.write(string + os.linesep)
|
||||
|
||||
def write(self, string):
|
||||
"""write a string in the output buffer"""
|
||||
self.out.write(string)
|
||||
|
||||
def begin_format(self):
|
||||
"""begin to format a layout"""
|
||||
self.section = 0
|
||||
|
||||
def end_format(self):
|
||||
"""finished to format a layout"""
|
||||
|
||||
def get_table_content(self, table):
|
||||
"""trick to get table content without actually writing it
|
||||
|
||||
return an aligned list of lists containing table cells values as string
|
||||
"""
|
||||
result = [[]]
|
||||
cols = table.cols
|
||||
for cell in self.compute_content(table):
|
||||
if cols == 0:
|
||||
result.append([])
|
||||
cols = table.cols
|
||||
cols -= 1
|
||||
result[-1].append(cell)
|
||||
# fill missing cells
|
||||
while len(result[-1]) < cols:
|
||||
result[-1].append(u'')
|
||||
return result
|
||||
|
||||
def compute_content(self, layout):
|
||||
"""trick to compute the formatting of children layout before actually
|
||||
writing it
|
||||
|
||||
return an iterator on strings (one for each child element)
|
||||
"""
|
||||
# Patch the underlying output stream with a fresh-generated stream,
|
||||
# which is used to store a temporary representation of a child
|
||||
# node.
|
||||
out = self.out
|
||||
try:
|
||||
for child in layout.children:
|
||||
stream = six.StringIO()
|
||||
self.out = stream
|
||||
child.accept(self)
|
||||
yield stream.getvalue()
|
||||
finally:
|
||||
self.out = out
|
||||
@@ -0,0 +1,182 @@
|
||||
# Copyright (c) 2015-2016 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Micro reports objects.
|
||||
|
||||
A micro report is a tree of layout and content objects.
|
||||
"""
|
||||
|
||||
from six import string_types
|
||||
|
||||
|
||||
class VNode(object):
|
||||
|
||||
def __init__(self, nid=None):
|
||||
self.id = nid
|
||||
# navigation
|
||||
self.parent = None
|
||||
self.children = []
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.children)
|
||||
|
||||
def append(self, child):
|
||||
"""add a node to children"""
|
||||
self.children.append(child)
|
||||
child.parent = self
|
||||
|
||||
def insert(self, index, child):
|
||||
"""insert a child node"""
|
||||
self.children.insert(index, child)
|
||||
child.parent = self
|
||||
|
||||
def _get_visit_name(self):
|
||||
"""
|
||||
return the visit name for the mixed class. When calling 'accept', the
|
||||
method <'visit_' + name returned by this method> will be called on the
|
||||
visitor
|
||||
"""
|
||||
try:
|
||||
return self.TYPE.replace('-', '_')
|
||||
except Exception:
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
def accept(self, visitor, *args, **kwargs):
|
||||
func = getattr(visitor, 'visit_%s' % self._get_visit_name())
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
def leave(self, visitor, *args, **kwargs):
|
||||
func = getattr(visitor, 'leave_%s' % self._get_visit_name())
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseLayout(VNode):
|
||||
"""base container node
|
||||
|
||||
attributes
|
||||
* children : components in this table (i.e. the table's cells)
|
||||
"""
|
||||
def __init__(self, children=(), **kwargs):
|
||||
super(BaseLayout, self).__init__(**kwargs)
|
||||
for child in children:
|
||||
if isinstance(child, VNode):
|
||||
self.append(child)
|
||||
else:
|
||||
self.add_text(child)
|
||||
|
||||
def append(self, child):
|
||||
"""overridden to detect problems easily"""
|
||||
assert child not in self.parents()
|
||||
VNode.append(self, child)
|
||||
|
||||
def parents(self):
|
||||
"""return the ancestor nodes"""
|
||||
assert self.parent is not self
|
||||
if self.parent is None:
|
||||
return []
|
||||
return [self.parent] + self.parent.parents()
|
||||
|
||||
def add_text(self, text):
|
||||
"""shortcut to add text data"""
|
||||
self.children.append(Text(text))
|
||||
|
||||
|
||||
# non container nodes #########################################################
|
||||
|
||||
class Text(VNode):
|
||||
"""a text portion
|
||||
|
||||
attributes :
|
||||
* data : the text value as an encoded or unicode string
|
||||
"""
|
||||
def __init__(self, data, escaped=True, **kwargs):
|
||||
super(Text, self).__init__(**kwargs)
|
||||
#if isinstance(data, unicode):
|
||||
# data = data.encode('ascii')
|
||||
assert isinstance(data, string_types), data.__class__
|
||||
self.escaped = escaped
|
||||
self.data = data
|
||||
|
||||
|
||||
class VerbatimText(Text):
|
||||
"""a verbatim text, display the raw data
|
||||
|
||||
attributes :
|
||||
* data : the text value as an encoded or unicode string
|
||||
"""
|
||||
|
||||
# container nodes #############################################################
|
||||
|
||||
class Section(BaseLayout):
|
||||
"""a section
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
a title may also be given to the constructor, it'll be added
|
||||
as a first element
|
||||
a description may also be given to the constructor, it'll be added
|
||||
as a first paragraph
|
||||
"""
|
||||
def __init__(self, title=None, description=None, **kwargs):
|
||||
super(Section, self).__init__(**kwargs)
|
||||
if description:
|
||||
self.insert(0, Paragraph([Text(description)]))
|
||||
if title:
|
||||
self.insert(0, Title(children=(title,)))
|
||||
|
||||
|
||||
class EvaluationSection(Section):
|
||||
|
||||
def __init__(self, message, **kwargs):
|
||||
super(EvaluationSection, self).__init__(**kwargs)
|
||||
title = Paragraph()
|
||||
title.append(Text("-" * len(message)))
|
||||
self.append(title)
|
||||
|
||||
message_body = Paragraph()
|
||||
message_body.append(Text(message))
|
||||
self.append(message_body)
|
||||
|
||||
|
||||
class Title(BaseLayout):
|
||||
"""a title
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
A title must not contains a section nor a paragraph!
|
||||
"""
|
||||
|
||||
|
||||
class Paragraph(BaseLayout):
|
||||
"""a simple text paragraph
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
|
||||
A paragraph must not contains a section !
|
||||
"""
|
||||
|
||||
|
||||
class Table(BaseLayout):
|
||||
"""some tabular data
|
||||
|
||||
attributes :
|
||||
* BaseLayout attributes
|
||||
* cols : the number of columns of the table (REQUIRED)
|
||||
* rheaders : the first row's elements are table's header
|
||||
* cheaders : the first col's elements are table's header
|
||||
* title : the table's optional title
|
||||
"""
|
||||
def __init__(self, cols, title=None,
|
||||
rheaders=0, cheaders=0,
|
||||
**kwargs):
|
||||
super(Table, self).__init__(**kwargs)
|
||||
assert isinstance(cols, int)
|
||||
self.cols = cols
|
||||
self.title = title
|
||||
self.rheaders = rheaders
|
||||
self.cheaders = cheaders
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2015-2016 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Text formatting drivers for ureports"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from pylint.reporters.ureports import BaseWriter
|
||||
|
||||
|
||||
TITLE_UNDERLINES = [u'', u'=', u'-', u'`', u'.', u'~', u'^']
|
||||
BULLETS = [u'*', u'-']
|
||||
|
||||
class TextWriter(BaseWriter):
|
||||
"""format layouts as text
|
||||
(ReStructured inspiration but not totally handled yet)
|
||||
"""
|
||||
def begin_format(self):
|
||||
super(TextWriter, self).begin_format()
|
||||
self.list_level = 0
|
||||
|
||||
def visit_section(self, layout):
|
||||
"""display a section as text
|
||||
"""
|
||||
self.section += 1
|
||||
self.writeln()
|
||||
self.format_children(layout)
|
||||
self.section -= 1
|
||||
self.writeln()
|
||||
|
||||
def visit_evaluationsection(self, layout):
|
||||
"""Display an evaluation section as a text."""
|
||||
self.section += 1
|
||||
self.format_children(layout)
|
||||
self.section -= 1
|
||||
self.writeln()
|
||||
|
||||
def visit_title(self, layout):
|
||||
title = u''.join(list(self.compute_content(layout)))
|
||||
self.writeln(title)
|
||||
try:
|
||||
self.writeln(TITLE_UNDERLINES[self.section] * len(title))
|
||||
except IndexError:
|
||||
print("FIXME TITLE TOO DEEP. TURNING TITLE INTO TEXT")
|
||||
|
||||
def visit_paragraph(self, layout):
|
||||
"""enter a paragraph"""
|
||||
self.format_children(layout)
|
||||
self.writeln()
|
||||
|
||||
def visit_table(self, layout):
|
||||
"""display a table as text"""
|
||||
table_content = self.get_table_content(layout)
|
||||
# get columns width
|
||||
cols_width = [0]*len(table_content[0])
|
||||
for row in table_content:
|
||||
for index, col in enumerate(row):
|
||||
cols_width[index] = max(cols_width[index], len(col))
|
||||
self.default_table(layout, table_content, cols_width)
|
||||
self.writeln()
|
||||
|
||||
def default_table(self, layout, table_content, cols_width):
|
||||
"""format a table"""
|
||||
cols_width = [size+1 for size in cols_width]
|
||||
format_strings = u' '.join([u'%%-%ss'] * len(cols_width))
|
||||
format_strings = format_strings % tuple(cols_width)
|
||||
format_strings = format_strings.split(u' ')
|
||||
table_linesep = u'\n+' + u'+'.join([u'-'*w for w in cols_width]) + u'+\n'
|
||||
headsep = u'\n+' + u'+'.join([u'='*w for w in cols_width]) + u'+\n'
|
||||
# FIXME: layout.cheaders
|
||||
self.write(table_linesep)
|
||||
for index, line in enumerate(table_content):
|
||||
self.write(u'|')
|
||||
for line_index, at_index in enumerate(line):
|
||||
self.write(format_strings[line_index] % at_index)
|
||||
self.write(u'|')
|
||||
if index == 0 and layout.rheaders:
|
||||
self.write(headsep)
|
||||
else:
|
||||
self.write(table_linesep)
|
||||
|
||||
def visit_verbatimtext(self, layout):
|
||||
"""display a verbatim layout as text (so difficult ;)
|
||||
"""
|
||||
self.writeln(u'::\n')
|
||||
for line in layout.data.splitlines():
|
||||
self.writeln(u' ' + line)
|
||||
self.writeln()
|
||||
|
||||
def visit_text(self, layout):
|
||||
"""add some text"""
|
||||
self.write(u'%s' % layout.data)
|
||||
@@ -0,0 +1 @@
|
||||
"{a[0]}".format(a=object) # [invalid-format-index]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import six
|
||||
|
||||
import pylint.lint
|
||||
|
||||
|
||||
def is_module(filename):
|
||||
return filename.endswith(".py")
|
||||
|
||||
|
||||
def is_package(filename, location):
|
||||
return os.path.exists(os.path.join(location, filename, '__init__.py'))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_stdout(out):
|
||||
sys.stdout = out
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__
|
||||
|
||||
|
||||
LIB_DIRS = [
|
||||
os.path.dirname(os.__file__),
|
||||
]
|
||||
MODULES_TO_CHECK = [(location, module) for location in LIB_DIRS for module in os.listdir(location)
|
||||
if is_module(module) or is_package(module, location)]
|
||||
MODULES_NAMES = [m[1] for m in MODULES_TO_CHECK]
|
||||
|
||||
|
||||
@pytest.mark.acceptance
|
||||
@pytest.mark.parametrize(("test_module_location", "test_module_name"),
|
||||
MODULES_TO_CHECK, ids=MODULES_NAMES)
|
||||
def test_libmodule(test_module_location, test_module_name):
|
||||
os.chdir(test_module_location)
|
||||
with _patch_stdout(six.StringIO()):
|
||||
try:
|
||||
pylint.lint.Run([test_module_name, '--enable=all'])
|
||||
except SystemExit as ex:
|
||||
assert ex.code != 32
|
||||
return
|
||||
|
||||
assert False, "shouldn't get there"
|
||||
@@ -0,0 +1,52 @@
|
||||
# pylint: disable=redefined-outer-name
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.lint import PyLinter
|
||||
# pylint: disable=no-name-in-module
|
||||
from pylint.testutils import MinimalTestReporter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linter(checker, register, enable, disable, reporter):
|
||||
_linter = PyLinter()
|
||||
_linter.set_reporter(reporter())
|
||||
checkers.initialize(_linter)
|
||||
if register:
|
||||
register(_linter)
|
||||
if checker:
|
||||
_linter.register_checker(checker(_linter))
|
||||
if disable:
|
||||
for msg in disable:
|
||||
_linter.disable(msg)
|
||||
if enable:
|
||||
for msg in enable:
|
||||
_linter.enable(msg)
|
||||
os.environ.pop('PYLINTRC', None)
|
||||
return _linter
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def checker():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def register():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def enable():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def disable():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def reporter():
|
||||
return MinimalTestReporter
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/python
|
||||
"""ttttttttttttttttttttoooooooooooooooooooooooooooooooooooooooooooooooooooooo lllllllllllooooooooooooooooooooooooooooonnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnngggggggggggggggggg"""
|
||||
@@ -0,0 +1,12 @@
|
||||
digraph "classes_No_Name" {
|
||||
charset="utf-8"
|
||||
rankdir=BT
|
||||
"0" [label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value()\l}", shape="record"];
|
||||
"1" [label="{DoNothing|\l|}", shape="record"];
|
||||
"2" [label="{Interface|\l|get_value()\lset_value()\l}", shape="record"];
|
||||
"3" [label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record"];
|
||||
"3" -> "0" [arrowhead="empty", arrowtail="none"];
|
||||
"0" -> "2" [arrowhead="empty", arrowtail="node", style="dashed"];
|
||||
"1" -> "0" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"];
|
||||
"1" -> "3" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="relation", style="solid"];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
""" docstring for file clientmodule.py """
|
||||
from data.suppliermodule_test import Interface, DoNothing
|
||||
|
||||
class Ancestor(object):
|
||||
""" Ancestor method """
|
||||
__implements__ = (Interface,)
|
||||
cls_member = DoNothing()
|
||||
|
||||
def __init__(self, value):
|
||||
local_variable = 0
|
||||
self.attr = 'this method shouldn\'t have a docstring'
|
||||
self.__value = value
|
||||
|
||||
def get_value(self):
|
||||
""" nice docstring ;-) """
|
||||
return self.__value
|
||||
|
||||
def set_value(self, value):
|
||||
self.__value = value
|
||||
return 'this method shouldn\'t have a docstring'
|
||||
|
||||
class Specialization(Ancestor):
|
||||
TYPE = 'final class'
|
||||
top = 'class'
|
||||
|
||||
def __init__(self, value, _id):
|
||||
Ancestor.__init__(self, value)
|
||||
self._id = _id
|
||||
self.relation = DoNothing()
|
||||
@@ -0,0 +1,8 @@
|
||||
digraph "packages_No_Name" {
|
||||
charset="utf-8"
|
||||
rankdir=BT
|
||||
"0" [label="data", shape="box"];
|
||||
"1" [label="data.clientmodule_test", shape="box"];
|
||||
"2" [label="data.suppliermodule_test", shape="box"];
|
||||
"1" -> "2" [arrowhead="open", arrowtail="none"];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
""" file suppliermodule.py """
|
||||
|
||||
class Interface:
|
||||
def get_value(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_value(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
class DoNothing: pass
|
||||
@@ -0,0 +1,4 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
TEST = map(str, (1, 2, 3)) # [bad-builtin]
|
||||
TEST1 = filter(str, (1, 2, 3)) # [bad-builtin]
|
||||
@@ -0,0 +1,28 @@
|
||||
# pylint: disable=literal-comparison,missing-docstring,misplaced-comparison-constant
|
||||
|
||||
X = 123
|
||||
Y = len('test')
|
||||
|
||||
if X is 0: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if Y is not 0: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if X == 0: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if Y != 0: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if X > 0: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if X < 0: # this is allowed
|
||||
pass
|
||||
|
||||
if 0 < X: # [compare-to-zero]
|
||||
pass
|
||||
|
||||
if 0 > X: # this is allowed
|
||||
pass
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Checks of Dosctrings 'docstring-first-line-empty' 'bad-docstring-quotes'"""
|
||||
|
||||
|
||||
def check_messages(*messages):
|
||||
"""
|
||||
docstring"""
|
||||
return messages
|
||||
|
||||
def function2():
|
||||
"""Test Ok"""
|
||||
pass
|
||||
|
||||
class FFFF(object):
|
||||
"""
|
||||
Test Docstring First Line Empty
|
||||
"""
|
||||
|
||||
def method1(self):
|
||||
'''
|
||||
Test Triple Single Quotes docstring
|
||||
'''
|
||||
pass
|
||||
|
||||
def method2(self):
|
||||
"bad docstring 1"
|
||||
pass
|
||||
|
||||
def method3(self):
|
||||
'bad docstring 2'
|
||||
pass
|
||||
|
||||
def method4(self):
|
||||
' """bad docstring 3 '
|
||||
pass
|
||||
|
||||
@check_messages('bad-open-mode', 'redundant-unittest-assert',
|
||||
'deprecated-module')
|
||||
def method5(self):
|
||||
"""Test OK 1 with decorators"""
|
||||
pass
|
||||
|
||||
def method6(self):
|
||||
r"""Test OK 2 with raw string"""
|
||||
pass
|
||||
|
||||
def method7(self):
|
||||
u"""Test OK 3 with unicode string"""
|
||||
pass
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Checks use of "else if" triggers a refactor message"""
|
||||
|
||||
def my_function():
|
||||
"""docstring"""
|
||||
myint = 2
|
||||
if myint > 5:
|
||||
pass
|
||||
else:
|
||||
if myint <= 5:
|
||||
pass
|
||||
else:
|
||||
myint = 3
|
||||
if myint > 2:
|
||||
if myint > 3:
|
||||
pass
|
||||
elif myint == 3:
|
||||
pass
|
||||
elif myint < 3:
|
||||
pass
|
||||
else:
|
||||
if myint:
|
||||
pass
|
||||
else:
|
||||
if myint:
|
||||
pass
|
||||
myint = 4
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# pylint: disable=literal-comparison,missing-docstring
|
||||
|
||||
X = ''
|
||||
Y = 'test'
|
||||
|
||||
if X is '': # [compare-to-empty-string]
|
||||
pass
|
||||
|
||||
if Y is not "": # [compare-to-empty-string]
|
||||
pass
|
||||
|
||||
if X == "": # [compare-to-empty-string]
|
||||
pass
|
||||
|
||||
if Y != '': # [compare-to-empty-string]
|
||||
pass
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Checks use of "too-complex" check"""
|
||||
|
||||
|
||||
def f1():
|
||||
"""McCabe rating: 1"""
|
||||
pass
|
||||
|
||||
|
||||
def f2(n):
|
||||
"""McCabe rating: 1"""
|
||||
k = n + 4
|
||||
s = k + n
|
||||
return s
|
||||
|
||||
|
||||
def f3(n):
|
||||
"""McCabe rating: 3"""
|
||||
if n > 3:
|
||||
return "bigger than three"
|
||||
elif n > 4:
|
||||
return "is never executed"
|
||||
else:
|
||||
return "smaller than or equal to three"
|
||||
|
||||
|
||||
def f4():
|
||||
"""McCabe rating: 2"""
|
||||
for i in range(10):
|
||||
print(i)
|
||||
|
||||
|
||||
def f5(mylist):
|
||||
"""McCabe rating: 2"""
|
||||
for i in mylist:
|
||||
print(i)
|
||||
else:
|
||||
print(None)
|
||||
|
||||
|
||||
def f6(n):
|
||||
"""McCabe rating: 2"""
|
||||
if n > 4:
|
||||
return f(n - 1)
|
||||
else:
|
||||
return n
|
||||
|
||||
|
||||
def f7():
|
||||
"""McCabe rating: 3"""
|
||||
def b():
|
||||
"""McCabe rating: 2"""
|
||||
def c():
|
||||
"""McCabe rating: 1"""
|
||||
pass
|
||||
c()
|
||||
b()
|
||||
|
||||
|
||||
def f8():
|
||||
"""McCabe rating: 4"""
|
||||
try:
|
||||
print(1)
|
||||
except TypeA:
|
||||
print(2)
|
||||
except TypeB:
|
||||
print(3)
|
||||
else:
|
||||
print(4)
|
||||
|
||||
|
||||
def f9():
|
||||
"""McCabe rating: 9"""
|
||||
myint = 2
|
||||
if myint > 5:
|
||||
pass
|
||||
else:
|
||||
if myint <= 5:
|
||||
pass
|
||||
else:
|
||||
myint = 3
|
||||
if myint > 2:
|
||||
if myint > 3:
|
||||
pass
|
||||
elif myint == 3:
|
||||
pass
|
||||
elif myint < 3:
|
||||
pass
|
||||
else:
|
||||
if myint:
|
||||
pass
|
||||
else:
|
||||
if myint:
|
||||
pass
|
||||
myint = 4
|
||||
|
||||
|
||||
def f10():
|
||||
"""McCabe rating: 11"""
|
||||
myint = 2
|
||||
if myint == 5:
|
||||
return myint
|
||||
elif myint == 6:
|
||||
return myint
|
||||
elif myint == 7:
|
||||
return myint
|
||||
elif myint == 8:
|
||||
return myint
|
||||
elif myint == 9:
|
||||
return myint
|
||||
elif myint == 10:
|
||||
if myint == 8:
|
||||
while True:
|
||||
return True
|
||||
elif myint == 8:
|
||||
with myint:
|
||||
return 8
|
||||
else:
|
||||
if myint == 2:
|
||||
return myint
|
||||
return myint
|
||||
return myint
|
||||
|
||||
|
||||
class MyClass1(object):
|
||||
"""Class of example to test mccabe"""
|
||||
_name = 'MyClass' # To force a tail.node=None
|
||||
|
||||
def method1():
|
||||
"""McCabe rating: 1"""
|
||||
pass
|
||||
|
||||
def method2(self, param1):
|
||||
"""McCabe rating: 18"""
|
||||
if not param1:
|
||||
pass
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
pass
|
||||
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
pass
|
||||
if param1:
|
||||
for value in range(5):
|
||||
pass
|
||||
|
||||
pass
|
||||
for count in range(6):
|
||||
with open('myfile') as fp:
|
||||
count += 1
|
||||
pass
|
||||
pass
|
||||
try:
|
||||
pass
|
||||
if not param1:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
if param1:
|
||||
raise BaseException('Error')
|
||||
with open('myfile2') as fp2:
|
||||
pass
|
||||
pass
|
||||
finally:
|
||||
if param1 is not None:
|
||||
pass
|
||||
for count2 in range(8):
|
||||
try:
|
||||
pass
|
||||
except BaseException('Error2'):
|
||||
pass
|
||||
return param1
|
||||
|
||||
|
||||
for count in range(10):
|
||||
if count == 1:
|
||||
exit(0)
|
||||
elif count == 2:
|
||||
exit(1)
|
||||
else:
|
||||
exit(2)
|
||||
|
||||
|
||||
def method3(self):
|
||||
try:
|
||||
if True:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
return True
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
class SomeException(Exception):
|
||||
pass
|
||||
|
||||
class SubclassException(SomeException):
|
||||
pass
|
||||
|
||||
AliasException = SomeException
|
||||
|
||||
try:
|
||||
pass
|
||||
except (SomeException, SomeException): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (SomeException, SubclassException): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (SomeException, AliasException): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (AliasException, SubclassException): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
# +1:[overlapping-except, overlapping-except, overlapping-except]
|
||||
except (SomeException, AliasException, SubclassException):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ArithmeticError, FloatingPointError): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, UnicodeDecodeError): # [overlapping-except]
|
||||
pass
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
import socket
|
||||
|
||||
try:
|
||||
pass
|
||||
except (IOError, OSError): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (socket.error, OSError): # [overlapping-except]
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ConnectionError, socket.error): # [overlapping-except]
|
||||
pass
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Checks variable types aren't redefined within a method or a function"""
|
||||
|
||||
# pylint: disable=too-few-public-methods,missing-docstring,unused-variable,invalid-name
|
||||
|
||||
_OK = True
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
class Klass(object):
|
||||
def __init__(self):
|
||||
self.var2 = 'var'
|
||||
|
||||
def __init__(self):
|
||||
self.var = True
|
||||
self.var1 = 2
|
||||
self.var2 = 1.
|
||||
self.var1 = 2. # [redefined-variable-type]
|
||||
self.a_str = "hello"
|
||||
a_str = False
|
||||
(a_str, b_str) = (1, 2) # no support for inference on tuple assignment
|
||||
a_str = 2.0 if self.var else 1.0 # no support for inference on ifexpr
|
||||
|
||||
def _getter(self):
|
||||
return self.a_str
|
||||
def _setter(self, val):
|
||||
self.a_str = val
|
||||
var2 = property(_getter, _setter)
|
||||
|
||||
def some_method(self):
|
||||
def func():
|
||||
var = 1
|
||||
test = 'bar'
|
||||
var = 'baz' # [redefined-variable-type]
|
||||
self.var = 1 # the rule checks for redefinitions in the scope of a function or method
|
||||
test = 'foo'
|
||||
myint = 2
|
||||
myint = False # [redefined-variable-type]
|
||||
|
||||
_OK = "This is OK" # [redefined-variable-type]
|
||||
|
||||
if _OK:
|
||||
SOME_FLOAT = 1.
|
||||
|
||||
def dummy_function():
|
||||
return 2
|
||||
|
||||
def other_function():
|
||||
instance = MyClass()
|
||||
instance = True # [redefined-variable-type]
|
||||
|
||||
SOME_FLOAT = dummy_function() # [redefined-variable-type]
|
||||
|
||||
A_GLOB = None
|
||||
A_GLOB = [1, 2, 3]
|
||||
|
||||
def func2(x):
|
||||
if x:
|
||||
var = 'foo'
|
||||
else:
|
||||
var = True
|
||||
|
||||
if x:
|
||||
var2 = 'foo'
|
||||
elif not x:
|
||||
var2 = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
if x:
|
||||
var3 = 'foo'
|
||||
var3 = 2 # [redefined-variable-type]
|
||||
else:
|
||||
pass
|
||||
|
||||
var = 2 # [redefined-variable-type]
|
||||
|
||||
if x:
|
||||
pass
|
||||
elif not x:
|
||||
var4 = True
|
||||
elif _OK:
|
||||
pass
|
||||
else:
|
||||
var4 = 2.
|
||||
var4 = 'baz' # [redefined-variable-type]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Derek Gustafson <degustaf@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.bad_builtin
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
|
||||
import pytest
|
||||
|
||||
from pylint.extensions.bad_builtin import BadBuiltinChecker
|
||||
from pylint.lint import fix_import_path
|
||||
|
||||
|
||||
EXPECTED = [
|
||||
"Used builtin function 'map'. Using a list comprehension can be clearer.",
|
||||
"Used builtin function 'filter'. Using a list comprehension can be clearer.",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def checker(checker):
|
||||
return BadBuiltinChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def disable(disable):
|
||||
return ['I']
|
||||
|
||||
|
||||
def test_types_redefined(linter):
|
||||
elif_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'bad_builtin.py')
|
||||
with fix_import_path([elif_test]):
|
||||
linter.check([elif_test])
|
||||
msgs = sorted(linter.reporter.messages, key=lambda item: item.line)
|
||||
assert len(msgs) == 2
|
||||
for msg, expected in zip(msgs, EXPECTED):
|
||||
assert msg.symbol == 'bad-builtin'
|
||||
assert msg.msg == expected
|
||||
File diff suppressed because it is too large
Load Diff
+111
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Unit tests for the pylint checkers in :mod:`pylint.extensions.check_docs`,
|
||||
in particular the parameter documentation checker `DocstringChecker`
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
|
||||
import pytest
|
||||
|
||||
import astroid
|
||||
|
||||
import pylint.extensions._check_docs_utils as utils
|
||||
|
||||
|
||||
@pytest.mark.parametrize("string,count", [
|
||||
('abc', 0),
|
||||
('', 0),
|
||||
(' abc', 2),
|
||||
('\n abc', 0),
|
||||
(' \n abc', 3),
|
||||
])
|
||||
def test_space_indentation(string, count):
|
||||
"""Test for pylint_plugin.ParamDocChecker"""
|
||||
assert utils.space_indentation(string) == count
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raise_node,expected", [
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
raise NotImplementedError #@
|
||||
'''), set(["NotImplementedError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
raise NotImplementedError("Not implemented!") #@
|
||||
'''), set(["NotImplementedError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
raise #@
|
||||
'''), set(["RuntimeError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
if another_func():
|
||||
raise #@
|
||||
'''), set(["RuntimeError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
try:
|
||||
another_func()
|
||||
raise #@
|
||||
except NameError:
|
||||
pass
|
||||
'''), set(["RuntimeError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
try:
|
||||
another_func()
|
||||
except NameError:
|
||||
raise #@
|
||||
'''), set(["NameError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except:
|
||||
raise #@
|
||||
'''), set()),
|
||||
|
||||
(astroid.extract_node('''
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except (RuntimeError, ValueError):
|
||||
raise #@
|
||||
'''), set(["RuntimeError", "ValueError"])),
|
||||
|
||||
(astroid.extract_node('''
|
||||
import not_a_module
|
||||
def my_func():
|
||||
try:
|
||||
fake_func()
|
||||
except not_a_module.Error:
|
||||
raise #@
|
||||
'''), set()),
|
||||
|
||||
])
|
||||
def test_exception(raise_node, expected):
|
||||
found = utils.possible_exc_types(raise_node)
|
||||
assert found == expected
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.check_mccabe
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
|
||||
import pytest
|
||||
|
||||
from pylint.extensions import mccabe
|
||||
|
||||
EXPECTED_MSGS = [
|
||||
"'f1' is too complex. The McCabe rating is 1",
|
||||
"'f2' is too complex. The McCabe rating is 1",
|
||||
"'f3' is too complex. The McCabe rating is 3",
|
||||
"'f4' is too complex. The McCabe rating is 2",
|
||||
"'f5' is too complex. The McCabe rating is 2",
|
||||
"'f6' is too complex. The McCabe rating is 2",
|
||||
"'f7' is too complex. The McCabe rating is 3",
|
||||
"'f8' is too complex. The McCabe rating is 4",
|
||||
"'f9' is too complex. The McCabe rating is 9",
|
||||
"'method1' is too complex. The McCabe rating is 1",
|
||||
"This 'for' is too complex. The McCabe rating is 4",
|
||||
"'method3' is too complex. The McCabe rating is 2",
|
||||
"'f10' is too complex. The McCabe rating is 11",
|
||||
"'method2' is too complex. The McCabe rating is 18",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def enable(enable):
|
||||
return ['too-complex']
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def disable(disable):
|
||||
return ['all']
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def register(register):
|
||||
return mccabe.register
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fname_mccabe_example():
|
||||
return osp.join(osp.dirname(osp.abspath(__file__)), 'data', 'mccabe.py')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("complexity, expected", [
|
||||
(0, EXPECTED_MSGS),
|
||||
(9, EXPECTED_MSGS[-2:]),
|
||||
])
|
||||
def test_max_mccabe_rate(linter, fname_mccabe_example, complexity, expected):
|
||||
linter.global_set_option('max-complexity', complexity)
|
||||
linter.check([fname_mccabe_example])
|
||||
real_msgs = [message.msg for message in linter.reporter.messages]
|
||||
assert sorted(expected) == sorted(real_msgs)
|
||||
+529
@@ -0,0 +1,529 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Unit tests for the raised exception documentation checking in the
|
||||
`DocstringChecker` in :mod:`pylint.extensions.check_docs`
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import CheckerTestCase, Message, set_config
|
||||
|
||||
from pylint.extensions.docparams import DocstringParameterChecker
|
||||
|
||||
|
||||
class TestDocstringCheckerRaise(CheckerTestCase):
|
||||
"""Tests for pylint_plugin.RaiseDocChecker"""
|
||||
CHECKER_CLASS = DocstringParameterChecker
|
||||
|
||||
def test_ignores_no_docstring(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
raise RuntimeError('hi') #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_unknown_style(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring."""
|
||||
raise RuntimeError('hi')
|
||||
''')
|
||||
raise_node = node.body[0]
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
@set_config(accept_no_raise_doc=False)
|
||||
def test_warns_unknown_style(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring."""
|
||||
raise RuntimeError('hi')
|
||||
''')
|
||||
raise_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_missing_sphinx_raises(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
raise RuntimeError('hi')
|
||||
raise NameError('hi')
|
||||
''')
|
||||
raise_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_missing_google_raises(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises:
|
||||
NameError: Never
|
||||
"""
|
||||
raise RuntimeError('hi')
|
||||
raise NameError('hi')
|
||||
''')
|
||||
raise_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_missing_numpy_raises(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises
|
||||
------
|
||||
NameError
|
||||
Never
|
||||
"""
|
||||
raise RuntimeError('hi')
|
||||
raise NameError('hi')
|
||||
''')
|
||||
raise_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignore_spurious_sphinx_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises RuntimeError: Always
|
||||
:except NameError: Never
|
||||
:raise OSError: Never
|
||||
:exception ValueError: Never
|
||||
"""
|
||||
raise RuntimeError('Blah') #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_all_sphinx_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises RuntimeError: Always
|
||||
:except NameError: Never
|
||||
:raise OSError: Never
|
||||
:exception ValueError: Never
|
||||
"""
|
||||
raise RuntimeError('hi') #@
|
||||
raise NameError('hi')
|
||||
raise OSError(2, 'abort!')
|
||||
raise ValueError('foo')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_all_google_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Always
|
||||
NameError: Never
|
||||
"""
|
||||
raise RuntimeError('hi') #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_all_numpy_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
Always
|
||||
NameError
|
||||
Never
|
||||
"""
|
||||
raise RuntimeError('hi') #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_finds_rethrown_sphinx_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_rethrown_google_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises:
|
||||
NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_rethrown_numpy_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises
|
||||
------
|
||||
NameError
|
||||
Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except RuntimeError:
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_finds_rethrown_sphinx_multiple_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except (RuntimeError, ValueError):
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError, ValueError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_rethrown_google_multiple_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises:
|
||||
NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except (RuntimeError, ValueError):
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError, ValueError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_rethrown_numpy_multiple_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises
|
||||
------
|
||||
NameError
|
||||
Sometimes
|
||||
"""
|
||||
try:
|
||||
fake_func()
|
||||
except (RuntimeError, ValueError):
|
||||
raise #@
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError, ValueError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_caught_sphinx_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
raise RuntimeError('hi') #@
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_caught_google_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises:
|
||||
NameError: Sometimes
|
||||
"""
|
||||
try:
|
||||
raise RuntimeError('hi') #@
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_caught_numpy_raises(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Raises
|
||||
------
|
||||
NameError
|
||||
Sometimes
|
||||
"""
|
||||
try:
|
||||
raise RuntimeError('hi') #@
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_missing_sphinx_raises_infer_from_instance(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
my_exception = RuntimeError('hi')
|
||||
raise my_exception #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_find_missing_sphinx_raises_infer_from_function(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
def ex_func(val):
|
||||
return RuntimeError(val)
|
||||
raise ex_func('hi') #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_raise_uninferable(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
from unknown import Unknown
|
||||
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
raise Unknown('hi') #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_returns_from_inner_functions(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
def ex_func(val):
|
||||
def inner_func(value):
|
||||
return OSError(value)
|
||||
return RuntimeError(val)
|
||||
raise ex_func('hi') #@
|
||||
raise NameError('hi')
|
||||
''')
|
||||
node = raise_node.frame()
|
||||
with self.assertAddsMessages(
|
||||
Message(
|
||||
msg_id='missing-raises-doc',
|
||||
node=node,
|
||||
args=('RuntimeError', ))):
|
||||
# we do NOT expect a warning about the OSError in inner_func!
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_returns_use_only_names(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def myfunc():
|
||||
"""This is a docstring
|
||||
|
||||
:raises NameError: Never
|
||||
"""
|
||||
def inner_func():
|
||||
return 42
|
||||
|
||||
raise inner_func() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_ignores_returns_use_only_exception_instances(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def myfunc():
|
||||
"""This is a docstring
|
||||
|
||||
:raises MyException: Never
|
||||
"""
|
||||
class MyException(Exception):
|
||||
pass
|
||||
def inner_func():
|
||||
return MyException
|
||||
|
||||
raise inner_func() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_no_crash_when_inferring_handlers(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
import collections
|
||||
|
||||
def test():
|
||||
"""raises
|
||||
|
||||
:raise U: pass
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except collections.U as exc:
|
||||
raise #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_no_crash_when_cant_find_exception(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
import collections
|
||||
|
||||
def test():
|
||||
"""raises
|
||||
|
||||
:raise U: pass
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except U as exc:
|
||||
raise #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
|
||||
def test_no_error_notimplemented_documented(self):
|
||||
raise_node = astroid.extract_node('''
|
||||
def my_func():
|
||||
"""
|
||||
Raises:
|
||||
NotImplementedError: When called.
|
||||
"""
|
||||
raise NotImplementedError #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_raise(raise_node)
|
||||
+572
@@ -0,0 +1,572 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
# Copyright (c) 2016 Moises Lopez <moylop260@vauxoo.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Unit tests for the return documentation checking in the
|
||||
`DocstringChecker` in :mod:`pylint.extensions.check_docs`
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import CheckerTestCase, Message, set_config
|
||||
|
||||
from pylint.extensions.docparams import DocstringParameterChecker
|
||||
|
||||
|
||||
class TestDocstringCheckerReturn(CheckerTestCase):
|
||||
"""Tests for pylint_plugin.RaiseDocChecker"""
|
||||
CHECKER_CLASS = DocstringParameterChecker
|
||||
|
||||
def test_ignores_no_docstring(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
return False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
@set_config(accept_no_return_doc=False)
|
||||
def test_warns_no_docstring(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node),
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_ignores_unknown_style(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring."""
|
||||
return False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_partial_sphinx_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: Always False
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_partial_sphinx_returns_type(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_missing_sphinx_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
:param doc_type: Sphinx
|
||||
:type doc_type: str
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node),
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_partial_google_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
Always False
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_partial_google_returns_type(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
bool:
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_missing_google_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Parameters:
|
||||
doc_type (str): Google
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node),
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_partial_numpy_returns_type(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
doc_type : str
|
||||
Numpy
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warn_missing_numpy_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
doc_type : str
|
||||
Numpy
|
||||
"""
|
||||
return False
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node),
|
||||
Message(msg_id='missing-return-type-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_find_sphinx_returns(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:return: Always False
|
||||
:rtype: bool
|
||||
"""
|
||||
return False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_find_google_returns(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
bool: Always False
|
||||
"""
|
||||
return False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_find_numpy_returns(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Always False
|
||||
"""
|
||||
return False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_ignores_sphinx_return_none(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
:param doc_type: Sphinx
|
||||
:type doc_type: str
|
||||
"""
|
||||
return #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_ignores_google_return_none(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Args:
|
||||
doc_type (str): Google
|
||||
"""
|
||||
return #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_ignores_numpy_return_none(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
doc_type : str
|
||||
Numpy
|
||||
"""
|
||||
return #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_sphinx_return_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: An object
|
||||
:rtype: :class:`mymodule.Class`
|
||||
"""
|
||||
return mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_google_return_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
mymodule.Class: An object
|
||||
"""
|
||||
return mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_numpy_return_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
mymodule.Class
|
||||
An object
|
||||
"""
|
||||
return mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_sphinx_return_list_of_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: An object
|
||||
:rtype: list(:class:`mymodule.Class`)
|
||||
"""
|
||||
return [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_google_return_list_of_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
list(:class:`mymodule.Class`): An object
|
||||
"""
|
||||
return [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_finds_numpy_return_list_of_custom_class(self):
|
||||
return_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list(:class:`mymodule.Class`)
|
||||
An object
|
||||
"""
|
||||
return [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warns_sphinx_return_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:rtype: list(:class:`mymodule.Class`)
|
||||
"""
|
||||
return [mymodule.Class()]
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warns_google_return_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
list(:class:`mymodule.Class`):
|
||||
"""
|
||||
return [mymodule.Class()]
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warns_numpy_return_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list(:class:`mymodule.Class`)
|
||||
"""
|
||||
return [mymodule.Class()]
|
||||
''')
|
||||
return_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-return-doc', node=node)):
|
||||
self.checker.visit_return(return_node)
|
||||
|
||||
def test_warns_sphinx_redundant_return_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: One
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_sphinx_redundant_rtype_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_google_redundant_return_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
One
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_google_redundant_rtype_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
int:
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_numpy_redundant_return_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
One
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_numpy_redundant_rtype_doc(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
"""
|
||||
return None
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_ignores_sphinx_redundant_return_doc_multiple_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: One
|
||||
:rtype: int
|
||||
|
||||
:returns: None sometimes
|
||||
:rtype: None
|
||||
"""
|
||||
if a_func():
|
||||
return None
|
||||
return 1
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_ignores_google_redundant_return_doc_multiple_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
int or None: One, or sometimes None.
|
||||
"""
|
||||
if a_func():
|
||||
return None
|
||||
return 1
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_ignores_numpy_redundant_return_doc_multiple_returns(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
One
|
||||
None
|
||||
Sometimes
|
||||
"""
|
||||
if a_func():
|
||||
return None
|
||||
return 1
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_ignore_sphinx_redundant_return_doc_yield(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func_with_yield(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: One
|
||||
:rtype: generator
|
||||
"""
|
||||
for value in range(3):
|
||||
yield value
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_google_redundant_return_doc_yield(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns:
|
||||
int: One
|
||||
"""
|
||||
yield 1
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_numpy_redundant_return_doc_yield(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
One
|
||||
"""
|
||||
yield 1
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-returns-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Glenn Matthews <glenn@e-dad.net>
|
||||
# Copyright (c) 2016 Ashley Whetter <ashley@awhetter.co.uk>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Unit tests for the yield documentation checking in the
|
||||
`DocstringChecker` in :mod:`pylint.extensions.check_docs`
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import CheckerTestCase, Message, set_config
|
||||
|
||||
from pylint.extensions.docparams import DocstringParameterChecker
|
||||
|
||||
|
||||
class TestDocstringCheckerYield(CheckerTestCase):
|
||||
"""Tests for pylint_plugin.RaiseDocChecker"""
|
||||
CHECKER_CLASS = DocstringParameterChecker
|
||||
|
||||
def test_ignores_no_docstring(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
yield False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
@set_config(accept_no_yields_doc=False)
|
||||
def test_warns_no_docstring(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node),
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_ignores_unknown_style(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring."""
|
||||
yield False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_partial_sphinx_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: Always False
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_partial_sphinx_yields_type(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_missing_sphinx_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
:param doc_type: Sphinx
|
||||
:type doc_type: str
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node),
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_partial_google_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
Always False
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_partial_google_yields_type(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
bool:
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_missing_google_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Parameters:
|
||||
doc_type (str): Google
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node),
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warn_missing_numpy_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self, doc_type):
|
||||
"""This is a docstring.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
doc_type : str
|
||||
Numpy
|
||||
"""
|
||||
yield False
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node),
|
||||
Message(msg_id='missing-yield-type-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_find_sphinx_yields(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:return: Always False
|
||||
:rtype: bool
|
||||
"""
|
||||
yield False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_find_google_yields(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
bool: Always False
|
||||
"""
|
||||
yield False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_find_numpy_yields(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
bool
|
||||
Always False
|
||||
"""
|
||||
yield False #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_sphinx_yield_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: An object
|
||||
:rtype: :class:`mymodule.Class`
|
||||
"""
|
||||
yield mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_google_yield_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
mymodule.Class: An object
|
||||
"""
|
||||
yield mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_numpy_yield_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
mymodule.Class
|
||||
An object
|
||||
"""
|
||||
yield mymodule.Class() #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_sphinx_yield_list_of_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:returns: An object
|
||||
:rtype: list(:class:`mymodule.Class`)
|
||||
"""
|
||||
yield [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_google_yield_list_of_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
list(:class:`mymodule.Class`): An object
|
||||
"""
|
||||
yield [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_finds_numpy_yield_list_of_custom_class(self):
|
||||
yield_node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
list(:class:`mymodule.Class`)
|
||||
An object
|
||||
"""
|
||||
yield [mymodule.Class()] #@
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warns_sphinx_yield_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
:rtype: list(:class:`mymodule.Class`)
|
||||
"""
|
||||
yield [mymodule.Class()]
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warns_google_yield_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
list(:class:`mymodule.Class`):
|
||||
"""
|
||||
yield [mymodule.Class()]
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
def test_warns_numpy_yield_list_of_custom_class_without_description(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
list(:class:`mymodule.Class`)
|
||||
"""
|
||||
yield [mymodule.Class()]
|
||||
''')
|
||||
yield_node = node.body[0]
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='missing-yield-doc', node=node)):
|
||||
self.checker.visit_yield(yield_node)
|
||||
|
||||
# No such thing as redundant yield documentation for sphinx because it
|
||||
# doesn't support yield documentation
|
||||
|
||||
def test_ignores_google_redundant_yield_doc_multiple_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
int or None: One, or sometimes None.
|
||||
"""
|
||||
if a_func():
|
||||
yield None
|
||||
yield 1
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_ignores_numpy_redundant_yield_doc_multiple_yields(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
int
|
||||
One
|
||||
None
|
||||
Sometimes
|
||||
"""
|
||||
if a_func():
|
||||
yield None
|
||||
yield 1
|
||||
''')
|
||||
with self.assertNoMessages():
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
# No such thing as redundant yield documentation for sphinx because it
|
||||
# doesn't support yield documentation
|
||||
|
||||
def test_warns_google_redundant_yield_doc_return(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields:
|
||||
int: One
|
||||
"""
|
||||
return 1
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-yields-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
|
||||
def test_warns_numpy_redundant_yield_doc_return(self):
|
||||
node = astroid.extract_node('''
|
||||
def my_func(self):
|
||||
"""This is a docstring.
|
||||
|
||||
Yields
|
||||
-------
|
||||
int
|
||||
One
|
||||
"""
|
||||
return 1
|
||||
''')
|
||||
with self.assertAddsMessages(
|
||||
Message(msg_id='redundant-yields-doc', node=node)):
|
||||
self.checker.visit_functiondef(node)
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.emptystring
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path as osp
|
||||
import unittest
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.extensions.comparetozero import CompareToZeroChecker
|
||||
from pylint.lint import PyLinter
|
||||
from pylint.reporters import BaseReporter
|
||||
|
||||
|
||||
class CompareToZeroTestReporter(BaseReporter):
|
||||
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
self.messages = []
|
||||
|
||||
|
||||
class CompareToZeroUsedTC(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._linter = PyLinter()
|
||||
cls._linter.set_reporter(CompareToZeroTestReporter())
|
||||
checkers.initialize(cls._linter)
|
||||
cls._linter.register_checker(CompareToZeroChecker(cls._linter))
|
||||
cls._linter.disable('I')
|
||||
|
||||
def test_comparetozero_message(self):
|
||||
elif_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'compare_to_zero.py')
|
||||
self._linter.check([elif_test])
|
||||
msgs = self._linter.reporter.messages
|
||||
self.assertEqual(len(msgs), 6)
|
||||
for msg in msgs:
|
||||
self.assertEqual(msg.symbol, 'compare-to-zero')
|
||||
self.assertEqual(msg.msg, 'Avoid comparisons to zero')
|
||||
self.assertEqual(msgs[0].line, 6)
|
||||
self.assertEqual(msgs[1].line, 9)
|
||||
self.assertEqual(msgs[2].line, 12)
|
||||
self.assertEqual(msgs[3].line, 15)
|
||||
self.assertEqual(msgs[4].line, 18)
|
||||
self.assertEqual(msgs[5].line, 24)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Derek Gustafson <degustaf@gmail.com>
|
||||
# Copyright (c) 2016 Luis Escobar <lescobar@vauxoo.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.check_docstring
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
|
||||
import pytest
|
||||
|
||||
from pylint.extensions.docstyle import DocStringStyleChecker
|
||||
|
||||
|
||||
EXPECTED_MSGS = [
|
||||
'First line empty in function docstring',
|
||||
'First line empty in class docstring',
|
||||
'First line empty in method docstring',
|
||||
'Bad docstring quotes in method, expected """, given \'\'\'',
|
||||
'Bad docstring quotes in method, expected """, given "',
|
||||
'Bad docstring quotes in method, expected """, given \'',
|
||||
'Bad docstring quotes in method, expected """, given \'',
|
||||
]
|
||||
|
||||
EXPECTED_SYMBOLS = [
|
||||
'docstring-first-line-empty',
|
||||
'docstring-first-line-empty',
|
||||
'docstring-first-line-empty',
|
||||
'bad-docstring-quotes',
|
||||
'bad-docstring-quotes',
|
||||
'bad-docstring-quotes',
|
||||
'bad-docstring-quotes',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def checker(checker):
|
||||
return DocStringStyleChecker
|
||||
|
||||
|
||||
def test_docstring_message(linter):
|
||||
docstring_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'docstring.py')
|
||||
linter.check([docstring_test])
|
||||
msgs = linter.reporter.messages
|
||||
assert len(msgs) == 7
|
||||
for msg, expected_symbol, expected_msg in zip(msgs,
|
||||
EXPECTED_SYMBOLS,
|
||||
EXPECTED_MSGS):
|
||||
assert msg.symbol == expected_symbol
|
||||
assert msg.msg == expected_msg
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright (c) 2015-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2015 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
|
||||
# Copyright (c) 2016-2017 Derek Gustafson <degustaf@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.check_elif
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
|
||||
import pytest
|
||||
|
||||
from pylint.extensions.check_elif import ElseifUsedChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def checker(checker):
|
||||
return ElseifUsedChecker
|
||||
|
||||
|
||||
def test_elseif_message(linter):
|
||||
elif_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'elif.py')
|
||||
linter.check([elif_test])
|
||||
msgs = linter.reporter.messages
|
||||
assert len(msgs) == 2
|
||||
for msg in msgs:
|
||||
assert msg.symbol == 'else-if-used'
|
||||
assert msg.msg == 'Consider using "elif" instead of "else if"'
|
||||
assert msgs[0].line == 9
|
||||
assert msgs[1].line == 21
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2016 Łukasz Rogalski <rogalski.91@gmail.com>
|
||||
# Copyright (c) 2016 Alexander Todorov <atodorov@otb.bg>
|
||||
# Copyright (c) 2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2017 Derek Gustafson <degustaf@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.emptystring
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
import pytest
|
||||
|
||||
from pylint.extensions.emptystring import CompareToEmptyStringChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def checker(checker):
|
||||
return CompareToEmptyStringChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def disable(disable):
|
||||
return ['I']
|
||||
|
||||
|
||||
def test_emptystring_message(linter):
|
||||
elif_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'empty_string_comparison.py')
|
||||
linter.check([elif_test])
|
||||
msgs = linter.reporter.messages
|
||||
expected_lineno = [6, 9, 12, 15]
|
||||
assert len(msgs) == len(expected_lineno)
|
||||
for msg, lineno in zip(msgs, expected_lineno):
|
||||
assert msg.symbol == 'compare-to-empty-string'
|
||||
assert msg.msg == 'Avoid comparisons to empty string'
|
||||
assert msg.line == lineno
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.overlapping_exceptions
|
||||
"""
|
||||
|
||||
from sys import version_info
|
||||
from os.path import join, dirname
|
||||
|
||||
from pylint.extensions.overlapping_exceptions import OverlappingExceptionsChecker
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def checker(checker):
|
||||
return OverlappingExceptionsChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def disable(disable):
|
||||
return ['I']
|
||||
|
||||
|
||||
def test_overlapping_exceptions(linter):
|
||||
test = join(dirname(__file__), 'data', 'overlapping_exceptions.py')
|
||||
linter.check([test])
|
||||
msgs = linter.reporter.messages
|
||||
|
||||
expected = [
|
||||
(13, 'Overlapping exceptions (SomeException and SomeException are the same)'),
|
||||
(18, 'Overlapping exceptions (SomeException is an ancestor class of SubclassException)'),
|
||||
(23, 'Overlapping exceptions (SomeException and AliasException are the same)'),
|
||||
(28, 'Overlapping exceptions (AliasException is an ancestor class of SubclassException)'),
|
||||
(34, 'Overlapping exceptions (SomeException and AliasException are the same)'),
|
||||
(34, 'Overlapping exceptions (SomeException is an ancestor class of SubclassException)'),
|
||||
(34, 'Overlapping exceptions (AliasException is an ancestor class of SubclassException)'),
|
||||
(39, 'Overlapping exceptions (ArithmeticError is an ancestor class of FloatingPointError)'),
|
||||
(44, 'Overlapping exceptions (ValueError is an ancestor class of UnicodeDecodeError)')
|
||||
]
|
||||
|
||||
assert len(msgs) == len(expected)
|
||||
for msg, exp in zip(msgs, expected):
|
||||
assert msg.msg_id == 'W0714'
|
||||
assert msg.symbol == 'overlapping-except'
|
||||
assert msg.category == 'warning'
|
||||
assert (msg.line, msg.msg) == exp
|
||||
|
||||
|
||||
@pytest.mark.skipif(version_info < (3, 3),
|
||||
reason="not relevant to Python version")
|
||||
def test_overlapping_exceptions_py33(linter):
|
||||
"""From Python 3.3 both IOError and socket.error are aliases for OSError."""
|
||||
test = join(dirname(__file__), 'data', 'overlapping_exceptions_py33.py')
|
||||
linter.check([test])
|
||||
msgs = linter.reporter.messages
|
||||
|
||||
expected = [
|
||||
(7, 'Overlapping exceptions (IOError and OSError are the same)'),
|
||||
(12, 'Overlapping exceptions (socket.error and OSError are the same)'),
|
||||
(17, 'Overlapping exceptions (socket.error is an ancestor class of ConnectionError)'),
|
||||
]
|
||||
|
||||
assert len(msgs) == len(expected)
|
||||
for msg, exp in zip(msgs, expected):
|
||||
assert msg.msg_id == 'W0714'
|
||||
assert msg.symbol == 'overlapping-except'
|
||||
assert msg.category == 'warning'
|
||||
assert (msg.line, msg.msg) == exp
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) 2016-2017 Claudiu Popa <pcmanticore@gmail.com>
|
||||
# Copyright (c) 2016-2017 Derek Gustafson <degustaf@gmail.com>
|
||||
|
||||
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
||||
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING
|
||||
|
||||
"""Tests for the pylint checker in :mod:`pylint.extensions.check_elif
|
||||
"""
|
||||
|
||||
import os.path as osp
|
||||
|
||||
import pytest
|
||||
|
||||
from pylint.extensions.redefined_variable_type import MultipleTypesChecker
|
||||
from pylint.lint import fix_import_path
|
||||
|
||||
|
||||
EXPECTED = [
|
||||
'Redefinition of self.var1 type from int to float',
|
||||
'Redefinition of var type from int to str',
|
||||
'Redefinition of myint type from int to bool',
|
||||
'Redefinition of _OK type from bool to str',
|
||||
'Redefinition of instance type from redefined.MyClass to bool',
|
||||
'Redefinition of SOME_FLOAT type from float to int',
|
||||
'Redefinition of var3 type from str to int',
|
||||
'Redefinition of var type from bool to int',
|
||||
'Redefinition of var4 type from float to str',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def checker(checker):
|
||||
return MultipleTypesChecker
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def disable(disable):
|
||||
return ['I']
|
||||
|
||||
|
||||
def test_types_redefined(linter):
|
||||
elif_test = osp.join(osp.dirname(osp.abspath(__file__)), 'data',
|
||||
'redefined.py')
|
||||
with fix_import_path([elif_test]):
|
||||
linter.check([elif_test])
|
||||
msgs = sorted(linter.reporter.messages, key=lambda item: item.line)
|
||||
assert len(msgs) == 9
|
||||
for msg, expected in zip(msgs, EXPECTED):
|
||||
assert msg.symbol == 'redefined-variable-type'
|
||||
assert msg.msg == expected
|
||||
@@ -0,0 +1,17 @@
|
||||
""" This should not warn about `prop` being abstract in Child """
|
||||
# pylint: disable=too-few-public-methods, no-absolute-import,metaclass-assignment
|
||||
|
||||
import abc
|
||||
|
||||
class Parent(object):
|
||||
"""Abstract Base Class """
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def prop(self):
|
||||
""" Abstract """
|
||||
|
||||
class Child(Parent):
|
||||
""" No warning for the following. """
|
||||
prop = property(lambda self: 1)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
"""Don't warn if the class is instantiated in its own body."""
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Ala(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def bala(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def portocala(cls):
|
||||
instance = cls()
|
||||
return instance
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
"""Check that instantiating a class with
|
||||
`abc.ABCMeta` as metaclass fails if it defines
|
||||
abstract methods.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-few-public-methods, missing-docstring
|
||||
# pylint: disable=no-absolute-import, metaclass-assignment
|
||||
# pylint: disable=abstract-method, import-error, wildcard-import
|
||||
|
||||
import abc
|
||||
from abc import ABCMeta
|
||||
from lala import Bala
|
||||
|
||||
|
||||
class GoodClass(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
class SecondGoodClass(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class ThirdGoodClass(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def test(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
class FourthGoodClass(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
class BadClass(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class SecondBadClass(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class ThirdBadClass(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
pass
|
||||
|
||||
class FourthBadClass(ThirdBadClass):
|
||||
pass
|
||||
|
||||
|
||||
class SomeMetaclass(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def prop(self):
|
||||
pass
|
||||
|
||||
class FifthGoodClass(SomeMetaclass):
|
||||
"""Don't consider this abstract if some attributes are
|
||||
there, but can't be inferred.
|
||||
"""
|
||||
prop = Bala # missing
|
||||
|
||||
|
||||
def main():
|
||||
""" do nothing """
|
||||
GoodClass()
|
||||
SecondGoodClass()
|
||||
ThirdGoodClass()
|
||||
FourthGoodClass()
|
||||
BadClass() # [abstract-class-instantiated]
|
||||
SecondBadClass() # [abstract-class-instantiated]
|
||||
ThirdBadClass() # [abstract-class-instantiated]
|
||||
FourthBadClass() # [abstract-class-instantiated]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[testoptions]
|
||||
max_pyver=3.0
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
abstract-class-instantiated:79:main:Abstract class 'BadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:80:main:Abstract class 'SecondBadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:81:main:Abstract class 'ThirdBadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:82:main:Abstract class 'FourthBadClass' with abstract methods instantiated
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
"""Check that instantiating a class with
|
||||
`abc.ABCMeta` as metaclass fails if it defines
|
||||
abstract methods.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-few-public-methods, missing-docstring
|
||||
# pylint: disable=abstract-method, import-error
|
||||
|
||||
import abc
|
||||
import weakref
|
||||
from lala import Bala
|
||||
|
||||
|
||||
class GoodClass(object, metaclass=abc.ABCMeta):
|
||||
pass
|
||||
|
||||
class SecondGoodClass(object, metaclass=abc.ABCMeta):
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class ThirdGoodClass(object, metaclass=abc.ABCMeta):
|
||||
""" This should not raise the warning. """
|
||||
def test(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
class BadClass(object, metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class SecondBadClass(object, metaclass=abc.ABCMeta):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
""" do nothing. """
|
||||
|
||||
class ThirdBadClass(SecondBadClass):
|
||||
pass
|
||||
|
||||
|
||||
class Structure(object, metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def __iter__(self):
|
||||
pass
|
||||
@abc.abstractmethod
|
||||
def __len__(self):
|
||||
pass
|
||||
@abc.abstractmethod
|
||||
def __contains__(self, _):
|
||||
pass
|
||||
@abc.abstractmethod
|
||||
def __hash__(self):
|
||||
pass
|
||||
|
||||
class Container(Structure):
|
||||
def __contains__(self, _):
|
||||
pass
|
||||
|
||||
class Sizable(Structure):
|
||||
def __len__(self):
|
||||
pass
|
||||
|
||||
class Hashable(Structure):
|
||||
__hash__ = 42
|
||||
|
||||
|
||||
class Iterator(Structure):
|
||||
def keys(self): # pylint: disable=no-self-use
|
||||
return iter([1, 2, 3])
|
||||
|
||||
__iter__ = keys
|
||||
|
||||
class AbstractSizable(Structure):
|
||||
@abc.abstractmethod
|
||||
def length(self):
|
||||
pass
|
||||
__len__ = length
|
||||
|
||||
class NoMroAbstractMethods(Container, Iterator, Sizable, Hashable):
|
||||
pass
|
||||
|
||||
class BadMroAbstractMethods(Container, Iterator, AbstractSizable):
|
||||
pass
|
||||
|
||||
class SomeMetaclass(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def prop(self):
|
||||
pass
|
||||
|
||||
class FourthGoodClass(SomeMetaclass):
|
||||
"""Don't consider this abstract if some attributes are
|
||||
there, but can't be inferred.
|
||||
"""
|
||||
prop = Bala # missing
|
||||
|
||||
|
||||
def main():
|
||||
""" do nothing """
|
||||
GoodClass()
|
||||
SecondGoodClass()
|
||||
ThirdGoodClass()
|
||||
FourthGoodClass()
|
||||
weakref.WeakKeyDictionary()
|
||||
weakref.WeakValueDictionary()
|
||||
NoMroAbstractMethods()
|
||||
|
||||
BadMroAbstractMethods() # [abstract-class-instantiated]
|
||||
BadClass() # [abstract-class-instantiated]
|
||||
SecondBadClass() # [abstract-class-instantiated]
|
||||
ThirdBadClass() # [abstract-class-instantiated]
|
||||
|
||||
|
||||
if 1: # pylint: disable=using-constant-test
|
||||
class FourthBadClass(object, metaclass=abc.ABCMeta):
|
||||
|
||||
def test(self):
|
||||
pass
|
||||
else:
|
||||
class FourthBadClass(object, metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
pass
|
||||
|
||||
|
||||
def main2():
|
||||
FourthBadClass() # [abstract-class-instantiated]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[testoptions]
|
||||
min_pyver=3.0
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
abstract-class-instantiated:108:main:Abstract class 'BadMroAbstractMethods' with abstract methods instantiated
|
||||
abstract-class-instantiated:109:main:Abstract class 'BadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:110:main:Abstract class 'SecondBadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:111:main:Abstract class 'ThirdBadClass' with abstract methods instantiated
|
||||
abstract-class-instantiated:128:main2:Abstract class 'FourthBadClass' with abstract methods instantiated
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Check that instantiating a class with `abc.ABCMeta` as ancestor fails if it
|
||||
defines abstract methods.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-few-public-methods, missing-docstring, no-init
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
|
||||
class BadClass(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
pass
|
||||
|
||||
def main():
|
||||
""" do nothing """
|
||||
BadClass() # [abstract-class-instantiated]
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[testoptions]
|
||||
min_pyver=3.4
|
||||
+1
@@ -0,0 +1 @@
|
||||
abstract-class-instantiated:19:main:Abstract class 'BadClass' with abstract methods instantiated
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user