adding new stuff
This commit is contained in:
parent
f84d7183aa
commit
9ef8a96f9a
1580 changed files with 0 additions and 0 deletions
46
plugins/bundle/python-mode/pymode/libs/pylint/__init__.py
Normal file
46
plugins/bundle/python-mode/pymode/libs/pylint/__init__.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
import sys
|
||||
|
||||
from .__pkginfo__ import version as __version__
|
||||
|
||||
def run_pylint():
|
||||
"""run pylint"""
|
||||
from pylint.lint import Run
|
||||
Run(sys.argv[1:])
|
||||
|
||||
def run_pylint_gui():
|
||||
"""run pylint-gui"""
|
||||
try:
|
||||
from pylint.gui import Run
|
||||
Run(sys.argv[1:])
|
||||
except ImportError:
|
||||
sys.exit('tkinter is not available')
|
||||
|
||||
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,3 @@
|
|||
#!/usr/bin/env python
|
||||
import pylint
|
||||
pylint.run_pylint()
|
||||
70
plugins/bundle/python-mode/pymode/libs/pylint/__pkginfo__.py
Normal file
70
plugins/bundle/python-mode/pymode/libs/pylint/__pkginfo__.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# pylint: disable=W0622,C0103
|
||||
# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""pylint packaging information"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
modname = distname = 'pylint'
|
||||
|
||||
numversion = (1, 4, 4)
|
||||
version = '.'.join([str(num) for num in numversion])
|
||||
|
||||
install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.3.6', 'six']
|
||||
|
||||
license = 'GPL'
|
||||
description = "python code static checker"
|
||||
web = 'http://www.pylint.org'
|
||||
mailinglist = "mailto://code-quality@python.org"
|
||||
author = 'Logilab'
|
||||
author_email = 'python-projects@lists.logilab.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 :: 3',
|
||||
'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 "pylint-gui", "pyreverse" (UML diagram generator)
|
||||
and "symilar" (an independent similarities checker)."""
|
||||
|
||||
from os.path import join
|
||||
scripts = [join('bin', filename)
|
||||
for filename in ('pylint', 'pylint-gui', "symilar", "epylint",
|
||||
"pyreverse")]
|
||||
|
||||
include_dirs = [join('pylint', 'test')]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,124 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""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-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 logilab.common.configuration 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')
|
||||
Binary file not shown.
1236
plugins/bundle/python-mode/pymode/libs/pylint/checkers/base.py
Normal file
1236
plugins/bundle/python-mode/pymode/libs/pylint/checkers/base.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,982 @@
|
|||
# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""classes checker for Python code
|
||||
"""
|
||||
from __future__ import generators
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import astroid
|
||||
from astroid import YES, Instance, are_exclusive, AssAttr, Class
|
||||
from astroid.bases import Generator, BUILTINS
|
||||
from astroid.inference import InferenceContext
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
PYMETHODS, overrides_a_method, check_messages, is_attr_private,
|
||||
is_attr_protected, node_frame_class, safe_infer, is_builtin_object,
|
||||
decorated_with_property, unimplemented_abstract_methods)
|
||||
import six
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
NEXT_METHOD = '__next__'
|
||||
else:
|
||||
NEXT_METHOD = 'next'
|
||||
ITER_METHODS = ('__iter__', '__getitem__')
|
||||
|
||||
def _called_in_methods(func, klass, methods):
|
||||
""" Check if the func was called in any of the given methods,
|
||||
belonging to the *klass*. Returns True if so, False otherwise.
|
||||
"""
|
||||
if not isinstance(func, astroid.Function):
|
||||
return False
|
||||
for method in methods:
|
||||
try:
|
||||
infered = klass.getattr(method)
|
||||
except astroid.NotFoundError:
|
||||
continue
|
||||
for infer_method in infered:
|
||||
for callfunc in infer_method.nodes_of_class(astroid.CallFunc):
|
||||
try:
|
||||
bound = next(callfunc.func.infer())
|
||||
except (astroid.InferenceError, StopIteration):
|
||||
continue
|
||||
if not isinstance(bound, astroid.BoundMethod):
|
||||
continue
|
||||
func_obj = bound._proxied
|
||||
if isinstance(func_obj, astroid.UnboundMethod):
|
||||
func_obj = func_obj._proxied
|
||||
if func_obj.name == func.name:
|
||||
return True
|
||||
return False
|
||||
|
||||
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 _is_attribute_property(name, klass):
|
||||
""" Check if the given attribute *name* is a property
|
||||
in the given *klass*.
|
||||
|
||||
It will look for `property` calls or for functions
|
||||
with the given name, decorated by `property` or `property`
|
||||
subclasses.
|
||||
Returns ``True`` if the name is a property in the given klass,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
|
||||
try:
|
||||
attributes = klass.getattr(name)
|
||||
except astroid.NotFoundError:
|
||||
return False
|
||||
property_name = "{0}.property".format(BUILTINS)
|
||||
for attr in attributes:
|
||||
try:
|
||||
infered = next(attr.infer())
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
if (isinstance(infered, astroid.Function) and
|
||||
decorated_with_property(infered)):
|
||||
return True
|
||||
if infered.pytype() == property_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
MSGS = {
|
||||
'F0202': ('Unable to check methods signature (%s / %s)',
|
||||
'method-check-failed',
|
||||
'Used when Pylint has been unable to check methods signature '
|
||||
'compatibility for an unexpected reason. Please report this kind '
|
||||
'if you don\'t make sense of it.'),
|
||||
|
||||
'E0202': ('An attribute defined in %s line %s hides this method',
|
||||
'method-hidden',
|
||||
'Used when a class defines a method which is hidden by an '
|
||||
'instance attribute from an ancestor class or set by some '
|
||||
'client code.'),
|
||||
'E0203': ('Access to member %r before its definition line %s',
|
||||
'access-member-before-definition',
|
||||
'Used when an instance member is accessed before it\'s actually '
|
||||
'assigned.'),
|
||||
'W0201': ('Attribute %r defined outside __init__',
|
||||
'attribute-defined-outside-init',
|
||||
'Used when an instance attribute is defined outside the __init__ '
|
||||
'method.'),
|
||||
|
||||
'W0212': ('Access to a protected member %s of a client class', # E0214
|
||||
'protected-access',
|
||||
'Used when a protected member (i.e. class member with a name '
|
||||
'beginning with an underscore) is access outside the class or a '
|
||||
'descendant of the class where it\'s defined.'),
|
||||
|
||||
'E0211': ('Method has no argument',
|
||||
'no-method-argument',
|
||||
'Used when a method which should have the bound instance as '
|
||||
'first argument has no argument defined.'),
|
||||
'E0213': ('Method should have "self" as first argument',
|
||||
'no-self-argument',
|
||||
'Used when a method has an attribute different the "self" as '
|
||||
'first argument. This is considered as an error since this is '
|
||||
'a so common convention that you shouldn\'t break it!'),
|
||||
'C0202': ('Class method %s should have %s as first argument',
|
||||
'bad-classmethod-argument',
|
||||
'Used when a class method has a first argument named differently '
|
||||
'than the value specified in valid-classmethod-first-arg option '
|
||||
'(default to "cls"), recommended to easily differentiate them '
|
||||
'from regular instance methods.'),
|
||||
'C0203': ('Metaclass method %s should have %s as first argument',
|
||||
'bad-mcs-method-argument',
|
||||
'Used when a metaclass method has a first agument named '
|
||||
'differently than the value specified in valid-classmethod-first'
|
||||
'-arg option (default to "cls"), recommended to easily '
|
||||
'differentiate them from regular instance methods.'),
|
||||
'C0204': ('Metaclass class method %s should have %s as first argument',
|
||||
'bad-mcs-classmethod-argument',
|
||||
'Used when a metaclass class method has a first argument named '
|
||||
'differently than the value specified in valid-metaclass-'
|
||||
'classmethod-first-arg option (default to "mcs"), recommended to '
|
||||
'easily differentiate them from regular instance methods.'),
|
||||
|
||||
'W0211': ('Static method with %r as first argument',
|
||||
'bad-staticmethod-argument',
|
||||
'Used when a static method has "self" or a value specified in '
|
||||
'valid-classmethod-first-arg option or '
|
||||
'valid-metaclass-classmethod-first-arg option as first argument.'
|
||||
),
|
||||
'R0201': ('Method could be a function',
|
||||
'no-self-use',
|
||||
'Used when a method doesn\'t use its bound instance, and so could '
|
||||
'be written as a function.'
|
||||
),
|
||||
|
||||
'E0221': ('Interface resolved to %s is not a class',
|
||||
'interface-is-not-class',
|
||||
'Used when a class claims to implement an interface which is not '
|
||||
'a class.'),
|
||||
'E0222': ('Missing method %r from %s interface',
|
||||
'missing-interface-method',
|
||||
'Used when a method declared in an interface is missing from a '
|
||||
'class implementing this interface'),
|
||||
'W0221': ('Arguments number differs from %s %r method',
|
||||
'arguments-differ',
|
||||
'Used when a method has a different number of arguments than in '
|
||||
'the implemented interface or in an overridden method.'),
|
||||
'W0222': ('Signature differs from %s %r method',
|
||||
'signature-differs',
|
||||
'Used when a method signature is different than in the '
|
||||
'implemented interface or in an overridden method.'),
|
||||
'W0223': ('Method %r is abstract in class %r but is not overridden',
|
||||
'abstract-method',
|
||||
'Used when an abstract method (i.e. raise NotImplementedError) is '
|
||||
'not overridden in concrete class.'
|
||||
),
|
||||
'F0220': ('failed to resolve interfaces implemented by %s (%s)',
|
||||
'unresolved-interface',
|
||||
'Used when a Pylint as failed to find interfaces implemented by '
|
||||
' a class'),
|
||||
|
||||
|
||||
'W0231': ('__init__ method from base class %r is not called',
|
||||
'super-init-not-called',
|
||||
'Used when an ancestor class method has an __init__ method '
|
||||
'which is not called by a derived class.'),
|
||||
'W0232': ('Class has no __init__ method',
|
||||
'no-init',
|
||||
'Used when a class has no __init__ method, neither its parent '
|
||||
'classes.'),
|
||||
'W0233': ('__init__ method from a non direct base class %r is called',
|
||||
'non-parent-init-called',
|
||||
'Used when an __init__ method is called on a class which is not '
|
||||
'in the direct ancestors for the analysed class.'),
|
||||
'W0234': ('__iter__ returns non-iterator',
|
||||
'non-iterator-returned',
|
||||
'Used when an __iter__ method returns something which is not an '
|
||||
'iterable (i.e. has no `%s` method)' % NEXT_METHOD),
|
||||
'E0235': ('__exit__ must accept 3 arguments: type, value, traceback',
|
||||
'bad-context-manager',
|
||||
'Used when the __exit__ special method, belonging to a '
|
||||
'context manager, does not accept 3 arguments '
|
||||
'(type, value, traceback).'),
|
||||
'E0236': ('Invalid object %r in __slots__, must contain '
|
||||
'only non empty strings',
|
||||
'invalid-slots-object',
|
||||
'Used when an invalid (non-string) object occurs in __slots__.'),
|
||||
'E0237': ('Assigning to attribute %r not defined in class slots',
|
||||
'assigning-non-slot',
|
||||
'Used when assigning to an attribute not defined '
|
||||
'in the class slots.'),
|
||||
'E0238': ('Invalid __slots__ object',
|
||||
'invalid-slots',
|
||||
'Used when an invalid __slots__ is found in class. '
|
||||
'Only a string, an iterable or a sequence is permitted.'),
|
||||
'E0239': ('Inheriting %r, which is not a class.',
|
||||
'inherit-non-class',
|
||||
'Used when a class inherits from something which is not a '
|
||||
'class.'),
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ClassChecker(BaseChecker):
|
||||
"""checks for :
|
||||
* methods without self as first argument
|
||||
* overridden methods signature
|
||||
* access only to existent members via self
|
||||
* attributes not defined in the __init__ method
|
||||
* supported interfaces implementation
|
||||
* unreachable code
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'classes'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -2
|
||||
# configuration options
|
||||
options = (('ignore-iface-methods',
|
||||
{'default' : (#zope interface
|
||||
'isImplementedBy', 'deferred', 'extends', 'names',
|
||||
'namesAndDescriptions', 'queryDescriptionFor', 'getBases',
|
||||
'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue',
|
||||
'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue',
|
||||
'isImplementedByInstancesOf',
|
||||
# twisted
|
||||
'adaptWith',
|
||||
# logilab.common interface
|
||||
'is_implemented_by'),
|
||||
'type' : 'csv',
|
||||
'metavar' : '<method names>',
|
||||
'help' : 'List of interface methods to ignore, \
|
||||
separated by a comma. This is used for instance to not check methods defines \
|
||||
in Zope\'s Interface base class.'}
|
||||
),
|
||||
('defining-attr-methods',
|
||||
{'default' : ('__init__', '__new__', 'setUp'),
|
||||
'type' : 'csv',
|
||||
'metavar' : '<method names>',
|
||||
'help' : 'List of method names used to declare (i.e. assign) \
|
||||
instance attributes.'}
|
||||
),
|
||||
('valid-classmethod-first-arg',
|
||||
{'default' : ('cls',),
|
||||
'type' : 'csv',
|
||||
'metavar' : '<argument names>',
|
||||
'help' : 'List of valid names for the first argument in \
|
||||
a class method.'}
|
||||
),
|
||||
('valid-metaclass-classmethod-first-arg',
|
||||
{'default' : ('mcs',),
|
||||
'type' : 'csv',
|
||||
'metavar' : '<argument names>',
|
||||
'help' : 'List of valid names for the first argument in \
|
||||
a metaclass class method.'}
|
||||
),
|
||||
('exclude-protected',
|
||||
{
|
||||
'default': (
|
||||
# namedtuple public API.
|
||||
'_asdict', '_fields', '_replace', '_source', '_make'),
|
||||
'type': 'csv',
|
||||
'metavar': '<protected access exclusions>',
|
||||
'help': ('List of member names, which should be excluded '
|
||||
'from the protected access warning.')}
|
||||
))
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self._accessed = []
|
||||
self._first_attrs = []
|
||||
self._meth_could_be_func = None
|
||||
|
||||
def visit_class(self, node):
|
||||
"""init visit variable _accessed and check interfaces
|
||||
"""
|
||||
self._accessed.append(defaultdict(list))
|
||||
self._check_bases_classes(node)
|
||||
self._check_interfaces(node)
|
||||
# if not an interface, exception, metaclass
|
||||
if node.type == 'class':
|
||||
try:
|
||||
node.local_attr('__init__')
|
||||
except astroid.NotFoundError:
|
||||
self.add_message('no-init', args=node, node=node)
|
||||
self._check_slots(node)
|
||||
self._check_proper_bases(node)
|
||||
|
||||
@check_messages('inherit-non-class')
|
||||
def _check_proper_bases(self, node):
|
||||
"""
|
||||
Detect that a class inherits something which is not
|
||||
a class or a type.
|
||||
"""
|
||||
for base in node.bases:
|
||||
ancestor = safe_infer(base)
|
||||
if ancestor in (YES, None):
|
||||
continue
|
||||
if (isinstance(ancestor, astroid.Instance) and
|
||||
ancestor.is_subtype_of('%s.type' % (BUILTINS,))):
|
||||
continue
|
||||
if not isinstance(ancestor, astroid.Class):
|
||||
self.add_message('inherit-non-class',
|
||||
args=base.as_string(), node=node)
|
||||
|
||||
@check_messages('access-member-before-definition',
|
||||
'attribute-defined-outside-init')
|
||||
def leave_class(self, cnode):
|
||||
"""close a class node:
|
||||
check that instance attributes are defined in __init__ and check
|
||||
access to existent members
|
||||
"""
|
||||
# check access to existent members on non metaclass classes
|
||||
accessed = self._accessed.pop()
|
||||
if cnode.type != 'metaclass':
|
||||
self._check_accessed_members(cnode, accessed)
|
||||
# checks attributes are defined in an allowed method such as __init__
|
||||
if not self.linter.is_message_enabled('attribute-defined-outside-init'):
|
||||
return
|
||||
defining_methods = self.config.defining_attr_methods
|
||||
current_module = cnode.root()
|
||||
for attr, nodes in six.iteritems(cnode.instance_attrs):
|
||||
# skip nodes which are not in the current module and it may screw up
|
||||
# the output, while it's not worth it
|
||||
nodes = [n for n in nodes if not
|
||||
isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))
|
||||
and n.root() is current_module]
|
||||
if not nodes:
|
||||
continue # error detected by typechecking
|
||||
# check if any method attr is defined in is a defining method
|
||||
if any(node.frame().name in defining_methods
|
||||
for node in nodes):
|
||||
continue
|
||||
|
||||
# check attribute is defined in a parent's __init__
|
||||
for parent in cnode.instance_attr_ancestors(attr):
|
||||
attr_defined = False
|
||||
# check if any parent method attr is defined in is a defining method
|
||||
for node in parent.instance_attrs[attr]:
|
||||
if node.frame().name in defining_methods:
|
||||
attr_defined = True
|
||||
if attr_defined:
|
||||
# we're done :)
|
||||
break
|
||||
else:
|
||||
# check attribute is defined as a class attribute
|
||||
try:
|
||||
cnode.local_attr(attr)
|
||||
except astroid.NotFoundError:
|
||||
for node in nodes:
|
||||
if node.frame().name not in defining_methods:
|
||||
# If the attribute was set by a callfunc in any
|
||||
# of the defining methods, then don't emit
|
||||
# the warning.
|
||||
if _called_in_methods(node.frame(), cnode,
|
||||
defining_methods):
|
||||
continue
|
||||
self.add_message('attribute-defined-outside-init',
|
||||
args=attr, node=node)
|
||||
|
||||
def visit_function(self, node):
|
||||
"""check method arguments, overriding"""
|
||||
# ignore actual functions
|
||||
if not node.is_method():
|
||||
return
|
||||
klass = node.parent.frame()
|
||||
self._meth_could_be_func = True
|
||||
# check first argument is self if this is actually a method
|
||||
self._check_first_arg_for_type(node, klass.type == 'metaclass')
|
||||
if node.name == '__init__':
|
||||
self._check_init(node)
|
||||
return
|
||||
# check signature if the method overloads inherited method
|
||||
for overridden in klass.local_attr_ancestors(node.name):
|
||||
# get astroid for the searched method
|
||||
try:
|
||||
meth_node = overridden[node.name]
|
||||
except KeyError:
|
||||
# we have found the method but it's not in the local
|
||||
# dictionary.
|
||||
# This may happen with astroid build from living objects
|
||||
continue
|
||||
if not isinstance(meth_node, astroid.Function):
|
||||
continue
|
||||
self._check_signature(node, meth_node, 'overridden')
|
||||
break
|
||||
if node.decorators:
|
||||
for decorator in node.decorators.nodes:
|
||||
if isinstance(decorator, astroid.Getattr) and \
|
||||
decorator.attrname in ('getter', 'setter', 'deleter'):
|
||||
# attribute affectation will call this method, not hiding it
|
||||
return
|
||||
if isinstance(decorator, astroid.Name) and decorator.name == 'property':
|
||||
# attribute affectation will either call a setter or raise
|
||||
# an attribute error, anyway not hiding the function
|
||||
return
|
||||
# check if the method is hidden by an attribute
|
||||
try:
|
||||
overridden = klass.instance_attr(node.name)[0] # XXX
|
||||
overridden_frame = overridden.frame()
|
||||
if (isinstance(overridden_frame, astroid.Function)
|
||||
and overridden_frame.type == 'method'):
|
||||
overridden_frame = overridden_frame.parent.frame()
|
||||
if (isinstance(overridden_frame, Class)
|
||||
and klass.is_subtype_of(overridden_frame.qname())):
|
||||
args = (overridden.root().name, overridden.fromlineno)
|
||||
self.add_message('method-hidden', args=args, node=node)
|
||||
except astroid.NotFoundError:
|
||||
pass
|
||||
|
||||
# check non-iterators in __iter__
|
||||
if node.name == '__iter__':
|
||||
self._check_iter(node)
|
||||
elif node.name == '__exit__':
|
||||
self._check_exit(node)
|
||||
|
||||
def _check_slots(self, node):
|
||||
if '__slots__' not in node.locals:
|
||||
return
|
||||
for slots in node.igetattr('__slots__'):
|
||||
# check if __slots__ is a valid type
|
||||
for meth in ITER_METHODS:
|
||||
try:
|
||||
slots.getattr(meth)
|
||||
break
|
||||
except astroid.NotFoundError:
|
||||
continue
|
||||
else:
|
||||
self.add_message('invalid-slots', node=node)
|
||||
continue
|
||||
|
||||
if isinstance(slots, astroid.Const):
|
||||
# a string, ignore the following checks
|
||||
continue
|
||||
if not hasattr(slots, 'itered'):
|
||||
# we can't obtain the values, maybe a .deque?
|
||||
continue
|
||||
|
||||
if isinstance(slots, astroid.Dict):
|
||||
values = [item[0] for item in slots.items]
|
||||
else:
|
||||
values = slots.itered()
|
||||
if values is YES:
|
||||
return
|
||||
|
||||
for elt in values:
|
||||
try:
|
||||
self._check_slots_elt(elt)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
def _check_slots_elt(self, elt):
|
||||
for infered in elt.infer():
|
||||
if infered is YES:
|
||||
continue
|
||||
if (not isinstance(infered, astroid.Const) or
|
||||
not isinstance(infered.value, six.string_types)):
|
||||
self.add_message('invalid-slots-object',
|
||||
args=infered.as_string(),
|
||||
node=elt)
|
||||
continue
|
||||
if not infered.value:
|
||||
self.add_message('invalid-slots-object',
|
||||
args=infered.as_string(),
|
||||
node=elt)
|
||||
|
||||
def _check_iter(self, node):
|
||||
try:
|
||||
infered = node.infer_call_result(node)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
|
||||
for infered_node in infered:
|
||||
if (infered_node is YES
|
||||
or isinstance(infered_node, Generator)):
|
||||
continue
|
||||
if isinstance(infered_node, astroid.Instance):
|
||||
try:
|
||||
infered_node.local_attr(NEXT_METHOD)
|
||||
except astroid.NotFoundError:
|
||||
self.add_message('non-iterator-returned',
|
||||
node=node)
|
||||
break
|
||||
|
||||
def _check_exit(self, node):
|
||||
positional = sum(1 for arg in node.args.args if arg.name != 'self')
|
||||
if positional < 3 and not node.args.vararg:
|
||||
self.add_message('bad-context-manager',
|
||||
node=node)
|
||||
elif positional > 3:
|
||||
self.add_message('bad-context-manager',
|
||||
node=node)
|
||||
|
||||
def leave_function(self, node):
|
||||
"""on method node, check if this method couldn't be a function
|
||||
|
||||
ignore class, static and abstract methods, initializer,
|
||||
methods overridden from a parent class and any
|
||||
kind of method defined in an interface for this warning
|
||||
"""
|
||||
if node.is_method():
|
||||
if node.args.args is not None:
|
||||
self._first_attrs.pop()
|
||||
if not self.linter.is_message_enabled('no-self-use'):
|
||||
return
|
||||
class_node = node.parent.frame()
|
||||
if (self._meth_could_be_func and node.type == 'method'
|
||||
and not node.name in PYMETHODS
|
||||
and not (node.is_abstract() or
|
||||
overrides_a_method(class_node, node.name))
|
||||
and class_node.type != 'interface'):
|
||||
self.add_message('no-self-use', node=node)
|
||||
|
||||
def visit_getattr(self, node):
|
||||
"""check if the getattr is an access to a class member
|
||||
if so, register it. Also check for access to protected
|
||||
class member from outside its class (but ignore __special__
|
||||
methods)
|
||||
"""
|
||||
attrname = node.attrname
|
||||
# Check self
|
||||
if self.is_first_attr(node):
|
||||
self._accessed[-1][attrname].append(node)
|
||||
return
|
||||
if not self.linter.is_message_enabled('protected-access'):
|
||||
return
|
||||
|
||||
self._check_protected_attribute_access(node)
|
||||
|
||||
def visit_assattr(self, node):
|
||||
if isinstance(node.ass_type(), astroid.AugAssign) and self.is_first_attr(node):
|
||||
self._accessed[-1][node.attrname].append(node)
|
||||
self._check_in_slots(node)
|
||||
|
||||
def _check_in_slots(self, node):
|
||||
""" Check that the given assattr node
|
||||
is defined in the class slots.
|
||||
"""
|
||||
infered = safe_infer(node.expr)
|
||||
if infered and isinstance(infered, Instance):
|
||||
klass = infered._proxied
|
||||
if '__slots__' not in klass.locals or not klass.newstyle:
|
||||
return
|
||||
|
||||
slots = klass.slots()
|
||||
if slots is None:
|
||||
return
|
||||
# If any ancestor doesn't use slots, the slots
|
||||
# defined for this class are superfluous.
|
||||
if any('__slots__' not in ancestor.locals and
|
||||
ancestor.name != 'object'
|
||||
for ancestor in klass.ancestors()):
|
||||
return
|
||||
|
||||
if not any(slot.value == node.attrname for slot in slots):
|
||||
# If we have a '__dict__' in slots, then
|
||||
# assigning any name is valid.
|
||||
if not any(slot.value == '__dict__' for slot in slots):
|
||||
if _is_attribute_property(node.attrname, klass):
|
||||
# Properties circumvent the slots mechanism,
|
||||
# so we should not emit a warning for them.
|
||||
return
|
||||
self.add_message('assigning-non-slot',
|
||||
args=(node.attrname, ), node=node)
|
||||
|
||||
@check_messages('protected-access')
|
||||
def visit_assign(self, assign_node):
|
||||
node = assign_node.targets[0]
|
||||
if not isinstance(node, AssAttr):
|
||||
return
|
||||
|
||||
if self.is_first_attr(node):
|
||||
return
|
||||
|
||||
self._check_protected_attribute_access(node)
|
||||
|
||||
def _check_protected_attribute_access(self, node):
|
||||
'''Given an attribute access node (set or get), check if attribute
|
||||
access is legitimate. Call _check_first_attr with node before calling
|
||||
this method. Valid cases are:
|
||||
* self._attr in a method or cls._attr in a classmethod. Checked by
|
||||
_check_first_attr.
|
||||
* Klass._attr inside "Klass" class.
|
||||
* Klass2._attr inside "Klass" class when Klass2 is a base class of
|
||||
Klass.
|
||||
'''
|
||||
attrname = node.attrname
|
||||
|
||||
if (is_attr_protected(attrname) and
|
||||
attrname not in self.config.exclude_protected):
|
||||
|
||||
klass = node_frame_class(node)
|
||||
|
||||
# XXX infer to be more safe and less dirty ??
|
||||
# in classes, check we are not getting a parent method
|
||||
# through the class object or through super
|
||||
callee = node.expr.as_string()
|
||||
|
||||
# We are not in a class, no remaining valid case
|
||||
if klass is None:
|
||||
self.add_message('protected-access', node=node, args=attrname)
|
||||
return
|
||||
|
||||
# If the expression begins with a call to super, that's ok.
|
||||
if isinstance(node.expr, astroid.CallFunc) and \
|
||||
isinstance(node.expr.func, astroid.Name) and \
|
||||
node.expr.func.name == 'super':
|
||||
return
|
||||
|
||||
# We are in a class, one remaining valid cases, Klass._attr inside
|
||||
# Klass
|
||||
if not (callee == klass.name or callee in klass.basenames):
|
||||
# Detect property assignments in the body of the class.
|
||||
# This is acceptable:
|
||||
#
|
||||
# class A:
|
||||
# b = property(lambda: self._b)
|
||||
|
||||
stmt = node.parent.statement()
|
||||
try:
|
||||
if (isinstance(stmt, astroid.Assign) and
|
||||
(stmt in klass.body or klass.parent_of(stmt)) and
|
||||
isinstance(stmt.value, astroid.CallFunc) and
|
||||
isinstance(stmt.value.func, astroid.Name) and
|
||||
stmt.value.func.name == 'property' and
|
||||
is_builtin_object(next(stmt.value.func.infer(), None))):
|
||||
return
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
self.add_message('protected-access', node=node, args=attrname)
|
||||
|
||||
def visit_name(self, node):
|
||||
"""check if the name handle an access to a class member
|
||||
if so, register it
|
||||
"""
|
||||
if self._first_attrs and (node.name == self._first_attrs[-1] or
|
||||
not self._first_attrs[-1]):
|
||||
self._meth_could_be_func = False
|
||||
|
||||
def _check_accessed_members(self, node, accessed):
|
||||
"""check that accessed members are defined"""
|
||||
# XXX refactor, probably much simpler now that E0201 is in type checker
|
||||
for attr, nodes in six.iteritems(accessed):
|
||||
# deactivate "except doesn't do anything", that's expected
|
||||
# pylint: disable=W0704
|
||||
try:
|
||||
# is it a class attribute ?
|
||||
node.local_attr(attr)
|
||||
# yes, stop here
|
||||
continue
|
||||
except astroid.NotFoundError:
|
||||
pass
|
||||
# is it an instance attribute of a parent class ?
|
||||
try:
|
||||
next(node.instance_attr_ancestors(attr))
|
||||
# yes, stop here
|
||||
continue
|
||||
except StopIteration:
|
||||
pass
|
||||
# is it an instance attribute ?
|
||||
try:
|
||||
defstmts = node.instance_attr(attr)
|
||||
except astroid.NotFoundError:
|
||||
pass
|
||||
else:
|
||||
# filter out augment assignment nodes
|
||||
defstmts = [stmt for stmt in defstmts if stmt not in nodes]
|
||||
if not defstmts:
|
||||
# only augment assignment for this node, no-member should be
|
||||
# triggered by the typecheck checker
|
||||
continue
|
||||
# filter defstmts to only pick the first one when there are
|
||||
# several assignments in the same scope
|
||||
scope = defstmts[0].scope()
|
||||
defstmts = [stmt for i, stmt in enumerate(defstmts)
|
||||
if i == 0 or stmt.scope() is not scope]
|
||||
# if there are still more than one, don't attempt to be smarter
|
||||
# than we can be
|
||||
if len(defstmts) == 1:
|
||||
defstmt = defstmts[0]
|
||||
# check that if the node is accessed in the same method as
|
||||
# it's defined, it's accessed after the initial assignment
|
||||
frame = defstmt.frame()
|
||||
lno = defstmt.fromlineno
|
||||
for _node in nodes:
|
||||
if _node.frame() is frame and _node.fromlineno < lno \
|
||||
and not are_exclusive(_node.statement(), defstmt,
|
||||
('AttributeError', 'Exception', 'BaseException')):
|
||||
self.add_message('access-member-before-definition',
|
||||
node=_node, args=(attr, lno))
|
||||
|
||||
def _check_first_arg_for_type(self, node, metaclass=0):
|
||||
"""check the name of first argument, expect:
|
||||
|
||||
* 'self' for a regular method
|
||||
* 'cls' for a class method or a metaclass regular method (actually
|
||||
valid-classmethod-first-arg value)
|
||||
* 'mcs' for a metaclass class method (actually
|
||||
valid-metaclass-classmethod-first-arg)
|
||||
* not one of the above for a static method
|
||||
"""
|
||||
# don't care about functions with unknown argument (builtins)
|
||||
if node.args.args is None:
|
||||
return
|
||||
first_arg = node.args.args and node.argnames()[0]
|
||||
self._first_attrs.append(first_arg)
|
||||
first = self._first_attrs[-1]
|
||||
# static method
|
||||
if node.type == 'staticmethod':
|
||||
if (first_arg == 'self' or
|
||||
first_arg in self.config.valid_classmethod_first_arg or
|
||||
first_arg in self.config.valid_metaclass_classmethod_first_arg):
|
||||
self.add_message('bad-staticmethod-argument', args=first, node=node)
|
||||
return
|
||||
self._first_attrs[-1] = None
|
||||
# class / regular method with no args
|
||||
elif not node.args.args:
|
||||
self.add_message('no-method-argument', node=node)
|
||||
# metaclass
|
||||
elif metaclass:
|
||||
# metaclass __new__ or classmethod
|
||||
if node.type == 'classmethod':
|
||||
self._check_first_arg_config(
|
||||
first,
|
||||
self.config.valid_metaclass_classmethod_first_arg, node,
|
||||
'bad-mcs-classmethod-argument', node.name)
|
||||
# metaclass regular method
|
||||
else:
|
||||
self._check_first_arg_config(
|
||||
first,
|
||||
self.config.valid_classmethod_first_arg, node,
|
||||
'bad-mcs-method-argument',
|
||||
node.name)
|
||||
# regular class
|
||||
else:
|
||||
# class method
|
||||
if node.type == 'classmethod':
|
||||
self._check_first_arg_config(
|
||||
first,
|
||||
self.config.valid_classmethod_first_arg, node,
|
||||
'bad-classmethod-argument',
|
||||
node.name)
|
||||
# regular method without self as argument
|
||||
elif first != 'self':
|
||||
self.add_message('no-self-argument', node=node)
|
||||
|
||||
def _check_first_arg_config(self, first, config, node, message,
|
||||
method_name):
|
||||
if first not in config:
|
||||
if len(config) == 1:
|
||||
valid = repr(config[0])
|
||||
else:
|
||||
valid = ', '.join(repr(v) for v in config[:-1])
|
||||
valid = '%s or %r' % (valid, config[-1])
|
||||
self.add_message(message, args=(method_name, valid), node=node)
|
||||
|
||||
def _check_bases_classes(self, node):
|
||||
"""check that the given class node implements abstract methods from
|
||||
base classes
|
||||
"""
|
||||
def is_abstract(method):
|
||||
return method.is_abstract(pass_is_abstract=False)
|
||||
|
||||
# check if this class abstract
|
||||
if class_is_abstract(node):
|
||||
return
|
||||
|
||||
methods = sorted(
|
||||
unimplemented_abstract_methods(node, is_abstract).items(),
|
||||
key=lambda item: item[0],
|
||||
)
|
||||
for name, method in methods:
|
||||
owner = method.parent.frame()
|
||||
if owner is node:
|
||||
continue
|
||||
# owner is not this class, it must be a parent class
|
||||
# check that the ancestor's method is not abstract
|
||||
if name in node.locals:
|
||||
# it is redefined as an attribute or with a descriptor
|
||||
continue
|
||||
self.add_message('abstract-method', node=node,
|
||||
args=(name, owner.name))
|
||||
|
||||
def _check_interfaces(self, node):
|
||||
"""check that the given class node really implements declared
|
||||
interfaces
|
||||
"""
|
||||
e0221_hack = [False]
|
||||
def iface_handler(obj):
|
||||
"""filter interface objects, it should be classes"""
|
||||
if not isinstance(obj, astroid.Class):
|
||||
e0221_hack[0] = True
|
||||
self.add_message('interface-is-not-class', node=node,
|
||||
args=(obj.as_string(),))
|
||||
return False
|
||||
return True
|
||||
ignore_iface_methods = self.config.ignore_iface_methods
|
||||
try:
|
||||
for iface in node.interfaces(handler_func=iface_handler):
|
||||
for imethod in iface.methods():
|
||||
name = imethod.name
|
||||
if name.startswith('_') or name in ignore_iface_methods:
|
||||
# don't check method beginning with an underscore,
|
||||
# usually belonging to the interface implementation
|
||||
continue
|
||||
# get class method astroid
|
||||
try:
|
||||
method = node_method(node, name)
|
||||
except astroid.NotFoundError:
|
||||
self.add_message('missing-interface-method',
|
||||
args=(name, iface.name),
|
||||
node=node)
|
||||
continue
|
||||
# ignore inherited methods
|
||||
if method.parent.frame() is not node:
|
||||
continue
|
||||
# check signature
|
||||
self._check_signature(method, imethod,
|
||||
'%s interface' % iface.name)
|
||||
except astroid.InferenceError:
|
||||
if e0221_hack[0]:
|
||||
return
|
||||
implements = Instance(node).getattr('__implements__')[0]
|
||||
assignment = implements.parent
|
||||
assert isinstance(assignment, astroid.Assign)
|
||||
# assignment.expr can be a Name or a Tuple or whatever.
|
||||
# Use as_string() for the message
|
||||
# FIXME: in case of multiple interfaces, find which one could not
|
||||
# be resolved
|
||||
self.add_message('unresolved-interface', node=implements,
|
||||
args=(node.name, assignment.value.as_string()))
|
||||
|
||||
def _check_init(self, node):
|
||||
"""check that the __init__ method call super or ancestors'__init__
|
||||
method
|
||||
"""
|
||||
if (not self.linter.is_message_enabled('super-init-not-called') and
|
||||
not self.linter.is_message_enabled('non-parent-init-called')):
|
||||
return
|
||||
klass_node = node.parent.frame()
|
||||
to_call = _ancestors_to_call(klass_node)
|
||||
not_called_yet = dict(to_call)
|
||||
for stmt in node.nodes_of_class(astroid.CallFunc):
|
||||
expr = stmt.func
|
||||
if not isinstance(expr, astroid.Getattr) \
|
||||
or expr.attrname != '__init__':
|
||||
continue
|
||||
# skip the test if using super
|
||||
if isinstance(expr.expr, astroid.CallFunc) and \
|
||||
isinstance(expr.expr.func, astroid.Name) and \
|
||||
expr.expr.func.name == 'super':
|
||||
return
|
||||
try:
|
||||
for klass in expr.expr.infer():
|
||||
if klass is YES:
|
||||
continue
|
||||
# The infered klass can be super(), which was
|
||||
# assigned to a variable and the `__init__`
|
||||
# was called later.
|
||||
#
|
||||
# base = super()
|
||||
# base.__init__(...)
|
||||
|
||||
if (isinstance(klass, astroid.Instance) and
|
||||
isinstance(klass._proxied, astroid.Class) and
|
||||
is_builtin_object(klass._proxied) and
|
||||
klass._proxied.name == 'super'):
|
||||
return
|
||||
try:
|
||||
del not_called_yet[klass]
|
||||
except KeyError:
|
||||
if klass not in to_call:
|
||||
self.add_message('non-parent-init-called',
|
||||
node=expr, args=klass.name)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
for klass, method in six.iteritems(not_called_yet):
|
||||
if klass.name == 'object' or method.parent.name == 'object':
|
||||
continue
|
||||
self.add_message('super-init-not-called', args=klass.name, node=node)
|
||||
|
||||
def _check_signature(self, method1, refmethod, class_type):
|
||||
"""check that the signature of the two given methods match
|
||||
|
||||
class_type is in 'class', 'interface'
|
||||
"""
|
||||
if not (isinstance(method1, astroid.Function)
|
||||
and isinstance(refmethod, astroid.Function)):
|
||||
self.add_message('method-check-failed',
|
||||
args=(method1, refmethod), node=method1)
|
||||
return
|
||||
# don't care about functions with unknown argument (builtins)
|
||||
if method1.args.args is None or refmethod.args.args is None:
|
||||
return
|
||||
# if we use *args, **kwargs, skip the below checks
|
||||
if method1.args.vararg or method1.args.kwarg:
|
||||
return
|
||||
if is_attr_private(method1.name):
|
||||
return
|
||||
if len(method1.args.args) != len(refmethod.args.args):
|
||||
self.add_message('arguments-differ',
|
||||
args=(class_type, method1.name),
|
||||
node=method1)
|
||||
elif len(method1.args.defaults) < len(refmethod.args.defaults):
|
||||
self.add_message('signature-differs',
|
||||
args=(class_type, method1.name),
|
||||
node=method1)
|
||||
|
||||
def is_first_attr(self, node):
|
||||
"""Check that attribute lookup name use first attribute variable name
|
||||
(self for method, cls for classmethod and mcs for metaclass).
|
||||
"""
|
||||
return self._first_attrs and isinstance(node.expr, astroid.Name) and \
|
||||
node.expr.name == self._first_attrs[-1]
|
||||
|
||||
def _ancestors_to_call(klass_node, method='__init__'):
|
||||
"""return a dictionary where keys are the list of base classes providing
|
||||
the queried method, and so that should/may be called from the method node
|
||||
"""
|
||||
to_call = {}
|
||||
for base_node in klass_node.ancestors(recurs=False):
|
||||
try:
|
||||
to_call[base_node] = next(base_node.igetattr(method))
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
return to_call
|
||||
|
||||
|
||||
def node_method(node, method_name):
|
||||
"""get astroid for <method_name> on the given class node, ensuring it
|
||||
is a Function node
|
||||
"""
|
||||
for n in node.local_attr(method_name):
|
||||
if isinstance(n, astroid.Function):
|
||||
return n
|
||||
raise astroid.NotFoundError(method_name)
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(ClassChecker(linter))
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""check for signs of poor design"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from astroid import If, InferenceError
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
# regexp for ignored argument name
|
||||
IGNORED_ARGUMENT_NAMES = re.compile('_.*')
|
||||
|
||||
|
||||
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.'),
|
||||
'R0923': ('Interface not implemented',
|
||||
'interface-not-implemented',
|
||||
'Used when an interface class is not implemented anywhere.'),
|
||||
}
|
||||
|
||||
|
||||
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'}
|
||||
),
|
||||
('ignored-argument-names',
|
||||
{'default' : IGNORED_ARGUMENT_NAMES,
|
||||
'type' :'regexp', 'metavar' : '<regexp>',
|
||||
'help' : 'Argument names that match this expression will be '
|
||||
'ignored. Default to name with leading underscore'}
|
||||
),
|
||||
('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).'}
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self._returns = None
|
||||
self._branches = None
|
||||
self._used_ifaces = None
|
||||
self._ifaces = None
|
||||
self._stmts = 0
|
||||
|
||||
def open(self):
|
||||
"""initialize visit variables"""
|
||||
self.stats = self.linter.add_stats()
|
||||
self._returns = []
|
||||
self._branches = defaultdict(int)
|
||||
self._used_ifaces = {}
|
||||
self._ifaces = []
|
||||
|
||||
def close(self):
|
||||
"""check that interface classes are used"""
|
||||
for iface in self._ifaces:
|
||||
if not iface in self._used_ifaces:
|
||||
self.add_message('interface-not-implemented', node=iface)
|
||||
|
||||
@check_messages('too-many-ancestors', 'too-many-instance-attributes',
|
||||
'too-few-public-methods', 'too-many-public-methods',
|
||||
'interface-not-implemented')
|
||||
def visit_class(self, node):
|
||||
"""check size of inheritance hierarchy and number of instance attributes
|
||||
"""
|
||||
# Is the total inheritance hierarchy is 7 or less?
|
||||
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))
|
||||
# Does the class contain less than 20 attributes for
|
||||
# non-GUI classes (40 for GUI)?
|
||||
# FIXME detect gui classes
|
||||
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))
|
||||
# update interface classes structures
|
||||
if node.type == 'interface' and node.name != 'Interface':
|
||||
self._ifaces.append(node)
|
||||
for parent in node.ancestors(False):
|
||||
if parent.name == 'Interface':
|
||||
continue
|
||||
self._used_ifaces[parent] = 1
|
||||
try:
|
||||
for iface in node.interfaces():
|
||||
self._used_ifaces[iface] = 1
|
||||
except InferenceError:
|
||||
# XXX log ?
|
||||
pass
|
||||
|
||||
@check_messages('too-few-public-methods', 'too-many-public-methods')
|
||||
def leave_class(self, node):
|
||||
"""check number of public methods"""
|
||||
my_methods = sum(1 for method in node.mymethods()
|
||||
if not method.name.startswith('_'))
|
||||
all_methods = sum(1 for method in node.methods()
|
||||
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':
|
||||
return
|
||||
|
||||
# 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')
|
||||
def visit_function(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
|
||||
if args is not None:
|
||||
ignored_args_num = len(
|
||||
[arg for arg in args
|
||||
if self.config.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
|
||||
|
||||
@check_messages('too-many-return-statements', 'too-many-branches',
|
||||
'too-many-arguments', 'too-many-locals',
|
||||
'too-many-statements')
|
||||
def leave_function(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))
|
||||
|
||||
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
|
||||
|
||||
def visit_if(self, node):
|
||||
"""increments the branches counter"""
|
||||
branches = 1
|
||||
# don't double count If nodes coming from some 'elif'
|
||||
if node.orelse and (len(node.orelse) > 1 or
|
||||
not isinstance(node.orelse[0], If)):
|
||||
branches += 1
|
||||
self._inc_branch(node, branches)
|
||||
self._stmts += branches
|
||||
|
||||
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,332 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""exceptions handling (raising, catching, exceptions classes) checker
|
||||
"""
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from astroid import YES, Instance, unpack_infer, List, Tuple
|
||||
from logilab.common.compat import builtins
|
||||
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
is_empty,
|
||||
is_raising,
|
||||
check_messages,
|
||||
inherit_from_std_ex,
|
||||
EXCEPTIONS_MODULE,
|
||||
has_known_bases,
|
||||
safe_infer)
|
||||
from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE
|
||||
|
||||
|
||||
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, (List, Tuple)):
|
||||
for elt in stmt.elts:
|
||||
inferred = safe_infer(elt)
|
||||
if inferred and inferred is not YES:
|
||||
yield elt, inferred
|
||||
return
|
||||
for infered in stmt.infer(context):
|
||||
if infered is 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 catched 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)}),
|
||||
'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 BaseException: %s',
|
||||
'catching-non-exception',
|
||||
'Used when a class which doesn\'t inherit from \
|
||||
BaseException 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.'),
|
||||
'W0704': ('Except doesn\'t do anything',
|
||||
'pointless-except',
|
||||
'Used when an except clause does nothing but "pass" and there is\
|
||||
no "else" clause.'),
|
||||
'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):"'),
|
||||
}
|
||||
|
||||
|
||||
class ExceptionsChecker(BaseChecker):
|
||||
"""checks for
|
||||
* excepts without exception filter
|
||||
* type of raise argument : string, Exceptions, other values
|
||||
"""
|
||||
|
||||
__implements__ = 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),)}
|
||||
),
|
||||
)
|
||||
|
||||
@check_messages('nonstandard-exception',
|
||||
'raising-bad-type', 'raising-non-exception',
|
||||
'notimplemented-raised', 'bad-exception-context')
|
||||
def visit_raise(self, node):
|
||||
"""visit raise possibly inferring value"""
|
||||
# ignore empty raise
|
||||
if node.exc is None:
|
||||
return
|
||||
if PY3K and node.cause:
|
||||
self._check_bad_exception_context(node)
|
||||
|
||||
expr = node.exc
|
||||
if self._check_raise_value(node, expr):
|
||||
return
|
||||
else:
|
||||
try:
|
||||
value = next(unpack_infer(expr))
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
self._check_raise_value(node, value)
|
||||
|
||||
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 = safe_infer(node.cause)
|
||||
if cause in (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.Class) and
|
||||
not inherit_from_std_ex(cause)):
|
||||
self.add_message('bad-exception-context',
|
||||
node=node)
|
||||
|
||||
def _check_raise_value(self, node, expr):
|
||||
"""check for bad values, string exception and class inheritance
|
||||
"""
|
||||
value_found = True
|
||||
if isinstance(expr, astroid.Const):
|
||||
value = expr.value
|
||||
if not isinstance(value, str):
|
||||
# raising-string will be emitted from python3 porting checker.
|
||||
self.add_message('raising-bad-type', node=node,
|
||||
args=value.__class__.__name__)
|
||||
elif ((isinstance(expr, astroid.Name) and
|
||||
expr.name in ('None', 'True', 'False')) or
|
||||
isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple,
|
||||
astroid.Module, astroid.Function))):
|
||||
emit = True
|
||||
if not PY3K and isinstance(expr, astroid.Tuple):
|
||||
# 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 = expr.elts[0]
|
||||
inferred = safe_infer(first)
|
||||
if isinstance(inferred, Instance):
|
||||
# pylint: disable=protected-access
|
||||
inferred = inferred._proxied
|
||||
if (inferred is YES or
|
||||
isinstance(inferred, astroid.Class)
|
||||
and inherit_from_std_ex(inferred)):
|
||||
emit = False
|
||||
if emit:
|
||||
self.add_message('raising-bad-type',
|
||||
node=node,
|
||||
args=expr.name)
|
||||
elif ((isinstance(expr, astroid.Name) and expr.name == 'NotImplemented')
|
||||
or (isinstance(expr, astroid.CallFunc) and
|
||||
isinstance(expr.func, astroid.Name) and
|
||||
expr.func.name == 'NotImplemented')):
|
||||
self.add_message('notimplemented-raised', node=node)
|
||||
elif isinstance(expr, (Instance, astroid.Class)):
|
||||
if isinstance(expr, Instance):
|
||||
# pylint: disable=protected-access
|
||||
expr = expr._proxied
|
||||
if (isinstance(expr, astroid.Class) and
|
||||
not inherit_from_std_ex(expr)):
|
||||
if expr.newstyle:
|
||||
self.add_message('raising-non-exception', node=node)
|
||||
else:
|
||||
if has_known_bases(expr):
|
||||
confidence = INFERENCE
|
||||
else:
|
||||
confidence = INFERENCE_FAILURE
|
||||
self.add_message(
|
||||
'nonstandard-exception', node=node,
|
||||
confidence=confidence)
|
||||
else:
|
||||
value_found = False
|
||||
else:
|
||||
value_found = False
|
||||
return value_found
|
||||
|
||||
def _check_catching_non_exception(self, handler, exc, part):
|
||||
if isinstance(exc, astroid.Tuple):
|
||||
# Check if it is a tuple of exceptions.
|
||||
inferred = [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 inherit_from_std_ex(node)
|
||||
for node in inferred):
|
||||
return
|
||||
|
||||
if not isinstance(exc, astroid.Class):
|
||||
# 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 inherit_from_std_ex(exc) and
|
||||
exc.root().name != BUILTINS_NAME):
|
||||
if has_known_bases(exc):
|
||||
self.add_message('catching-non-exception',
|
||||
node=handler.type,
|
||||
args=(exc.name, ))
|
||||
|
||||
@check_messages('bare-except', 'broad-except', 'pointless-except',
|
||||
'binary-op-exception', 'bad-except-order',
|
||||
'catching-non-exception')
|
||||
def visit_tryexcept(self, node):
|
||||
"""check for empty except"""
|
||||
exceptions_classes = []
|
||||
nb_handlers = len(node.handlers)
|
||||
for index, handler in enumerate(node.handlers):
|
||||
# single except doing nothing but "pass" without else clause
|
||||
if is_empty(handler.body) and not node.orelse:
|
||||
self.add_message('pointless-except',
|
||||
node=handler.type or handler.body[0])
|
||||
if handler.type is None:
|
||||
if not 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 YES:
|
||||
continue
|
||||
if (isinstance(exc, astroid.Instance)
|
||||
and 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.Class):
|
||||
continue
|
||||
|
||||
exc_ancestors = [anc for anc in exc.ancestors()
|
||||
if isinstance(anc, astroid.Class)]
|
||||
for previous_exc in exceptions_classes:
|
||||
if previous_exc in exc_ancestors:
|
||||
msg = '%s is an ancestor class of %s' % (
|
||||
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 == EXCEPTIONS_MODULE
|
||||
and not is_raising(handler.body)):
|
||||
self.add_message('broad-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))
|
||||
968
plugins/bundle/python-mode/pymode/libs/pylint/checkers/format.py
Normal file
968
plugins/bundle/python-mode/pymode/libs/pylint/checkers/format.py
Normal file
|
|
@ -0,0 +1,968 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Python code format's checker.
|
||||
|
||||
By default try to follow Guido's style guide :
|
||||
|
||||
http://www.python.org/doc/essays/styleguide.html
|
||||
|
||||
Some parts of the process_token method is based from The Tab Nanny std module.
|
||||
"""
|
||||
|
||||
import keyword
|
||||
import sys
|
||||
import tokenize
|
||||
from functools import reduce # pylint: disable=redefined-builtin
|
||||
|
||||
import six
|
||||
from six.moves import zip, map, filter # pylint: disable=redefined-builtin
|
||||
|
||||
from astroid import nodes
|
||||
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
from pylint.utils import WarningScope, OPTION_RGX
|
||||
|
||||
_CONTINUATION_BLOCK_OPENERS = ['elif', 'except', 'for', 'if', 'while', 'def', 'class']
|
||||
_KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not',
|
||||
'raise', 'return', 'while', 'yield']
|
||||
if sys.version_info < (3, 0):
|
||||
_KEYWORD_TOKENS.append('print')
|
||||
|
||||
_SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=',
|
||||
'+=', '-=', '*=', '**=', '/=', '//=', '&=', '|=', '^=',
|
||||
'%=', '>>=', '<<=']
|
||||
_OPENING_BRACKETS = ['(', '[', '{']
|
||||
_CLOSING_BRACKETS = [')', ']', '}']
|
||||
_TAB_LENGTH = 8
|
||||
|
||||
_EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT])
|
||||
_JUNK_TOKENS = (tokenize.COMMENT, tokenize.NL)
|
||||
|
||||
# Whitespace checking policy constants
|
||||
_MUST = 0
|
||||
_MUST_NOT = 1
|
||||
_IGNORE = 2
|
||||
|
||||
# Whitespace checking config constants
|
||||
_DICT_SEPARATOR = 'dict-separator'
|
||||
_TRAILING_COMMA = 'trailing-comma'
|
||||
_NO_SPACE_CHECK_CHOICES = [_TRAILING_COMMA, _DICT_SEPARATOR]
|
||||
|
||||
MSGS = {
|
||||
'C0301': ('Line too long (%s/%s)',
|
||||
'line-too-long',
|
||||
'Used when a line is longer than a given number of characters.'),
|
||||
'C0302': ('Too many lines in module (%s/%s)', # was W0302
|
||||
'too-many-lines',
|
||||
'Used when a module has too much lines, reducing its readability.'
|
||||
),
|
||||
'C0303': ('Trailing whitespace',
|
||||
'trailing-whitespace',
|
||||
'Used when there is whitespace between the end of a line and the '
|
||||
'newline.'),
|
||||
'C0304': ('Final newline missing',
|
||||
'missing-final-newline',
|
||||
'Used when the last line in a file is missing a newline.'),
|
||||
'W0311': ('Bad indentation. Found %s %s, expected %s',
|
||||
'bad-indentation',
|
||||
'Used when an unexpected number of indentation\'s tabulations or '
|
||||
'spaces has been found.'),
|
||||
'C0330': ('Wrong %s indentation%s.\n%s%s',
|
||||
'bad-continuation',
|
||||
'TODO'),
|
||||
'W0312': ('Found indentation with %ss instead of %ss',
|
||||
'mixed-indentation',
|
||||
'Used when there are some mixed tabs and spaces in a module.'),
|
||||
'W0301': ('Unnecessary semicolon', # was W0106
|
||||
'unnecessary-semicolon',
|
||||
'Used when a statement is ended by a semi-colon (";"), which \
|
||||
isn\'t necessary (that\'s python, not C ;).'),
|
||||
'C0321': ('More than one statement on a single line',
|
||||
'multiple-statements',
|
||||
'Used when more than on statement are found on the same line.',
|
||||
{'scope': WarningScope.NODE}),
|
||||
'C0325' : ('Unnecessary parens after %r keyword',
|
||||
'superfluous-parens',
|
||||
'Used when a single item in parentheses follows an if, for, or '
|
||||
'other keyword.'),
|
||||
'C0326': ('%s space %s %s %s\n%s',
|
||||
'bad-whitespace',
|
||||
('Used when a wrong number of spaces is used around an operator, '
|
||||
'bracket or block opener.'),
|
||||
{'old_names': [('C0323', 'no-space-after-operator'),
|
||||
('C0324', 'no-space-after-comma'),
|
||||
('C0322', 'no-space-before-operator')]}),
|
||||
'W0332': ('Use of "l" as long integer identifier',
|
||||
'lowercase-l-suffix',
|
||||
'Used when a lower case "l" is used to mark a long integer. You '
|
||||
'should use a upper case "L" since the letter "l" looks too much '
|
||||
'like the digit "1"',
|
||||
{'maxversion': (3, 0)}),
|
||||
'C0327': ('Mixed line endings LF and CRLF',
|
||||
'mixed-line-endings',
|
||||
'Used when there are mixed (LF and CRLF) newline signs in a file.'),
|
||||
'C0328': ('Unexpected line ending format. There is \'%s\' while it should be \'%s\'.',
|
||||
'unexpected-line-ending-format',
|
||||
'Used when there is different newline than expected.'),
|
||||
}
|
||||
|
||||
|
||||
def _underline_token(token):
|
||||
length = token[3][1] - token[2][1]
|
||||
offset = token[2][1]
|
||||
return token[4] + (' ' * offset) + ('^' * length)
|
||||
|
||||
|
||||
def _column_distance(token1, token2):
|
||||
if token1 == token2:
|
||||
return 0
|
||||
if token2[3] < token1[3]:
|
||||
token1, token2 = token2, token1
|
||||
if token1[3][0] != token2[2][0]:
|
||||
return None
|
||||
return token2[2][1] - token1[3][1]
|
||||
|
||||
|
||||
def _last_token_on_line_is(tokens, line_end, token):
|
||||
return (line_end > 0 and tokens.token(line_end-1) == token or
|
||||
line_end > 1 and tokens.token(line_end-2) == token
|
||||
and tokens.type(line_end-1) == tokenize.COMMENT)
|
||||
|
||||
|
||||
def _token_followed_by_eol(tokens, position):
|
||||
return (tokens.type(position+1) == tokenize.NL or
|
||||
tokens.type(position+1) == tokenize.COMMENT and
|
||||
tokens.type(position+2) == tokenize.NL)
|
||||
|
||||
|
||||
def _get_indent_length(line):
|
||||
"""Return the length of the indentation on the given token's line."""
|
||||
result = 0
|
||||
for char in line:
|
||||
if char == ' ':
|
||||
result += 1
|
||||
elif char == '\t':
|
||||
result += _TAB_LENGTH
|
||||
else:
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def _get_indent_hint_line(bar_positions, bad_position):
|
||||
"""Return a line with |s for each of the positions in the given lists."""
|
||||
if not bar_positions:
|
||||
return ''
|
||||
markers = [(pos, '|') for pos in bar_positions]
|
||||
markers.append((bad_position, '^'))
|
||||
markers.sort()
|
||||
line = [' '] * (markers[-1][0] + 1)
|
||||
for position, marker in markers:
|
||||
line[position] = marker
|
||||
return ''.join(line)
|
||||
|
||||
|
||||
class _ContinuedIndent(object):
|
||||
__slots__ = ('valid_outdent_offsets',
|
||||
'valid_continuation_offsets',
|
||||
'context_type',
|
||||
'token',
|
||||
'position')
|
||||
|
||||
def __init__(self,
|
||||
context_type,
|
||||
token,
|
||||
position,
|
||||
valid_outdent_offsets,
|
||||
valid_continuation_offsets):
|
||||
self.valid_outdent_offsets = valid_outdent_offsets
|
||||
self.valid_continuation_offsets = valid_continuation_offsets
|
||||
self.context_type = context_type
|
||||
self.position = position
|
||||
self.token = token
|
||||
|
||||
|
||||
# The contexts for hanging indents.
|
||||
# A hanging indented dictionary value after :
|
||||
HANGING_DICT_VALUE = 'dict-value'
|
||||
# Hanging indentation in an expression.
|
||||
HANGING = 'hanging'
|
||||
# Hanging indentation in a block header.
|
||||
HANGING_BLOCK = 'hanging-block'
|
||||
# Continued indentation inside an expression.
|
||||
CONTINUED = 'continued'
|
||||
# Continued indentation in a block header.
|
||||
CONTINUED_BLOCK = 'continued-block'
|
||||
|
||||
SINGLE_LINE = 'single'
|
||||
WITH_BODY = 'multi'
|
||||
|
||||
_CONTINUATION_MSG_PARTS = {
|
||||
HANGING_DICT_VALUE: ('hanging', ' in dict value'),
|
||||
HANGING: ('hanging', ''),
|
||||
HANGING_BLOCK: ('hanging', ' before block'),
|
||||
CONTINUED: ('continued', ''),
|
||||
CONTINUED_BLOCK: ('continued', ' before block'),
|
||||
}
|
||||
|
||||
|
||||
def _Offsets(*args):
|
||||
"""Valid indentation offsets for a continued line."""
|
||||
return dict((a, None) for a in args)
|
||||
|
||||
|
||||
def _BeforeBlockOffsets(single, with_body):
|
||||
"""Valid alternative indent offsets for continued lines before blocks.
|
||||
|
||||
:param single: Valid offset for statements on a single logical line.
|
||||
:param with_body: Valid offset for statements on several lines.
|
||||
"""
|
||||
return {single: SINGLE_LINE, with_body: WITH_BODY}
|
||||
|
||||
|
||||
class TokenWrapper(object):
|
||||
"""A wrapper for readable access to token information."""
|
||||
|
||||
def __init__(self, tokens):
|
||||
self._tokens = tokens
|
||||
|
||||
def token(self, idx):
|
||||
return self._tokens[idx][1]
|
||||
|
||||
def type(self, idx):
|
||||
return self._tokens[idx][0]
|
||||
|
||||
def start_line(self, idx):
|
||||
return self._tokens[idx][2][0]
|
||||
|
||||
def start_col(self, idx):
|
||||
return self._tokens[idx][2][1]
|
||||
|
||||
def line(self, idx):
|
||||
return self._tokens[idx][4]
|
||||
|
||||
|
||||
class ContinuedLineState(object):
|
||||
"""Tracker for continued indentation inside a logical line."""
|
||||
|
||||
def __init__(self, tokens, config):
|
||||
self._line_start = -1
|
||||
self._cont_stack = []
|
||||
self._is_block_opener = False
|
||||
self.retained_warnings = []
|
||||
self._config = config
|
||||
self._tokens = TokenWrapper(tokens)
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
return bool(self._cont_stack)
|
||||
|
||||
@property
|
||||
def _block_indent_size(self):
|
||||
return len(self._config.indent_string.replace('\t', ' ' * _TAB_LENGTH))
|
||||
|
||||
@property
|
||||
def _continuation_size(self):
|
||||
return self._config.indent_after_paren
|
||||
|
||||
def handle_line_start(self, pos):
|
||||
"""Record the first non-junk token at the start of a line."""
|
||||
if self._line_start > -1:
|
||||
return
|
||||
self._is_block_opener = self._tokens.token(pos) in _CONTINUATION_BLOCK_OPENERS
|
||||
self._line_start = pos
|
||||
|
||||
def next_physical_line(self):
|
||||
"""Prepares the tracker for a new physical line (NL)."""
|
||||
self._line_start = -1
|
||||
self._is_block_opener = False
|
||||
|
||||
def next_logical_line(self):
|
||||
"""Prepares the tracker for a new logical line (NEWLINE).
|
||||
|
||||
A new logical line only starts with block indentation.
|
||||
"""
|
||||
self.next_physical_line()
|
||||
self.retained_warnings = []
|
||||
self._cont_stack = []
|
||||
|
||||
def add_block_warning(self, token_position, state, valid_offsets):
|
||||
self.retained_warnings.append((token_position, state, valid_offsets))
|
||||
|
||||
def get_valid_offsets(self, idx):
|
||||
"""Returns the valid offsets for the token at the given position."""
|
||||
# The closing brace on a dict or the 'for' in a dict comprehension may
|
||||
# reset two indent levels because the dict value is ended implicitly
|
||||
stack_top = -1
|
||||
if self._tokens.token(idx) in ('}', 'for') and self._cont_stack[-1].token == ':':
|
||||
stack_top = -2
|
||||
indent = self._cont_stack[stack_top]
|
||||
if self._tokens.token(idx) in _CLOSING_BRACKETS:
|
||||
valid_offsets = indent.valid_outdent_offsets
|
||||
else:
|
||||
valid_offsets = indent.valid_continuation_offsets
|
||||
return indent, valid_offsets.copy()
|
||||
|
||||
def _hanging_indent_after_bracket(self, bracket, position):
|
||||
"""Extracts indentation information for a hanging indent."""
|
||||
indentation = _get_indent_length(self._tokens.line(position))
|
||||
if self._is_block_opener and self._continuation_size == self._block_indent_size:
|
||||
return _ContinuedIndent(
|
||||
HANGING_BLOCK,
|
||||
bracket,
|
||||
position,
|
||||
_Offsets(indentation + self._continuation_size, indentation),
|
||||
_BeforeBlockOffsets(indentation + self._continuation_size,
|
||||
indentation + self._continuation_size * 2))
|
||||
elif bracket == ':':
|
||||
# If the dict key was on the same line as the open brace, the new
|
||||
# correct indent should be relative to the key instead of the
|
||||
# current indent level
|
||||
paren_align = self._cont_stack[-1].valid_outdent_offsets
|
||||
next_align = self._cont_stack[-1].valid_continuation_offsets.copy()
|
||||
next_align_keys = list(next_align.keys())
|
||||
next_align[next_align_keys[0] + self._continuation_size] = True
|
||||
# Note that the continuation of
|
||||
# d = {
|
||||
# 'a': 'b'
|
||||
# 'c'
|
||||
# }
|
||||
# is handled by the special-casing for hanging continued string indents.
|
||||
return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align)
|
||||
else:
|
||||
return _ContinuedIndent(
|
||||
HANGING,
|
||||
bracket,
|
||||
position,
|
||||
_Offsets(indentation, indentation + self._continuation_size),
|
||||
_Offsets(indentation + self._continuation_size))
|
||||
|
||||
def _continuation_inside_bracket(self, bracket, pos):
|
||||
"""Extracts indentation information for a continued indent."""
|
||||
indentation = _get_indent_length(self._tokens.line(pos))
|
||||
token_start = self._tokens.start_col(pos)
|
||||
next_token_start = self._tokens.start_col(pos + 1)
|
||||
if self._is_block_opener and next_token_start - indentation == self._block_indent_size:
|
||||
return _ContinuedIndent(
|
||||
CONTINUED_BLOCK,
|
||||
bracket,
|
||||
pos,
|
||||
_Offsets(token_start),
|
||||
_BeforeBlockOffsets(next_token_start, next_token_start + self._continuation_size))
|
||||
else:
|
||||
return _ContinuedIndent(
|
||||
CONTINUED,
|
||||
bracket,
|
||||
pos,
|
||||
_Offsets(token_start),
|
||||
_Offsets(next_token_start))
|
||||
|
||||
def pop_token(self):
|
||||
self._cont_stack.pop()
|
||||
|
||||
def push_token(self, token, position):
|
||||
"""Pushes a new token for continued indentation on the stack.
|
||||
|
||||
Tokens that can modify continued indentation offsets are:
|
||||
* opening brackets
|
||||
* 'lambda'
|
||||
* : inside dictionaries
|
||||
|
||||
push_token relies on the caller to filter out those
|
||||
interesting tokens.
|
||||
|
||||
:param token: The concrete token
|
||||
:param position: The position of the token in the stream.
|
||||
"""
|
||||
if _token_followed_by_eol(self._tokens, position):
|
||||
self._cont_stack.append(
|
||||
self._hanging_indent_after_bracket(token, position))
|
||||
else:
|
||||
self._cont_stack.append(
|
||||
self._continuation_inside_bracket(token, position))
|
||||
|
||||
|
||||
class FormatChecker(BaseTokenChecker):
|
||||
"""checks for :
|
||||
* unauthorized constructions
|
||||
* strict indentation
|
||||
* line length
|
||||
"""
|
||||
|
||||
__implements__ = (ITokenChecker, IAstroidChecker, IRawChecker)
|
||||
|
||||
# configuration section name
|
||||
name = 'format'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
# configuration options
|
||||
# for available dict keys/values see the optik parser 'add_option' method
|
||||
options = (('max-line-length',
|
||||
{'default' : 100, 'type' : "int", 'metavar' : '<int>',
|
||||
'help' : 'Maximum number of characters on a single line.'}),
|
||||
('ignore-long-lines',
|
||||
{'type': 'regexp', 'metavar': '<regexp>',
|
||||
'default': r'^\s*(# )?<?https?://\S+>?$',
|
||||
'help': ('Regexp for a line that is allowed to be longer than '
|
||||
'the limit.')}),
|
||||
('single-line-if-stmt',
|
||||
{'default': False, 'type' : 'yn', 'metavar' : '<y_or_n>',
|
||||
'help' : ('Allow the body of an if to be on the same '
|
||||
'line as the test if there is no else.')}),
|
||||
('no-space-check',
|
||||
{'default': ','.join(_NO_SPACE_CHECK_CHOICES),
|
||||
'type': 'multiple_choice',
|
||||
'choices': _NO_SPACE_CHECK_CHOICES,
|
||||
'help': ('List of optional constructs for which whitespace '
|
||||
'checking is disabled')}),
|
||||
('max-module-lines',
|
||||
{'default' : 1000, 'type' : 'int', 'metavar' : '<int>',
|
||||
'help': 'Maximum number of lines in a module'}
|
||||
),
|
||||
('indent-string',
|
||||
{'default' : ' ', 'type' : "string", 'metavar' : '<string>',
|
||||
'help' : 'String used as indentation unit. This is usually '
|
||||
'" " (4 spaces) or "\\t" (1 tab).'}),
|
||||
('indent-after-paren',
|
||||
{'type': 'int', 'metavar': '<int>', 'default': 4,
|
||||
'help': 'Number of spaces of indent required inside a hanging '
|
||||
' or continued line.'}),
|
||||
('expected-line-ending-format',
|
||||
{'type': 'choice', 'metavar': '<empty or LF or CRLF>', 'default': '',
|
||||
'choices': ['', 'LF', 'CRLF'],
|
||||
'help': ('Expected format of line ending, '
|
||||
'e.g. empty (any line ending), LF or CRLF.')}),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseTokenChecker.__init__(self, linter)
|
||||
self._lines = None
|
||||
self._visited_lines = None
|
||||
self._bracket_stack = [None]
|
||||
|
||||
def _pop_token(self):
|
||||
self._bracket_stack.pop()
|
||||
self._current_line.pop_token()
|
||||
|
||||
def _push_token(self, token, idx):
|
||||
self._bracket_stack.append(token)
|
||||
self._current_line.push_token(token, idx)
|
||||
|
||||
def new_line(self, tokens, line_end, line_start):
|
||||
"""a new line has been encountered, process it if necessary"""
|
||||
if _last_token_on_line_is(tokens, line_end, ';'):
|
||||
self.add_message('unnecessary-semicolon', line=tokens.start_line(line_end))
|
||||
|
||||
line_num = tokens.start_line(line_start)
|
||||
line = tokens.line(line_start)
|
||||
if tokens.type(line_start) not in _JUNK_TOKENS:
|
||||
self._lines[line_num] = line.split('\n')[0]
|
||||
self.check_lines(line, line_num)
|
||||
|
||||
def process_module(self, module):
|
||||
self._keywords_with_parens = set()
|
||||
if 'print_function' in module.future_imports:
|
||||
self._keywords_with_parens.add('print')
|
||||
|
||||
def _check_keyword_parentheses(self, tokens, start):
|
||||
"""Check that there are not unnecessary parens after a keyword.
|
||||
|
||||
Parens are unnecessary if there is exactly one balanced outer pair on a
|
||||
line, and it is followed by a colon, and contains no commas (i.e. is not a
|
||||
tuple).
|
||||
|
||||
Args:
|
||||
tokens: list of Tokens; the entire list of Tokens.
|
||||
start: int; the position of the keyword in the token list.
|
||||
"""
|
||||
# If the next token is not a paren, we're fine.
|
||||
if self._inside_brackets(':') and tokens[start][1] == 'for':
|
||||
self._pop_token()
|
||||
if tokens[start+1][1] != '(':
|
||||
return
|
||||
|
||||
found_and_or = False
|
||||
depth = 0
|
||||
keyword_token = tokens[start][1]
|
||||
line_num = tokens[start][2][0]
|
||||
|
||||
for i in range(start, len(tokens) - 1):
|
||||
token = tokens[i]
|
||||
|
||||
# If we hit a newline, then assume any parens were for continuation.
|
||||
if token[0] == tokenize.NL:
|
||||
return
|
||||
|
||||
if token[1] == '(':
|
||||
depth += 1
|
||||
elif token[1] == ')':
|
||||
depth -= 1
|
||||
if not depth:
|
||||
# ')' can't happen after if (foo), since it would be a syntax error.
|
||||
if (tokens[i+1][1] in (':', ')', ']', '}', 'in') or
|
||||
tokens[i+1][0] in (tokenize.NEWLINE,
|
||||
tokenize.ENDMARKER,
|
||||
tokenize.COMMENT)):
|
||||
# The empty tuple () is always accepted.
|
||||
if i == start + 2:
|
||||
return
|
||||
if keyword_token == 'not':
|
||||
if not found_and_or:
|
||||
self.add_message('superfluous-parens', line=line_num,
|
||||
args=keyword_token)
|
||||
elif keyword_token in ('return', 'yield'):
|
||||
self.add_message('superfluous-parens', line=line_num,
|
||||
args=keyword_token)
|
||||
elif keyword_token not in self._keywords_with_parens:
|
||||
if not (tokens[i+1][1] == 'in' and found_and_or):
|
||||
self.add_message('superfluous-parens', line=line_num,
|
||||
args=keyword_token)
|
||||
return
|
||||
elif depth == 1:
|
||||
# This is a tuple, which is always acceptable.
|
||||
if token[1] == ',':
|
||||
return
|
||||
# 'and' and 'or' are the only boolean operators with lower precedence
|
||||
# than 'not', so parens are only required when they are found.
|
||||
elif token[1] in ('and', 'or'):
|
||||
found_and_or = True
|
||||
# A yield inside an expression must always be in parentheses,
|
||||
# quit early without error.
|
||||
elif token[1] == 'yield':
|
||||
return
|
||||
# A generator expression always has a 'for' token in it, and
|
||||
# the 'for' token is only legal inside parens when it is in a
|
||||
# generator expression. The parens are necessary here, so bail
|
||||
# without an error.
|
||||
elif token[1] == 'for':
|
||||
return
|
||||
|
||||
def _opening_bracket(self, tokens, i):
|
||||
self._push_token(tokens[i][1], i)
|
||||
# Special case: ignore slices
|
||||
if tokens[i][1] == '[' and tokens[i+1][1] == ':':
|
||||
return
|
||||
|
||||
if (i > 0 and (tokens[i-1][0] == tokenize.NAME and
|
||||
not (keyword.iskeyword(tokens[i-1][1]))
|
||||
or tokens[i-1][1] in _CLOSING_BRACKETS)):
|
||||
self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT))
|
||||
else:
|
||||
self._check_space(tokens, i, (_IGNORE, _MUST_NOT))
|
||||
|
||||
def _closing_bracket(self, tokens, i):
|
||||
if self._inside_brackets(':'):
|
||||
self._pop_token()
|
||||
self._pop_token()
|
||||
# Special case: ignore slices
|
||||
if tokens[i-1][1] == ':' and tokens[i][1] == ']':
|
||||
return
|
||||
policy_before = _MUST_NOT
|
||||
if tokens[i][1] in _CLOSING_BRACKETS and tokens[i-1][1] == ',':
|
||||
if _TRAILING_COMMA in self.config.no_space_check:
|
||||
policy_before = _IGNORE
|
||||
|
||||
self._check_space(tokens, i, (policy_before, _IGNORE))
|
||||
|
||||
def _check_equals_spacing(self, tokens, i):
|
||||
"""Check the spacing of a single equals sign."""
|
||||
if self._inside_brackets('(') or self._inside_brackets('lambda'):
|
||||
self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT))
|
||||
else:
|
||||
self._check_space(tokens, i, (_MUST, _MUST))
|
||||
|
||||
def _open_lambda(self, tokens, i): # pylint:disable=unused-argument
|
||||
self._push_token('lambda', i)
|
||||
|
||||
def _handle_colon(self, tokens, i):
|
||||
# Special case: ignore slices
|
||||
if self._inside_brackets('['):
|
||||
return
|
||||
if (self._inside_brackets('{') and
|
||||
_DICT_SEPARATOR in self.config.no_space_check):
|
||||
policy = (_IGNORE, _IGNORE)
|
||||
else:
|
||||
policy = (_MUST_NOT, _MUST)
|
||||
self._check_space(tokens, i, policy)
|
||||
|
||||
if self._inside_brackets('lambda'):
|
||||
self._pop_token()
|
||||
elif self._inside_brackets('{'):
|
||||
self._push_token(':', i)
|
||||
|
||||
def _handle_comma(self, tokens, i):
|
||||
# Only require a following whitespace if this is
|
||||
# not a hanging comma before a closing bracket.
|
||||
if tokens[i+1][1] in _CLOSING_BRACKETS:
|
||||
self._check_space(tokens, i, (_MUST_NOT, _IGNORE))
|
||||
else:
|
||||
self._check_space(tokens, i, (_MUST_NOT, _MUST))
|
||||
if self._inside_brackets(':'):
|
||||
self._pop_token()
|
||||
|
||||
def _check_surrounded_by_space(self, tokens, i):
|
||||
"""Check that a binary operator is surrounded by exactly one space."""
|
||||
self._check_space(tokens, i, (_MUST, _MUST))
|
||||
|
||||
def _check_space(self, tokens, i, policies):
|
||||
def _policy_string(policy):
|
||||
if policy == _MUST:
|
||||
return 'Exactly one', 'required'
|
||||
else:
|
||||
return 'No', 'allowed'
|
||||
|
||||
def _name_construct(token):
|
||||
if token[1] == ',':
|
||||
return 'comma'
|
||||
elif token[1] == ':':
|
||||
return ':'
|
||||
elif token[1] in '()[]{}':
|
||||
return 'bracket'
|
||||
elif token[1] in ('<', '>', '<=', '>=', '!=', '=='):
|
||||
return 'comparison'
|
||||
else:
|
||||
if self._inside_brackets('('):
|
||||
return 'keyword argument assignment'
|
||||
else:
|
||||
return 'assignment'
|
||||
|
||||
good_space = [True, True]
|
||||
token = tokens[i]
|
||||
pairs = [(tokens[i-1], token), (token, tokens[i+1])]
|
||||
|
||||
for other_idx, (policy, token_pair) in enumerate(zip(policies, pairs)):
|
||||
if token_pair[other_idx][0] in _EOL or policy == _IGNORE:
|
||||
continue
|
||||
|
||||
distance = _column_distance(*token_pair)
|
||||
if distance is None:
|
||||
continue
|
||||
good_space[other_idx] = (
|
||||
(policy == _MUST and distance == 1) or
|
||||
(policy == _MUST_NOT and distance == 0))
|
||||
|
||||
warnings = []
|
||||
if not any(good_space) and policies[0] == policies[1]:
|
||||
warnings.append((policies[0], 'around'))
|
||||
else:
|
||||
for ok, policy, position in zip(good_space, policies, ('before', 'after')):
|
||||
if not ok:
|
||||
warnings.append((policy, position))
|
||||
for policy, position in warnings:
|
||||
construct = _name_construct(token)
|
||||
count, state = _policy_string(policy)
|
||||
self.add_message('bad-whitespace', line=token[2][0],
|
||||
args=(count, state, position, construct,
|
||||
_underline_token(token)))
|
||||
|
||||
def _inside_brackets(self, left):
|
||||
return self._bracket_stack[-1] == left
|
||||
|
||||
def _prepare_token_dispatcher(self):
|
||||
raw = [
|
||||
(_KEYWORD_TOKENS,
|
||||
self._check_keyword_parentheses),
|
||||
|
||||
(_OPENING_BRACKETS, self._opening_bracket),
|
||||
|
||||
(_CLOSING_BRACKETS, self._closing_bracket),
|
||||
|
||||
(['='], self._check_equals_spacing),
|
||||
|
||||
(_SPACED_OPERATORS, self._check_surrounded_by_space),
|
||||
|
||||
([','], self._handle_comma),
|
||||
|
||||
([':'], self._handle_colon),
|
||||
|
||||
(['lambda'], self._open_lambda),
|
||||
|
||||
]
|
||||
|
||||
dispatch = {}
|
||||
for tokens, handler in raw:
|
||||
for token in tokens:
|
||||
dispatch[token] = handler
|
||||
return dispatch
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
"""process tokens and search for :
|
||||
|
||||
_ non strict indentation (i.e. not always using the <indent> parameter as
|
||||
indent unit)
|
||||
_ too long lines (i.e. longer than <max_chars>)
|
||||
_ optionally bad construct (if given, bad_construct must be a compiled
|
||||
regular expression).
|
||||
"""
|
||||
self._bracket_stack = [None]
|
||||
indents = [0]
|
||||
check_equal = False
|
||||
line_num = 0
|
||||
self._lines = {}
|
||||
self._visited_lines = {}
|
||||
token_handlers = self._prepare_token_dispatcher()
|
||||
self._last_line_ending = None
|
||||
|
||||
self._current_line = ContinuedLineState(tokens, self.config)
|
||||
for idx, (tok_type, token, start, _, line) in enumerate(tokens):
|
||||
if start[0] != line_num:
|
||||
line_num = start[0]
|
||||
# A tokenizer oddity: if an indented line contains a multi-line
|
||||
# docstring, the line member of the INDENT token does not contain
|
||||
# the full line; therefore we check the next token on the line.
|
||||
if tok_type == tokenize.INDENT:
|
||||
self.new_line(TokenWrapper(tokens), idx-1, idx+1)
|
||||
else:
|
||||
self.new_line(TokenWrapper(tokens), idx-1, idx)
|
||||
|
||||
if tok_type == tokenize.NEWLINE:
|
||||
# a program statement, or ENDMARKER, will eventually follow,
|
||||
# after some (possibly empty) run of tokens of the form
|
||||
# (NL | COMMENT)* (INDENT | DEDENT+)?
|
||||
# If an INDENT appears, setting check_equal is wrong, and will
|
||||
# be undone when we see the INDENT.
|
||||
check_equal = True
|
||||
self._process_retained_warnings(TokenWrapper(tokens), idx)
|
||||
self._current_line.next_logical_line()
|
||||
self._check_line_ending(token, line_num)
|
||||
elif tok_type == tokenize.INDENT:
|
||||
check_equal = False
|
||||
self.check_indent_level(token, indents[-1]+1, line_num)
|
||||
indents.append(indents[-1]+1)
|
||||
elif tok_type == tokenize.DEDENT:
|
||||
# there's nothing we need to check here! what's important is
|
||||
# that when the run of DEDENTs ends, the indentation of the
|
||||
# program statement (or ENDMARKER) that triggered the run is
|
||||
# equal to what's left at the top of the indents stack
|
||||
check_equal = True
|
||||
if len(indents) > 1:
|
||||
del indents[-1]
|
||||
elif tok_type == tokenize.NL:
|
||||
self._check_continued_indentation(TokenWrapper(tokens), idx+1)
|
||||
self._current_line.next_physical_line()
|
||||
elif tok_type != tokenize.COMMENT:
|
||||
self._current_line.handle_line_start(idx)
|
||||
# This is the first concrete token following a NEWLINE, so it
|
||||
# must be the first token of the next program statement, or an
|
||||
# ENDMARKER; the "line" argument exposes the leading whitespace
|
||||
# for this statement; in the case of ENDMARKER, line is an empty
|
||||
# string, so will properly match the empty string with which the
|
||||
# "indents" stack was seeded
|
||||
if check_equal:
|
||||
check_equal = False
|
||||
self.check_indent_level(line, indents[-1], line_num)
|
||||
|
||||
if tok_type == tokenize.NUMBER and token.endswith('l'):
|
||||
self.add_message('lowercase-l-suffix', line=line_num)
|
||||
|
||||
try:
|
||||
handler = token_handlers[token]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
handler(tokens, idx)
|
||||
|
||||
line_num -= 1 # to be ok with "wc -l"
|
||||
if line_num > self.config.max_module_lines:
|
||||
# Get the line where the too-many-lines (or its message id)
|
||||
# was disabled or default to 1.
|
||||
symbol = self.linter.msgs_store.check_message_id('too-many-lines')
|
||||
names = (symbol.msgid, 'too-many-lines')
|
||||
line = next(filter(None,
|
||||
map(self.linter._pragma_lineno.get, names)), 1)
|
||||
self.add_message('too-many-lines',
|
||||
args=(line_num, self.config.max_module_lines),
|
||||
line=line)
|
||||
|
||||
def _check_line_ending(self, line_ending, line_num):
|
||||
# check if line endings are mixed
|
||||
if self._last_line_ending is not None:
|
||||
if line_ending != self._last_line_ending:
|
||||
self.add_message('mixed-line-endings', line=line_num)
|
||||
|
||||
self._last_line_ending = line_ending
|
||||
|
||||
# check if line ending is as expected
|
||||
expected = self.config.expected_line_ending_format
|
||||
if expected:
|
||||
# reduce multiple \n\n\n\n to one \n
|
||||
line_ending = reduce(lambda x, y: x + y if x != y else x, line_ending, "")
|
||||
line_ending = 'LF' if line_ending == '\n' else 'CRLF'
|
||||
if line_ending != expected:
|
||||
self.add_message('unexpected-line-ending-format', args=(line_ending, expected),
|
||||
line=line_num)
|
||||
|
||||
|
||||
def _process_retained_warnings(self, tokens, current_pos):
|
||||
single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':')
|
||||
|
||||
for indent_pos, state, offsets in self._current_line.retained_warnings:
|
||||
block_type = offsets[tokens.start_col(indent_pos)]
|
||||
hints = dict((k, v) for k, v in six.iteritems(offsets)
|
||||
if v != block_type)
|
||||
if single_line_block_stmt and block_type == WITH_BODY:
|
||||
self._add_continuation_message(state, hints, tokens, indent_pos)
|
||||
elif not single_line_block_stmt and block_type == SINGLE_LINE:
|
||||
self._add_continuation_message(state, hints, tokens, indent_pos)
|
||||
|
||||
def _check_continued_indentation(self, tokens, next_idx):
|
||||
def same_token_around_nl(token_type):
|
||||
return (tokens.type(next_idx) == token_type and
|
||||
tokens.type(next_idx-2) == token_type)
|
||||
|
||||
# Do not issue any warnings if the next line is empty.
|
||||
if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL:
|
||||
return
|
||||
|
||||
state, valid_offsets = self._current_line.get_valid_offsets(next_idx)
|
||||
# Special handling for hanging comments and strings. If the last line ended
|
||||
# with a comment (string) and the new line contains only a comment, the line
|
||||
# may also be indented to the start of the previous token.
|
||||
if same_token_around_nl(tokenize.COMMENT) or same_token_around_nl(tokenize.STRING):
|
||||
valid_offsets[tokens.start_col(next_idx-2)] = True
|
||||
|
||||
# We can only decide if the indentation of a continued line before opening
|
||||
# a new block is valid once we know of the body of the block is on the
|
||||
# same line as the block opener. Since the token processing is single-pass,
|
||||
# emitting those warnings is delayed until the block opener is processed.
|
||||
if (state.context_type in (HANGING_BLOCK, CONTINUED_BLOCK)
|
||||
and tokens.start_col(next_idx) in valid_offsets):
|
||||
self._current_line.add_block_warning(next_idx, state, valid_offsets)
|
||||
elif tokens.start_col(next_idx) not in valid_offsets:
|
||||
self._add_continuation_message(state, valid_offsets, tokens, next_idx)
|
||||
|
||||
def _add_continuation_message(self, state, offsets, tokens, position):
|
||||
readable_type, readable_position = _CONTINUATION_MSG_PARTS[state.context_type]
|
||||
hint_line = _get_indent_hint_line(offsets, tokens.start_col(position))
|
||||
self.add_message(
|
||||
'bad-continuation',
|
||||
line=tokens.start_line(position),
|
||||
args=(readable_type, readable_position, tokens.line(position), hint_line))
|
||||
|
||||
@check_messages('multiple-statements')
|
||||
def visit_default(self, node):
|
||||
"""check the node line number and check it if not yet done"""
|
||||
if not node.is_statement:
|
||||
return
|
||||
if not node.root().pure_python:
|
||||
return # XXX block visit of child nodes
|
||||
prev_sibl = node.previous_sibling()
|
||||
if prev_sibl is not None:
|
||||
prev_line = prev_sibl.fromlineno
|
||||
else:
|
||||
# The line on which a finally: occurs in a try/finally
|
||||
# is not directly represented in the AST. We infer it
|
||||
# by taking the last line of the body and adding 1, which
|
||||
# should be the line of finally:
|
||||
if (isinstance(node.parent, nodes.TryFinally)
|
||||
and node in node.parent.finalbody):
|
||||
prev_line = node.parent.body[0].tolineno + 1
|
||||
else:
|
||||
prev_line = node.parent.statement().fromlineno
|
||||
line = node.fromlineno
|
||||
assert line, node
|
||||
if prev_line == line and self._visited_lines.get(line) != 2:
|
||||
self._check_multi_statement_line(node, line)
|
||||
return
|
||||
if line in self._visited_lines:
|
||||
return
|
||||
try:
|
||||
tolineno = node.blockstart_tolineno
|
||||
except AttributeError:
|
||||
tolineno = node.tolineno
|
||||
assert tolineno, node
|
||||
lines = []
|
||||
for line in range(line, tolineno + 1):
|
||||
self._visited_lines[line] = 1
|
||||
try:
|
||||
lines.append(self._lines[line].rstrip())
|
||||
except KeyError:
|
||||
lines.append('')
|
||||
|
||||
def _check_multi_statement_line(self, node, line):
|
||||
"""Check for lines containing multiple statements."""
|
||||
# Do not warn about multiple nested context managers
|
||||
# in with statements.
|
||||
if isinstance(node, nodes.With):
|
||||
return
|
||||
# For try... except... finally..., the two nodes
|
||||
# appear to be on the same line due to how the AST is built.
|
||||
if (isinstance(node, nodes.TryExcept) and
|
||||
isinstance(node.parent, nodes.TryFinally)):
|
||||
return
|
||||
if (isinstance(node.parent, nodes.If) and not node.parent.orelse
|
||||
and self.config.single_line_if_stmt):
|
||||
return
|
||||
self.add_message('multiple-statements', node=node)
|
||||
self._visited_lines[line] = 2
|
||||
|
||||
def check_lines(self, lines, i):
|
||||
"""check lines have less than a maximum number of characters
|
||||
"""
|
||||
max_chars = self.config.max_line_length
|
||||
ignore_long_line = self.config.ignore_long_lines
|
||||
|
||||
for line in lines.splitlines(True):
|
||||
if not line.endswith('\n'):
|
||||
self.add_message('missing-final-newline', line=i)
|
||||
else:
|
||||
stripped_line = line.rstrip()
|
||||
if line[len(stripped_line):] not in ('\n', '\r\n'):
|
||||
self.add_message('trailing-whitespace', line=i)
|
||||
# Don't count excess whitespace in the line length.
|
||||
line = stripped_line
|
||||
mobj = OPTION_RGX.search(line)
|
||||
if mobj and mobj.group(1).split('=', 1)[0].strip() == 'disable':
|
||||
line = line.split('#')[0].rstrip()
|
||||
|
||||
if len(line) > max_chars and not ignore_long_line.search(line):
|
||||
self.add_message('line-too-long', line=i, args=(len(line), max_chars))
|
||||
i += 1
|
||||
|
||||
def check_indent_level(self, string, expected, line_num):
|
||||
"""return the indent level of the string
|
||||
"""
|
||||
indent = self.config.indent_string
|
||||
if indent == '\\t': # \t is not interpreted in the configuration file
|
||||
indent = '\t'
|
||||
level = 0
|
||||
unit_size = len(indent)
|
||||
while string[:unit_size] == indent:
|
||||
string = string[unit_size:]
|
||||
level += 1
|
||||
suppl = ''
|
||||
while string and string[0] in ' \t':
|
||||
if string[0] != indent[0]:
|
||||
if string[0] == '\t':
|
||||
args = ('tab', 'space')
|
||||
else:
|
||||
args = ('space', 'tab')
|
||||
self.add_message('mixed-indentation', args=args, line=line_num)
|
||||
return level
|
||||
suppl += string[0]
|
||||
string = string[1:]
|
||||
if level != expected or suppl:
|
||||
i_type = 'spaces'
|
||||
if indent[0] == '\t':
|
||||
i_type = 'tabs'
|
||||
self.add_message('bad-indentation', line=line_num,
|
||||
args=(level * unit_size + len(suppl), i_type,
|
||||
expected * unit_size))
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(FormatChecker(linter))
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""imports checkers for Python code"""
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import six
|
||||
from six.moves import map # pylint: disable=redefined-builtin
|
||||
|
||||
from logilab.common.graph import get_cycles, DotBackend
|
||||
from logilab.common.ureports import VerbatimText, Paragraph
|
||||
|
||||
import astroid
|
||||
from astroid import are_exclusive
|
||||
from astroid.modutils import get_module_part, is_standard_module
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.utils import EmptyReport
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import check_messages, is_import_error
|
||||
|
||||
def _except_import_error(node):
|
||||
"""
|
||||
Check if the try-except node has an ImportError handler.
|
||||
Return True if an ImportError handler was infered, False otherwise.
|
||||
"""
|
||||
if not isinstance(node, astroid.TryExcept):
|
||||
return
|
||||
return any(map(is_import_error, node.handlers))
|
||||
|
||||
def get_first_import(node, context, name, base, level):
|
||||
"""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.From):
|
||||
if level == first.level and any(
|
||||
fullname == '%s.%s' % (first.modname, iname[0])
|
||||
for iname in first.names):
|
||||
found = True
|
||||
break
|
||||
if found and not are_exclusive(first, node):
|
||||
return first
|
||||
|
||||
# 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(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 modname in dependencies:
|
||||
if modname not in done:
|
||||
done[modname] = 1
|
||||
printer.emit_node(modname)
|
||||
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 = {
|
||||
'F0401': ('Unable to import %s',
|
||||
'import-error',
|
||||
'Used when pylint has been unable to import a module.'),
|
||||
'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.',
|
||||
{'maxversion': (3, 0)}),
|
||||
}
|
||||
|
||||
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 sys.version_info < (3,):
|
||||
deprecated_modules = ('regsub', 'TERMIOS', 'Bastion', 'rexec')
|
||||
else:
|
||||
deprecated_modules = ('stringprep', 'optparse')
|
||||
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)'}
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, linter=None):
|
||||
BaseChecker.__init__(self, linter)
|
||||
self.stats = None
|
||||
self.import_graph = 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),
|
||||
)
|
||||
|
||||
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 = defaultdict(set)
|
||||
|
||||
def close(self):
|
||||
"""called before visiting project (i.e set of modules)"""
|
||||
# don't try to compute cycles if the associated message is disabled
|
||||
if self.linter.is_message_enabled('cyclic-import'):
|
||||
vertices = list(self.import_graph)
|
||||
for cycle in get_cycles(self.import_graph, vertices=vertices):
|
||||
self.add_message('cyclic-import', args=' -> '.join(cycle))
|
||||
|
||||
def visit_import(self, node):
|
||||
"""triggered when an import statement is seen"""
|
||||
modnode = node.root()
|
||||
for name, _ in node.names:
|
||||
importedmodnode = self.get_imported_module(node, name)
|
||||
if importedmodnode is None:
|
||||
continue
|
||||
self._check_relative_import(modnode, node, importedmodnode, name)
|
||||
self._add_imported_module(node, importedmodnode.name)
|
||||
self._check_deprecated_module(node, name)
|
||||
self._check_reimport(node, name)
|
||||
|
||||
# TODO This appears to be the list of all messages of the checker...
|
||||
# @check_messages('W0410', 'W0401', 'W0403', 'W0402', 'W0404', 'W0406', 'F0401')
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_from(self, node):
|
||||
"""triggered when a from statement is seen"""
|
||||
basename = node.modname
|
||||
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.From)
|
||||
and prev.modname == '__future__'):
|
||||
self.add_message('misplaced-future', node=node)
|
||||
return
|
||||
for name, _ in node.names:
|
||||
if name == '*':
|
||||
self.add_message('wildcard-import', args=basename, node=node)
|
||||
modnode = node.root()
|
||||
importedmodnode = self.get_imported_module(node, basename)
|
||||
if importedmodnode is None:
|
||||
return
|
||||
self._check_relative_import(modnode, node, importedmodnode, basename)
|
||||
self._check_deprecated_module(node, basename)
|
||||
for name, _ in node.names:
|
||||
if name != '*':
|
||||
self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name))
|
||||
self._check_reimport(node, name, basename, node.level)
|
||||
|
||||
def get_imported_module(self, importnode, modname):
|
||||
try:
|
||||
return importnode.do_import_module(modname)
|
||||
except astroid.InferenceError as ex:
|
||||
if str(ex) != modname:
|
||||
args = '%r (%s)' % (modname, ex)
|
||||
else:
|
||||
args = repr(modname)
|
||||
if not _except_import_error(importnode.parent):
|
||||
self.add_message("import-error", args=args, 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
|
||||
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)
|
||||
|
||||
def _add_imported_module(self, node, importedmodname):
|
||||
"""notify an imported module, used to analyze dependencies"""
|
||||
try:
|
||||
importedmodname = get_module_part(importedmodname)
|
||||
except ImportError:
|
||||
pass
|
||||
context_name = node.root().name
|
||||
if context_name == importedmodname:
|
||||
# module importing itself !
|
||||
self.add_message('import-self', node=node)
|
||||
elif not is_standard_module(importedmodname):
|
||||
# handle dependencies
|
||||
importedmodnames = self.stats['dependencies'].setdefault(
|
||||
importedmodname, set())
|
||||
if not context_name in importedmodnames:
|
||||
importedmodnames.add(context_name)
|
||||
# update import graph
|
||||
mgraph = self.import_graph[context_name]
|
||||
if importedmodname not in mgraph:
|
||||
mgraph.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, name, 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 context, level in contexts:
|
||||
first = get_first_import(node, context, name, basename, level)
|
||||
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 EmptyReport()
|
||||
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 EmptyReport()
|
||||
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 register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(ImportsChecker(linter))
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
# Copyright (c) 2009-2010 Google, Inc.
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""checker for use of Python logging
|
||||
"""
|
||||
|
||||
import astroid
|
||||
from pylint import checkers
|
||||
from pylint import interfaces
|
||||
from pylint.checkers import utils
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
import six
|
||||
|
||||
|
||||
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 but 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 few arguments.'),
|
||||
'E1206': ('Not enough arguments for logging format string',
|
||||
'logging-too-few-args',
|
||||
'Used when a logging format string is given too many arguments'),
|
||||
}
|
||||
|
||||
|
||||
CHECKED_CONVENIENCE_FUNCTIONS = set([
|
||||
'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn',
|
||||
'warning'])
|
||||
|
||||
def is_method_call(callfunc_node, types=(), methods=()):
|
||||
"""Determines if a CallFunc node represents a method call.
|
||||
|
||||
Args:
|
||||
callfunc_node: The CallFunc AST node to check.
|
||||
types: Optional sequence of caller type names to restrict check.
|
||||
methods: Optional sequence of method names to restrict check.
|
||||
|
||||
Returns:
|
||||
True, if the node represents a method call for the given type and
|
||||
method names, False otherwise.
|
||||
"""
|
||||
if not isinstance(callfunc_node, astroid.CallFunc):
|
||||
return False
|
||||
func = utils.safe_infer(callfunc_node.func)
|
||||
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_from(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_callfunc(self, node):
|
||||
"""Checks calls to logging methods."""
|
||||
def is_logging_name():
|
||||
return (isinstance(node.func, astroid.Getattr) 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.Class) 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) and node.args[format_pos].op == '%':
|
||||
self.add_message('logging-not-lazy', node=node)
|
||||
elif isinstance(node.args[format_pos], astroid.CallFunc):
|
||||
self._check_call_func(node.args[format_pos])
|
||||
elif isinstance(node.args[format_pos], astroid.Const):
|
||||
self._check_format_string(node, format_pos)
|
||||
|
||||
def _check_call_func(self, callfunc_node):
|
||||
"""Checks that function call is not format_string.format().
|
||||
|
||||
Args:
|
||||
callfunc_node: CallFunc AST node to be checked.
|
||||
"""
|
||||
if is_method_call(callfunc_node, ('str', 'unicode'), ('format',)):
|
||||
self.add_message('logging-format-interpolation', node=callfunc_node)
|
||||
|
||||
def _check_format_string(self, node, format_arg):
|
||||
"""Checks that format string tokens match the supplied arguments.
|
||||
|
||||
Args:
|
||||
node: AST node to be checked.
|
||||
format_arg: 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 _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 of AST nodes that are arguments for a log format string.
|
||||
|
||||
Returns:
|
||||
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))
|
||||
104
plugins/bundle/python-mode/pymode/libs/pylint/checkers/misc.py
Normal file
104
plugins/bundle/python-mode/pymode/libs/pylint/checkers/misc.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# pylint: disable=W0511
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
""" Copyright (c) 2000-2010 LOGILAB S.A. (Paris, FRANCE).
|
||||
http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
|
||||
Check source code is ascii only or has an encoding declaration (PEP 263)
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
import six
|
||||
|
||||
|
||||
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):
|
||||
# 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
|
||||
self.add_message('fixme', args=line[match.start(1):-1], 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]))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker"""
|
||||
linter.register_checker(EncodingChecker(linter))
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
# Copyright (c) 2005-2014 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""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,
|
||||
has_known_bases,
|
||||
node_frame_class,
|
||||
)
|
||||
|
||||
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_class(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_callfunc(self, node):
|
||||
"""check property usage"""
|
||||
parent = node.parent.frame()
|
||||
if (isinstance(parent, astroid.Class) 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_function(self, node):
|
||||
"""check use of super"""
|
||||
# ignore actual functions or method within a new style class
|
||||
if not node.is_method():
|
||||
return
|
||||
klass = node.parent.frame()
|
||||
for stmt in node.nodes_of_class(astroid.CallFunc):
|
||||
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.Getattr):
|
||||
continue
|
||||
call = expr.expr
|
||||
# skip the test if using super
|
||||
if isinstance(call, astroid.CallFunc) and \
|
||||
isinstance(call.func, astroid.Name) and \
|
||||
call.func.name == 'super':
|
||||
confidence = (INFERENCE if has_known_bases(klass)
|
||||
else INFERENCE_FAILURE)
|
||||
if not klass.newstyle:
|
||||
# super should not be used on an old style class
|
||||
self.add_message('super-on-old-class', node=node,
|
||||
confidence=confidence)
|
||||
else:
|
||||
# super first arg should be the class
|
||||
if not call.args and sys.version_info[0] == 3:
|
||||
# unless Python 3
|
||||
continue
|
||||
|
||||
try:
|
||||
supcls = (call.args and next(call.args[0].infer())
|
||||
or None)
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
if supcls is None:
|
||||
self.add_message('missing-super-argument', node=call,
|
||||
confidence=confidence)
|
||||
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 is not astroid.YES:
|
||||
name = supcls.name
|
||||
else:
|
||||
if hasattr(call.args[0], 'name'):
|
||||
name = call.args[0].name
|
||||
if name is not None:
|
||||
self.add_message('bad-super-call',
|
||||
node=call,
|
||||
args=(name, ),
|
||||
confidence=confidence)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(NewStyleConflictChecker(linter))
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
# Copyright 2014 Google Inc.
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Check Python 2 code for Python 2/3 source-compatible issues."""
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
import astroid
|
||||
from astroid import bases
|
||||
from pylint import checkers, interfaces
|
||||
from pylint.utils import WarningScope
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
_ZERO = re.compile("^0+$")
|
||||
|
||||
def _is_old_octal(literal):
|
||||
if _ZERO.match(literal):
|
||||
return False
|
||||
if re.match('0\d+', literal):
|
||||
try:
|
||||
int(literal, 8)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_dict_node(node):
|
||||
inferred_types = set()
|
||||
try:
|
||||
inferred = node.infer()
|
||||
for inferred_node in inferred:
|
||||
inferred_types.add(inferred_node)
|
||||
except (astroid.InferenceError, astroid.UnresolvableName):
|
||||
pass
|
||||
return (not inferred_types
|
||||
or any(isinstance(x, astroid.Dict) for x in inferred_types))
|
||||
|
||||
def _is_builtin(node):
|
||||
return getattr(node, 'name', None) in ('__builtin__', 'builtins')
|
||||
|
||||
_accepts_iterator = {'iter', 'list', 'tuple', 'sorted', 'set', 'sum', 'any',
|
||||
'all', 'enumerate', 'dict'}
|
||||
|
||||
def _in_iterating_context(node):
|
||||
"""Check if the node is being used as an iterator.
|
||||
|
||||
Definition is taken from lib2to3.fixer_util.in_special_context().
|
||||
"""
|
||||
parent = node.parent
|
||||
# Since a call can't be the loop variant we only need to know if the node's
|
||||
# parent is a 'for' loop to know it's being used as the iterator for the
|
||||
# loop.
|
||||
if isinstance(parent, astroid.For):
|
||||
return True
|
||||
# Need to make sure the use of the node is in the iterator part of the
|
||||
# comprehension.
|
||||
elif isinstance(parent, astroid.Comprehension):
|
||||
if parent.iter == node:
|
||||
return True
|
||||
# Various built-ins can take in an iterable or list and lead to the same
|
||||
# value.
|
||||
elif isinstance(parent, astroid.CallFunc):
|
||||
if isinstance(parent.func, astroid.Name):
|
||||
parent_scope = parent.func.lookup(parent.func.name)[0]
|
||||
if _is_builtin(parent_scope) and parent.func.name in _accepts_iterator:
|
||||
return True
|
||||
elif isinstance(parent.func, astroid.Getattr):
|
||||
if parent.func.attrname == 'join':
|
||||
return True
|
||||
# If the call is in an unpacking, there's no need to warn,
|
||||
# since it can be considered iterating.
|
||||
elif (isinstance(parent, astroid.Assign) and
|
||||
isinstance(parent.targets[0], (astroid.List, astroid.Tuple))):
|
||||
if len(parent.targets[0].elts) > 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Python3Checker(checkers.BaseChecker):
|
||||
|
||||
__implements__ = interfaces.IAstroidChecker
|
||||
enabled = False
|
||||
name = 'python3'
|
||||
|
||||
msgs = {
|
||||
# Errors for what will syntactically break in Python 3, warnings for
|
||||
# everything else.
|
||||
'E1601': ('print statement used',
|
||||
'print-statement',
|
||||
'Used when a print statement is used '
|
||||
'(`print` is a function in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'E1602': ('Parameter unpacking specified',
|
||||
'parameter-unpacking',
|
||||
'Used when parameter unpacking is specified for a function'
|
||||
"(Python 3 doesn't allow it)",
|
||||
{'maxversion': (3, 0)}),
|
||||
'E1603': ('Implicit unpacking of exceptions is not supported '
|
||||
'in Python 3',
|
||||
'unpacking-in-except',
|
||||
'Python3 will not allow implicit unpacking of '
|
||||
'exceptions in except clauses. '
|
||||
'See http://www.python.org/dev/peps/pep-3110/',
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W0712', 'unpacking-in-except')]}),
|
||||
'E1604': ('Use raise ErrorClass(args) instead of '
|
||||
'raise ErrorClass, args.',
|
||||
'old-raise-syntax',
|
||||
"Used when the alternate raise syntax "
|
||||
"'raise foo, bar' is used "
|
||||
"instead of 'raise foo(bar)'.",
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W0121', 'old-raise-syntax')]}),
|
||||
'E1605': ('Use of the `` operator',
|
||||
'backtick',
|
||||
'Used when the deprecated "``" (backtick) operator is used '
|
||||
'instead of the str() function.',
|
||||
{'scope': WarningScope.NODE,
|
||||
'maxversion': (3, 0),
|
||||
'old_names': [('W0333', 'backtick')]}),
|
||||
'W1601': ('apply built-in referenced',
|
||||
'apply-builtin',
|
||||
'Used when the apply built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1602': ('basestring built-in referenced',
|
||||
'basestring-builtin',
|
||||
'Used when the basestring built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1603': ('buffer built-in referenced',
|
||||
'buffer-builtin',
|
||||
'Used when the buffer built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1604': ('cmp built-in referenced',
|
||||
'cmp-builtin',
|
||||
'Used when the cmp built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1605': ('coerce built-in referenced',
|
||||
'coerce-builtin',
|
||||
'Used when the coerce built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1606': ('execfile built-in referenced',
|
||||
'execfile-builtin',
|
||||
'Used when the execfile built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1607': ('file built-in referenced',
|
||||
'file-builtin',
|
||||
'Used when the file built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1608': ('long built-in referenced',
|
||||
'long-builtin',
|
||||
'Used when the long built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1609': ('raw_input built-in referenced',
|
||||
'raw_input-builtin',
|
||||
'Used when the raw_input built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1610': ('reduce built-in referenced',
|
||||
'reduce-builtin',
|
||||
'Used when the reduce built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1611': ('StandardError built-in referenced',
|
||||
'standarderror-builtin',
|
||||
'Used when the StandardError built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1612': ('unicode built-in referenced',
|
||||
'unicode-builtin',
|
||||
'Used when the unicode built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1613': ('xrange built-in referenced',
|
||||
'xrange-builtin',
|
||||
'Used when the xrange built-in function is referenced '
|
||||
'(missing from Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1614': ('__coerce__ method defined',
|
||||
'coerce-method',
|
||||
'Used when a __coerce__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1615': ('__delslice__ method defined',
|
||||
'delslice-method',
|
||||
'Used when a __delslice__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1616': ('__getslice__ method defined',
|
||||
'getslice-method',
|
||||
'Used when a __getslice__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1617': ('__setslice__ method defined',
|
||||
'setslice-method',
|
||||
'Used when a __setslice__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1618': ('import missing `from __future__ import absolute_import`',
|
||||
'no-absolute-import',
|
||||
'Used when an import is not accompanied by '
|
||||
'``from __future__ import absolute_import`` '
|
||||
'(default behaviour in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1619': ('division w/o __future__ statement',
|
||||
'old-division',
|
||||
'Used for non-floor division w/o a float literal or '
|
||||
'``from __future__ import division`` '
|
||||
'(Python 3 returns a float for int division unconditionally)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1620': ('Calling a dict.iter*() method',
|
||||
'dict-iter-method',
|
||||
'Used for calls to dict.iterkeys(), itervalues() or iteritems() '
|
||||
'(Python 3 lacks these methods)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1621': ('Calling a dict.view*() method',
|
||||
'dict-view-method',
|
||||
'Used for calls to dict.viewkeys(), viewvalues() or viewitems() '
|
||||
'(Python 3 lacks these methods)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1622': ('Called a next() method on an object',
|
||||
'next-method-called',
|
||||
"Used when an object's next() method is called "
|
||||
'(Python 3 uses the next() built-in function)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1623': ("Assigning to a class' __metaclass__ attribute",
|
||||
'metaclass-assignment',
|
||||
"Used when a metaclass is specified by assigning to __metaclass__ "
|
||||
'(Python 3 specifies the metaclass as a class statement argument)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1624': ('Indexing exceptions will not work on Python 3',
|
||||
'indexing-exception',
|
||||
'Indexing exceptions will not work on Python 3. Use '
|
||||
'`exception.args[index]` instead.',
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W0713', 'indexing-exception')]}),
|
||||
'W1625': ('Raising a string exception',
|
||||
'raising-string',
|
||||
'Used when a string exception is raised. This will not '
|
||||
'work on Python 3.',
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W0701', 'raising-string')]}),
|
||||
'W1626': ('reload built-in referenced',
|
||||
'reload-builtin',
|
||||
'Used when the reload built-in function is referenced '
|
||||
'(missing from Python 3). You can use instead imp.reload '
|
||||
'or importlib.reload.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1627': ('__oct__ method defined',
|
||||
'oct-method',
|
||||
'Used when a __oct__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1628': ('__hex__ method defined',
|
||||
'hex-method',
|
||||
'Used when a __hex__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1629': ('__nonzero__ method defined',
|
||||
'nonzero-method',
|
||||
'Used when a __nonzero__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1630': ('__cmp__ method defined',
|
||||
'cmp-method',
|
||||
'Used when a __cmp__ method is defined '
|
||||
'(method is not used by Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
# 'W1631': replaced by W1636
|
||||
'W1632': ('input built-in referenced',
|
||||
'input-builtin',
|
||||
'Used when the input built-in is referenced '
|
||||
'(backwards-incompatible semantics in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1633': ('round built-in referenced',
|
||||
'round-builtin',
|
||||
'Used when the round built-in is referenced '
|
||||
'(backwards-incompatible semantics in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1634': ('intern built-in referenced',
|
||||
'intern-builtin',
|
||||
'Used when the intern built-in is referenced '
|
||||
'(Moved to sys.intern in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1635': ('unichr built-in referenced',
|
||||
'unichr-builtin',
|
||||
'Used when the unichr built-in is referenced '
|
||||
'(Use chr in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1636': ('map built-in referenced when not iterating',
|
||||
'map-builtin-not-iterating',
|
||||
'Used when the map built-in is referenced in a non-iterating '
|
||||
'context (returns an iterator in Python 3)',
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W1631', 'implicit-map-evaluation')]}),
|
||||
'W1637': ('zip built-in referenced when not iterating',
|
||||
'zip-builtin-not-iterating',
|
||||
'Used when the zip built-in is referenced in a non-iterating '
|
||||
'context (returns an iterator in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1638': ('range built-in referenced when not iterating',
|
||||
'range-builtin-not-iterating',
|
||||
'Used when the range built-in is referenced in a non-iterating '
|
||||
'context (returns an iterator in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1639': ('filter built-in referenced when not iterating',
|
||||
'filter-builtin-not-iterating',
|
||||
'Used when the filter built-in is referenced in a non-iterating '
|
||||
'context (returns an iterator in Python 3)',
|
||||
{'maxversion': (3, 0)}),
|
||||
'W1640': ('Using the cmp argument for list.sort / sorted',
|
||||
'using-cmp-argument',
|
||||
'Using the cmp argument for list.sort or the sorted '
|
||||
'builtin should be avoided, since it was removed in '
|
||||
'Python 3. Using either `key` or `functools.cmp_to_key` '
|
||||
'should be preferred.',
|
||||
{'maxversion': (3, 0)}),
|
||||
}
|
||||
|
||||
_bad_builtins = frozenset([
|
||||
'apply',
|
||||
'basestring',
|
||||
'buffer',
|
||||
'cmp',
|
||||
'coerce',
|
||||
'execfile',
|
||||
'file',
|
||||
'input', # Not missing, but incompatible semantics
|
||||
'intern',
|
||||
'long',
|
||||
'raw_input',
|
||||
'reduce',
|
||||
'round', # Not missing, but incompatible semantics
|
||||
'StandardError',
|
||||
'unichr',
|
||||
'unicode',
|
||||
'xrange',
|
||||
'reload',
|
||||
])
|
||||
|
||||
_unused_magic_methods = frozenset([
|
||||
'__coerce__',
|
||||
'__delslice__',
|
||||
'__getslice__',
|
||||
'__setslice__',
|
||||
'__oct__',
|
||||
'__hex__',
|
||||
'__nonzero__',
|
||||
'__cmp__',
|
||||
])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._future_division = False
|
||||
self._future_absolute_import = False
|
||||
super(Python3Checker, self).__init__(*args, **kwargs)
|
||||
|
||||
def visit_module(self, node): # pylint: disable=unused-argument
|
||||
"""Clear checker state after previous module."""
|
||||
self._future_division = False
|
||||
self._future_absolute_import = False
|
||||
|
||||
def visit_function(self, node):
|
||||
if node.is_method() and node.name in self._unused_magic_methods:
|
||||
method_name = node.name
|
||||
if node.name.startswith('__'):
|
||||
method_name = node.name[2:-2]
|
||||
self.add_message(method_name + '-method', node=node)
|
||||
|
||||
@utils.check_messages('parameter-unpacking')
|
||||
def visit_arguments(self, node):
|
||||
for arg in node.args:
|
||||
if isinstance(arg, astroid.Tuple):
|
||||
self.add_message('parameter-unpacking', node=arg)
|
||||
|
||||
def visit_name(self, node):
|
||||
"""Detect when a "bad" built-in is referenced."""
|
||||
found_node = node.lookup(node.name)[0]
|
||||
if _is_builtin(found_node):
|
||||
if node.name in self._bad_builtins:
|
||||
message = node.name.lower() + '-builtin'
|
||||
self.add_message(message, node=node)
|
||||
|
||||
@utils.check_messages('print-statement')
|
||||
def visit_print(self, node):
|
||||
self.add_message('print-statement', node=node)
|
||||
|
||||
@utils.check_messages('no-absolute-import')
|
||||
def visit_from(self, node):
|
||||
if node.modname == '__future__':
|
||||
for name, _ in node.names:
|
||||
if name == 'division':
|
||||
self._future_division = True
|
||||
elif name == 'absolute_import':
|
||||
self._future_absolute_import = True
|
||||
elif not self._future_absolute_import:
|
||||
self.add_message('no-absolute-import', node=node)
|
||||
|
||||
@utils.check_messages('no-absolute-import')
|
||||
def visit_import(self, node):
|
||||
if not self._future_absolute_import:
|
||||
self.add_message('no-absolute-import', node=node)
|
||||
|
||||
@utils.check_messages('metaclass-assignment')
|
||||
def visit_class(self, node):
|
||||
if '__metaclass__' in node.locals:
|
||||
self.add_message('metaclass-assignment', node=node)
|
||||
|
||||
@utils.check_messages('old-division')
|
||||
def visit_binop(self, node):
|
||||
if not self._future_division and node.op == '/':
|
||||
for arg in (node.left, node.right):
|
||||
if isinstance(arg, astroid.Const) and isinstance(arg.value, float):
|
||||
break
|
||||
else:
|
||||
self.add_message('old-division', node=node)
|
||||
|
||||
def _check_cmp_argument(self, node):
|
||||
# Check that the `cmp` argument is used
|
||||
args = []
|
||||
if (isinstance(node.func, astroid.Getattr)
|
||||
and node.func.attrname == 'sort'):
|
||||
inferred = utils.safe_infer(node.func.expr)
|
||||
if not inferred:
|
||||
return
|
||||
|
||||
builtins_list = "{}.list".format(bases.BUILTINS)
|
||||
if (isinstance(inferred, astroid.List)
|
||||
or inferred.qname() == builtins_list):
|
||||
args = node.args
|
||||
|
||||
elif (isinstance(node.func, astroid.Name)
|
||||
and node.func.name == 'sorted'):
|
||||
inferred = utils.safe_infer(node.func)
|
||||
if not inferred:
|
||||
return
|
||||
|
||||
builtins_sorted = "{}.sorted".format(bases.BUILTINS)
|
||||
if inferred.qname() == builtins_sorted:
|
||||
args = node.args
|
||||
|
||||
for arg in args:
|
||||
if isinstance(arg, astroid.Keyword) and arg.arg == 'cmp':
|
||||
self.add_message('using-cmp-argument', node=node)
|
||||
return
|
||||
|
||||
def visit_callfunc(self, node):
|
||||
self._check_cmp_argument(node)
|
||||
|
||||
if isinstance(node.func, astroid.Getattr):
|
||||
if any([node.args, node.starargs, node.kwargs]):
|
||||
return
|
||||
if node.func.attrname == 'next':
|
||||
self.add_message('next-method-called', node=node)
|
||||
else:
|
||||
if _check_dict_node(node.func.expr):
|
||||
if node.func.attrname in ('iterkeys', 'itervalues', 'iteritems'):
|
||||
self.add_message('dict-iter-method', node=node)
|
||||
elif node.func.attrname in ('viewkeys', 'viewvalues', 'viewitems'):
|
||||
self.add_message('dict-view-method', node=node)
|
||||
elif isinstance(node.func, astroid.Name):
|
||||
found_node = node.func.lookup(node.func.name)[0]
|
||||
if _is_builtin(found_node):
|
||||
if node.func.name in ('filter', 'map', 'range', 'zip'):
|
||||
if not _in_iterating_context(node):
|
||||
checker = '{}-builtin-not-iterating'.format(node.func.name)
|
||||
self.add_message(checker, node=node)
|
||||
|
||||
|
||||
@utils.check_messages('indexing-exception')
|
||||
def visit_subscript(self, node):
|
||||
""" Look for indexing exceptions. """
|
||||
try:
|
||||
for infered in node.value.infer():
|
||||
if not isinstance(infered, astroid.Instance):
|
||||
continue
|
||||
if utils.inherit_from_std_ex(infered):
|
||||
self.add_message('indexing-exception', node=node)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
|
||||
@utils.check_messages('unpacking-in-except')
|
||||
def visit_excepthandler(self, node):
|
||||
"""Visit an except handler block and check for exception unpacking."""
|
||||
if isinstance(node.name, (astroid.Tuple, astroid.List)):
|
||||
self.add_message('unpacking-in-except', node=node)
|
||||
|
||||
@utils.check_messages('backtick')
|
||||
def visit_backquote(self, node):
|
||||
self.add_message('backtick', node=node)
|
||||
|
||||
@utils.check_messages('raising-string', 'old-raise-syntax')
|
||||
def visit_raise(self, node):
|
||||
"""Visit a raise statement and check for raising
|
||||
strings or old-raise-syntax.
|
||||
"""
|
||||
if (node.exc is not None and
|
||||
node.inst is not None and
|
||||
node.tback is None):
|
||||
self.add_message('old-raise-syntax', node=node)
|
||||
|
||||
# Ignore empty raise.
|
||||
if node.exc is None:
|
||||
return
|
||||
expr = node.exc
|
||||
if self._check_raise_value(node, expr):
|
||||
return
|
||||
else:
|
||||
try:
|
||||
value = next(astroid.unpack_infer(expr))
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
self._check_raise_value(node, value)
|
||||
|
||||
def _check_raise_value(self, node, expr):
|
||||
if isinstance(expr, astroid.Const):
|
||||
value = expr.value
|
||||
if isinstance(value, str):
|
||||
self.add_message('raising-string', node=node)
|
||||
return True
|
||||
|
||||
|
||||
class Python3TokenChecker(checkers.BaseTokenChecker):
|
||||
__implements__ = interfaces.ITokenChecker
|
||||
name = 'python3'
|
||||
enabled = False
|
||||
|
||||
msgs = {
|
||||
'E1606': ('Use of long suffix',
|
||||
'long-suffix',
|
||||
'Used when "l" or "L" is used to mark a long integer. '
|
||||
'This will not work in Python 3, since `int` and `long` '
|
||||
'types have merged.',
|
||||
{'maxversion': (3, 0)}),
|
||||
'E1607': ('Use of the <> operator',
|
||||
'old-ne-operator',
|
||||
'Used when the deprecated "<>" operator is used instead '
|
||||
'of "!=". This is removed in Python 3.',
|
||||
{'maxversion': (3, 0),
|
||||
'old_names': [('W0331', 'old-ne-operator')]}),
|
||||
'E1608': ('Use of old octal literal',
|
||||
'old-octal-literal',
|
||||
'Usen when encountering the old octal syntax, '
|
||||
'removed in Python 3. To use the new syntax, '
|
||||
'prepend 0o on the number.',
|
||||
{'maxversion': (3, 0)}),
|
||||
}
|
||||
|
||||
def process_tokens(self, tokens):
|
||||
for idx, (tok_type, token, start, _, _) in enumerate(tokens):
|
||||
if tok_type == tokenize.NUMBER:
|
||||
if token.lower().endswith('l'):
|
||||
# This has a different semantic than lowercase-l-suffix.
|
||||
self.add_message('long-suffix', line=start[0])
|
||||
elif _is_old_octal(token):
|
||||
self.add_message('old-octal-literal', line=start[0])
|
||||
if tokens[idx][1] == '<>':
|
||||
self.add_message('old-ne-operator', line=tokens[idx][2][0])
|
||||
|
||||
|
||||
def register(linter):
|
||||
linter.register_checker(Python3Checker(linter))
|
||||
linter.register_checker(Python3TokenChecker(linter))
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
""" Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
|
||||
http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
|
||||
Raw metrics checker
|
||||
"""
|
||||
|
||||
import tokenize
|
||||
|
||||
# pylint now requires pylint >= 2.2, so this is no longer necessary
|
||||
#if not hasattr(tokenize, 'NL'):
|
||||
# raise ValueError("tokenize.NL doesn't exist -- tokenize module too old")
|
||||
|
||||
from logilab.common.ureports import Table
|
||||
|
||||
from pylint.interfaces import ITokenChecker
|
||||
from pylint.utils import EmptyReport
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.reporters import diff_string
|
||||
|
||||
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 EmptyReport()
|
||||
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 tok_type == 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,372 @@
|
|||
# pylint: disable=W0622
|
||||
# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""a similarities / code duplication command line tool and pylint checker
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
from logilab.common.ureports import Table
|
||||
|
||||
from pylint.interfaces import IRawChecker
|
||||
from pylint.checkers import BaseChecker, table_lines_from_stats
|
||||
|
||||
import six
|
||||
from six.moves import zip
|
||||
|
||||
|
||||
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 = lambda: [line.decode(encoding) for line in stream]
|
||||
try:
|
||||
self.linesets.append(LineSet(streamid,
|
||||
readlines(),
|
||||
self.ignore_comments,
|
||||
self.ignore_docstrings,
|
||||
self.ignore_imports))
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""start looking for similarities and display results on stdout"""
|
||||
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,250 @@
|
|||
# Copyright 2014 Michal Nowikowski.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Checker for spelling errors in comments and docstrings.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
import string
|
||||
import re
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
maketrans = str.maketrans
|
||||
else:
|
||||
maketrans = string.maketrans
|
||||
|
||||
from pylint.interfaces import ITokenChecker, IAstroidChecker
|
||||
from pylint.checkers import BaseTokenChecker
|
||||
from pylint.checkers.utils import check_messages
|
||||
|
||||
try:
|
||||
import enchant
|
||||
except ImportError:
|
||||
enchant = None
|
||||
|
||||
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."
|
||||
|
||||
table = maketrans("", "")
|
||||
|
||||
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.'}),
|
||||
)
|
||||
|
||||
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"])
|
||||
|
||||
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()
|
||||
|
||||
# Prepare regex for stripping punctuation signs from text.
|
||||
# ' and _ are treated in a special way.
|
||||
puncts = string.punctuation.replace("'", "").replace("_", "")
|
||||
self.punctuation_regex = re.compile('[%s]' % re.escape(puncts))
|
||||
self.initialized = True
|
||||
|
||||
def close(self):
|
||||
if self.private_dict_file:
|
||||
self.private_dict_file.close()
|
||||
|
||||
def _check_spelling(self, msgid, line, line_num):
|
||||
line2 = line.strip()
|
||||
# Replace ['afadf with afadf (but preserve don't)
|
||||
line2 = re.sub("'([^a-zA-Z]|$)", " ", line2)
|
||||
# Replace afadf'] with afadf (but preserve don't)
|
||||
line2 = re.sub("([^a-zA-Z]|^)'", " ", line2)
|
||||
# Replace punctuation signs with space e.g. and/or -> and or
|
||||
line2 = self.punctuation_regex.sub(' ', line2)
|
||||
|
||||
words = []
|
||||
for word in line2.split():
|
||||
# Skip words with digits.
|
||||
if len(re.findall(r"\d", word)) > 0:
|
||||
continue
|
||||
|
||||
# Skip words with mixed big and small letters,
|
||||
# they are probaly class names.
|
||||
if (len(re.findall("[A-Z]", word)) > 0 and
|
||||
len(re.findall("[a-z]", word)) > 0 and
|
||||
len(word) > 2):
|
||||
continue
|
||||
|
||||
# Skip words with _ - they are probably function parameter names.
|
||||
if word.count('_') > 0:
|
||||
continue
|
||||
|
||||
words.append(word)
|
||||
|
||||
# Go through words and check them.
|
||||
for word in words:
|
||||
# Skip words from ignore list.
|
||||
if word in self.ignore_list:
|
||||
continue
|
||||
|
||||
orig_word = word
|
||||
word = word.lower()
|
||||
|
||||
# Strip starting u' from unicode literals and r' from raw strings.
|
||||
if (word.startswith("u'") or
|
||||
word.startswith('u"') or
|
||||
word.startswith("r'") or
|
||||
word.startswith('r"')) and len(word) > 2:
|
||||
word = word[2:]
|
||||
|
||||
# If it is a known word, then continue.
|
||||
try:
|
||||
if self.spelling_dict.check(word):
|
||||
continue
|
||||
except enchant.errors.Error:
|
||||
# this can only happen in docstrings, not comments
|
||||
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 word not in self.unknown_words:
|
||||
self.private_dict_file.write("%s\n" % word)
|
||||
self.unknown_words.add(word)
|
||||
else:
|
||||
# Present up to 4 suggestions.
|
||||
# TODO: add support for customising this.
|
||||
suggestions = self.spelling_dict.suggest(word)[:4]
|
||||
|
||||
m = re.search(r"(\W|^)(%s)(\W|$)" % word, line.lower())
|
||||
if m:
|
||||
# Start position of second group in regex.
|
||||
col = m.regs[2][0]
|
||||
else:
|
||||
col = line.lower().index(word)
|
||||
indicator = (" " * col) + ("^" * len(word))
|
||||
|
||||
self.add_message(msgid, line=line_num,
|
||||
args=(orig_word, line,
|
||||
indicator,
|
||||
"' 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:
|
||||
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_class(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
@check_messages('wrong-spelling-in-docstring')
|
||||
def visit_function(self, node):
|
||||
if not self.initialized:
|
||||
return
|
||||
self._check_docstring(node)
|
||||
|
||||
def _check_docstring(self, node):
|
||||
"""check the node has any spelling errors"""
|
||||
docstring = node.doc
|
||||
if not docstring:
|
||||
return
|
||||
|
||||
start_line = node.lineno + 1
|
||||
|
||||
# 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))
|
||||
216
plugins/bundle/python-mode/pymode/libs/pylint/checkers/stdlib.py
Normal file
216
plugins/bundle/python-mode/pymode/libs/pylint/checkers/stdlib.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Checkers for various standard library functions."""
|
||||
|
||||
import six
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from astroid.bases import Instance
|
||||
|
||||
from pylint.interfaces import IAstroidChecker
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers import utils
|
||||
|
||||
|
||||
TYPECHECK_COMPARISON_OPERATORS = frozenset(('is', 'is not', '==', '!=', 'in', 'not in'))
|
||||
LITERAL_NODE_TYPES = (astroid.Const, astroid.Dict, astroid.List, astroid.Set)
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
OPEN_MODULE = '_io'
|
||||
TYPE_QNAME = 'builtins.type'
|
||||
else:
|
||||
OPEN_MODULE = '__builtin__'
|
||||
TYPE_QNAME = '__builtin__.type'
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _is_one_arg_pos_call(call):
|
||||
"""Is this a call with exactly 1 argument,
|
||||
where that argument is positional?
|
||||
"""
|
||||
return (isinstance(call, astroid.CallFunc)
|
||||
and len(call.args) == 1
|
||||
and not isinstance(call.args[0], astroid.Keyword))
|
||||
|
||||
|
||||
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.'),
|
||||
'W1504': ('Using type() instead of isinstance() for a typecheck.',
|
||||
'unidiomatic-typecheck',
|
||||
'The idiomatic way to perform an explicit typecheck in '
|
||||
'Python is to use isinstance(x, Y) rather than '
|
||||
'type(x) == Y, type(x) is Y. Though there are unusual '
|
||||
'situations where these give different results.')
|
||||
}
|
||||
|
||||
@utils.check_messages('bad-open-mode', 'redundant-unittest-assert')
|
||||
def visit_callfunc(self, node):
|
||||
"""Visit a CallFunc node."""
|
||||
if hasattr(node, 'func'):
|
||||
infer = utils.safe_infer(node.func)
|
||||
if infer:
|
||||
if infer.root().name == OPEN_MODULE:
|
||||
if getattr(node.func, 'name', None) in ('open', 'file'):
|
||||
self._check_open_mode(node)
|
||||
if infer.root().name == 'unittest.case':
|
||||
self._check_redundant_assert(node, infer)
|
||||
|
||||
@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)
|
||||
|
||||
@utils.check_messages('unidiomatic-typecheck')
|
||||
def visit_compare(self, node):
|
||||
operator, right = node.ops[0]
|
||||
if operator in TYPECHECK_COMPARISON_OPERATORS:
|
||||
left = node.left
|
||||
if _is_one_arg_pos_call(left):
|
||||
self._check_type_x_is_y(node, left, operator, right)
|
||||
|
||||
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 _check_type_x_is_y(self, node, left, operator, right):
|
||||
"""Check for expressions like type(x) == Y."""
|
||||
left_func = utils.safe_infer(left.func)
|
||||
if not (isinstance(left_func, astroid.Class)
|
||||
and left_func.qname() == TYPE_QNAME):
|
||||
return
|
||||
|
||||
if operator in ('is', 'is not') and _is_one_arg_pos_call(right):
|
||||
right_func = utils.safe_infer(right.func)
|
||||
if (isinstance(right_func, astroid.Class)
|
||||
and right_func.qname() == TYPE_QNAME):
|
||||
# type(x) == type(a)
|
||||
right_arg = utils.safe_infer(right.args[0])
|
||||
if not isinstance(right_arg, LITERAL_NODE_TYPES):
|
||||
# not e.g. type(x) == type([])
|
||||
return
|
||||
self.add_message('unidiomatic-typecheck', node=node)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(StdlibChecker(linter))
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
# Copyright (c) 2009-2010 Arista Networks, Inc. - James Lingard
|
||||
# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Checker for string formatting operations.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
import string
|
||||
import numbers
|
||||
|
||||
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
|
||||
|
||||
import six
|
||||
|
||||
|
||||
_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 conWtains 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"),
|
||||
|
||||
'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",
|
||||
"Usen 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.Backquote,
|
||||
astroid.Lambda, astroid.Function,
|
||||
astroid.ListComp, astroid.SetComp, astroid.GenExpr)
|
||||
|
||||
if _PY3K:
|
||||
import _string
|
||||
|
||||
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):
|
||||
keyname, fielditerator = format_string._formatter_field_name_split()
|
||||
# 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:
|
||||
# probably the format string is invalid
|
||||
# should we check the argument of the ValueError?
|
||||
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)
|
||||
keys.append((keyname, list(fielditerator)))
|
||||
else:
|
||||
num_args += 1
|
||||
return keys, num_args, len(manual_pos_arg)
|
||||
|
||||
def get_args(callfunc):
|
||||
""" Get the arguments from the given `CallFunc` 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.
|
||||
"""
|
||||
positional = 0
|
||||
named = {}
|
||||
|
||||
for arg in callfunc.args:
|
||||
if isinstance(arg, astroid.Keyword):
|
||||
named[arg.arg] = utils.safe_infer(arg.value)
|
||||
else:
|
||||
positional += 1
|
||||
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,
|
||||
# supress 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):
|
||||
num_args = len(args.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)
|
||||
|
||||
|
||||
class StringMethodsChecker(BaseChecker):
|
||||
__implements__ = (IAstroidChecker,)
|
||||
name = 'string'
|
||||
msgs = {
|
||||
'E1310': ("Suspicious argument in %s.%s call",
|
||||
"bad-str-strip-call",
|
||||
"The argument to a str.{l,r,}strip call contains a"
|
||||
" duplicate character, "),
|
||||
}
|
||||
|
||||
@check_messages(*(MSGS.keys()))
|
||||
def visit_callfunc(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.Getattr)
|
||||
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 node.starargs or node.kwargs:
|
||||
# TODO: Don't complicate the logic, skip these for now.
|
||||
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.YES:
|
||||
# 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.YES:
|
||||
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(specifier)
|
||||
except (IndexError, TypeError):
|
||||
warn_error = True
|
||||
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(StringMethodsChecker(linter))
|
||||
linter.register_checker(StringConstantChecker(linter))
|
||||
|
|
@ -0,0 +1,627 @@
|
|||
# Copyright (c) 2006-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""try to find more bugs in the code using astroid inference capabilities
|
||||
"""
|
||||
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import astroid
|
||||
from astroid import InferenceError, NotFoundError, YES, Instance
|
||||
from astroid.bases import BUILTINS
|
||||
|
||||
from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.checkers.utils import (
|
||||
safe_infer, is_super,
|
||||
check_messages, decorated_with_property)
|
||||
|
||||
MSGS = {
|
||||
'E1101': ('%s %r has no %r member',
|
||||
'no-member',
|
||||
'Used when a variable is accessed for an unexistent member.',
|
||||
{'old_names': [('E1103', 'maybe-no-member')]}),
|
||||
'E1102': ('%s is not callable',
|
||||
'not-callable',
|
||||
'Used when an object being called has been inferred to a non \
|
||||
callable object'),
|
||||
'E1111': ('Assigning to function call which doesn\'t return',
|
||||
'assignment-from-no-return',
|
||||
'Used when an assignment is done on a function call but the \
|
||||
inferred function doesn\'t return anything.'),
|
||||
'W1111': ('Assigning to function call which only returns None',
|
||||
'assignment-from-none',
|
||||
'Used when an assignment is done on a function call but the \
|
||||
inferred function returns nothing but None.'),
|
||||
|
||||
'E1120': ('No value for argument %s in %s call',
|
||||
'no-value-for-parameter',
|
||||
'Used when a function call passes too few arguments.'),
|
||||
'E1121': ('Too many positional arguments for %s call',
|
||||
'too-many-function-args',
|
||||
'Used when a function call passes too many positional \
|
||||
arguments.'),
|
||||
'E1123': ('Unexpected keyword argument %r in %s call',
|
||||
'unexpected-keyword-arg',
|
||||
'Used when a function call passes a keyword argument that \
|
||||
doesn\'t correspond to one of the function\'s parameter names.'),
|
||||
'E1124': ('Argument %r passed by position and keyword in %s call',
|
||||
'redundant-keyword-arg',
|
||||
'Used when a function call would result in assigning multiple \
|
||||
values to a function parameter, one value from a positional \
|
||||
argument and one from a keyword argument.'),
|
||||
'E1125': ('Missing mandatory keyword argument %r in %s call',
|
||||
'missing-kwoa',
|
||||
('Used when a function call does not pass a mandatory'
|
||||
' keyword-only argument.'),
|
||||
{'minversion': (3, 0)}),
|
||||
'E1126': ('Sequence index is not an int, slice, or instance with __index__',
|
||||
'invalid-sequence-index',
|
||||
'Used when a sequence type is indexed with an invalid type. '
|
||||
'Valid types are ints, slices, and objects with an __index__ '
|
||||
'method.'),
|
||||
'E1127': ('Slice index is not an int, None, or instance with __index__',
|
||||
'invalid-slice-index',
|
||||
'Used when a slice index is not an integer, None, or an object \
|
||||
with an __index__ method.'),
|
||||
}
|
||||
|
||||
# builtin sequence types in Python 2 and 3.
|
||||
SEQUENCE_TYPES = set(['str', 'unicode', 'list', 'tuple', 'bytearray',
|
||||
'xrange', 'range', 'bytes', 'memoryview'])
|
||||
|
||||
def _determine_callable(callable_obj):
|
||||
# Ordering is important, since BoundMethod is a subclass of UnboundMethod,
|
||||
# and Function inherits Lambda.
|
||||
if isinstance(callable_obj, astroid.BoundMethod):
|
||||
# Bound methods have an extra implicit 'self' argument.
|
||||
return callable_obj, 1, callable_obj.type
|
||||
elif isinstance(callable_obj, astroid.UnboundMethod):
|
||||
return callable_obj, 0, 'unbound method'
|
||||
elif isinstance(callable_obj, astroid.Function):
|
||||
return callable_obj, 0, callable_obj.type
|
||||
elif isinstance(callable_obj, astroid.Lambda):
|
||||
return callable_obj, 0, 'lambda'
|
||||
elif isinstance(callable_obj, astroid.Class):
|
||||
# Class instantiation, lookup __new__ instead.
|
||||
# If we only find object.__new__, we can safely check __init__
|
||||
# instead.
|
||||
try:
|
||||
# Use the last definition of __new__.
|
||||
new = callable_obj.local_attr('__new__')[-1]
|
||||
except astroid.NotFoundError:
|
||||
new = None
|
||||
|
||||
if not new or new.parent.scope().name == 'object':
|
||||
try:
|
||||
# Use the last definition of __init__.
|
||||
callable_obj = callable_obj.local_attr('__init__')[-1]
|
||||
except astroid.NotFoundError:
|
||||
# do nothing, covered by no-init.
|
||||
raise ValueError
|
||||
else:
|
||||
callable_obj = new
|
||||
|
||||
if not isinstance(callable_obj, astroid.Function):
|
||||
raise ValueError
|
||||
# both have an extra implicit 'cls'/'self' argument.
|
||||
return callable_obj, 1, 'constructor'
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
class TypeChecker(BaseChecker):
|
||||
"""try to find bugs in the code using type inference
|
||||
"""
|
||||
|
||||
__implements__ = (IAstroidChecker,)
|
||||
|
||||
# configuration section name
|
||||
name = 'typecheck'
|
||||
# messages
|
||||
msgs = MSGS
|
||||
priority = -1
|
||||
# configuration options
|
||||
options = (('ignore-mixin-members',
|
||||
{'default' : True, 'type' : 'yn', 'metavar': '<y_or_n>',
|
||||
'help' : 'Tells whether missing members accessed in mixin \
|
||||
class should be ignored. A mixin class is detected if its name ends with \
|
||||
"mixin" (case insensitive).'}
|
||||
),
|
||||
('ignored-modules',
|
||||
{'default': (),
|
||||
'type': 'csv',
|
||||
'metavar': '<module names>',
|
||||
'help': 'List of module names for which member attributes \
|
||||
should not be checked (useful for modules/projects where namespaces are \
|
||||
manipulated during runtime and thus existing member attributes cannot be \
|
||||
deduced by static analysis'},
|
||||
),
|
||||
('ignored-classes',
|
||||
{'default' : ('SQLObject',),
|
||||
'type' : 'csv',
|
||||
'metavar' : '<members names>',
|
||||
'help' : 'List of classes names for which member attributes \
|
||||
should not be checked (useful for classes with attributes dynamically set).'}
|
||||
),
|
||||
|
||||
('zope',
|
||||
{'default' : False, 'type' : 'yn', 'metavar': '<y_or_n>',
|
||||
'help' : 'When zope mode is activated, add a predefined set \
|
||||
of Zope acquired attributes to generated-members.'}
|
||||
),
|
||||
('generated-members',
|
||||
{'default' : ('REQUEST', 'acl_users', 'aq_parent'),
|
||||
'type' : 'string',
|
||||
'metavar' : '<members names>',
|
||||
'help' : 'List of members which are set dynamically and \
|
||||
missed by pylint inference system, and so shouldn\'t trigger E0201 when \
|
||||
accessed. Python regular expressions are accepted.'}
|
||||
),
|
||||
)
|
||||
|
||||
def open(self):
|
||||
# do this in open since config not fully initialized in __init__
|
||||
self.generated_members = list(self.config.generated_members)
|
||||
if self.config.zope:
|
||||
self.generated_members.extend(('REQUEST', 'acl_users', 'aq_parent'))
|
||||
|
||||
def visit_assattr(self, node):
|
||||
if isinstance(node.ass_type(), astroid.AugAssign):
|
||||
self.visit_getattr(node)
|
||||
|
||||
def visit_delattr(self, node):
|
||||
self.visit_getattr(node)
|
||||
|
||||
@check_messages('no-member')
|
||||
def visit_getattr(self, node):
|
||||
"""check that the accessed attribute exists
|
||||
|
||||
to avoid to much false positives for now, we'll consider the code as
|
||||
correct if a single of the inferred nodes has the accessed attribute.
|
||||
|
||||
function/method, super call and metaclasses are ignored
|
||||
"""
|
||||
# generated_members may containt regular expressions
|
||||
# (surrounded by quote `"` and followed by a comma `,`)
|
||||
# REQUEST,aq_parent,"[a-zA-Z]+_set{1,2}"' =>
|
||||
# ('REQUEST', 'aq_parent', '[a-zA-Z]+_set{1,2}')
|
||||
if isinstance(self.config.generated_members, str):
|
||||
gen = shlex.shlex(self.config.generated_members)
|
||||
gen.whitespace += ','
|
||||
gen.wordchars += '[]-+'
|
||||
self.config.generated_members = tuple(tok.strip('"') for tok in gen)
|
||||
for pattern in self.config.generated_members:
|
||||
# attribute is marked as generated, stop here
|
||||
if re.match(pattern, node.attrname):
|
||||
return
|
||||
try:
|
||||
infered = list(node.expr.infer())
|
||||
except InferenceError:
|
||||
return
|
||||
# list of (node, nodename) which are missing the attribute
|
||||
missingattr = set()
|
||||
ignoremim = self.config.ignore_mixin_members
|
||||
inference_failure = False
|
||||
for owner in infered:
|
||||
# skip yes object
|
||||
if owner is YES:
|
||||
inference_failure = True
|
||||
continue
|
||||
# skip None anyway
|
||||
if isinstance(owner, astroid.Const) and owner.value is None:
|
||||
continue
|
||||
# XXX "super" / metaclass call
|
||||
if is_super(owner) or getattr(owner, 'type', None) == 'metaclass':
|
||||
continue
|
||||
name = getattr(owner, 'name', 'None')
|
||||
if name in self.config.ignored_classes:
|
||||
continue
|
||||
if ignoremim and name[-5:].lower() == 'mixin':
|
||||
continue
|
||||
try:
|
||||
if not [n for n in owner.getattr(node.attrname)
|
||||
if not isinstance(n.statement(), astroid.AugAssign)]:
|
||||
missingattr.add((owner, name))
|
||||
continue
|
||||
except AttributeError:
|
||||
# XXX method / function
|
||||
continue
|
||||
except NotFoundError:
|
||||
if isinstance(owner, astroid.Function) and owner.decorators:
|
||||
continue
|
||||
if isinstance(owner, Instance) and owner.has_dynamic_getattr():
|
||||
continue
|
||||
# explicit skipping of module member access
|
||||
if owner.root().name in self.config.ignored_modules:
|
||||
continue
|
||||
if isinstance(owner, astroid.Class):
|
||||
# Look up in the metaclass only if the owner is itself
|
||||
# a class.
|
||||
# TODO: getattr doesn't return by default members
|
||||
# from the metaclass, because handling various cases
|
||||
# of methods accessible from the metaclass itself
|
||||
# and/or subclasses only is too complicated for little to
|
||||
# no benefit.
|
||||
metaclass = owner.metaclass()
|
||||
try:
|
||||
if metaclass and metaclass.getattr(node.attrname):
|
||||
continue
|
||||
except NotFoundError:
|
||||
pass
|
||||
missingattr.add((owner, name))
|
||||
continue
|
||||
# stop on the first found
|
||||
break
|
||||
else:
|
||||
# we have not found any node with the attributes, display the
|
||||
# message for infered nodes
|
||||
done = set()
|
||||
for owner, name in missingattr:
|
||||
if isinstance(owner, Instance):
|
||||
actual = owner._proxied
|
||||
else:
|
||||
actual = owner
|
||||
if actual in done:
|
||||
continue
|
||||
done.add(actual)
|
||||
confidence = INFERENCE if not inference_failure else INFERENCE_FAILURE
|
||||
self.add_message('no-member', node=node,
|
||||
args=(owner.display_type(), name,
|
||||
node.attrname),
|
||||
confidence=confidence)
|
||||
|
||||
@check_messages('assignment-from-no-return', 'assignment-from-none')
|
||||
def visit_assign(self, node):
|
||||
"""check that if assigning to a function call, the function is
|
||||
possibly returning something valuable
|
||||
"""
|
||||
if not isinstance(node.value, astroid.CallFunc):
|
||||
return
|
||||
function_node = safe_infer(node.value.func)
|
||||
# skip class, generator and incomplete function definition
|
||||
if not (isinstance(function_node, astroid.Function) and
|
||||
function_node.root().fully_defined()):
|
||||
return
|
||||
if function_node.is_generator() \
|
||||
or function_node.is_abstract(pass_is_abstract=False):
|
||||
return
|
||||
returns = list(function_node.nodes_of_class(astroid.Return,
|
||||
skip_klass=astroid.Function))
|
||||
if len(returns) == 0:
|
||||
self.add_message('assignment-from-no-return', node=node)
|
||||
else:
|
||||
for rnode in returns:
|
||||
if not (isinstance(rnode.value, astroid.Const)
|
||||
and rnode.value.value is None
|
||||
or rnode.value is None):
|
||||
break
|
||||
else:
|
||||
self.add_message('assignment-from-none', node=node)
|
||||
|
||||
def _check_uninferable_callfunc(self, node):
|
||||
"""
|
||||
Check that the given uninferable CallFunc node does not
|
||||
call an actual function.
|
||||
"""
|
||||
if not isinstance(node.func, astroid.Getattr):
|
||||
return
|
||||
|
||||
# Look for properties. First, obtain
|
||||
# the lhs of the Getattr node and search the attribute
|
||||
# there. If that attribute is a property or a subclass of properties,
|
||||
# then most likely it's not callable.
|
||||
|
||||
# TODO: since astroid doesn't understand descriptors very well
|
||||
# we will not handle them here, right now.
|
||||
|
||||
expr = node.func.expr
|
||||
klass = safe_infer(expr)
|
||||
if (klass is None or klass is astroid.YES or
|
||||
not isinstance(klass, astroid.Instance)):
|
||||
return
|
||||
|
||||
try:
|
||||
attrs = klass._proxied.getattr(node.func.attrname)
|
||||
except astroid.NotFoundError:
|
||||
return
|
||||
|
||||
for attr in attrs:
|
||||
if attr is astroid.YES:
|
||||
continue
|
||||
if not isinstance(attr, astroid.Function):
|
||||
continue
|
||||
|
||||
# Decorated, see if it is decorated with a property.
|
||||
# Also, check the returns and see if they are callable.
|
||||
if decorated_with_property(attr):
|
||||
if all(return_node.callable()
|
||||
for return_node in attr.infer_call_result(node)):
|
||||
continue
|
||||
else:
|
||||
self.add_message('not-callable', node=node,
|
||||
args=node.func.as_string())
|
||||
break
|
||||
|
||||
@check_messages(*(list(MSGS.keys())))
|
||||
def visit_callfunc(self, node):
|
||||
"""check that called functions/methods are inferred to callable objects,
|
||||
and that the arguments passed to the function match the parameters in
|
||||
the inferred function's definition
|
||||
"""
|
||||
# Build the set of keyword arguments, checking for duplicate keywords,
|
||||
# and count the positional arguments.
|
||||
keyword_args = set()
|
||||
num_positional_args = 0
|
||||
for arg in node.args:
|
||||
if isinstance(arg, astroid.Keyword):
|
||||
keyword_args.add(arg.arg)
|
||||
else:
|
||||
num_positional_args += 1
|
||||
|
||||
called = safe_infer(node.func)
|
||||
# only function, generator and object defining __call__ are allowed
|
||||
if called is not None and not called.callable():
|
||||
self.add_message('not-callable', node=node,
|
||||
args=node.func.as_string())
|
||||
|
||||
self._check_uninferable_callfunc(node)
|
||||
|
||||
try:
|
||||
called, implicit_args, callable_name = _determine_callable(called)
|
||||
except ValueError:
|
||||
# Any error occurred during determining the function type, most of
|
||||
# those errors are handled by different warnings.
|
||||
return
|
||||
num_positional_args += implicit_args
|
||||
if called.args.args is None:
|
||||
# Built-in functions have no argument information.
|
||||
return
|
||||
|
||||
if len(called.argnames()) != len(set(called.argnames())):
|
||||
# Duplicate parameter name (see E9801). We can't really make sense
|
||||
# of the function call in this case, so just return.
|
||||
return
|
||||
|
||||
# Analyze the list of formal parameters.
|
||||
num_mandatory_parameters = len(called.args.args) - len(called.args.defaults)
|
||||
parameters = []
|
||||
parameter_name_to_index = {}
|
||||
for i, arg in enumerate(called.args.args):
|
||||
if isinstance(arg, astroid.Tuple):
|
||||
name = None
|
||||
# Don't store any parameter names within the tuple, since those
|
||||
# are not assignable from keyword arguments.
|
||||
else:
|
||||
if isinstance(arg, astroid.Keyword):
|
||||
name = arg.arg
|
||||
else:
|
||||
assert isinstance(arg, astroid.AssName)
|
||||
# This occurs with:
|
||||
# def f( (a), (b) ): pass
|
||||
name = arg.name
|
||||
parameter_name_to_index[name] = i
|
||||
if i >= num_mandatory_parameters:
|
||||
defval = called.args.defaults[i - num_mandatory_parameters]
|
||||
else:
|
||||
defval = None
|
||||
parameters.append([(name, defval), False])
|
||||
|
||||
kwparams = {}
|
||||
for i, arg in enumerate(called.args.kwonlyargs):
|
||||
if isinstance(arg, astroid.Keyword):
|
||||
name = arg.arg
|
||||
else:
|
||||
assert isinstance(arg, astroid.AssName)
|
||||
name = arg.name
|
||||
kwparams[name] = [called.args.kw_defaults[i], False]
|
||||
|
||||
# Match the supplied arguments against the function parameters.
|
||||
|
||||
# 1. Match the positional arguments.
|
||||
for i in range(num_positional_args):
|
||||
if i < len(parameters):
|
||||
parameters[i][1] = True
|
||||
elif called.args.vararg is not None:
|
||||
# The remaining positional arguments get assigned to the *args
|
||||
# parameter.
|
||||
break
|
||||
else:
|
||||
# Too many positional arguments.
|
||||
self.add_message('too-many-function-args',
|
||||
node=node, args=(callable_name,))
|
||||
break
|
||||
|
||||
# 2. Match the keyword arguments.
|
||||
for keyword in keyword_args:
|
||||
if keyword in parameter_name_to_index:
|
||||
i = parameter_name_to_index[keyword]
|
||||
if parameters[i][1]:
|
||||
# Duplicate definition of function parameter.
|
||||
self.add_message('redundant-keyword-arg',
|
||||
node=node, args=(keyword, callable_name))
|
||||
else:
|
||||
parameters[i][1] = True
|
||||
elif keyword in kwparams:
|
||||
if kwparams[keyword][1]: # XXX is that even possible?
|
||||
# Duplicate definition of function parameter.
|
||||
self.add_message('redundant-keyword-arg', node=node,
|
||||
args=(keyword, callable_name))
|
||||
else:
|
||||
kwparams[keyword][1] = True
|
||||
elif called.args.kwarg is not None:
|
||||
# The keyword argument gets assigned to the **kwargs parameter.
|
||||
pass
|
||||
else:
|
||||
# Unexpected keyword argument.
|
||||
self.add_message('unexpected-keyword-arg', node=node,
|
||||
args=(keyword, callable_name))
|
||||
|
||||
# 3. Match the *args, if any. Note that Python actually processes
|
||||
# *args _before_ any keyword arguments, but we wait until after
|
||||
# looking at the keyword arguments so as to make a more conservative
|
||||
# guess at how many values are in the *args sequence.
|
||||
if node.starargs is not None:
|
||||
for i in range(num_positional_args, len(parameters)):
|
||||
[(name, defval), assigned] = parameters[i]
|
||||
# Assume that *args provides just enough values for all
|
||||
# non-default parameters after the last parameter assigned by
|
||||
# the positional arguments but before the first parameter
|
||||
# assigned by the keyword arguments. This is the best we can
|
||||
# get without generating any false positives.
|
||||
if (defval is not None) or assigned:
|
||||
break
|
||||
parameters[i][1] = True
|
||||
|
||||
# 4. Match the **kwargs, if any.
|
||||
if node.kwargs is not None:
|
||||
for i, [(name, defval), assigned] in enumerate(parameters):
|
||||
# Assume that *kwargs provides values for all remaining
|
||||
# unassigned named parameters.
|
||||
if name is not None:
|
||||
parameters[i][1] = True
|
||||
else:
|
||||
# **kwargs can't assign to tuples.
|
||||
pass
|
||||
|
||||
# Check that any parameters without a default have been assigned
|
||||
# values.
|
||||
for [(name, defval), assigned] in parameters:
|
||||
if (defval is None) and not assigned:
|
||||
if name is None:
|
||||
display_name = '<tuple>'
|
||||
else:
|
||||
display_name = repr(name)
|
||||
self.add_message('no-value-for-parameter', node=node,
|
||||
args=(display_name, callable_name))
|
||||
|
||||
for name in kwparams:
|
||||
defval, assigned = kwparams[name]
|
||||
if defval is None and not assigned:
|
||||
self.add_message('missing-kwoa', node=node,
|
||||
args=(name, callable_name))
|
||||
|
||||
@check_messages('invalid-sequence-index')
|
||||
def visit_extslice(self, node):
|
||||
# Check extended slice objects as if they were used as a sequence
|
||||
# index to check if the object being sliced can support them
|
||||
return self.visit_index(node)
|
||||
|
||||
@check_messages('invalid-sequence-index')
|
||||
def visit_index(self, node):
|
||||
if not node.parent or not hasattr(node.parent, "value"):
|
||||
return
|
||||
|
||||
# Look for index operations where the parent is a sequence type.
|
||||
# If the types can be determined, only allow indices to be int,
|
||||
# slice or instances with __index__.
|
||||
|
||||
parent_type = safe_infer(node.parent.value)
|
||||
if not isinstance(parent_type, (astroid.Class, astroid.Instance)):
|
||||
return
|
||||
|
||||
# Determine what method on the parent this index will use
|
||||
# The parent of this node will be a Subscript, and the parent of that
|
||||
# node determines if the Subscript is a get, set, or delete operation.
|
||||
operation = node.parent.parent
|
||||
if isinstance(operation, astroid.Assign):
|
||||
methodname = '__setitem__'
|
||||
elif isinstance(operation, astroid.Delete):
|
||||
methodname = '__delitem__'
|
||||
else:
|
||||
methodname = '__getitem__'
|
||||
|
||||
# Check if this instance's __getitem__, __setitem__, or __delitem__, as
|
||||
# appropriate to the statement, is implemented in a builtin sequence
|
||||
# type. This way we catch subclasses of sequence types but skip classes
|
||||
# that override __getitem__ and which may allow non-integer indices.
|
||||
try:
|
||||
methods = parent_type.getattr(methodname)
|
||||
if methods is astroid.YES:
|
||||
return
|
||||
itemmethod = methods[0]
|
||||
except (astroid.NotFoundError, IndexError):
|
||||
return
|
||||
|
||||
if not isinstance(itemmethod, astroid.Function):
|
||||
return
|
||||
if itemmethod.root().name != BUILTINS:
|
||||
return
|
||||
if not itemmethod.parent:
|
||||
return
|
||||
if itemmethod.parent.name not in SEQUENCE_TYPES:
|
||||
return
|
||||
|
||||
# For ExtSlice objects coming from visit_extslice, no further
|
||||
# inference is necessary, since if we got this far the ExtSlice
|
||||
# is an error.
|
||||
if isinstance(node, astroid.ExtSlice):
|
||||
index_type = node
|
||||
else:
|
||||
index_type = safe_infer(node)
|
||||
if index_type is None or index_type is astroid.YES:
|
||||
return
|
||||
|
||||
# Constants must be of type int
|
||||
if isinstance(index_type, astroid.Const):
|
||||
if isinstance(index_type.value, int):
|
||||
return
|
||||
# Instance values must be int, slice, or have an __index__ method
|
||||
elif isinstance(index_type, astroid.Instance):
|
||||
if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'):
|
||||
return
|
||||
try:
|
||||
index_type.getattr('__index__')
|
||||
return
|
||||
except astroid.NotFoundError:
|
||||
pass
|
||||
|
||||
# Anything else is an error
|
||||
self.add_message('invalid-sequence-index', node=node)
|
||||
|
||||
@check_messages('invalid-slice-index')
|
||||
def visit_slice(self, node):
|
||||
# Check the type of each part of the slice
|
||||
for index in (node.lower, node.upper, node.step):
|
||||
if index is None:
|
||||
continue
|
||||
|
||||
index_type = safe_infer(index)
|
||||
if index_type is None or index_type is astroid.YES:
|
||||
continue
|
||||
|
||||
# Constants must of type int or None
|
||||
if isinstance(index_type, astroid.Const):
|
||||
if isinstance(index_type.value, (int, type(None))):
|
||||
continue
|
||||
# Instance values must be of type int, None or an object
|
||||
# with __index__
|
||||
elif isinstance(index_type, astroid.Instance):
|
||||
if index_type.pytype() in (BUILTINS + '.int',
|
||||
BUILTINS + '.NoneType'):
|
||||
continue
|
||||
|
||||
try:
|
||||
index_type.getattr('__index__')
|
||||
return
|
||||
except astroid.NotFoundError:
|
||||
pass
|
||||
|
||||
# Anything else is an error
|
||||
self.add_message('invalid-slice-index', node=node)
|
||||
|
||||
def register(linter):
|
||||
"""required method to auto register this checker """
|
||||
linter.register_checker(TypeChecker(linter))
|
||||
564
plugins/bundle/python-mode/pymode/libs/pylint/checkers/utils.py
Normal file
564
plugins/bundle/python-mode/pymode/libs/pylint/checkers/utils.py
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
# pylint: disable=W0611
|
||||
#
|
||||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""some functions that may be useful for various checkers
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import string
|
||||
|
||||
import astroid
|
||||
from astroid import scoped_nodes
|
||||
from logilab.common.compat import builtins
|
||||
|
||||
BUILTINS_NAME = builtins.__name__
|
||||
COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr
|
||||
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'))
|
||||
|
||||
|
||||
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.AssAttr):
|
||||
return (True, (node.attrname, 'object %r' % (node.expr.as_string(),)))
|
||||
elif isinstance(node, astroid.AssName):
|
||||
name = node.name
|
||||
if is_builtin(name):
|
||||
return (True, (name, 'builtins'))
|
||||
else:
|
||||
stmts = node.lookup(name)[1]
|
||||
if (stmts and not isinstance(stmts[0].ass_type(),
|
||||
(astroid.Assign, astroid.AugAssign,
|
||||
astroid.ExceptHandler))):
|
||||
return (True, (name, 'outer scope (line %s)' % stmts[0].fromlineno))
|
||||
return (False, None)
|
||||
|
||||
|
||||
def safe_infer(node):
|
||||
"""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()
|
||||
value = next(inferit)
|
||||
except astroid.InferenceError:
|
||||
return
|
||||
try:
|
||||
next(inferit)
|
||||
return # None if there is ambiguity on the inferred node
|
||||
except astroid.InferenceError:
|
||||
return # there is some kind of ambiguity
|
||||
except StopIteration:
|
||||
return value
|
||||
|
||||
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
|
||||
|
||||
def is_empty(body):
|
||||
"""return true if the given node does nothing but 'pass'"""
|
||||
return len(body) == 1 and isinstance(body[0], astroid.Pass)
|
||||
|
||||
builtins = builtins.__dict__.copy()
|
||||
SPECIAL_BUILTINS = ('__builtins__',) # '__path__', '__file__')
|
||||
|
||||
def is_builtin_object(node):
|
||||
"""Returns True if the given node is an object from the __builtin__ module."""
|
||||
return node and node.root().name == BUILTINS_NAME
|
||||
|
||||
def is_builtin(name): # was is_native_builtin
|
||||
"""return true if <name> could be considered as a builtin defined by python
|
||||
"""
|
||||
if name in builtins:
|
||||
return True
|
||||
if name in SPECIAL_BUILTINS:
|
||||
return True
|
||||
return False
|
||||
|
||||
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.AssName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
elif isinstance(_node, astroid.For):
|
||||
for ass_node in _node.target.nodes_of_class(astroid.AssName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
elif isinstance(_node, astroid.With):
|
||||
for expr, ids in _node.items:
|
||||
if expr.parent_of(var_node):
|
||||
break
|
||||
if (ids and
|
||||
isinstance(ids, astroid.AssName) and
|
||||
ids.name == varname):
|
||||
return True
|
||||
elif isinstance(_node, (astroid.Lambda, astroid.Function)):
|
||||
if _node.args.is_argument(varname):
|
||||
return True
|
||||
if getattr(_node, 'name', None) == varname:
|
||||
return True
|
||||
break
|
||||
elif isinstance(_node, astroid.ExceptHandler):
|
||||
if isinstance(_node.name, astroid.AssName):
|
||||
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.AssName):
|
||||
if ass_node.name == varname:
|
||||
return True
|
||||
for imp_node in _node.nodes_of_class((astroid.From, astroid.Import)):
|
||||
if varname in [name[1] or name[0] for name in imp_node.names]:
|
||||
return True
|
||||
_node = _node.previous_sibling()
|
||||
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.Function):
|
||||
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) or
|
||||
isinstance(parent, (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 AssName, Tuple or List node
|
||||
"""
|
||||
while node and isinstance(node, (astroid.AssName,
|
||||
astroid.Tuple,
|
||||
astroid.List)):
|
||||
node = node.parent
|
||||
return node
|
||||
|
||||
def overrides_an_abstract_method(class_node, name):
|
||||
"""return True if pnode is a parent of node"""
|
||||
for ancestor in class_node.ancestors():
|
||||
if name in ancestor and isinstance(ancestor[name], astroid.Function) and \
|
||||
ancestor[name].is_abstract(pass_is_abstract=False):
|
||||
return True
|
||||
return False
|
||||
|
||||
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.Function):
|
||||
return True
|
||||
return False
|
||||
|
||||
PYMETHODS = set(('__new__', '__init__', '__del__', '__hash__',
|
||||
'__str__', '__repr__',
|
||||
'__len__', '__iter__',
|
||||
'__delete__', '__get__', '__set__',
|
||||
'__getitem__', '__setitem__', '__delitem__', '__contains__',
|
||||
'__getattribute__', '__getattr__', '__setattr__', '__delattr__',
|
||||
'__call__',
|
||||
'__enter__', '__exit__',
|
||||
'__cmp__', '__ge__', '__gt__', '__le__', '__lt__', '__eq__',
|
||||
'__nonzero__', '__neg__', '__invert__',
|
||||
'__mul__', '__imul__', '__rmul__',
|
||||
'__div__', '__idiv__', '__rdiv__',
|
||||
'__add__', '__iadd__', '__radd__',
|
||||
'__sub__', '__isub__', '__rsub__',
|
||||
'__pow__', '__ipow__', '__rpow__',
|
||||
'__mod__', '__imod__', '__rmod__',
|
||||
'__and__', '__iand__', '__rand__',
|
||||
'__or__', '__ior__', '__ror__',
|
||||
'__xor__', '__ixor__', '__rxor__',
|
||||
# XXX To be continued
|
||||
))
|
||||
|
||||
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 not 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.Class):
|
||||
if klass.parent is None:
|
||||
klass = None
|
||||
else:
|
||||
klass = klass.parent.frame()
|
||||
|
||||
return klass
|
||||
|
||||
def is_super_call(expr):
|
||||
"""return True if expression node is a function call and if function name
|
||||
is super. Check before that you're in a method.
|
||||
"""
|
||||
return (isinstance(expr, astroid.CallFunc) and
|
||||
isinstance(expr.func, astroid.Name) and
|
||||
expr.func.name == 'super')
|
||||
|
||||
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(callfunc_node, position=None, keyword=None):
|
||||
"""Returns the specified argument from a function call.
|
||||
|
||||
:param callfunc_node: Node representing a function call to check.
|
||||
:param int position: position of the argument.
|
||||
:param str keyword: the keyword of the argument.
|
||||
|
||||
:returns: The node representing the argument, None if the argument is not found.
|
||||
:raises ValueError: if both position and keyword are None.
|
||||
:raises NoSuchArgumentError: if no argument at the provided position or with
|
||||
the provided keyword.
|
||||
"""
|
||||
if position is None and keyword is None:
|
||||
raise ValueError('Must specify at least one of: position or keyword.')
|
||||
try:
|
||||
if position is not None and not isinstance(callfunc_node.args[position], astroid.Keyword):
|
||||
return callfunc_node.args[position]
|
||||
except IndexError as error:
|
||||
raise NoSuchArgumentError(error)
|
||||
if keyword:
|
||||
for arg in callfunc_node.args:
|
||||
if isinstance(arg, astroid.Keyword) and arg.arg == keyword:
|
||||
return arg.value
|
||||
raise NoSuchArgumentError
|
||||
|
||||
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
|
||||
return any(inherit_from_std_ex(parent)
|
||||
for parent in node.ancestors(recurs=False))
|
||||
|
||||
def is_import_error(handler):
|
||||
"""
|
||||
Check if the given exception handler catches
|
||||
ImportError.
|
||||
|
||||
:param handler: A node, representing an ExceptHandler node.
|
||||
:returns: True if the handler catches ImportError, False otherwise.
|
||||
"""
|
||||
names = None
|
||||
if isinstance(handler.type, astroid.Tuple):
|
||||
names = [name for name in handler.type.elts
|
||||
if isinstance(name, astroid.Name)]
|
||||
elif isinstance(handler.type, astroid.Name):
|
||||
names = [handler.type]
|
||||
else:
|
||||
# Don't try to infer that.
|
||||
return
|
||||
for name in names:
|
||||
try:
|
||||
for infered in name.infer():
|
||||
if (isinstance(infered, astroid.Class) and
|
||||
inherit_from_std_ex(infered) and
|
||||
infered.name == 'ImportError'):
|
||||
return True
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
|
||||
def has_known_bases(klass):
|
||||
"""Returns 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)
|
||||
# TODO: check for A->B->A->B pattern in class structure too?
|
||||
if (not isinstance(result, astroid.Class) or
|
||||
result is klass or
|
||||
not has_known_bases(result)):
|
||||
klass._all_bases_known = False
|
||||
return False
|
||||
klass._all_bases_known = True
|
||||
return True
|
||||
|
||||
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:
|
||||
for infered in decorator.infer():
|
||||
if isinstance(infered, astroid.Class):
|
||||
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
|
||||
except astroid.InferenceError:
|
||||
pass
|
||||
|
||||
|
||||
def decorated_with_abc(func):
|
||||
"""Determine if the `func` node is decorated with `abc` decorators."""
|
||||
if func.decorators:
|
||||
for node in func.decorators.nodes:
|
||||
try:
|
||||
infered = next(node.infer())
|
||||
except astroid.InferenceError:
|
||||
continue
|
||||
if infered and infered.qname() in ABC_METHODS:
|
||||
return True
|
||||
|
||||
|
||||
def unimplemented_abstract_methods(node, is_abstract_cb=decorated_with_abc):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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.AssName):
|
||||
infered = safe_infer(obj)
|
||||
if not infered:
|
||||
continue
|
||||
if not isinstance(infered, astroid.Function):
|
||||
if obj.name in visited:
|
||||
del visited[obj.name]
|
||||
if isinstance(infered, astroid.Function):
|
||||
# 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
|
||||
1069
plugins/bundle/python-mode/pymode/libs/pylint/checkers/variables.py
Normal file
1069
plugins/bundle/python-mode/pymode/libs/pylint/checkers/variables.py
Normal file
File diff suppressed because it is too large
Load diff
157
plugins/bundle/python-mode/pymode/libs/pylint/config.py
Normal file
157
plugins/bundle/python-mode/pymode/libs/pylint/config.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""utilities for Pylint configuration :
|
||||
|
||||
* pylintrc
|
||||
* pylint.d (PYLINTHOME)
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
from __future__ import print_function
|
||||
|
||||
import pickle
|
||||
import os
|
||||
import sys
|
||||
from os.path import exists, isfile, join, expanduser, abspath, dirname
|
||||
|
||||
# pylint home is used to save old runs results ################################
|
||||
|
||||
USER_HOME = expanduser('~')
|
||||
if 'PYLINTHOME' in os.environ:
|
||||
PYLINT_HOME = os.environ['PYLINTHOME']
|
||||
if USER_HOME == '~':
|
||||
USER_HOME = dirname(PYLINT_HOME)
|
||||
elif USER_HOME == '~':
|
||||
PYLINT_HOME = ".pylint.d"
|
||||
else:
|
||||
PYLINT_HOME = join(USER_HOME, '.pylint.d')
|
||||
|
||||
def get_pdata_path(base_name, recurs):
|
||||
"""return the path of the file which should contain old search data for the
|
||||
given base_name with the given options values
|
||||
"""
|
||||
base_name = base_name.replace(os.sep, '_')
|
||||
return join(PYLINT_HOME, "%s%s%s"%(base_name, recurs, '.stats'))
|
||||
|
||||
def load_results(base):
|
||||
"""try to unpickle and return data from file if it exists and is not
|
||||
corrupted
|
||||
|
||||
return an empty dictionary if it doesn't exists
|
||||
"""
|
||||
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):
|
||||
"""pickle results"""
|
||||
if not 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)
|
||||
|
||||
# location of the configuration file ##########################################
|
||||
|
||||
|
||||
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 exists('pylintrc'):
|
||||
return abspath('pylintrc')
|
||||
if isfile('__init__.py'):
|
||||
curdir = abspath(os.getcwd())
|
||||
while isfile(join(curdir, '__init__.py')):
|
||||
curdir = abspath(join(curdir, '..'))
|
||||
if isfile(join(curdir, 'pylintrc')):
|
||||
return join(curdir, 'pylintrc')
|
||||
if 'PYLINTRC' in os.environ and exists(os.environ['PYLINTRC']):
|
||||
pylintrc = os.environ['PYLINTRC']
|
||||
else:
|
||||
user_home = expanduser('~')
|
||||
if user_home == '~' or user_home == '/root':
|
||||
pylintrc = ".pylintrc"
|
||||
else:
|
||||
pylintrc = join(user_home, '.pylintrc')
|
||||
if not isfile(pylintrc):
|
||||
pylintrc = join(user_home, '.config', 'pylintrc')
|
||||
if not isfile(pylintrc):
|
||||
if 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()
|
||||
|
||||
# evaluation messages #########################################################
|
||||
|
||||
def get_note_message(note):
|
||||
"""return a message according to note
|
||||
note is a float < 10 (10 is the highest note)
|
||||
"""
|
||||
assert note <= 10, "Note is %.2f. Either you cheated, or pylint's \
|
||||
broken!" % note
|
||||
if note < 0:
|
||||
msg = 'You have to do something quick !'
|
||||
elif note < 1:
|
||||
msg = 'Hey! This is really dreadful. Or maybe pylint is buggy?'
|
||||
elif note < 2:
|
||||
msg = "Come on! You can't be proud of this code"
|
||||
elif note < 3:
|
||||
msg = 'Hum... Needs work.'
|
||||
elif note < 4:
|
||||
msg = 'Wouldn\'t you be a bit lazy?'
|
||||
elif note < 5:
|
||||
msg = 'A little more work would make it acceptable.'
|
||||
elif note < 6:
|
||||
msg = 'Just the bare minimum. Give it a bit more polish. '
|
||||
elif note < 7:
|
||||
msg = 'This is okay-ish, but I\'m sure you can do better.'
|
||||
elif note < 8:
|
||||
msg = 'If you commit now, people should not be making nasty \
|
||||
comments about you on c.l.py'
|
||||
elif note < 9:
|
||||
msg = 'That\'s pretty good. Good work mate.'
|
||||
elif note < 10:
|
||||
msg = 'So close to being perfect...'
|
||||
else:
|
||||
msg = 'Wow ! Now this deserves our uttermost respect.\nPlease send \
|
||||
your code to python-projects@logilab.org'
|
||||
return msg
|
||||
177
plugins/bundle/python-mode/pymode/libs/pylint/epylint.py
Normal file
177
plugins/bundle/python-mode/pymode/libs/pylint/epylint.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# -*- 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) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""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 sys, os
|
||||
import os.path as osp
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
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
|
||||
from pylint import lint as lint_mod
|
||||
lint_path = lint_mod.__file__
|
||||
options = options or ['--disable=C,R,I']
|
||||
cmd = [sys.executable, lint_path] + options + [
|
||||
'--msg-template', '{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}',
|
||||
'-r', 'n', child_path]
|
||||
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,
|
||||
script='epylint'):
|
||||
"""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
|
||||
if os.name == 'nt':
|
||||
script += '.bat'
|
||||
command_line = script + ' ' + command_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
|
||||
p = Popen(command_line, shell=True, stdout=stdout, stderr=stderr,
|
||||
env=_get_env(), universal_newlines=True)
|
||||
p.wait()
|
||||
# Return standard output and error
|
||||
if return_std:
|
||||
return (p.stdout, p.stderr)
|
||||
|
||||
|
||||
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()
|
||||
531
plugins/bundle/python-mode/pymode/libs/pylint/gui.py
Normal file
531
plugins/bundle/python-mode/pymode/libs/pylint/gui.py
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Tkinker gui for pylint"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from threading import Thread
|
||||
|
||||
import six
|
||||
|
||||
from six.moves.tkinter import (
|
||||
Tk, Frame, Listbox, Entry, Label, Button, Scrollbar,
|
||||
Checkbutton, Radiobutton, IntVar, StringVar, PanedWindow,
|
||||
TOP, LEFT, RIGHT, BOTTOM, END, X, Y, BOTH, SUNKEN, W,
|
||||
HORIZONTAL, DISABLED, NORMAL, W,
|
||||
)
|
||||
from six.moves.tkinter_tkfiledialog import (
|
||||
askopenfilename, askdirectory,
|
||||
)
|
||||
|
||||
import pylint.lint
|
||||
from pylint.reporters.guireporter import GUIReporter
|
||||
|
||||
HOME = os.path.expanduser('~/')
|
||||
HISTORY = '.pylint-gui-history'
|
||||
COLORS = {'(I)':'green',
|
||||
'(C)':'blue', '(R)':'darkblue',
|
||||
'(W)':'black', '(E)':'darkred',
|
||||
'(F)':'red'}
|
||||
|
||||
|
||||
def convert_to_string(msg):
|
||||
"""make a string representation of a message"""
|
||||
module_object = msg.module
|
||||
if msg.obj:
|
||||
module_object += ".%s" % msg.obj
|
||||
return "(%s) %s [%d]: %s" % (msg.C, module_object, msg.line, msg.msg)
|
||||
|
||||
class BasicStream(object):
|
||||
'''
|
||||
used in gui reporter instead of writing to stdout, it is written to
|
||||
this stream and saved in contents
|
||||
'''
|
||||
def __init__(self, gui):
|
||||
"""init"""
|
||||
self.curline = ""
|
||||
self.gui = gui
|
||||
self.contents = []
|
||||
self.outdict = {}
|
||||
self.currout = None
|
||||
self.next_title = None
|
||||
|
||||
def write(self, text):
|
||||
"""write text to the stream"""
|
||||
if re.match('^--+$', text.strip()) or re.match('^==+$', text.strip()):
|
||||
if self.currout:
|
||||
self.outdict[self.currout].remove(self.next_title)
|
||||
self.outdict[self.currout].pop()
|
||||
self.currout = self.next_title
|
||||
self.outdict[self.currout] = ['']
|
||||
|
||||
if text.strip():
|
||||
self.next_title = text.strip()
|
||||
|
||||
if text.startswith(os.linesep):
|
||||
self.contents.append('')
|
||||
if self.currout:
|
||||
self.outdict[self.currout].append('')
|
||||
self.contents[-1] += text.strip(os.linesep)
|
||||
if self.currout:
|
||||
self.outdict[self.currout][-1] += text.strip(os.linesep)
|
||||
if text.endswith(os.linesep) and text.strip():
|
||||
self.contents.append('')
|
||||
if self.currout:
|
||||
self.outdict[self.currout].append('')
|
||||
|
||||
def fix_contents(self):
|
||||
"""finalize what the contents of the dict should look like before output"""
|
||||
for item in self.outdict:
|
||||
num_empty = self.outdict[item].count('')
|
||||
for _ in range(num_empty):
|
||||
self.outdict[item].remove('')
|
||||
if self.outdict[item]:
|
||||
self.outdict[item].pop(0)
|
||||
|
||||
def output_contents(self):
|
||||
"""output contents of dict to the gui, and set the rating"""
|
||||
self.fix_contents()
|
||||
self.gui.tabs = self.outdict
|
||||
try:
|
||||
self.gui.rating.set(self.outdict['Global evaluation'][0])
|
||||
except KeyError:
|
||||
self.gui.rating.set('Error')
|
||||
self.gui.refresh_results_window()
|
||||
|
||||
#reset stream variables for next run
|
||||
self.contents = []
|
||||
self.outdict = {}
|
||||
self.currout = None
|
||||
self.next_title = None
|
||||
|
||||
|
||||
class LintGui(object):
|
||||
"""Build and control a window to interact with pylint"""
|
||||
|
||||
def __init__(self, root=None):
|
||||
"""init"""
|
||||
self.root = root or Tk()
|
||||
self.root.title('Pylint')
|
||||
#reporter
|
||||
self.reporter = None
|
||||
#message queue for output from reporter
|
||||
self.msg_queue = six.moves.queue.Queue()
|
||||
self.msgs = []
|
||||
self.visible_msgs = []
|
||||
self.filenames = []
|
||||
self.rating = StringVar()
|
||||
self.tabs = {}
|
||||
self.report_stream = BasicStream(self)
|
||||
#gui objects
|
||||
self.lb_messages = None
|
||||
self.showhistory = None
|
||||
self.results = None
|
||||
self.btnRun = None
|
||||
self.information_box = None
|
||||
self.convention_box = None
|
||||
self.refactor_box = None
|
||||
self.warning_box = None
|
||||
self.error_box = None
|
||||
self.fatal_box = None
|
||||
self.txtModule = None
|
||||
self.status = None
|
||||
self.msg_type_dict = None
|
||||
self.init_gui()
|
||||
|
||||
def init_gui(self):
|
||||
"""init helper"""
|
||||
|
||||
window = PanedWindow(self.root, orient="vertical")
|
||||
window.pack(side=TOP, fill=BOTH, expand=True)
|
||||
|
||||
top_pane = Frame(window)
|
||||
window.add(top_pane)
|
||||
mid_pane = Frame(window)
|
||||
window.add(mid_pane)
|
||||
bottom_pane = Frame(window)
|
||||
window.add(bottom_pane)
|
||||
|
||||
#setting up frames
|
||||
top_frame = Frame(top_pane)
|
||||
mid_frame = Frame(top_pane)
|
||||
history_frame = Frame(top_pane)
|
||||
radio_frame = Frame(mid_pane)
|
||||
rating_frame = Frame(mid_pane)
|
||||
res_frame = Frame(mid_pane)
|
||||
check_frame = Frame(bottom_pane)
|
||||
msg_frame = Frame(bottom_pane)
|
||||
btn_frame = Frame(bottom_pane)
|
||||
top_frame.pack(side=TOP, fill=X)
|
||||
mid_frame.pack(side=TOP, fill=X)
|
||||
history_frame.pack(side=TOP, fill=BOTH, expand=True)
|
||||
radio_frame.pack(side=TOP, fill=X)
|
||||
rating_frame.pack(side=TOP, fill=X)
|
||||
res_frame.pack(side=TOP, fill=BOTH, expand=True)
|
||||
check_frame.pack(side=TOP, fill=X)
|
||||
msg_frame.pack(side=TOP, fill=BOTH, expand=True)
|
||||
btn_frame.pack(side=TOP, fill=X)
|
||||
|
||||
# Binding F5 application-wide to run lint
|
||||
self.root.bind('<F5>', self.run_lint)
|
||||
|
||||
#Message ListBox
|
||||
rightscrollbar = Scrollbar(msg_frame)
|
||||
rightscrollbar.pack(side=RIGHT, fill=Y)
|
||||
bottomscrollbar = Scrollbar(msg_frame, orient=HORIZONTAL)
|
||||
bottomscrollbar.pack(side=BOTTOM, fill=X)
|
||||
self.lb_messages = Listbox(
|
||||
msg_frame,
|
||||
yscrollcommand=rightscrollbar.set,
|
||||
xscrollcommand=bottomscrollbar.set,
|
||||
bg="white")
|
||||
self.lb_messages.bind("<Double-Button-1>", self.show_sourcefile)
|
||||
self.lb_messages.pack(expand=True, fill=BOTH)
|
||||
rightscrollbar.config(command=self.lb_messages.yview)
|
||||
bottomscrollbar.config(command=self.lb_messages.xview)
|
||||
|
||||
#History ListBoxes
|
||||
rightscrollbar2 = Scrollbar(history_frame)
|
||||
rightscrollbar2.pack(side=RIGHT, fill=Y)
|
||||
bottomscrollbar2 = Scrollbar(history_frame, orient=HORIZONTAL)
|
||||
bottomscrollbar2.pack(side=BOTTOM, fill=X)
|
||||
self.showhistory = Listbox(
|
||||
history_frame,
|
||||
yscrollcommand=rightscrollbar2.set,
|
||||
xscrollcommand=bottomscrollbar2.set,
|
||||
bg="white")
|
||||
self.showhistory.pack(expand=True, fill=BOTH)
|
||||
rightscrollbar2.config(command=self.showhistory.yview)
|
||||
bottomscrollbar2.config(command=self.showhistory.xview)
|
||||
self.showhistory.bind('<Double-Button-1>', self.select_recent_file)
|
||||
self.set_history_window()
|
||||
|
||||
#status bar
|
||||
self.status = Label(self.root, text="", bd=1, relief=SUNKEN, anchor=W)
|
||||
self.status.pack(side=BOTTOM, fill=X)
|
||||
|
||||
#labelbl_ratingls
|
||||
lbl_rating_label = Label(rating_frame, text='Rating:')
|
||||
lbl_rating_label.pack(side=LEFT)
|
||||
lbl_rating = Label(rating_frame, textvariable=self.rating)
|
||||
lbl_rating.pack(side=LEFT)
|
||||
Label(mid_frame, text='Recently Used:').pack(side=LEFT)
|
||||
Label(top_frame, text='Module or package').pack(side=LEFT)
|
||||
|
||||
#file textbox
|
||||
self.txt_module = Entry(top_frame, background='white')
|
||||
self.txt_module.bind('<Return>', self.run_lint)
|
||||
self.txt_module.pack(side=LEFT, expand=True, fill=X)
|
||||
|
||||
#results box
|
||||
rightscrollbar = Scrollbar(res_frame)
|
||||
rightscrollbar.pack(side=RIGHT, fill=Y)
|
||||
bottomscrollbar = Scrollbar(res_frame, orient=HORIZONTAL)
|
||||
bottomscrollbar.pack(side=BOTTOM, fill=X)
|
||||
self.results = Listbox(
|
||||
res_frame,
|
||||
yscrollcommand=rightscrollbar.set,
|
||||
xscrollcommand=bottomscrollbar.set,
|
||||
bg="white", font="Courier")
|
||||
self.results.pack(expand=True, fill=BOTH, side=BOTTOM)
|
||||
rightscrollbar.config(command=self.results.yview)
|
||||
bottomscrollbar.config(command=self.results.xview)
|
||||
|
||||
#buttons
|
||||
Button(top_frame, text='Open', command=self.file_open).pack(side=LEFT)
|
||||
Button(top_frame, text='Open Package',
|
||||
command=(lambda: self.file_open(package=True))).pack(side=LEFT)
|
||||
|
||||
self.btnRun = Button(top_frame, text='Run', command=self.run_lint)
|
||||
self.btnRun.pack(side=LEFT)
|
||||
Button(btn_frame, text='Quit', command=self.quit).pack(side=BOTTOM)
|
||||
|
||||
#radio buttons
|
||||
self.information_box = IntVar()
|
||||
self.convention_box = IntVar()
|
||||
self.refactor_box = IntVar()
|
||||
self.warning_box = IntVar()
|
||||
self.error_box = IntVar()
|
||||
self.fatal_box = IntVar()
|
||||
i = Checkbutton(check_frame, text="Information", fg=COLORS['(I)'],
|
||||
variable=self.information_box, command=self.refresh_msg_window)
|
||||
c = Checkbutton(check_frame, text="Convention", fg=COLORS['(C)'],
|
||||
variable=self.convention_box, command=self.refresh_msg_window)
|
||||
r = Checkbutton(check_frame, text="Refactor", fg=COLORS['(R)'],
|
||||
variable=self.refactor_box, command=self.refresh_msg_window)
|
||||
w = Checkbutton(check_frame, text="Warning", fg=COLORS['(W)'],
|
||||
variable=self.warning_box, command=self.refresh_msg_window)
|
||||
e = Checkbutton(check_frame, text="Error", fg=COLORS['(E)'],
|
||||
variable=self.error_box, command=self.refresh_msg_window)
|
||||
f = Checkbutton(check_frame, text="Fatal", fg=COLORS['(F)'],
|
||||
variable=self.fatal_box, command=self.refresh_msg_window)
|
||||
i.select()
|
||||
c.select()
|
||||
r.select()
|
||||
w.select()
|
||||
e.select()
|
||||
f.select()
|
||||
i.pack(side=LEFT)
|
||||
c.pack(side=LEFT)
|
||||
r.pack(side=LEFT)
|
||||
w.pack(side=LEFT)
|
||||
e.pack(side=LEFT)
|
||||
f.pack(side=LEFT)
|
||||
|
||||
#check boxes
|
||||
self.box = StringVar()
|
||||
# XXX should be generated
|
||||
report = Radiobutton(
|
||||
radio_frame, text="Report", variable=self.box,
|
||||
value="Report", command=self.refresh_results_window)
|
||||
raw_met = Radiobutton(
|
||||
radio_frame, text="Raw metrics", variable=self.box,
|
||||
value="Raw metrics", command=self.refresh_results_window)
|
||||
dup = Radiobutton(
|
||||
radio_frame, text="Duplication", variable=self.box,
|
||||
value="Duplication", command=self.refresh_results_window)
|
||||
ext = Radiobutton(
|
||||
radio_frame, text="External dependencies",
|
||||
variable=self.box, value="External dependencies",
|
||||
command=self.refresh_results_window)
|
||||
stat = Radiobutton(
|
||||
radio_frame, text="Statistics by type",
|
||||
variable=self.box, value="Statistics by type",
|
||||
command=self.refresh_results_window)
|
||||
msg_cat = Radiobutton(
|
||||
radio_frame, text="Messages by category",
|
||||
variable=self.box, value="Messages by category",
|
||||
command=self.refresh_results_window)
|
||||
msg = Radiobutton(
|
||||
radio_frame, text="Messages", variable=self.box,
|
||||
value="Messages", command=self.refresh_results_window)
|
||||
source_file = Radiobutton(
|
||||
radio_frame, text="Source File", variable=self.box,
|
||||
value="Source File", command=self.refresh_results_window)
|
||||
report.select()
|
||||
report.grid(column=0, row=0, sticky=W)
|
||||
raw_met.grid(column=1, row=0, sticky=W)
|
||||
dup.grid(column=2, row=0, sticky=W)
|
||||
msg.grid(column=3, row=0, sticky=W)
|
||||
stat.grid(column=0, row=1, sticky=W)
|
||||
msg_cat.grid(column=1, row=1, sticky=W)
|
||||
ext.grid(column=2, row=1, sticky=W)
|
||||
source_file.grid(column=3, row=1, sticky=W)
|
||||
|
||||
#dictionary for check boxes and associated error term
|
||||
self.msg_type_dict = {
|
||||
'I': lambda: self.information_box.get() == 1,
|
||||
'C': lambda: self.convention_box.get() == 1,
|
||||
'R': lambda: self.refactor_box.get() == 1,
|
||||
'E': lambda: self.error_box.get() == 1,
|
||||
'W': lambda: self.warning_box.get() == 1,
|
||||
'F': lambda: self.fatal_box.get() == 1
|
||||
}
|
||||
self.txt_module.focus_set()
|
||||
|
||||
|
||||
def select_recent_file(self, event): # pylint: disable=unused-argument
|
||||
"""adds the selected file in the history listbox to the Module box"""
|
||||
if not self.showhistory.size():
|
||||
return
|
||||
|
||||
selected = self.showhistory.curselection()
|
||||
item = self.showhistory.get(selected)
|
||||
#update module
|
||||
self.txt_module.delete(0, END)
|
||||
self.txt_module.insert(0, item)
|
||||
|
||||
def refresh_msg_window(self):
|
||||
"""refresh the message window with current output"""
|
||||
#clear the window
|
||||
self.lb_messages.delete(0, END)
|
||||
self.visible_msgs = []
|
||||
for msg in self.msgs:
|
||||
if self.msg_type_dict.get(msg.C)():
|
||||
self.visible_msgs.append(msg)
|
||||
msg_str = convert_to_string(msg)
|
||||
self.lb_messages.insert(END, msg_str)
|
||||
fg_color = COLORS.get(msg_str[:3], 'black')
|
||||
self.lb_messages.itemconfigure(END, fg=fg_color)
|
||||
|
||||
def refresh_results_window(self):
|
||||
"""refresh the results window with current output"""
|
||||
#clear the window
|
||||
self.results.delete(0, END)
|
||||
try:
|
||||
for res in self.tabs[self.box.get()]:
|
||||
self.results.insert(END, res)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def process_incoming(self):
|
||||
"""process the incoming messages from running pylint"""
|
||||
while self.msg_queue.qsize():
|
||||
try:
|
||||
msg = self.msg_queue.get(0)
|
||||
if msg == "DONE":
|
||||
self.report_stream.output_contents()
|
||||
return False
|
||||
|
||||
#adding message to list of msgs
|
||||
self.msgs.append(msg)
|
||||
|
||||
#displaying msg if message type is selected in check box
|
||||
if self.msg_type_dict.get(msg.C)():
|
||||
self.visible_msgs.append(msg)
|
||||
msg_str = convert_to_string(msg)
|
||||
self.lb_messages.insert(END, msg_str)
|
||||
fg_color = COLORS.get(msg_str[:3], 'black')
|
||||
self.lb_messages.itemconfigure(END, fg=fg_color)
|
||||
|
||||
except six.moves.queue.Empty:
|
||||
pass
|
||||
return True
|
||||
|
||||
def periodic_call(self):
|
||||
"""determine when to unlock the run button"""
|
||||
if self.process_incoming():
|
||||
self.root.after(100, self.periodic_call)
|
||||
else:
|
||||
#enabling button so it can be run again
|
||||
self.btnRun.config(state=NORMAL)
|
||||
|
||||
def mainloop(self):
|
||||
"""launch the mainloop of the application"""
|
||||
self.root.mainloop()
|
||||
|
||||
def quit(self, _=None):
|
||||
"""quit the application"""
|
||||
self.root.quit()
|
||||
|
||||
def halt(self): # pylint: disable=no-self-use
|
||||
"""program halt placeholder"""
|
||||
return
|
||||
|
||||
def file_open(self, package=False, _=None):
|
||||
"""launch a file browser"""
|
||||
if not package:
|
||||
filename = askopenfilename(parent=self.root,
|
||||
filetypes=[('pythonfiles', '*.py'),
|
||||
('allfiles', '*')],
|
||||
title='Select Module')
|
||||
else:
|
||||
filename = askdirectory(title="Select A Folder", mustexist=1)
|
||||
|
||||
if filename == ():
|
||||
return
|
||||
|
||||
self.txt_module.delete(0, END)
|
||||
self.txt_module.insert(0, filename)
|
||||
|
||||
def update_filenames(self):
|
||||
"""update the list of recent filenames"""
|
||||
filename = self.txt_module.get()
|
||||
if not filename:
|
||||
filename = os.getcwd()
|
||||
if filename+'\n' in self.filenames:
|
||||
index = self.filenames.index(filename+'\n')
|
||||
self.filenames.pop(index)
|
||||
|
||||
#ensure only 10 most recent are stored
|
||||
if len(self.filenames) == 10:
|
||||
self.filenames.pop()
|
||||
self.filenames.insert(0, filename+'\n')
|
||||
|
||||
def set_history_window(self):
|
||||
"""update the history window with info from the history file"""
|
||||
#clear the window
|
||||
self.showhistory.delete(0, END)
|
||||
# keep the last 10 most recent files
|
||||
try:
|
||||
view_history = open(HOME+HISTORY, 'r')
|
||||
for hist in view_history.readlines():
|
||||
if not hist in self.filenames:
|
||||
self.filenames.append(hist)
|
||||
self.showhistory.insert(END, hist.split('\n')[0])
|
||||
view_history.close()
|
||||
except IOError:
|
||||
# do nothing since history file will be created later
|
||||
return
|
||||
|
||||
def run_lint(self, _=None):
|
||||
"""launches pylint"""
|
||||
self.update_filenames()
|
||||
self.root.configure(cursor='watch')
|
||||
self.reporter = GUIReporter(self, output=self.report_stream)
|
||||
module = self.txt_module.get()
|
||||
if not module:
|
||||
module = os.getcwd()
|
||||
|
||||
#cleaning up msgs and windows
|
||||
self.msgs = []
|
||||
self.visible_msgs = []
|
||||
self.lb_messages.delete(0, END)
|
||||
self.tabs = {}
|
||||
self.results.delete(0, END)
|
||||
self.btnRun.config(state=DISABLED)
|
||||
|
||||
#setting up a worker thread to run pylint
|
||||
worker = Thread(target=lint_thread, args=(module, self.reporter, self,))
|
||||
self.periodic_call()
|
||||
worker.start()
|
||||
|
||||
# Overwrite the .pylint-gui-history file with all the new recently added files
|
||||
# in order from filenames but only save last 10 files
|
||||
write_history = open(HOME+HISTORY, 'w')
|
||||
write_history.writelines(self.filenames)
|
||||
write_history.close()
|
||||
self.set_history_window()
|
||||
|
||||
self.root.configure(cursor='')
|
||||
|
||||
def show_sourcefile(self, event=None): # pylint: disable=unused-argument
|
||||
selected = self.lb_messages.curselection()
|
||||
if not selected:
|
||||
return
|
||||
|
||||
msg = self.visible_msgs[int(selected[0])]
|
||||
scroll = msg.line - 3
|
||||
if scroll < 0:
|
||||
scroll = 0
|
||||
|
||||
self.tabs["Source File"] = open(msg.path, "r").readlines()
|
||||
self.box.set("Source File")
|
||||
self.refresh_results_window()
|
||||
self.results.yview(scroll)
|
||||
self.results.select_set(msg.line - 1)
|
||||
|
||||
|
||||
def lint_thread(module, reporter, gui):
|
||||
"""thread for pylint"""
|
||||
gui.status.text = "processing module(s)"
|
||||
pylint.lint.Run(args=[module], reporter=reporter, exit=False)
|
||||
gui.msg_queue.put("DONE")
|
||||
|
||||
|
||||
def Run(args):
|
||||
"""launch pylint gui from args"""
|
||||
if args:
|
||||
print('USAGE: pylint-gui\n launch a simple pylint gui using Tk')
|
||||
sys.exit(1)
|
||||
gui = LintGui()
|
||||
gui.mainloop()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
Run(sys.argv[1:])
|
||||
84
plugins/bundle/python-mode/pymode/libs/pylint/interfaces.py
Normal file
84
plugins/bundle/python-mode/pymode/libs/pylint/interfaces.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Interfaces for Pylint objects"""
|
||||
from collections import namedtuple
|
||||
|
||||
from logilab.common.interface import Interface
|
||||
|
||||
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 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 add_message(self, msg_id, location, msg):
|
||||
"""add a message of a given type
|
||||
|
||||
msg_id is a message identifier
|
||||
location is a 3-uple (module, object, line)
|
||||
msg is the actual message
|
||||
"""
|
||||
|
||||
def display_results(self, layout):
|
||||
"""display results encapsulated in the layout tree
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ('IRawChecker', 'IAstroidChecker', 'ITokenChecker', 'IReporter')
|
||||
1397
plugins/bundle/python-mode/pymode/libs/pylint/lint.py
Normal file
1397
plugins/bundle/python-mode/pymode/libs/pylint/lint.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
pyreverse.extensions
|
||||
"""
|
||||
|
||||
__revision__ = "$Id $"
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
# Copyright (c) 2000-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""handle diagram generation options for class diagram or default diagrams
|
||||
"""
|
||||
|
||||
from logilab.common.compat import builtins
|
||||
|
||||
import astroid
|
||||
from astroid.utils import LocalsVisitor
|
||||
|
||||
from pylint.pyreverse.diagrams import PackageDiagram, ClassDiagram
|
||||
|
||||
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:
|
||||
if self.config.classes:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
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.Class)
|
||||
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 astroid.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 astroid.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_class(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_from(self, node):
|
||||
"""visit astroid.From 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 linker: astroid.inspector.Linker(IdGeneratorMixIn, LocalsVisitor)
|
||||
:param project: astroid.manager.Project
|
||||
"""
|
||||
|
||||
# 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,247 @@
|
|||
# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""diagram objects
|
||||
"""
|
||||
|
||||
import astroid
|
||||
from pylint.pyreverse.utils import is_interface, FilterMixIn
|
||||
|
||||
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 = []
|
||||
for node_name, ass_nodes in list(node.instance_attrs_type.items()) + \
|
||||
list(node.locals_type.items()):
|
||||
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.Function) 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.Class) \
|
||||
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.Class)]
|
||||
|
||||
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')
|
||||
124
plugins/bundle/python-mode/pymode/libs/pylint/pyreverse/main.py
Normal file
124
plugins/bundle/python-mode/pymode/libs/pylint/pyreverse/main.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# # Copyright (c) 2000-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""
|
||||
%prog [options] <packages>
|
||||
|
||||
create UML diagrams for classes and modules in <packages>
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys, os
|
||||
from logilab.common.configuration import ConfigurationMixIn
|
||||
from astroid.manager import AstroidManager
|
||||
from astroid.inspector import Linker
|
||||
|
||||
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.")),
|
||||
)
|
||||
# FIXME : quiet mode
|
||||
#( ('quiet',
|
||||
#dict(help='run quietly', action='store_true', short='q')), )
|
||||
|
||||
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()
|
||||
self.manager = AstroidManager()
|
||||
self.register_options_provider(self.manager)
|
||||
args = self.load_command_line_configuration()
|
||||
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 = self.manager.project_from_files(args)
|
||||
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:])
|
||||
132
plugins/bundle/python-mode/pymode/libs/pylint/pyreverse/utils.py
Normal file
132
plugins/bundle/python-mode/pymode/libs/pylint/pyreverse/utils.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Copyright (c) 2002-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""
|
||||
generic classes/functions for pyreverse core/extensions
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
########### 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]
|
||||
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2008-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Utilities for creating VCG and Dot diagrams"""
|
||||
|
||||
from logilab.common.vcgutils import VCGPrinter
|
||||
from logilab.common.graph import DotBackend
|
||||
|
||||
from pylint.pyreverse.utils import is_exception
|
||||
|
||||
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, additionnal_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,133 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""utilities methods and classes for reporters"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import locale
|
||||
import os
|
||||
|
||||
|
||||
from pylint import utils
|
||||
|
||||
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.include_ids = None # Deprecated
|
||||
# self.symbols = None # Deprecated
|
||||
self.section = 0
|
||||
self.out = None
|
||||
self.out_encoding = None
|
||||
self.encode = 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.
|
||||
|
||||
Invokes the legacy add_message API by default."""
|
||||
self.add_message(
|
||||
msg.msg_id, (msg.abspath, msg.module, msg.obj, msg.line, msg.column),
|
||||
msg.msg)
|
||||
|
||||
def add_message(self, msg_id, location, msg):
|
||||
"""Deprecated, do not use."""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_output(self, output=None):
|
||||
"""set output stream"""
|
||||
self.out = output or sys.stdout
|
||||
# py3k streams handle their encoding :
|
||||
if sys.version_info >= (3, 0):
|
||||
self.encode = lambda x: x
|
||||
return
|
||||
|
||||
def encode(string):
|
||||
if not isinstance(string, unicode):
|
||||
return string
|
||||
encoding = (getattr(self.out, 'encoding', None) or
|
||||
locale.getdefaultlocale()[1] 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')
|
||||
self.encode = encode
|
||||
|
||||
def writeln(self, string=''):
|
||||
"""write a line in the output buffer"""
|
||||
print(self.encode(string), file=self.out)
|
||||
|
||||
def display_results(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()
|
||||
|
||||
# Event callbacks
|
||||
|
||||
def on_set_current_module(self, module, filepath):
|
||||
"""starting analyzis of a module"""
|
||||
pass
|
||||
|
||||
def on_close(self, stats, previous_stats):
|
||||
"""global end of analyzis"""
|
||||
pass
|
||||
|
||||
|
||||
class CollectingReporter(BaseReporter):
|
||||
"""collects messages"""
|
||||
|
||||
name = 'collector'
|
||||
|
||||
def __init__(self):
|
||||
BaseReporter.__init__(self)
|
||||
self.messages = []
|
||||
|
||||
def handle_message(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
|
||||
def initialize(linter):
|
||||
"""initialize linter with reporters in this package """
|
||||
utils.register_plugins(linter, __path__[0])
|
||||
Binary file not shown.
|
|
@ -0,0 +1,27 @@
|
|||
""" reporter used by gui.py """
|
||||
|
||||
import sys
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
from logilab.common.ureports import TextWriter
|
||||
|
||||
|
||||
class GUIReporter(BaseReporter):
|
||||
"""saves messages"""
|
||||
|
||||
__implements__ = IReporter
|
||||
extension = ''
|
||||
|
||||
def __init__(self, gui, output=sys.stdout):
|
||||
"""init"""
|
||||
BaseReporter.__init__(self, output)
|
||||
self.gui = gui
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different type and in the context of path"""
|
||||
self.gui.msg_queue.put(msg)
|
||||
|
||||
def _display(self, layout):
|
||||
"""launch layouts display"""
|
||||
TextWriter().format(layout, self.out)
|
||||
101
plugins/bundle/python-mode/pymode/libs/pylint/reporters/html.py
Normal file
101
plugins/bundle/python-mode/pymode/libs/pylint/reporters/html.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""HTML reporter"""
|
||||
|
||||
import itertools
|
||||
import string
|
||||
import sys
|
||||
|
||||
from logilab.common.ureports import HTMLWriter, Section, Table
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
|
||||
|
||||
class HTMLReporter(BaseReporter):
|
||||
"""report messages and layouts in HTML"""
|
||||
|
||||
__implements__ = IReporter
|
||||
name = 'html'
|
||||
extension = 'html'
|
||||
|
||||
def __init__(self, output=sys.stdout):
|
||||
BaseReporter.__init__(self, output)
|
||||
self.msgs = []
|
||||
# Add placeholders for title and parsed messages
|
||||
self.header = None
|
||||
self.msgargs = []
|
||||
|
||||
@staticmethod
|
||||
def _parse_msg_template(msg_template):
|
||||
formatter = string.Formatter()
|
||||
parsed = formatter.parse(msg_template)
|
||||
for item in parsed:
|
||||
if item[1]:
|
||||
yield item[1]
|
||||
|
||||
def _parse_template(self):
|
||||
"""Helper function to parse the message template"""
|
||||
self.header = []
|
||||
if self.linter.config.msg_template:
|
||||
msg_template = self.linter.config.msg_template
|
||||
else:
|
||||
msg_template = '{category}{module}{obj}{line}{column}{msg}'
|
||||
|
||||
_header, _msgs = itertools.tee(self._parse_msg_template(msg_template))
|
||||
self.header = list(_header)
|
||||
self.msgargs = list(_msgs)
|
||||
|
||||
def handle_message(self, msg):
|
||||
"""manage message of different type and in the context of path"""
|
||||
|
||||
# It would be better to do this in init, but currently we do not
|
||||
# have access to the linter (as it is setup in lint.set_reporter()
|
||||
# Therefore we try to parse just the once.
|
||||
if self.header is None:
|
||||
self._parse_template()
|
||||
|
||||
# We want to add the lines given by the template
|
||||
self.msgs += [str(getattr(msg, field)) for field in self.msgargs]
|
||||
|
||||
def set_output(self, output=None):
|
||||
"""set output stream
|
||||
|
||||
messages buffered for old output is processed first"""
|
||||
if self.out and self.msgs:
|
||||
self._display(Section())
|
||||
BaseReporter.set_output(self, output)
|
||||
|
||||
def _display(self, layout):
|
||||
"""launch layouts display
|
||||
|
||||
overridden from BaseReporter to add insert the messages section
|
||||
(in add_message, message is not displayed, just collected so it
|
||||
can be displayed in an html table)
|
||||
"""
|
||||
if self.msgs:
|
||||
# add stored messages to the layout
|
||||
msgs = self.header
|
||||
cols = len(self.header)
|
||||
msgs += self.msgs
|
||||
sect = Section('Messages')
|
||||
layout.append(sect)
|
||||
sect.append(Table(cols=cols, children=msgs, rheaders=1))
|
||||
self.msgs = []
|
||||
HTMLWriter().format(layout, self.out)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(HTMLReporter)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""JSON reporter"""
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import json
|
||||
import sys
|
||||
from cgi import escape
|
||||
|
||||
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, message):
|
||||
"""Manage message of different type and in the context of path."""
|
||||
|
||||
self.messages.append({
|
||||
'type': message.category,
|
||||
'module': message.module,
|
||||
'obj': message.obj,
|
||||
'line': message.line,
|
||||
'column': message.column,
|
||||
'path': message.path,
|
||||
'symbol': message.symbol,
|
||||
'message': escape(message.msg or ''),
|
||||
})
|
||||
|
||||
def _display(self, layout):
|
||||
"""Launch layouts display"""
|
||||
if self.messages:
|
||||
print(json.dumps(self.messages, indent=4), file=self.out)
|
||||
|
||||
|
||||
def register(linter):
|
||||
"""Register the reporter classes with the linter."""
|
||||
linter.register_reporter(JSONReporter)
|
||||
146
plugins/bundle/python-mode/pymode/libs/pylint/reporters/text.py
Normal file
146
plugins/bundle/python-mode/pymode/libs/pylint/reporters/text.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""Plain text reporters:
|
||||
|
||||
:text: the default one grouping messages by module
|
||||
:colorized: an ANSI colorized text reporter
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import warnings
|
||||
|
||||
from logilab.common.ureports import TextWriter
|
||||
from logilab.common.textutils import colorize_ansi
|
||||
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.reporters import BaseReporter
|
||||
import six
|
||||
|
||||
TITLE_UNDERLINES = ['', '=', '-', '.']
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
412
plugins/bundle/python-mode/pymode/libs/pylint/testutils.py
Normal file
412
plugins/bundle/python-mode/pymode/libs/pylint/testutils.py
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""functional/non regression tests for pylint"""
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import unittest
|
||||
import tempfile
|
||||
import tokenize
|
||||
|
||||
from glob import glob
|
||||
from os import linesep, getcwd, sep
|
||||
from os.path import abspath, basename, dirname, isdir, join, splitext
|
||||
|
||||
from astroid import test_utils
|
||||
|
||||
from pylint import checkers
|
||||
from pylint.utils import PyLintASTWalker
|
||||
from pylint.reporters import BaseReporter
|
||||
from pylint.interfaces import IReporter
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
import six
|
||||
from six.moves import StringIO
|
||||
|
||||
|
||||
# Utils
|
||||
|
||||
SYS_VERS_STR = '%d%d%d' % sys.version_info[:3]
|
||||
TITLE_UNDERLINES = ['', '=', '-', '.']
|
||||
PREFIX = abspath(dirname(__file__))
|
||||
PY3K = sys.version_info[0] == 3
|
||||
|
||||
def fix_path():
|
||||
sys.path.insert(0, PREFIX)
|
||||
|
||||
def get_tests_info(input_dir, msg_dir, prefix, suffix):
|
||||
"""get python input examples and output messages
|
||||
|
||||
We use following conventions for input files and messages:
|
||||
for different inputs:
|
||||
test for python >= x.y -> input = <name>_pyxy.py
|
||||
test for python < x.y -> input = <name>_py_xy.py
|
||||
for one input and different messages:
|
||||
message for python >= x.y -> message = <name>_pyxy.txt
|
||||
lower versions -> message with highest num
|
||||
"""
|
||||
result = []
|
||||
for fname in glob(join(input_dir, prefix + '*' + suffix)):
|
||||
infile = basename(fname)
|
||||
fbase = splitext(infile)[0]
|
||||
# filter input files :
|
||||
pyrestr = fbase.rsplit('_py', 1)[-1] # like _26 or 26
|
||||
if pyrestr.isdigit(): # '24', '25'...
|
||||
if SYS_VERS_STR < pyrestr:
|
||||
continue
|
||||
if pyrestr.startswith('_') and pyrestr[1:].isdigit():
|
||||
# skip test for higher python versions
|
||||
if SYS_VERS_STR >= pyrestr[1:]:
|
||||
continue
|
||||
messages = glob(join(msg_dir, fbase + '*.txt'))
|
||||
# the last one will be without ext, i.e. for all or upper versions:
|
||||
if messages:
|
||||
for outfile in sorted(messages, reverse=True):
|
||||
py_rest = outfile.rsplit('_py', 1)[-1][:-4]
|
||||
if py_rest.isdigit() and SYS_VERS_STR >= py_rest:
|
||||
break
|
||||
else:
|
||||
# This will provide an error message indicating the missing filename.
|
||||
outfile = join(msg_dir, fbase + '.txt')
|
||||
result.append((infile, outfile))
|
||||
return result
|
||||
|
||||
|
||||
class TestReporter(BaseReporter):
|
||||
"""reporter storing plain text messages"""
|
||||
|
||||
__implements__ = IReporter
|
||||
|
||||
def __init__(self): # pylint: disable=super-init-not-called
|
||||
|
||||
self.message_ids = {}
|
||||
self.reset()
|
||||
self.path_strip_prefix = getcwd() + sep
|
||||
|
||||
def reset(self):
|
||||
self.out = StringIO()
|
||||
self.messages = []
|
||||
|
||||
def add_message(self, msg_id, location, msg):
|
||||
"""manage message of different type and in the context of path """
|
||||
_, _, obj, line, _ = location
|
||||
self.message_ids[msg_id] = 1
|
||||
if obj:
|
||||
obj = ':%s' % obj
|
||||
sigle = msg_id[0]
|
||||
if PY3K and linesep != '\n':
|
||||
# 2to3 writes os.linesep instead of using
|
||||
# the previosly used line separators
|
||||
msg = msg.replace('\r\n', '\n')
|
||||
self.messages.append('%s:%3s%s: %s' % (sigle, line, obj, msg))
|
||||
|
||||
def finalize(self):
|
||||
self.messages.sort()
|
||||
for msg in self.messages:
|
||||
print(msg, file=self.out)
|
||||
result = self.out.getvalue()
|
||||
self.reset()
|
||||
return result
|
||||
|
||||
def display_results(self, layout):
|
||||
"""ignore layouts"""
|
||||
|
||||
|
||||
class Message(collections.namedtuple('Message',
|
||||
['msg_id', 'line', 'node', 'args'])):
|
||||
def __new__(cls, msg_id, line=None, node=None, args=None):
|
||||
return tuple.__new__(cls, (msg_id, line, node, args))
|
||||
|
||||
|
||||
class UnittestLinter(object):
|
||||
"""A fake linter class to capture checker messages."""
|
||||
# pylint: disable=unused-argument, no-self-use
|
||||
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
self.stats = {}
|
||||
|
||||
def release_messages(self):
|
||||
try:
|
||||
return self._messages
|
||||
finally:
|
||||
self._messages = []
|
||||
|
||||
def add_message(self, msg_id, line=None, node=None, args=None,
|
||||
confidence=None):
|
||||
self._messages.append(Message(msg_id, line, node, args))
|
||||
|
||||
def is_message_enabled(self, *unused_args):
|
||||
return True
|
||||
|
||||
def add_stats(self, **kwargs):
|
||||
for name, value in six.iteritems(kwargs):
|
||||
self.stats[name] = value
|
||||
return self.stats
|
||||
|
||||
@property
|
||||
def options_providers(self):
|
||||
return linter.options_providers
|
||||
|
||||
def set_config(**kwargs):
|
||||
"""Decorator for setting config values on a checker."""
|
||||
def _Wrapper(fun):
|
||||
@functools.wraps(fun)
|
||||
def _Forward(self):
|
||||
for key, value in six.iteritems(kwargs):
|
||||
setattr(self.checker.config, key, value)
|
||||
if isinstance(self, CheckerTestCase):
|
||||
# reopen checker in case, it may be interested in configuration change
|
||||
self.checker.open()
|
||||
fun(self)
|
||||
|
||||
return _Forward
|
||||
return _Wrapper
|
||||
|
||||
|
||||
class CheckerTestCase(unittest.TestCase):
|
||||
"""A base testcase class for unittesting individual checker classes."""
|
||||
CHECKER_CLASS = None
|
||||
CONFIG = {}
|
||||
|
||||
def setUp(self):
|
||||
self.linter = UnittestLinter()
|
||||
self.checker = self.CHECKER_CLASS(self.linter) # pylint: disable=not-callable
|
||||
for key, value in six.iteritems(self.CONFIG):
|
||||
setattr(self.checker.config, key, value)
|
||||
self.checker.open()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertNoMessages(self):
|
||||
"""Assert that no messages are added by the given method."""
|
||||
with self.assertAddsMessages():
|
||||
yield
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertAddsMessages(self, *messages):
|
||||
"""Assert that exactly the given method adds the given messages.
|
||||
|
||||
The list of messages must exactly match *all* the messages added by the
|
||||
method. Additionally, we check to see whether the args in each message can
|
||||
actually be substituted into the message string.
|
||||
"""
|
||||
yield
|
||||
got = self.linter.release_messages()
|
||||
msg = ('Expected messages did not match actual.\n'
|
||||
'Expected:\n%s\nGot:\n%s' % ('\n'.join(repr(m) for m in messages),
|
||||
'\n'.join(repr(m) for m in got)))
|
||||
self.assertEqual(list(messages), got, msg)
|
||||
|
||||
def walk(self, node):
|
||||
"""recursive walk on the given node"""
|
||||
walker = PyLintASTWalker(linter)
|
||||
walker.add_checker(self.checker)
|
||||
walker.walk(node)
|
||||
|
||||
|
||||
# Init
|
||||
test_reporter = TestReporter()
|
||||
linter = PyLinter()
|
||||
linter.set_reporter(test_reporter)
|
||||
linter.config.persistent = 0
|
||||
checkers.initialize(linter)
|
||||
linter.global_set_option('required-attributes', ('__revision__',))
|
||||
|
||||
if linesep != '\n':
|
||||
LINE_RGX = re.compile(linesep)
|
||||
def ulines(string):
|
||||
return LINE_RGX.sub('\n', string)
|
||||
else:
|
||||
def ulines(string):
|
||||
return string
|
||||
|
||||
INFO_TEST_RGX = re.compile(r'^func_i\d\d\d\d$')
|
||||
|
||||
def exception_str(self, ex): # pylint: disable=unused-argument
|
||||
"""function used to replace default __str__ method of exception instances"""
|
||||
return 'in %s\n:: %s' % (ex.file, ', '.join(ex.args))
|
||||
|
||||
# Test classes
|
||||
|
||||
class LintTestUsingModule(unittest.TestCase):
|
||||
INPUT_DIR = None
|
||||
DEFAULT_PACKAGE = 'input'
|
||||
package = DEFAULT_PACKAGE
|
||||
linter = linter
|
||||
module = None
|
||||
depends = None
|
||||
output = None
|
||||
_TEST_TYPE = 'module'
|
||||
maxDiff = None
|
||||
|
||||
def shortDescription(self):
|
||||
values = {'mode' : self._TEST_TYPE,
|
||||
'input': self.module,
|
||||
'pkg': self.package,
|
||||
'cls': self.__class__.__name__}
|
||||
|
||||
if self.package == self.DEFAULT_PACKAGE:
|
||||
msg = '%(mode)s test of input file "%(input)s" (%(cls)s)'
|
||||
else:
|
||||
msg = '%(mode)s test of input file "%(input)s" in "%(pkg)s" (%(cls)s)'
|
||||
return msg % values
|
||||
|
||||
def test_functionality(self):
|
||||
tocheck = [self.package+'.'+self.module]
|
||||
if self.depends:
|
||||
tocheck += [self.package+'.%s' % name.replace('.py', '')
|
||||
for name, _ in self.depends]
|
||||
self._test(tocheck)
|
||||
|
||||
def _check_result(self, got):
|
||||
self.assertMultiLineEqual(self._get_expected().strip()+'\n',
|
||||
got.strip()+'\n')
|
||||
|
||||
def _test(self, tocheck):
|
||||
if INFO_TEST_RGX.match(self.module):
|
||||
self.linter.enable('I')
|
||||
else:
|
||||
self.linter.disable('I')
|
||||
try:
|
||||
self.linter.check(tocheck)
|
||||
except Exception as ex:
|
||||
# need finalization to restore a correct state
|
||||
self.linter.reporter.finalize()
|
||||
ex.file = tocheck
|
||||
print(ex)
|
||||
ex.__str__ = exception_str
|
||||
raise
|
||||
self._check_result(self.linter.reporter.finalize())
|
||||
|
||||
def _has_output(self):
|
||||
return not self.module.startswith('func_noerror_')
|
||||
|
||||
def _get_expected(self):
|
||||
if self._has_output() and self.output:
|
||||
with open(self.output, 'U') as fobj:
|
||||
return fobj.read().strip() + '\n'
|
||||
else:
|
||||
return ''
|
||||
|
||||
class LintTestUsingFile(LintTestUsingModule):
|
||||
|
||||
_TEST_TYPE = 'file'
|
||||
|
||||
def test_functionality(self):
|
||||
importable = join(self.INPUT_DIR, self.module)
|
||||
# python also prefers packages over simple modules.
|
||||
if not isdir(importable):
|
||||
importable += '.py'
|
||||
tocheck = [importable]
|
||||
if self.depends:
|
||||
tocheck += [join(self.INPUT_DIR, name) for name, _file in self.depends]
|
||||
self._test(tocheck)
|
||||
|
||||
class LintTestUpdate(LintTestUsingModule):
|
||||
|
||||
_TEST_TYPE = 'update'
|
||||
|
||||
def _check_result(self, got):
|
||||
if self._has_output():
|
||||
try:
|
||||
expected = self._get_expected()
|
||||
except IOError:
|
||||
expected = ''
|
||||
if got != expected:
|
||||
with open(self.output, 'w') as fobj:
|
||||
fobj.write(got)
|
||||
|
||||
# Callback
|
||||
|
||||
def cb_test_gen(base_class):
|
||||
def call(input_dir, msg_dir, module_file, messages_file, dependencies):
|
||||
# pylint: disable=no-init
|
||||
class LintTC(base_class):
|
||||
module = module_file.replace('.py', '')
|
||||
output = messages_file
|
||||
depends = dependencies or None
|
||||
INPUT_DIR = input_dir
|
||||
MSG_DIR = msg_dir
|
||||
return LintTC
|
||||
return call
|
||||
|
||||
# Main function
|
||||
|
||||
def make_tests(input_dir, msg_dir, filter_rgx, callbacks):
|
||||
"""generate tests classes from test info
|
||||
|
||||
return the list of generated test classes
|
||||
"""
|
||||
if filter_rgx:
|
||||
is_to_run = re.compile(filter_rgx).search
|
||||
else:
|
||||
is_to_run = lambda x: 1
|
||||
tests = []
|
||||
for module_file, messages_file in (
|
||||
get_tests_info(input_dir, msg_dir, 'func_', '')
|
||||
):
|
||||
if not is_to_run(module_file) or module_file.endswith('.pyc'):
|
||||
continue
|
||||
base = module_file.replace('func_', '').replace('.py', '')
|
||||
|
||||
dependencies = get_tests_info(input_dir, msg_dir, base, '.py')
|
||||
|
||||
for callback in callbacks:
|
||||
test = callback(input_dir, msg_dir, module_file, messages_file,
|
||||
dependencies)
|
||||
if test:
|
||||
tests.append(test)
|
||||
return tests
|
||||
|
||||
def tokenize_str(code):
|
||||
return list(tokenize.generate_tokens(StringIO(code).readline))
|
||||
|
||||
@contextlib.contextmanager
|
||||
def create_tempfile(content=None):
|
||||
"""Create a new temporary file.
|
||||
|
||||
If *content* parameter is given, then it will be written
|
||||
in the temporary file, before passing it back.
|
||||
This is a context manager and should be used with a *with* statement.
|
||||
"""
|
||||
# Can't use tempfile.NamedTemporaryFile here
|
||||
# because on Windows the file must be closed before writing to it,
|
||||
# see http://bugs.python.org/issue14243
|
||||
fd, tmp = tempfile.mkstemp()
|
||||
if content:
|
||||
if sys.version_info >= (3, 0):
|
||||
# erff
|
||||
os.write(fd, bytes(content, 'ascii'))
|
||||
else:
|
||||
os.write(fd, content)
|
||||
try:
|
||||
yield tmp
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.remove(tmp)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def create_file_backed_module(code):
|
||||
"""Create an astroid module for the given code, backed by a real file."""
|
||||
with create_tempfile() as temp:
|
||||
module = test_utils.build_module(code)
|
||||
module.file = temp
|
||||
yield module
|
||||
924
plugins/bundle/python-mode/pymode/libs/pylint/utils.py
Normal file
924
plugins/bundle/python-mode/pymode/libs/pylint/utils.py
Normal file
|
|
@ -0,0 +1,924 @@
|
|||
# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
|
||||
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
"""some various utilities and helper classes, most of them used in the
|
||||
main pylint class
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tokenize
|
||||
import warnings
|
||||
from os.path import dirname, basename, splitext, exists, isdir, join, normpath
|
||||
|
||||
import six
|
||||
from six.moves import zip # pylint: disable=redefined-builtin
|
||||
|
||||
from logilab.common.interface import implements
|
||||
from logilab.common.textutils import normalize_text
|
||||
from logilab.common.configuration import rest_format_section
|
||||
from logilab.common.ureports import Section
|
||||
|
||||
from astroid import nodes, Module
|
||||
from astroid.modutils import modpath_from_file, get_module_files, \
|
||||
file_from_modpath, load_module_from_file
|
||||
|
||||
from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED
|
||||
|
||||
|
||||
class UnknownMessage(Exception):
|
||||
"""raised when a unregistered message id is encountered"""
|
||||
|
||||
class EmptyReport(Exception):
|
||||
"""raised when a report is empty and so should not be displayed"""
|
||||
|
||||
|
||||
MSG_TYPES = {
|
||||
'I' : 'info',
|
||||
'C' : 'convention',
|
||||
'R' : 'refactor',
|
||||
'W' : 'warning',
|
||||
'E' : 'error',
|
||||
'F' : 'fatal'
|
||||
}
|
||||
MSG_TYPES_LONG = {v: k for k, v in six.iteritems(MSG_TYPES)}
|
||||
|
||||
MSG_TYPES_STATUS = {
|
||||
'I' : 0,
|
||||
'C' : 16,
|
||||
'R' : 8,
|
||||
'W' : 4,
|
||||
'E' : 2,
|
||||
'F' : 1
|
||||
}
|
||||
|
||||
_MSG_ORDER = 'EWRCIF'
|
||||
MSG_STATE_SCOPE_CONFIG = 0
|
||||
MSG_STATE_SCOPE_MODULE = 1
|
||||
MSG_STATE_CONFIDENCE = 2
|
||||
|
||||
OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)')
|
||||
|
||||
# The line/node distinction does not apply to fatal errors and reports.
|
||||
_SCOPE_EXEMPT = 'FR'
|
||||
|
||||
class WarningScope(object):
|
||||
LINE = 'line-based-msg'
|
||||
NODE = 'node-based-msg'
|
||||
|
||||
_MsgBase = collections.namedtuple(
|
||||
'_MsgBase',
|
||||
['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence',
|
||||
'abspath', 'path', 'module', 'obj', 'line', 'column'])
|
||||
|
||||
|
||||
class Message(_MsgBase):
|
||||
"""This class represent a message to be issued by the reporters"""
|
||||
def __new__(cls, msg_id, symbol, location, msg, confidence):
|
||||
return _MsgBase.__new__(
|
||||
cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]],
|
||||
confidence, *location)
|
||||
|
||||
def format(self, template):
|
||||
"""Format the message according to the given template.
|
||||
|
||||
The template format is the one of the format method :
|
||||
cf. http://docs.python.org/2/library/string.html#formatstrings
|
||||
"""
|
||||
# For some reason, _asdict on derived namedtuples does not work with
|
||||
# Python 3.4. Needs some investigation.
|
||||
return template.format(**dict(zip(self._fields, self)))
|
||||
|
||||
|
||||
def get_module_and_frameid(node):
|
||||
"""return the module name and the frame id in the module"""
|
||||
frame = node.frame()
|
||||
module, obj = '', []
|
||||
while frame:
|
||||
if isinstance(frame, Module):
|
||||
module = frame.name
|
||||
else:
|
||||
obj.append(getattr(frame, 'name', '<lambda>'))
|
||||
try:
|
||||
frame = frame.parent.frame()
|
||||
except AttributeError:
|
||||
frame = None
|
||||
obj.reverse()
|
||||
return module, '.'.join(obj)
|
||||
|
||||
def category_id(cid):
|
||||
cid = cid.upper()
|
||||
if cid in MSG_TYPES:
|
||||
return cid
|
||||
return MSG_TYPES_LONG.get(cid)
|
||||
|
||||
|
||||
def _decoding_readline(stream, module):
|
||||
return lambda: stream.readline().decode(module.file_encoding,
|
||||
'replace')
|
||||
|
||||
|
||||
def tokenize_module(module):
|
||||
with module.stream() as stream:
|
||||
readline = stream.readline
|
||||
if sys.version_info < (3, 0):
|
||||
if module.file_encoding is not None:
|
||||
readline = _decoding_readline(stream, module)
|
||||
return list(tokenize.generate_tokens(readline))
|
||||
return list(tokenize.tokenize(readline))
|
||||
|
||||
def build_message_def(checker, msgid, msg_tuple):
|
||||
if implements(checker, (IRawChecker, ITokenChecker)):
|
||||
default_scope = WarningScope.LINE
|
||||
else:
|
||||
default_scope = WarningScope.NODE
|
||||
options = {}
|
||||
if len(msg_tuple) > 3:
|
||||
(msg, symbol, descr, options) = msg_tuple
|
||||
elif len(msg_tuple) > 2:
|
||||
(msg, symbol, descr) = msg_tuple[:3]
|
||||
else:
|
||||
# messages should have a symbol, but for backward compatibility
|
||||
# they may not.
|
||||
(msg, descr) = msg_tuple
|
||||
warnings.warn("[pylint 0.26] description of message %s doesn't include "
|
||||
"a symbolic name" % msgid, DeprecationWarning)
|
||||
symbol = None
|
||||
options.setdefault('scope', default_scope)
|
||||
return MessageDefinition(checker, msgid, msg, descr, symbol, **options)
|
||||
|
||||
|
||||
class MessageDefinition(object):
|
||||
def __init__(self, checker, msgid, msg, descr, symbol, scope,
|
||||
minversion=None, maxversion=None, old_names=None):
|
||||
self.checker = checker
|
||||
assert len(msgid) == 5, 'Invalid message id %s' % msgid
|
||||
assert msgid[0] in MSG_TYPES, \
|
||||
'Bad message type %s in %r' % (msgid[0], msgid)
|
||||
self.msgid = msgid
|
||||
self.msg = msg
|
||||
self.descr = descr
|
||||
self.symbol = symbol
|
||||
self.scope = scope
|
||||
self.minversion = minversion
|
||||
self.maxversion = maxversion
|
||||
self.old_names = old_names or []
|
||||
|
||||
def may_be_emitted(self):
|
||||
"""return True if message may be emitted using the current interpreter"""
|
||||
if self.minversion is not None and self.minversion > sys.version_info:
|
||||
return False
|
||||
if self.maxversion is not None and self.maxversion <= sys.version_info:
|
||||
return False
|
||||
return True
|
||||
|
||||
def format_help(self, checkerref=False):
|
||||
"""return the help string for the given message id"""
|
||||
desc = self.descr
|
||||
if checkerref:
|
||||
desc += ' This message belongs to the %s checker.' % \
|
||||
self.checker.name
|
||||
title = self.msg
|
||||
if self.symbol:
|
||||
msgid = '%s (%s)' % (self.symbol, self.msgid)
|
||||
else:
|
||||
msgid = self.msgid
|
||||
if self.minversion or self.maxversion:
|
||||
restr = []
|
||||
if self.minversion:
|
||||
restr.append('< %s' % '.'.join([str(n) for n in self.minversion]))
|
||||
if self.maxversion:
|
||||
restr.append('>= %s' % '.'.join([str(n) for n in self.maxversion]))
|
||||
restr = ' or '.join(restr)
|
||||
if checkerref:
|
||||
desc += " It can't be emitted when using Python %s." % restr
|
||||
else:
|
||||
desc += " This message can't be emitted when using Python %s." % restr
|
||||
desc = normalize_text(' '.join(desc.split()), indent=' ')
|
||||
if title != '%s':
|
||||
title = title.splitlines()[0]
|
||||
return ':%s: *%s*\n%s' % (msgid, title, desc)
|
||||
return ':%s:\n%s' % (msgid, desc)
|
||||
|
||||
|
||||
class MessagesHandlerMixIn(object):
|
||||
"""a mix-in class containing all the messages related methods for the main
|
||||
lint class
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._msgs_state = {}
|
||||
self.msg_status = 0
|
||||
|
||||
def _checker_messages(self, checker):
|
||||
for checker in self._checkers[checker.lower()]:
|
||||
for msgid in checker.msgs:
|
||||
yield msgid
|
||||
|
||||
def disable(self, msgid, scope='package', line=None, ignore_unknown=False):
|
||||
"""don't output message of the given id"""
|
||||
assert scope in ('package', 'module')
|
||||
# handle disable=all by disabling all categories
|
||||
if msgid == 'all':
|
||||
for msgid in MSG_TYPES:
|
||||
self.disable(msgid, scope, line)
|
||||
return
|
||||
# msgid is a category?
|
||||
catid = category_id(msgid)
|
||||
if catid is not None:
|
||||
for _msgid in self.msgs_store._msgs_by_category.get(catid):
|
||||
self.disable(_msgid, scope, line)
|
||||
return
|
||||
# msgid is a checker name?
|
||||
if msgid.lower() in self._checkers:
|
||||
msgs_store = self.msgs_store
|
||||
for checker in self._checkers[msgid.lower()]:
|
||||
for _msgid in checker.msgs:
|
||||
if _msgid in msgs_store._alternative_names:
|
||||
self.disable(_msgid, scope, line)
|
||||
return
|
||||
# msgid is report id?
|
||||
if msgid.lower().startswith('rp'):
|
||||
self.disable_report(msgid)
|
||||
return
|
||||
|
||||
try:
|
||||
# msgid is a symbolic or numeric msgid.
|
||||
msg = self.msgs_store.check_message_id(msgid)
|
||||
except UnknownMessage:
|
||||
if ignore_unknown:
|
||||
return
|
||||
raise
|
||||
|
||||
if scope == 'module':
|
||||
self.file_state.set_msg_status(msg, line, False)
|
||||
if msg.symbol != 'locally-disabled':
|
||||
self.add_message('locally-disabled', line=line,
|
||||
args=(msg.symbol, msg.msgid))
|
||||
|
||||
else:
|
||||
msgs = self._msgs_state
|
||||
msgs[msg.msgid] = False
|
||||
# sync configuration object
|
||||
self.config.disable = [mid for mid, val in six.iteritems(msgs)
|
||||
if not val]
|
||||
|
||||
def enable(self, msgid, scope='package', line=None, ignore_unknown=False):
|
||||
"""reenable message of the given id"""
|
||||
assert scope in ('package', 'module')
|
||||
catid = category_id(msgid)
|
||||
# msgid is a category?
|
||||
if catid is not None:
|
||||
for msgid in self.msgs_store._msgs_by_category.get(catid):
|
||||
self.enable(msgid, scope, line)
|
||||
return
|
||||
# msgid is a checker name?
|
||||
if msgid.lower() in self._checkers:
|
||||
for checker in self._checkers[msgid.lower()]:
|
||||
for msgid_ in checker.msgs:
|
||||
self.enable(msgid_, scope, line)
|
||||
return
|
||||
# msgid is report id?
|
||||
if msgid.lower().startswith('rp'):
|
||||
self.enable_report(msgid)
|
||||
return
|
||||
|
||||
try:
|
||||
# msgid is a symbolic or numeric msgid.
|
||||
msg = self.msgs_store.check_message_id(msgid)
|
||||
except UnknownMessage:
|
||||
if ignore_unknown:
|
||||
return
|
||||
raise
|
||||
|
||||
if scope == 'module':
|
||||
self.file_state.set_msg_status(msg, line, True)
|
||||
self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid))
|
||||
else:
|
||||
msgs = self._msgs_state
|
||||
msgs[msg.msgid] = True
|
||||
# sync configuration object
|
||||
self.config.enable = [mid for mid, val in six.iteritems(msgs) if val]
|
||||
|
||||
def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED):
|
||||
"""Returns the scope at which a message was enabled/disabled."""
|
||||
if self.config.confidence and confidence.name not in self.config.confidence:
|
||||
return MSG_STATE_CONFIDENCE
|
||||
try:
|
||||
if line in self.file_state._module_msgs_state[msgid]:
|
||||
return MSG_STATE_SCOPE_MODULE
|
||||
except (KeyError, TypeError):
|
||||
return MSG_STATE_SCOPE_CONFIG
|
||||
|
||||
def is_message_enabled(self, msg_descr, line=None, confidence=None):
|
||||
"""return true if the message associated to the given message id is
|
||||
enabled
|
||||
|
||||
msgid may be either a numeric or symbolic message id.
|
||||
"""
|
||||
if self.config.confidence and confidence:
|
||||
if confidence.name not in self.config.confidence:
|
||||
return False
|
||||
try:
|
||||
msgid = self.msgs_store.check_message_id(msg_descr).msgid
|
||||
except UnknownMessage:
|
||||
# The linter checks for messages that are not registered
|
||||
# due to version mismatch, just treat them as message IDs
|
||||
# for now.
|
||||
msgid = msg_descr
|
||||
if line is None:
|
||||
return self._msgs_state.get(msgid, True)
|
||||
try:
|
||||
return self.file_state._module_msgs_state[msgid][line]
|
||||
except KeyError:
|
||||
return self._msgs_state.get(msgid, True)
|
||||
|
||||
def add_message(self, msg_descr, line=None, node=None, args=None, confidence=UNDEFINED):
|
||||
"""Adds a message given by ID or name.
|
||||
|
||||
If provided, the message string is expanded using args
|
||||
|
||||
AST checkers should must the node argument (but may optionally
|
||||
provide line if the line number is different), raw and token checkers
|
||||
must provide the line argument.
|
||||
"""
|
||||
msg_info = self.msgs_store.check_message_id(msg_descr)
|
||||
msgid = msg_info.msgid
|
||||
# backward compatibility, message may not have a symbol
|
||||
symbol = msg_info.symbol or msgid
|
||||
# Fatal messages and reports are special, the node/scope distinction
|
||||
# does not apply to them.
|
||||
if msgid[0] not in _SCOPE_EXEMPT:
|
||||
if msg_info.scope == WarningScope.LINE:
|
||||
assert node is None and line is not None, (
|
||||
'Message %s must only provide line, got line=%s, node=%s' % (msgid, line, node))
|
||||
elif msg_info.scope == WarningScope.NODE:
|
||||
# Node-based warnings may provide an override line.
|
||||
assert node is not None, 'Message %s must provide Node, got None'
|
||||
|
||||
if line is None and node is not None:
|
||||
line = node.fromlineno
|
||||
if hasattr(node, 'col_offset'):
|
||||
col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
|
||||
else:
|
||||
col_offset = None
|
||||
# should this message be displayed
|
||||
if not self.is_message_enabled(msgid, line, confidence):
|
||||
self.file_state.handle_ignored_message(
|
||||
self.get_message_state_scope(msgid, line, confidence),
|
||||
msgid, line, node, args, confidence)
|
||||
return
|
||||
# update stats
|
||||
msg_cat = MSG_TYPES[msgid[0]]
|
||||
self.msg_status |= MSG_TYPES_STATUS[msgid[0]]
|
||||
self.stats[msg_cat] += 1
|
||||
self.stats['by_module'][self.current_name][msg_cat] += 1
|
||||
try:
|
||||
self.stats['by_msg'][symbol] += 1
|
||||
except KeyError:
|
||||
self.stats['by_msg'][symbol] = 1
|
||||
# expand message ?
|
||||
msg = msg_info.msg
|
||||
if args:
|
||||
msg %= args
|
||||
# get module and object
|
||||
if node is None:
|
||||
module, obj = self.current_name, ''
|
||||
abspath = self.current_file
|
||||
else:
|
||||
module, obj = get_module_and_frameid(node)
|
||||
abspath = node.root().file
|
||||
path = abspath.replace(self.reporter.path_strip_prefix, '')
|
||||
# add the message
|
||||
self.reporter.handle_message(
|
||||
Message(msgid, symbol,
|
||||
(abspath, path, module, obj, line or 1, col_offset or 0), msg, confidence))
|
||||
|
||||
def print_full_documentation(self):
|
||||
"""output a full documentation in ReST format"""
|
||||
print("Pylint global options and switches")
|
||||
print("----------------------------------")
|
||||
print("")
|
||||
print("Pylint provides global options and switches.")
|
||||
print("")
|
||||
|
||||
by_checker = {}
|
||||
for checker in self.get_checkers():
|
||||
if checker.name == 'master':
|
||||
if checker.options:
|
||||
for section, options in checker.options_by_section():
|
||||
if section is None:
|
||||
title = 'General options'
|
||||
else:
|
||||
title = '%s options' % section.capitalize()
|
||||
print(title)
|
||||
print('~' * len(title))
|
||||
rest_format_section(sys.stdout, None, options)
|
||||
print("")
|
||||
else:
|
||||
try:
|
||||
by_checker[checker.name][0] += checker.options_and_values()
|
||||
by_checker[checker.name][1].update(checker.msgs)
|
||||
by_checker[checker.name][2] += checker.reports
|
||||
except KeyError:
|
||||
by_checker[checker.name] = [list(checker.options_and_values()),
|
||||
dict(checker.msgs),
|
||||
list(checker.reports)]
|
||||
|
||||
print("Pylint checkers' options and switches")
|
||||
print("-------------------------------------")
|
||||
print("")
|
||||
print("Pylint checkers can provide three set of features:")
|
||||
print("")
|
||||
print("* options that control their execution,")
|
||||
print("* messages that they can raise,")
|
||||
print("* reports that they can generate.")
|
||||
print("")
|
||||
print("Below is a list of all checkers and their features.")
|
||||
print("")
|
||||
|
||||
for checker, (options, msgs, reports) in six.iteritems(by_checker):
|
||||
title = '%s checker' % (checker.replace("_", " ").title())
|
||||
print(title)
|
||||
print('~' * len(title))
|
||||
print("")
|
||||
print("Verbatim name of the checker is ``%s``." % checker)
|
||||
print("")
|
||||
if options:
|
||||
title = 'Options'
|
||||
print(title)
|
||||
print('^' * len(title))
|
||||
rest_format_section(sys.stdout, None, options)
|
||||
print("")
|
||||
if msgs:
|
||||
title = 'Messages'
|
||||
print(title)
|
||||
print('~' * len(title))
|
||||
for msgid, msg in sorted(six.iteritems(msgs),
|
||||
key=lambda kv: (_MSG_ORDER.index(kv[0][0]), kv[1])):
|
||||
msg = build_message_def(checker, msgid, msg)
|
||||
print(msg.format_help(checkerref=False))
|
||||
print("")
|
||||
if reports:
|
||||
title = 'Reports'
|
||||
print(title)
|
||||
print('~' * len(title))
|
||||
for report in reports:
|
||||
print(':%s: %s' % report[:2])
|
||||
print("")
|
||||
print("")
|
||||
|
||||
|
||||
class FileState(object):
|
||||
"""Hold internal state specific to the currently analyzed file"""
|
||||
|
||||
def __init__(self, modname=None):
|
||||
self.base_name = modname
|
||||
self._module_msgs_state = {}
|
||||
self._raw_module_msgs_state = {}
|
||||
self._ignored_msgs = collections.defaultdict(set)
|
||||
self._suppression_mapping = {}
|
||||
|
||||
def collect_block_lines(self, msgs_store, module_node):
|
||||
"""Walk the AST to collect block level options line numbers."""
|
||||
for msg, lines in six.iteritems(self._module_msgs_state):
|
||||
self._raw_module_msgs_state[msg] = lines.copy()
|
||||
orig_state = self._module_msgs_state.copy()
|
||||
self._module_msgs_state = {}
|
||||
self._suppression_mapping = {}
|
||||
self._collect_block_lines(msgs_store, module_node, orig_state)
|
||||
|
||||
def _collect_block_lines(self, msgs_store, node, msg_state):
|
||||
"""Recursivly walk (depth first) AST to collect block level options line
|
||||
numbers.
|
||||
"""
|
||||
for child in node.get_children():
|
||||
self._collect_block_lines(msgs_store, child, msg_state)
|
||||
first = node.fromlineno
|
||||
last = node.tolineno
|
||||
# first child line number used to distinguish between disable
|
||||
# which are the first child of scoped node with those defined later.
|
||||
# For instance in the code below:
|
||||
#
|
||||
# 1. def meth8(self):
|
||||
# 2. """test late disabling"""
|
||||
# 3. # pylint: disable=E1102
|
||||
# 4. print self.blip
|
||||
# 5. # pylint: disable=E1101
|
||||
# 6. print self.bla
|
||||
#
|
||||
# E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
|
||||
#
|
||||
# this is necessary to disable locally messages applying to class /
|
||||
# function using their fromlineno
|
||||
if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and node.body:
|
||||
firstchildlineno = node.body[0].fromlineno
|
||||
else:
|
||||
firstchildlineno = last
|
||||
for msgid, lines in six.iteritems(msg_state):
|
||||
for lineno, state in list(lines.items()):
|
||||
original_lineno = lineno
|
||||
if first <= lineno <= last:
|
||||
# Set state for all lines for this block, if the
|
||||
# warning is applied to nodes.
|
||||
if msgs_store.check_message_id(msgid).scope == WarningScope.NODE:
|
||||
if lineno > firstchildlineno:
|
||||
state = True
|
||||
first_, last_ = node.block_range(lineno)
|
||||
else:
|
||||
first_ = lineno
|
||||
last_ = last
|
||||
for line in range(first_, last_+1):
|
||||
# do not override existing entries
|
||||
if not line in self._module_msgs_state.get(msgid, ()):
|
||||
if line in lines: # state change in the same block
|
||||
state = lines[line]
|
||||
original_lineno = line
|
||||
if not state:
|
||||
self._suppression_mapping[(msgid, line)] = original_lineno
|
||||
try:
|
||||
self._module_msgs_state[msgid][line] = state
|
||||
except KeyError:
|
||||
self._module_msgs_state[msgid] = {line: state}
|
||||
del lines[lineno]
|
||||
|
||||
def set_msg_status(self, msg, line, status):
|
||||
"""Set status (enabled/disable) for a given message at a given line"""
|
||||
assert line > 0
|
||||
try:
|
||||
self._module_msgs_state[msg.msgid][line] = status
|
||||
except KeyError:
|
||||
self._module_msgs_state[msg.msgid] = {line: status}
|
||||
|
||||
def handle_ignored_message(self, state_scope, msgid, line,
|
||||
node, args, confidence): # pylint: disable=unused-argument
|
||||
"""Report an ignored message.
|
||||
|
||||
state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
|
||||
depending on whether the message was disabled locally in the module,
|
||||
or globally. The other arguments are the same as for add_message.
|
||||
"""
|
||||
if state_scope == MSG_STATE_SCOPE_MODULE:
|
||||
try:
|
||||
orig_line = self._suppression_mapping[(msgid, line)]
|
||||
self._ignored_msgs[(msgid, orig_line)].add(line)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def iter_spurious_suppression_messages(self, msgs_store):
|
||||
for warning, lines in six.iteritems(self._raw_module_msgs_state):
|
||||
for line, enable in six.iteritems(lines):
|
||||
if not enable and (warning, line) not in self._ignored_msgs:
|
||||
yield 'useless-suppression', line, \
|
||||
(msgs_store.get_msg_display_string(warning),)
|
||||
# don't use iteritems here, _ignored_msgs may be modified by add_message
|
||||
for (warning, from_), lines in list(self._ignored_msgs.items()):
|
||||
for line in lines:
|
||||
yield 'suppressed-message', line, \
|
||||
(msgs_store.get_msg_display_string(warning), from_)
|
||||
|
||||
|
||||
class MessagesStore(object):
|
||||
"""The messages store knows information about every possible message but has
|
||||
no particular state during analysis.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Primary registry for all active messages (i.e. all messages
|
||||
# that can be emitted by pylint for the underlying Python
|
||||
# version). It contains the 1:1 mapping from symbolic names
|
||||
# to message definition objects.
|
||||
self._messages = {}
|
||||
# Maps alternative names (numeric IDs, deprecated names) to
|
||||
# message definitions. May contain several names for each definition
|
||||
# object.
|
||||
self._alternative_names = {}
|
||||
self._msgs_by_category = collections.defaultdict(list)
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
"""The list of all active messages."""
|
||||
return six.itervalues(self._messages)
|
||||
|
||||
def add_renamed_message(self, old_id, old_symbol, new_symbol):
|
||||
"""Register the old ID and symbol for a warning that was renamed.
|
||||
|
||||
This allows users to keep using the old ID/symbol in suppressions.
|
||||
"""
|
||||
msg = self.check_message_id(new_symbol)
|
||||
msg.old_names.append((old_id, old_symbol))
|
||||
self._alternative_names[old_id] = msg
|
||||
self._alternative_names[old_symbol] = msg
|
||||
|
||||
def register_messages(self, checker):
|
||||
"""register a dictionary of messages
|
||||
|
||||
Keys are message ids, values are a 2-uple with the message type and the
|
||||
message itself
|
||||
|
||||
message ids should be a string of len 4, where the two first characters
|
||||
are the checker id and the two last the message id in this checker
|
||||
"""
|
||||
chkid = None
|
||||
for msgid, msg_tuple in six.iteritems(checker.msgs):
|
||||
msg = build_message_def(checker, msgid, msg_tuple)
|
||||
assert msg.symbol not in self._messages, \
|
||||
'Message symbol %r is already defined' % msg.symbol
|
||||
# avoid duplicate / malformed ids
|
||||
assert msg.msgid not in self._alternative_names, \
|
||||
'Message id %r is already defined' % msgid
|
||||
assert chkid is None or chkid == msg.msgid[1:3], \
|
||||
'Inconsistent checker part in message id %r' % msgid
|
||||
chkid = msg.msgid[1:3]
|
||||
self._messages[msg.symbol] = msg
|
||||
self._alternative_names[msg.msgid] = msg
|
||||
for old_id, old_symbol in msg.old_names:
|
||||
self._alternative_names[old_id] = msg
|
||||
self._alternative_names[old_symbol] = msg
|
||||
self._msgs_by_category[msg.msgid[0]].append(msg.msgid)
|
||||
|
||||
def check_message_id(self, msgid):
|
||||
"""returns the Message object for this message.
|
||||
|
||||
msgid may be either a numeric or symbolic id.
|
||||
|
||||
Raises UnknownMessage if the message id is not defined.
|
||||
"""
|
||||
if msgid[1:].isdigit():
|
||||
msgid = msgid.upper()
|
||||
for source in (self._alternative_names, self._messages):
|
||||
try:
|
||||
return source[msgid]
|
||||
except KeyError:
|
||||
pass
|
||||
raise UnknownMessage('No such message id %s' % msgid)
|
||||
|
||||
def get_msg_display_string(self, msgid):
|
||||
"""Generates a user-consumable representation of a message.
|
||||
|
||||
Can be just the message ID or the ID and the symbol.
|
||||
"""
|
||||
return repr(self.check_message_id(msgid).symbol)
|
||||
|
||||
def help_message(self, msgids):
|
||||
"""display help messages for the given message identifiers"""
|
||||
for msgid in msgids:
|
||||
try:
|
||||
print(self.check_message_id(msgid).format_help(checkerref=True))
|
||||
print("")
|
||||
except UnknownMessage as ex:
|
||||
print(ex)
|
||||
print("")
|
||||
continue
|
||||
|
||||
def list_messages(self):
|
||||
"""output full messages list documentation in ReST format"""
|
||||
msgs = sorted(six.itervalues(self._messages), key=lambda msg: msg.msgid)
|
||||
for msg in msgs:
|
||||
if not msg.may_be_emitted():
|
||||
continue
|
||||
print(msg.format_help(checkerref=False))
|
||||
print("")
|
||||
|
||||
|
||||
class ReportsHandlerMixIn(object):
|
||||
"""a mix-in class containing all the reports and stats manipulation
|
||||
related methods for the main lint class
|
||||
"""
|
||||
def __init__(self):
|
||||
self._reports = collections.defaultdict(list)
|
||||
self._reports_state = {}
|
||||
|
||||
def report_order(self):
|
||||
""" Return a list of reports, sorted in the order
|
||||
in which they must be called.
|
||||
"""
|
||||
return list(self._reports)
|
||||
|
||||
def register_report(self, reportid, r_title, r_cb, checker):
|
||||
"""register a report
|
||||
|
||||
reportid is the unique identifier for the report
|
||||
r_title the report's title
|
||||
r_cb the method to call to make the report
|
||||
checker is the checker defining the report
|
||||
"""
|
||||
reportid = reportid.upper()
|
||||
self._reports[checker].append((reportid, r_title, r_cb))
|
||||
|
||||
def enable_report(self, reportid):
|
||||
"""disable the report of the given id"""
|
||||
reportid = reportid.upper()
|
||||
self._reports_state[reportid] = True
|
||||
|
||||
def disable_report(self, reportid):
|
||||
"""disable the report of the given id"""
|
||||
reportid = reportid.upper()
|
||||
self._reports_state[reportid] = False
|
||||
|
||||
def report_is_enabled(self, reportid):
|
||||
"""return true if the report associated to the given identifier is
|
||||
enabled
|
||||
"""
|
||||
return self._reports_state.get(reportid, True)
|
||||
|
||||
def make_reports(self, stats, old_stats):
|
||||
"""render registered reports"""
|
||||
sect = Section('Report',
|
||||
'%s statements analysed.'% (self.stats['statement']))
|
||||
for checker in self.report_order():
|
||||
for reportid, r_title, r_cb in self._reports[checker]:
|
||||
if not self.report_is_enabled(reportid):
|
||||
continue
|
||||
report_sect = Section(r_title)
|
||||
try:
|
||||
r_cb(report_sect, stats, old_stats)
|
||||
except EmptyReport:
|
||||
continue
|
||||
report_sect.report_id = reportid
|
||||
sect.append(report_sect)
|
||||
return sect
|
||||
|
||||
def add_stats(self, **kwargs):
|
||||
"""add some stats entries to the statistic dictionary
|
||||
raise an AssertionError if there is a key conflict
|
||||
"""
|
||||
for key, value in six.iteritems(kwargs):
|
||||
if key[-1] == '_':
|
||||
key = key[:-1]
|
||||
assert key not in self.stats
|
||||
self.stats[key] = value
|
||||
return self.stats
|
||||
|
||||
|
||||
def expand_modules(files_or_modules, black_list):
|
||||
"""take a list of files/modules/packages and return the list of tuple
|
||||
(file, module name) which have to be actually checked
|
||||
"""
|
||||
result = []
|
||||
errors = []
|
||||
for something in files_or_modules:
|
||||
if exists(something):
|
||||
# this is a file or a directory
|
||||
try:
|
||||
modname = '.'.join(modpath_from_file(something))
|
||||
except ImportError:
|
||||
modname = splitext(basename(something))[0]
|
||||
if isdir(something):
|
||||
filepath = join(something, '__init__.py')
|
||||
else:
|
||||
filepath = something
|
||||
else:
|
||||
# suppose it's a module or package
|
||||
modname = something
|
||||
try:
|
||||
filepath = file_from_modpath(modname.split('.'))
|
||||
if filepath is None:
|
||||
errors.append({'key' : 'ignored-builtin-module', 'mod': modname})
|
||||
continue
|
||||
except (ImportError, SyntaxError) as ex:
|
||||
# FIXME p3k : the SyntaxError is a Python bug and should be
|
||||
# removed as soon as possible http://bugs.python.org/issue10588
|
||||
errors.append({'key': 'fatal', 'mod': modname, 'ex': ex})
|
||||
continue
|
||||
filepath = normpath(filepath)
|
||||
result.append({'path': filepath, 'name': modname, 'isarg': True,
|
||||
'basepath': filepath, 'basename': modname})
|
||||
if not (modname.endswith('.__init__') or modname == '__init__') \
|
||||
and '__init__.py' in filepath:
|
||||
for subfilepath in get_module_files(dirname(filepath), black_list):
|
||||
if filepath == subfilepath:
|
||||
continue
|
||||
submodname = '.'.join(modpath_from_file(subfilepath))
|
||||
result.append({'path': subfilepath, 'name': submodname,
|
||||
'isarg': False,
|
||||
'basepath': filepath, 'basename': modname})
|
||||
return result, errors
|
||||
|
||||
|
||||
class PyLintASTWalker(object):
|
||||
|
||||
def __init__(self, linter):
|
||||
# callbacks per node types
|
||||
self.nbstatements = 1
|
||||
self.visit_events = collections.defaultdict(list)
|
||||
self.leave_events = collections.defaultdict(list)
|
||||
self.linter = linter
|
||||
|
||||
def _is_method_enabled(self, method):
|
||||
if not hasattr(method, 'checks_msgs'):
|
||||
return True
|
||||
for msg_desc in method.checks_msgs:
|
||||
if self.linter.is_message_enabled(msg_desc):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_checker(self, checker):
|
||||
"""walk to the checker's dir and collect visit and leave methods"""
|
||||
# XXX : should be possible to merge needed_checkers and add_checker
|
||||
vcids = set()
|
||||
lcids = set()
|
||||
visits = self.visit_events
|
||||
leaves = self.leave_events
|
||||
for member in dir(checker):
|
||||
cid = member[6:]
|
||||
if cid == 'default':
|
||||
continue
|
||||
if member.startswith('visit_'):
|
||||
v_meth = getattr(checker, member)
|
||||
# don't use visit_methods with no activated message:
|
||||
if self._is_method_enabled(v_meth):
|
||||
visits[cid].append(v_meth)
|
||||
vcids.add(cid)
|
||||
elif member.startswith('leave_'):
|
||||
l_meth = getattr(checker, member)
|
||||
# don't use leave_methods with no activated message:
|
||||
if self._is_method_enabled(l_meth):
|
||||
leaves[cid].append(l_meth)
|
||||
lcids.add(cid)
|
||||
visit_default = getattr(checker, 'visit_default', None)
|
||||
if visit_default:
|
||||
for cls in nodes.ALL_NODE_CLASSES:
|
||||
cid = cls.__name__.lower()
|
||||
if cid not in vcids:
|
||||
visits[cid].append(visit_default)
|
||||
# for now we have no "leave_default" method in Pylint
|
||||
|
||||
def walk(self, astroid):
|
||||
"""call visit events of astroid checkers for the given node, recurse on
|
||||
its children, then leave events.
|
||||
"""
|
||||
cid = astroid.__class__.__name__.lower()
|
||||
if astroid.is_statement:
|
||||
self.nbstatements += 1
|
||||
# generate events for this node on each checker
|
||||
for cb in self.visit_events.get(cid, ()):
|
||||
cb(astroid)
|
||||
# recurse on children
|
||||
for child in astroid.get_children():
|
||||
self.walk(child)
|
||||
for cb in self.leave_events.get(cid, ()):
|
||||
cb(astroid)
|
||||
|
||||
|
||||
PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll')
|
||||
|
||||
def register_plugins(linter, directory):
|
||||
"""load all module and package in the given directory, looking for a
|
||||
'register' function in each one, used to register pylint checkers
|
||||
"""
|
||||
imported = {}
|
||||
for filename in os.listdir(directory):
|
||||
base, extension = splitext(filename)
|
||||
if base in imported or base == '__pycache__':
|
||||
continue
|
||||
if extension in PY_EXTS and base != '__init__' or (
|
||||
not extension and isdir(join(directory, base))):
|
||||
try:
|
||||
module = load_module_from_file(join(directory, filename))
|
||||
except ValueError:
|
||||
# empty module name (usually emacs auto-save files)
|
||||
continue
|
||||
except ImportError as exc:
|
||||
print("Problem importing module %s: %s" % (filename, exc),
|
||||
file=sys.stderr)
|
||||
else:
|
||||
if hasattr(module, 'register'):
|
||||
module.register(linter)
|
||||
imported[base] = 1
|
||||
|
||||
def get_global_option(checker, option, default=None):
|
||||
""" Retrieve an option defined by the given *checker* or
|
||||
by all known option providers.
|
||||
|
||||
It will look in the list of all options providers
|
||||
until the given *option* will be found.
|
||||
If the option wasn't found, the *default* value will be returned.
|
||||
"""
|
||||
# First, try in the given checker's config.
|
||||
# After that, look in the options providers.
|
||||
|
||||
try:
|
||||
return getattr(checker.config, option.replace("-", "_"))
|
||||
except AttributeError:
|
||||
pass
|
||||
for provider in checker.linter.options_providers:
|
||||
for options in provider.options:
|
||||
if options[0] == option:
|
||||
return getattr(provider.config, option.replace("-", "_"))
|
||||
return default
|
||||
Loading…
Add table
Add a link
Reference in a new issue