added git and edited settings.py

This commit is contained in:
Esther Kleinhenz
2018-06-27 09:56:44 +02:00
commit 0bc13ada46
5609 changed files with 708000 additions and 0 deletions
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)
@@ -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
@@ -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]
@@ -0,0 +1,2 @@
[testoptions]
max_pyver=3.0
@@ -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
@@ -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]
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.0
@@ -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
@@ -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]
@@ -0,0 +1,2 @@
[testoptions]
min_pyver=3.4
@@ -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