diff options
69 files changed, 921 insertions, 425 deletions
@@ -2,6 +2,11 @@ ChangeLog for Pylint ==================== -- + * Each message now comes with a confidence level attached, and + can be filtered base on this level. This allows to filter out + all messages that were emitted even though an inference failure + happened during checking. + * Improved presenting unused-import message. Closes issue #293. * Add new checker for finding spelling errors. New messages: @@ -72,6 +77,28 @@ ChangeLog for Pylint * Don't emit 'protected-access' if the attribute is accessed using a property defined at the class level. + * Detect calls of the parent's __init__, through a binded super() call. + + * Check that a class has an explicitly defined metaclass before + emitting 'old-style-class' for Python 2. + + * Emit 'catching-non-exception' for non-class nodes. Closes issue #303. + + * Order of reporting is consistent. + + * Add a new warning, 'boolean-datetime', emitted when an instance + of 'datetime.time' is used in a boolean context. Closes issue #239. + + * Fix a crash which ocurred while checking for 'method-hidden', + when the parent frame was something different than a function. + + * Generate html output for missing files. Closes issue #320. + + * Fix a false positive with 'too-many-format-args', when the format + string contains mixed attribute access arguments and manual + fields. Closes issue #322. + + 2014-07-26 -- 1.3.0 * Allow hanging continued indentation for implicitly concatenated diff --git a/checkers/__init__.py b/checkers/__init__.py index 693a5ff..8710f4a 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -46,6 +46,8 @@ 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>, @@ -90,9 +92,9 @@ class BaseChecker(OptionsProviderMixIn): OptionsProviderMixIn.__init__(self) self.linter = linter - def add_message(self, msg_id, line=None, node=None, args=None): + 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) + self.linter.add_message(msg_id, line, node, args, confidence) # dummy methods implementing the IChecker interface diff --git a/checkers/base.py b/checkers/base.py index 6e5804f..298ac5f 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -16,13 +16,15 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """basic checker for Python code""" +import collections +import itertools import sys import astroid from logilab.common.ureports import Table from astroid import are_exclusive, InferenceError import astroid.bases -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import EmptyReport from pylint.reporters import diff_string from pylint.checkers import BaseChecker @@ -34,6 +36,7 @@ from pylint.checkers.utils import ( overrides_a_method, safe_infer, get_argument_from_call, + has_known_bases, NoSuchArgumentError, is_import_error, ) @@ -944,6 +947,7 @@ class NameChecker(_BasicChecker): _BasicChecker.__init__(self, linter) self._name_category = {} self._name_group = {} + self._bad_names = {} def open(self): self.stats = self.linter.add_stats(badname_module=0, @@ -961,6 +965,25 @@ class NameChecker(_BasicChecker): @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): self._check_name('module', node.name.split('.')[-1], node) + self._bad_names = {} + + def leave_module(self, node): + for category, all_groups in self._bad_names.iteritems(): + if len(all_groups) < 2: + continue + groups = collections.defaultdict(list) + min_warnings = sys.maxint + for group in all_groups.itervalues(): + groups[len(group)].append(group) + min_warnings = min(len(group), min_warnings) + if len(groups[min_warnings]) > 1: + by_line = sorted(groups[min_warnings], + key=lambda group: min(warning[0].lineno for warning in group)) + warnings = itertools.chain(*by_line[1:]) + else: + warnings = groups[min_warnings][0] + for args in warnings: + self._raise_name_warning(*args) @check_messages('blacklisted-name', 'invalid-name') def visit_class(self, node): @@ -973,10 +996,15 @@ class NameChecker(_BasicChecker): def visit_function(self, node): # Do not emit any warnings if the method is just an implementation # of a base class method. - if node.is_method() and overrides_a_method(node.parent.frame(), node.name): - return + confidence = HIGH + if node.is_method(): + if overrides_a_method(node.parent.frame(), node.name): + return + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) + self._check_name(_determine_function_name_type(node), - node.name, node) + node.name, node, confidence) # Check argument names args = node.args.args if args is not None: @@ -1025,12 +1053,22 @@ class NameChecker(_BasicChecker): def _find_name_group(self, node_type): return self._name_group.get(node_type, node_type) - def _is_multi_naming_match(self, match): + def _is_multi_naming_match(self, match, node_type, confidence): return (match is not None and match.lastgroup is not None and - match.lastgroup not in EXEMPT_NAME_CATEGORIES) - - def _check_name(self, node_type, name, node): + match.lastgroup not in EXEMPT_NAME_CATEGORIES + and (node_type != 'method' or confidence != INFERENCE_FAILURE)) + + def _raise_name_warning(self, node, node_type, name, confidence): + type_label = _NAME_TYPES[node_type][1] + hint = '' + if self.config.include_naming_hint: + hint = ' (hint: %s)' % (getattr(self.config, node_type + '_name_hint')) + self.add_message('invalid-name', node=node, args=(type_label, name, hint), + confidence=confidence) + self.stats['badname_' + node_type] += 1 + + def _check_name(self, node_type, name, node, confidence=HIGH): """check for a name using the type's regexp""" if is_inside_except(node): clobbering, _ = clobber_in_except(node) @@ -1045,20 +1083,14 @@ class NameChecker(_BasicChecker): regexp = getattr(self.config, node_type + '_rgx') match = regexp.match(name) - if self._is_multi_naming_match(match): + if self._is_multi_naming_match(match, node_type, confidence): name_group = self._find_name_group(node_type) - if name_group not in self._name_category: - self._name_category[name_group] = match.lastgroup - elif self._name_category[name_group] != match.lastgroup: - match = None + bad_name_group = self._bad_names.setdefault(name_group, {}) + warnings = bad_name_group.setdefault(match.lastgroup, []) + warnings.append((node, node_type, name, confidence)) if match is None: - type_label = _NAME_TYPES[node_type][1] - hint = '' - if self.config.include_naming_hint: - hint = ' (hint: %s)' % (getattr(self.config, node_type + '_name_hint')) - self.add_message('invalid-name', node=node, args=(type_label, name, hint)) - self.stats['badname_' + node_type] += 1 + self._raise_name_warning(node, node_type, name, confidence) class DocStringChecker(_BasicChecker): @@ -1109,6 +1141,8 @@ class DocStringChecker(_BasicChecker): ftype = node.is_method() and 'method' or 'function' if isinstance(node.parent.frame(), astroid.Class): overridden = False + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if node.name in ancestor and \ @@ -1116,11 +1150,13 @@ class DocStringChecker(_BasicChecker): overridden = True break self._check_docstring(ftype, node, - report_missing=not overridden) + report_missing=not overridden, + confidence=confidence) else: self._check_docstring(ftype, node) - def _check_docstring(self, node_type, node, report_missing=True): + def _check_docstring(self, node_type, node, report_missing=True, + confidence=HIGH): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: @@ -1146,10 +1182,12 @@ class DocStringChecker(_BasicChecker): return elif func.bound.name in ('str', 'unicode', 'bytes'): return - self.add_message('missing-docstring', node=node, args=(node_type,)) + self.add_message('missing-docstring', node=node, args=(node_type,), + confidence=confidence) elif not docstring.strip(): self.stats['undocumented_'+node_type] += 1 - self.add_message('empty-docstring', node=node, args=(node_type,)) + self.add_message('empty-docstring', node=node, args=(node_type,), + confidence=confidence) class PassChecker(_BasicChecker): diff --git a/checkers/classes.py b/checkers/classes.py index ebca3f2..aa05558 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -377,7 +377,8 @@ a metaclass class method.'} try: overridden = klass.instance_attr(node.name)[0] # XXX overridden_frame = overridden.frame() - if overridden_frame.type == 'method': + 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())): @@ -818,6 +819,17 @@ a metaclass class method.'} klass = expr.expr.infer().next() 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: diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 81520ce..9b1a071 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -19,14 +19,44 @@ import sys from logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ import astroid -from astroid import YES, Instance, unpack_infer +from astroid import YES, Instance, unpack_infer, List, Tuple from pylint.checkers import BaseChecker from pylint.checkers.utils import ( is_empty, is_raising, check_messages, inherit_from_std_ex, - EXCEPTIONS_MODULE) -from pylint.interfaces import IAstroidChecker + EXCEPTIONS_MODULE, has_known_bases) +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'). + """ + # TODO: the same code as unpack_infer, except for the annotated + # return. We need this type of annotation only here and + # there is no point in complicating the API for unpack_infer. + # If the need arises, this behaviour can be promoted to unpack_infer + # as well. + if isinstance(stmt, (List, Tuple)): + for elt in stmt.elts: + for infered_elt in unpack_infer(elt, context): + yield elt, infered_elt + return + # if infered is a final node, return it and stop + infered = next(stmt.infer(context)) + if infered is stmt: + yield stmt, infered + return + # else, infer recursivly, except YES object that should be returned as is + for infered in stmt.infer(context): + if infered is YES: + yield stmt, infered + else: + for inf_inf in unpack_infer(infered, context): + yield stmt, inf_inf def infer_bases(klass): """ Fully infer the bases of the klass node. @@ -204,7 +234,9 @@ class ExceptionsChecker(BaseChecker): if expr.newstyle: self.add_message('raising-non-exception', node=node) else: - self.add_message('nonstandard-exception', node=node) + self.add_message( + 'nonstandard-exception', node=node, + confidence=INFERENCE if has_known_bases(expr) else INFERENCE_FAILURE) else: value_found = False else: @@ -255,13 +287,36 @@ class ExceptionsChecker(BaseChecker): node=handler, args=handler.type.op) else: try: - excs = list(unpack_infer(handler.type)) + excs = list(_annotated_unpack_infer(handler.type)) except astroid.InferenceError: continue - for exc in excs: - # XXX skip other non class nodes - if exc is YES or not isinstance(exc, astroid.Class): + for part, exc in excs: + if exc is YES: continue + if isinstance(exc, astroid.Instance) and inherit_from_std_ex(exc): + exc = exc._proxied + 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(), )) + continue + exc_ancestors = [anc for anc in exc.ancestors() if isinstance(anc, astroid.Class)] for previous_exc in exceptions_classes: @@ -289,7 +344,7 @@ class ExceptionsChecker(BaseChecker): node=handler.type, args=(exc.name, )) - exceptions_classes += excs + exceptions_classes += [exc for _, exc in excs] def register(linter): diff --git a/checkers/misc.py b/checkers/misc.py index b53f882..b27b86a 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -54,6 +54,17 @@ class EncodingChecker(BaseChecker): '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 diff --git a/checkers/newstyle.py b/checkers/newstyle.py index cc8f640..1946f94 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -19,9 +19,9 @@ import sys import astroid -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.checkers import BaseChecker -from pylint.checkers.utils import check_messages +from pylint.checkers.utils import check_messages, has_known_bases MSGS = { 'E1001': ('Use of __slots__ on an old style class', @@ -74,15 +74,21 @@ class NewStyleConflictChecker(BaseChecker): @check_messages('slots-on-old-class', 'old-style-class') def visit_class(self, node): - """check __slots__ usage + """ Check __slots__ in old style classes and old + style class definition. """ if '__slots__' in node and not node.newstyle: - self.add_message('slots-on-old-class', node=node) + 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': - self.add_message('old-style-class', node=node) + 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): @@ -91,9 +97,12 @@ class NewStyleConflictChecker(BaseChecker): 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) + 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): @@ -111,9 +120,12 @@ class NewStyleConflictChecker(BaseChecker): 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) + 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: @@ -127,7 +139,8 @@ class NewStyleConflictChecker(BaseChecker): continue if supcls is None: - self.add_message('missing-super-argument', node=call) + self.add_message('missing-super-argument', node=call, + confidence=confidence) continue if klass is not supcls: @@ -143,7 +156,8 @@ class NewStyleConflictChecker(BaseChecker): if name is not None: self.add_message('bad-super-call', node=call, - args=(name, )) + args=(name, ), + confidence=confidence) def register(linter): diff --git a/checkers/stdlib.py b/checkers/stdlib.py index 9913e99..9a56434 100644 --- a/checkers/stdlib.py +++ b/checkers/stdlib.py @@ -19,6 +19,7 @@ import re import sys import astroid +from astroid.bases import Instance from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker @@ -40,6 +41,13 @@ class OpenModeChecker(BaseChecker): 'bad-open-mode', 'Python supports: r, w, a modes with b, +, and U options. ' 'See http://docs.python.org/2/library/functions.html#open'), + 'W1502': ('Using datetime.time in a boolean context.', + 'boolean-datetime', + 'Using datetetime.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)}), } @utils.check_messages('bad-open-mode') @@ -51,6 +59,36 @@ class OpenModeChecker(BaseChecker): if getattr(node.func, 'name', None) in ('open', 'file'): self._check_open_mode(node) + @utils.check_messages('boolean-datetime') + def visit_unaryop(self, node): + if node.op == 'not': + self._check_datetime(node.operand) + + @utils.check_messages('boolean-datetime') + def visit_if(self, node): + self._check_datetime(node.test) + + @utils.check_messages('boolean-datetime') + def visit_ifexp(self, node): + self._check_datetime(node.test) + + @utils.check_messages('boolean-datetime') + def visit_boolop(self, node): + for value in node.values: + self._check_datetime(value) + + def _check_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: diff --git a/checkers/strings.py b/checkers/strings.py index a30d29c..40995f6 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -178,6 +178,7 @@ def parse_format_method_string(format_string): 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(keyname) keyname = int(keyname) keys.append((keyname, list(fielditerator))) else: @@ -363,8 +364,6 @@ class StringMethodsChecker(BaseChecker): self.add_message('bad-format-string', node=node) return - manual_fields = set(field[0] for field in fields - if isinstance(field[0], numbers.Number)) named_fields = set(field[0] for field in fields if isinstance(field[0], basestring)) if num_args and manual_pos: @@ -405,12 +404,7 @@ class StringMethodsChecker(BaseChecker): # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if positional > num_args: - # We can have two possibilities: - # * "{0} {1}".format(a, b) - # * "{} {} {}".format(a, b, c, d) - # We can check the manual keys for the first one. - if len(manual_fields) != positional: - self.add_message('too-many-format-args', node=node) + self.add_message('too-many-format-args', node=node) elif positional < num_args: self.add_message('too-few-format-args', node=node) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index a8851a2..27ed9f5 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -23,23 +23,19 @@ import astroid from astroid import InferenceError, NotFoundError, YES, Instance from astroid.bases import BUILTINS -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer, is_super, check_messages MSGS = { 'E1101': ('%s %r has no %r member', 'no-member', - 'Used when a variable is accessed for an unexistent 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'), - 'E1103': ('%s %r has no %r member (but some types could not be inferred)', - 'maybe-no-member', - 'Used when a variable is accessed for an unexistent member, but \ - astroid was not able to interpret all possible types of this \ - variable.'), 'E1111': ('Assigning to function call which doesn\'t return', 'assignment-from-no-return', 'Used when an assignment is done on a function call but the \ @@ -187,7 +183,7 @@ accessed. Python regular expressions are accepted.'} def visit_delattr(self, node): self.visit_getattr(node) - @check_messages('no-member', 'maybe-no-member') + @check_messages('no-member') def visit_getattr(self, node): """check that the accessed attribute exists @@ -279,13 +275,11 @@ accessed. Python regular expressions are accepted.'} if actual in done: continue done.add(actual) - if inference_failure: - msgid = 'maybe-no-member' - else: - msgid = 'no-member' - self.add_message(msgid, node=node, + confidence = INFERENCE if not inference_failure else INFERENCE_FAILURE + self.add_message('no-member', node=node, args=(owner.display_type(), name, - node.attrname)) + node.attrname), + confidence=confidence) @check_messages('assignment-from-no-return', 'assignment-from-none') def visit_assign(self, node): diff --git a/checkers/utils.py b/checkers/utils.py index 000aee7..af1440d 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -417,7 +417,7 @@ def get_argument_from_call(callfunc_node, position=None, keyword=None): try: if position is not None and not isinstance(callfunc_node.args[position], astroid.Keyword): return callfunc_node.args[position] - except IndexError, error: + except IndexError as error: raise NoSuchArgumentError(error) if keyword: for arg in callfunc_node.args: @@ -462,3 +462,22 @@ def is_import_error(handler): 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 + try: + for base in klass.bases: + result = base.infer().next() + # 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 + except astroid.InferenceError: + klass._all_bases_known = False + return False + klass._all_bases_known = True + return True diff --git a/checkers/variables.py b/checkers/variables.py index a88bf3b..71b4be4 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -25,14 +25,14 @@ from astroid import are_exclusive, builtin_lookup, AstroidBuildingException from logilab.common.modutils import file_from_modpath -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import get_global_option from pylint.checkers import BaseChecker from pylint.checkers.utils import ( PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, assign_parent, check_messages, is_inside_except, clobber_in_except, - get_all_elements) + get_all_elements, has_known_bases) SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") @@ -137,6 +137,38 @@ def _detect_global_scope(node, frame, defframe): # and the definition of the first depends on the second. return frame.lineno < defframe.lineno +def _fix_dot_imports(not_consumed): + """ Try to fix imports with multiple dots, by returning a dictionary + with the import names expanded. The function unflattens root imports, + like 'xml' (when we have both 'xml.etree' and 'xml.sax'), to 'xml.etree' + and 'xml.sax' respectively. + """ + # TODO: this should be improved in issue astroid #46 + names = {} + for name, stmts in not_consumed.iteritems(): + if any(isinstance(stmt, astroid.AssName) + and isinstance(stmt.ass_type(), astroid.AugAssign) + for stmt in stmts): + continue + for stmt in stmts: + if not isinstance(stmt, (astroid.From, astroid.Import)): + continue + for imports in stmt.names: + second_name = None + if imports[0] == "*": + # In case of wildcard imports, + # pick the name from inside the imported module. + second_name = name + else: + if imports[0].find(".") > -1 or name in imports: + # Most likely something like 'xml.etree', + # which will appear in the .locals as 'xml'. + # Only pick the name if it wasn't consumed. + second_name = imports[0] + if second_name and second_name not in names: + names[second_name] = stmt + return sorted(names.items(), key=lambda a: a[1].fromlineno) + MSGS = { 'E0601': ('Using variable %r before assignment', @@ -324,36 +356,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' self._check_imports(not_consumed) def _check_imports(self, not_consumed): - # Fix local names in node.locals dict (xml.etree instead of xml). - # TODO: this should be improved in issue astroid#46 - local_names = {} - for name, stmts in not_consumed.iteritems(): - if any(isinstance(stmt, astroid.AssName) - and isinstance(stmt.ass_type(), astroid.AugAssign) - for stmt in stmts): - continue - for stmt in stmts: - if not isinstance(stmt, (astroid.From, astroid.Import)): - continue - for imports in stmt.names: - name2 = None - if imports[0] == "*": - # In case of wildcard import, - # pick the name from inside of imported module. - name2 = name - else: - if imports[0].find(".") > -1 or name in imports: - # Most likely something like 'xml.etree', - # which will appear in the .locals as - # 'xml'. - # Only pick the name if it wasn't consumed. - name2 = imports[0] - if name2 and name2 not in local_names: - local_names[name2] = stmt - local_names = sorted(local_names.iteritems(), - key=lambda a: a[1].fromlineno) - - # Check for unused imports. + local_names = _fix_dot_imports(not_consumed) checked = set() for name, stmt in local_names: for imports in stmt.names: @@ -486,6 +489,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' klass = node.parent.frame() if is_method and (klass.type == 'interface' or node.is_abstract()): return + if is_method and isinstance(klass, astroid.Class): + confidence = INFERENCE if has_known_bases(klass) else INFERENCE_FAILURE + else: + confidence = HIGH authorized_rgx = self.config.dummy_variables_rgx called_overridden = False argnames = node.argnames() @@ -539,7 +546,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' # don't check callback arguments XXX should be configurable if node.name.startswith('cb_') or node.name.endswith('_cb'): continue - self.add_message('unused-argument', args=name, node=stmt) + self.add_message('unused-argument', args=name, node=stmt, + confidence=confidence) else: if stmt.parent and isinstance(stmt.parent, astroid.Assign): if name in nonlocal_names: diff --git a/doc/options.rst b/doc/options.rst index 4a159f2..702d93c 100644 --- a/doc/options.rst +++ b/doc/options.rst @@ -91,8 +91,8 @@ However, intra-module consistency should still be required, to make changes inside a single file easier. For this case, PyLint supports regular expression with several named capturing group. -The capturing group of the first valid match taints the module and enforces the -same group to be triggered on every subsequent occurrence of this name. +Rather than emitting name warnings immediately, PyLint will determine the +prevalent naming style inside each module and enforce it on all names. Consider the following (simplified) example:: @@ -101,16 +101,17 @@ Consider the following (simplified) example:: The regular expression defines two naming styles, ``snake`` for snake-case names, and ``camel`` for camel-case names. -In ``sample.py``, the function name on line 1 will taint the module and enforce -the match of named group ``snake`` for the remainder of the module:: +In ``sample.py``, the function name on line 1 and 7 will mark the module +and enforce the match of named group ``snake`` for the remaining names in +the module:: - def trigger_snake_case(arg): + def valid_snake_case(arg): ... def InvalidCamelCase(arg): ... - def valid_snake_case(arg): + def more_valid_snake_case(arg): ... Because of this, the name on line 4 will trigger an ``invalid-name`` warning, diff --git a/interfaces.py b/interfaces.py index 50f2c83..ea3b40f 100644 --- a/interfaces.py +++ b/interfaces.py @@ -11,9 +11,21 @@ # 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 @@ -25,6 +25,7 @@ Display help messages about given message identifiers and exit. """ +from __future__ import print_function # import this first to avoid builtin namespace pollution from pylint.checkers import utils #pylint: disable=unused-import @@ -51,7 +52,7 @@ from pylint.utils import ( PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, MessagesStore, FileState, EmptyReport, expand_modules, tokenize_module) -from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker +from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker, CONFIDENCE_LEVELS from pylint.checkers import (BaseTokenChecker, table_lines_from_stats, initialize as checkers_initialize) @@ -238,6 +239,15 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 'help' : 'Add a comment according to your evaluation note. ' 'This is used by the global evaluation report (RP0004).'}), + ('confidence', + {'type' : 'multiple_choice', 'metavar': '<levels>', + 'default': '', + 'choices': [c.name for c in CONFIDENCE_LEVELS], + 'group': 'Messages control', + 'help' : 'Only show warnings with the listed confidence levels.' + ' Leave empty to show all. Valid levels: %s' % ( + ', '.join(c.name for c in CONFIDENCE_LEVELS),)}), + ('enable', {'type' : 'csv', 'metavar': '<msg ids>', 'short': 'e', @@ -404,12 +414,24 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, try: BaseTokenChecker.set_option(self, optname, value, action, optdict) except UnsupportedAction: - print >> sys.stderr, 'option %s can\'t be read from config file' % \ - optname + print('option %s can\'t be read from config file' % \ + optname, file=sys.stderr) def register_reporter(self, reporter_class): self._reporters[reporter_class.name] = reporter_class + def report_order(self): + reports = sorted(self._reports, key=lambda x: getattr(x, 'name', '')) + try: + # Remove the current reporter and add it + # at the end of the list. + reports.pop(reports.index(self)) + except ValueError: + pass + else: + reports.append(self) + return reports + # checkers manipulation methods ############################################ def register_checker(self, checker): @@ -624,11 +646,11 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, """return a ast(roid) representation for a module""" try: return MANAGER.ast_from_file(filepath, modname, source=True) - except SyntaxError, ex: + except SyntaxError as ex: self.add_message('syntax-error', line=ex.lineno, args=ex.msg) - except AstroidBuildingException, ex: + except AstroidBuildingException as ex: self.add_message('parse-error', args=ex) - except Exception, ex: + except Exception as ex: import traceback traceback.print_exc() self.add_message('astroid-error', args=(ex.__class__, ex)) @@ -638,7 +660,7 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, # call raw checkers if possible try: tokens = tokenize_module(astroid) - except tokenize.TokenError, ex: + except tokenize.TokenError as ex: self.add_message('syntax-error', line=ex.args[1][0], args=ex.args[0]) return @@ -695,6 +717,11 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, if self.config.persistent: config.save_results(self.stats, self.file_state.base_name) else: + if self.config.output_format == 'html': + # No output will be emitted for the html + # reporter if the file doesn't exist, so emit + # the results here. + self.reporter.display_results(Section()) self.reporter.on_close(self.stats, {}) # specific reports ######################################################## @@ -709,7 +736,7 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, evaluation = self.config.evaluation try: note = eval(evaluation, {}, self.stats) - except Exception, ex: + except Exception as ex: msg = 'An exception occurred while rating: %s' % ex else: stats['global_note'] = note @@ -849,8 +876,8 @@ group are mutually exclusive.'), 'rcfile': (self.cb_set_rcfile, True), 'load-plugins': (self.cb_add_plugins, True), }) - except ArgumentPreprocessingError, ex: - print >> sys.stderr, ex + except ArgumentPreprocessingError as ex: + print(ex, file=sys.stderr) sys.exit(32) self.linter = linter = self.LinterClass(( @@ -879,6 +906,12 @@ group are mutually exclusive.'), 'group': 'Commands', 'level': 1, 'help' : "Generate pylint's messages."}), + ('list-conf-levels', + {'action' : 'callback', + 'callback' : self.cb_list_confidence_levels, + 'group': 'Commands', 'level': 1, + 'help' : "Generate pylint's messages."}), + ('full-documentation', {'action' : 'callback', 'metavar': '<msg-id>', 'callback' : self.cb_full_documentation, @@ -968,18 +1001,18 @@ group are mutually exclusive.'), linter.set_reporter(reporter) try: args = linter.load_command_line_configuration(args) - except SystemExit, exc: + except SystemExit as exc: if exc.code == 2: # bad options exc.code = 32 raise if not args: - print linter.help() + print(linter.help()) sys.exit(32) # insert current working directory to the python path to have a correct # behaviour linter.prepare_import_path(args) if self.linter.config.profile: - print >> sys.stderr, '** profiled run' + print('** profiled run', file=sys.stderr) import cProfile, pstats cProfile.runctx('linter.check(%r)' % args, globals(), locals(), 'stones.prof') @@ -1037,6 +1070,11 @@ group are mutually exclusive.'), self.linter.msgs_store.list_messages() sys.exit(0) + def cb_list_confidence_levels(self, option, optname, value, parser): + for level in CONFIDENCE_LEVELS: + print('%-18s: %s' % level) + sys.exit(0) + def cb_init_hook(optname, value): """exec arbitrary code to set sys.path for instance""" exec value diff --git a/reporters/__init__.py b/reporters/__init__.py index 12d193f..429155a 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -17,7 +17,6 @@ import sys import locale import os -from pylint.utils import MSG_TYPES from pylint import utils @@ -28,10 +27,6 @@ if sys.version_info >= (3, 0): def cmp(a, b): return (a > b) - (a < b) -if sys.version_info < (2, 6): - import stringformat - stringformat.init(True) - def diff_string(old, new): """given a old and new int value, return a string representing the difference @@ -41,27 +36,6 @@ def diff_string(old, new): return diff_str -class Message(object): - """This class represent a message to be issued by the reporters""" - - def __init__(self, reporter, msg_id, location, msg): - self.msg_id = msg_id - self.abspath, self.module, self.obj, self.line, self.column = location - self.path = self.abspath.replace(reporter.path_strip_prefix, '') - self.msg = msg - self.C = msg_id[0] - self.category = MSG_TYPES[msg_id[0]] - self.symbol = reporter.linter.msgs_store.check_message_id(msg_id).symbol - - 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 - """ - return template.format(**(self.__dict__)) - - class BaseReporter(object): """base class for reporters @@ -82,9 +56,16 @@ class BaseReporter(object): # 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): - """Client API to send a message""" - # Shall we store the message objects somewhere, do some validity checking ? + """Deprecated, do not use.""" raise NotImplementedError def set_output(self, output=None): diff --git a/reporters/guireporter.py b/reporters/guireporter.py index 331eb17..f908f76 100644 --- a/reporters/guireporter.py +++ b/reporters/guireporter.py @@ -3,7 +3,8 @@ import sys from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter +from pylint import utils from logilab.common.ureports import TextWriter @@ -18,10 +19,9 @@ class GUIReporter(BaseReporter): BaseReporter.__init__(self, output) self.gui = gui - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - message = Message(self, msg_id, location, msg) - self.gui.msg_queue.put(message) + self.gui.msg_queue.put(msg) def _display(self, layout): """launch layouts display""" diff --git a/reporters/html.py b/reporters/html.py index 71d46eb..3e5a1a7 100644 --- a/reporters/html.py +++ b/reporters/html.py @@ -18,8 +18,9 @@ from cgi import escape from logilab.common.ureports import HTMLWriter, Section, Table +from pylint import utils from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter class HTMLReporter(BaseReporter): @@ -33,9 +34,8 @@ class HTMLReporter(BaseReporter): BaseReporter.__init__(self, output) self.msgs = [] - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - msg = Message(self, msg_id, location, msg) self.msgs += (msg.category, msg.module, msg.obj, str(msg.line), str(msg.column), escape(msg.msg)) diff --git a/reporters/text.py b/reporters/text.py index 04245f7..48b5155 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -23,7 +23,7 @@ from logilab.common.ureports import TextWriter from logilab.common.textutils import colorize_ansi from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter TITLE_UNDERLINES = ['', '=', '-', '.'] @@ -48,16 +48,15 @@ class TextReporter(BaseReporter): """Convenience method to write a formated message with class default template""" self.writeln(msg.format(self._template)) - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - m = Message(self, msg_id, location, msg) - if m.module not in self._modules: - if m.module: - self.writeln('************* Module %s' % m.module) - self._modules.add(m.module) + 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(m) + self.write_message(msg) def _display(self, layout): """launch layouts display""" @@ -114,11 +113,10 @@ class ColorizedTextReporter(TextReporter): except KeyError: return None, None - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different types, and colorize output using ansi escape codes """ - msg = Message(self, msg_id, location, msg) if msg.module not in self._modules: color, style = self._get_decoration('S') if msg.module: @@ -130,8 +128,10 @@ class ColorizedTextReporter(TextReporter): self.writeln(modsep) self._modules.add(msg.module) color, style = self._get_decoration(msg.C) - for attr in ('msg', 'symbol', 'category', 'C'): - setattr(msg, attr, colorize_ansi(getattr(msg, attr), color, style)) + + msg = msg._replace( + **{attr: colorize_ansi(getattr(msg, attr), color, style) + for attr in ('msg', 'symbol', 'category', 'C')}) self.write_message(msg) diff --git a/test/functional/access_to__name__.txt b/test/functional/access_to__name__.txt index 2c0a953..ecf5ffd 100644 --- a/test/functional/access_to__name__.txt +++ b/test/functional/access_to__name__.txt @@ -1,3 +1,3 @@ old-style-class:7:Aaaa:Old-style class defined. -no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member -no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member +no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member:INFERENCE +no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member:INFERENCE diff --git a/test/functional/anomalous_unicode_escape.txt b/test/functional/anomalous_unicode_escape.txt index 5c1acc7..c242cb9 100644 --- a/test/functional/anomalous_unicode_escape.txt +++ b/test/functional/anomalous_unicode_escape.txt @@ -1,3 +1,3 @@ -anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. -anomalous-unicode-escape-in-string:6::Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix. -anomalous-unicode-escape-in-string:8::Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:5::"Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix." +anomalous-unicode-escape-in-string:6::"Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix." +anomalous-unicode-escape-in-string:8::"Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix." diff --git a/test/functional/bad_continuation.txt b/test/functional/bad_continuation.txt index 732858e..f970780 100644 --- a/test/functional/bad_continuation.txt +++ b/test/functional/bad_continuation.txt @@ -1,63 +1,63 @@ -bad-continuation:12::Wrong hanging indentation. +bad-continuation:12::"Wrong hanging indentation. ] # [bad-continuation] -| ^| -bad-continuation:17::Wrong continued indentation. +| ^|" +bad-continuation:17::"Wrong continued indentation. 7, # [bad-continuation] - | ^ -bad-continuation:25::Wrong hanging indentation. + | ^" +bad-continuation:25::"Wrong hanging indentation. 'b': 2, # [bad-continuation] - ^| -bad-continuation:31::Wrong hanging indentation. + ^|" +bad-continuation:31::"Wrong hanging indentation. 'b': 2, # [bad-continuation] - ^| -bad-continuation:39::Wrong continued indentation. + ^|" +bad-continuation:39::"Wrong continued indentation. # [bad-continuation] is not accepted - | | ^ -bad-continuation:40::Wrong continued indentation. + | | ^" +bad-continuation:40::"Wrong continued indentation. 'contents', # [bad-continuation] nor this. - | ^ -bad-continuation:49::Wrong hanging indentation in dict value. + | ^" +bad-continuation:49::"Wrong hanging indentation in dict value. 'value2', # [bad-continuation] - | ^ | -bad-continuation:59::Wrong continued indentation. + | ^ |" +bad-continuation:59::"Wrong continued indentation. 'wrong', # [bad-continuation] - ^ | -bad-continuation:83::Wrong hanging indentation in dict value. + ^ |" +bad-continuation:83::"Wrong hanging indentation in dict value. 'value1', # [bad-continuation] -^ | | -bad-continuation:87::Wrong hanging indentation in dict value. +^ | |" +bad-continuation:87::"Wrong hanging indentation in dict value. 'value1', # [bad-continuation] - ^ | | -bad-continuation:104::Wrong hanging indentation before block. + ^ | |" +bad-continuation:104::"Wrong hanging indentation before block. some_arg, # [bad-continuation] - ^ | -bad-continuation:105::Wrong hanging indentation before block. + ^ |" +bad-continuation:105::"Wrong hanging indentation before block. some_other_arg): # [bad-continuation] - ^ | -bad-continuation:125::Wrong continued indentation. - "b") # [bad-continuation] - ^ | -bad-continuation:139::Wrong hanging indentation before block. + ^ |" +bad-continuation:125::"Wrong continued indentation. + ""b"") # [bad-continuation] + ^ |" +bad-continuation:139::"Wrong hanging indentation before block. ): pass # [bad-continuation] -| ^| -bad-continuation:142::Wrong continued indentation before block. +| ^|" +bad-continuation:142::"Wrong continued indentation before block. 2): # [bad-continuation] - ^ | -bad-continuation:150::Wrong continued indentation. + ^ |" +bad-continuation:150::"Wrong continued indentation. 2 and # [bad-continuation] - | ^ -bad-continuation:155::Wrong hanging indentation before block. + | ^" +bad-continuation:155::"Wrong hanging indentation before block. 2): pass # [bad-continuation] - ^ | | -bad-continuation:162::Wrong continued indentation before block. + ^ | |" +bad-continuation:162::"Wrong continued indentation before block. 2 or # [bad-continuation] - |^ | -bad-continuation:166::Wrong continued indentation before block. + |^ |" +bad-continuation:166::"Wrong continued indentation before block. 2): pass # [bad-continuation] - ^ | | -bad-continuation:172::Wrong hanging indentation before block. + ^ | |" +bad-continuation:172::"Wrong hanging indentation before block. 2): # [bad-continuation] - ^ | | -bad-continuation:183::Wrong continued indentation. + ^ | |" +bad-continuation:183::"Wrong continued indentation. 2): # [bad-continuation] - ^ | + ^ |" diff --git a/test/functional/bad_open_mode.txt b/test/functional/bad_open_mode.txt index 356e02e..d0bb8bb 100644 --- a/test/functional/bad_open_mode.txt +++ b/test/functional/bad_open_mode.txt @@ -1,7 +1,7 @@ -bad-open-mode:4::"rw" is not a valid mode for open. -bad-open-mode:5::"rw" is not a valid mode for open. -bad-open-mode:6::"rw" is not a valid mode for open. -bad-open-mode:7::"U+" is not a valid mode for open. -bad-open-mode:8::"rb+" is not a valid mode for open. -bad-open-mode:9::"Uw" is not a valid mode for open. -bad-open-mode:13::"Uwz" is not a valid mode for open. +bad-open-mode:4::"""rw"" is not a valid mode for open." +bad-open-mode:5::"""rw"" is not a valid mode for open." +bad-open-mode:6::"""rw"" is not a valid mode for open." +bad-open-mode:7::"""U+"" is not a valid mode for open." +bad-open-mode:8::"""rb+"" is not a valid mode for open." +bad-open-mode:9::"""Uw"" is not a valid mode for open." +bad-open-mode:13::"""Uwz"" is not a valid mode for open." diff --git a/test/functional/boolean_datetime.py b/test/functional/boolean_datetime.py new file mode 100644 index 0000000..0ea9d27 --- /dev/null +++ b/test/functional/boolean_datetime.py @@ -0,0 +1,30 @@ +""" Checks for boolean uses of datetime.time. """
+# pylint: disable=superfluous-parens
+import datetime
+
+if datetime.time(0, 0, 0): # [boolean-datetime]
+ print("datetime.time(0,0,0) is not a bug!")
+else:
+ print("datetime.time(0,0,0) is a bug!")
+
+if not datetime.time(0, 0, 1): # [boolean-datetime]
+ print("datetime.time(0,0,1) is not a bug!")
+else:
+ print("datetime.time(0,0,1) is a bug!")
+
+DATA = not datetime.time(0, 0, 0) # [boolean-datetime]
+DATA = True if datetime.time(0, 0, 0) else False # [boolean-datetime]
+DATA = datetime.time(0, 0, 0) or True # [boolean-datetime]
+DATA = datetime.time(0, 0, 0) and True # [boolean-datetime]
+DATA = False or True or datetime.time(0, 0, 0) # [boolean-datetime]
+DATA = False and datetime.time(0, 0, 0) or True # [boolean-datetime]
+
+
+def cant_infer(data):
+ """ Can't infer what data is """
+ hophop = not data
+ troptrop = True if data else False
+ toptop = data or True
+ return hophop, troptrop, toptop
+
+cant_infer(datetime.time(0, 0, 0))
diff --git a/test/functional/boolean_datetime.txt b/test/functional/boolean_datetime.txt new file mode 100644 index 0000000..f3287d4 --- /dev/null +++ b/test/functional/boolean_datetime.txt @@ -0,0 +1,8 @@ +boolean-datetime:5::Using datetime.time in a boolean context. +boolean-datetime:10::Using datetime.time in a boolean context. +boolean-datetime:15::Using datetime.time in a boolean context. +boolean-datetime:16::Using datetime.time in a boolean context. +boolean-datetime:17::Using datetime.time in a boolean context. +boolean-datetime:18::Using datetime.time in a boolean context. +boolean-datetime:19::Using datetime.time in a boolean context. +boolean-datetime:20::Using datetime.time in a boolean context. diff --git a/test/functional/class_members_py27.txt b/test/functional/class_members_py27.txt index 3e88b55..e5e6005 100644 --- a/test/functional/class_members_py27.txt +++ b/test/functional/class_members_py27.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member
-no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member
-no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member
-no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member
-no-member:50::Instance of 'TestMetaclass' has no 'register' member
-no-member:51::Instance of 'UsingMetaclass' has no 'test' member
+no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE +no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE +no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE +no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE +no-member:50::Instance of 'TestMetaclass' has no 'register' member:INFERENCE +no-member:51::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE diff --git a/test/functional/class_members_py30.txt b/test/functional/class_members_py30.txt index 0cb808f..4696579 100644 --- a/test/functional/class_members_py30.txt +++ b/test/functional/class_members_py30.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member
-no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member
-no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member
-no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member
-no-member:48::Instance of 'TestMetaclass' has no 'register' member
-no-member:49::Instance of 'UsingMetaclass' has no 'test' member
+no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE
+no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE
+no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE
+no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE
+no-member:48::Instance of 'TestMetaclass' has no 'register' member:INFERENCE
+no-member:49::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE
diff --git a/test/functional/confidence_filter.py b/test/functional/confidence_filter.py new file mode 100644 index 0000000..64cc8c4 --- /dev/null +++ b/test/functional/confidence_filter.py @@ -0,0 +1,14 @@ +"""Test for the confidence filter.""" + +class Client(object): + """use provider class""" + + def __init__(self): + self.set_later = 0 + + def set_set_later(self, value): + """set set_later attribute (introduce an inference ambiguity)""" + self.set_later = value + +print Client().set_later.lower() +print Client().foo # [no-member] diff --git a/test/functional/confidence_filter.rc b/test/functional/confidence_filter.rc new file mode 100644 index 0000000..5d21cb5 --- /dev/null +++ b/test/functional/confidence_filter.rc @@ -0,0 +1,3 @@ +[Messages Control] +disable=no-init,too-few-public-methods,undefined-variable +confidence=INFERENCE,HIGH,UNDEFINED diff --git a/test/functional/confidence_filter.txt b/test/functional/confidence_filter.txt new file mode 100644 index 0000000..308035d --- /dev/null +++ b/test/functional/confidence_filter.txt @@ -0,0 +1 @@ +no-member:14::Instance of 'Client' has no 'foo' member:INFERENCE diff --git a/test/functional/crash_missing_module_type.py b/test/functional/crash_missing_module_type.py new file mode 100644 index 0000000..a471ad8 --- /dev/null +++ b/test/functional/crash_missing_module_type.py @@ -0,0 +1,18 @@ +""" Test for a crash found in
+https://bitbucket.org/logilab/astroid/issue/45/attributeerror-module-object-has-no#comment-11944673
+"""
+# pylint: disable=no-init, invalid-name, too-few-public-methods, redefined-outer-name
+def decor(trop):
+ """ decorator """
+ return trop
+
+class Foo(object):
+ """ Class """
+ @decor
+ def prop(self):
+ """ method """
+ return self
+
+if __name__ == '__main__':
+ trop = Foo()
+ trop.prop = 42
diff --git a/test/functional/crash_missing_module_type.txt b/test/functional/crash_missing_module_type.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/functional/crash_missing_module_type.txt diff --git a/test/functional/docstrings.txt b/test/functional/docstrings.txt index 1ea5c7d..e30e462 100644 --- a/test/functional/docstrings.txt +++ b/test/functional/docstrings.txt @@ -1,8 +1,8 @@ -missing-docstring:1::Missing module docstring
-empty-docstring:6:function0:Empty function docstring
-missing-docstring:10:function1:Missing function docstring
-missing-docstring:23:AAAA:Missing class docstring
-missing-docstring:40:AAAA.method1:Missing method docstring
-empty-docstring:48:AAAA.method3:Empty method docstring
-empty-docstring:62:DDDD.method2:Empty method docstring
-missing-docstring:70:DDDD.method4:Missing method docstring
+missing-docstring:1::Missing module docstring +empty-docstring:6:function0:Empty function docstring +missing-docstring:10:function1:Missing function docstring +missing-docstring:23:AAAA:Missing class docstring +missing-docstring:40:AAAA.method1:Missing method docstring:INFERENCE +empty-docstring:48:AAAA.method3:Empty method docstring:INFERENCE +empty-docstring:62:DDDD.method2:Empty method docstring:INFERENCE +missing-docstring:70:DDDD.method4:Missing method docstring:INFERENCE diff --git a/test/functional/exception_is_binary_op.txt b/test/functional/exception_is_binary_op.txt index 039bc3f..e6512c9 100644 --- a/test/functional/exception_is_binary_op.txt +++ b/test/functional/exception_is_binary_op.txt @@ -1,4 +1,4 @@ -binary-op-exception:5::Exception to catch is the result of a binary "or" operation -binary-op-exception:7::Exception to catch is the result of a binary "and" operation -binary-op-exception:9::Exception to catch is the result of a binary "or" operation -binary-op-exception:11::Exception to catch is the result of a binary "or" operation +binary-op-exception:5::"Exception to catch is the result of a binary ""or"" operation" +binary-op-exception:7::"Exception to catch is the result of a binary ""and"" operation" +binary-op-exception:9::"Exception to catch is the result of a binary ""or"" operation" +binary-op-exception:11::"Exception to catch is the result of a binary ""or"" operation" diff --git a/test/functional/future_unicode_literals.txt b/test/functional/future_unicode_literals.txt index c490da5..60d291e 100644 --- a/test/functional/future_unicode_literals.txt +++ b/test/functional/future_unicode_literals.txt @@ -1 +1 @@ -anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:5::"Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix." diff --git a/test/functional/import_error.txt b/test/functional/import_error.txt index af7f739..0602d2a 100644 --- a/test/functional/import_error.txt +++ b/test/functional/import_error.txt @@ -1,2 +1,2 @@ -import-error:3::Unable to import 'totally_missing'
-import-error:16::Unable to import 'maybe_missing_2'
+import-error:3::Unable to import 'totally_missing' +import-error:16::Unable to import 'maybe_missing_2' diff --git a/test/functional/invalid_encoded_data.txt b/test/functional/invalid_encoded_data.txt index dc23869..c6e69dc 100644 --- a/test/functional/invalid_encoded_data.txt +++ b/test/functional/invalid_encoded_data.txt @@ -1 +1 @@ -invalid-encoded-data:5::Cannot decode using encoding "utf-8", unexpected byte at position 11 +invalid-encoded-data:5::"Cannot decode using encoding ""utf-8"", unexpected byte at position 11" diff --git a/test/functional/invalid_exceptions_caught.py b/test/functional/invalid_exceptions_caught.py index 99872d2..0bd0f7b 100644 --- a/test/functional/invalid_exceptions_caught.py +++ b/test/functional/invalid_exceptions_caught.py @@ -45,3 +45,21 @@ try: 1 + 3 except (SkipException, SecondSkipException): print "caught" + +try: + 1 + 42 +# +1:[catching-non-exception,catching-non-exception] +except (None, list()): + print "caught" + +try: + 1 + 24 +except None: # [catching-non-exception] + print "caught" + +EXCEPTION = None +EXCEPTION = ZeroDivisionError +try: + 1 + 46 +except EXCEPTION: + print "caught" diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt index 530ef9a..ce9ab2a 100644 --- a/test/functional/invalid_exceptions_caught.txt +++ b/test/functional/invalid_exceptions_caught.txt @@ -1,3 +1,6 @@ -catching-non-exception:25::Catching an exception which doesn't inherit from BaseException: MyException -catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MyException -catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MySecondException +catching-non-exception:25::"Catching an exception which doesn't inherit from BaseException: MyException" +catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MyException" +catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MySecondException" +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: None" +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: list()" +catching-non-exception:57::"Catching an exception which doesn't inherit from BaseException: None" diff --git a/test/functional/invalid_exceptions_raised.txt b/test/functional/invalid_exceptions_raised.txt index 756d92c..54ea0f4 100644 --- a/test/functional/invalid_exceptions_raised.txt +++ b/test/functional/invalid_exceptions_raised.txt @@ -1,9 +1,9 @@ +nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE raising-non-exception:23:bad_case0:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:23:bad_case0:Exception doesn't inherit from standard "Exception" class raising-non-exception:27:bad_case1:Raising a new style class which doesn't inherit from BaseException -raising-non-exception:33:bad_case2:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:33:bad_case2:Exception doesn't inherit from standard "Exception" class +nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE old-raise-syntax:33:bad_case2:Use raise ErrorClass(args) instead of raise ErrorClass, args. +raising-non-exception:33:bad_case2:Raising a new style class which doesn't inherit from BaseException raising-non-exception:37:bad_case3:Raising a new style class which doesn't inherit from BaseException notimplemented-raised:42:bad_case4:NotImplemented raised - should raise NotImplementedError old-raise-syntax:42:bad_case4:Use raise ErrorClass(args) instead of raise ErrorClass, args. diff --git a/test/functional/invalid_name.txt b/test/functional/invalid_name.txt index c722861..0c8eafe 100644 --- a/test/functional/invalid_name.txt +++ b/test/functional/invalid_name.txt @@ -1,2 +1,2 @@ -invalid-name:10::Invalid constant name "aaa"
-invalid-name:14::Invalid constant name "time"
+invalid-name:10::"Invalid constant name ""aaa""" +invalid-name:14::"Invalid constant name ""time""" diff --git a/test/input/func_typecheck_getattr.py b/test/functional/member_checks.py index b120ca4..fccf160 100644 --- a/test/input/func_typecheck_getattr.py +++ b/test/functional/member_checks.py @@ -1,7 +1,6 @@ # pylint: disable= """check getattr if inference succeed""" -__revision__ = None class Provider(object): """provide some attributes and method""" @@ -16,13 +15,13 @@ class Provider(object): print 'hop hop hop', self -class Client: +class Client(object): """use provider class""" def __init__(self): self._prov = Provider() self._prov_attr = Provider.cattr - self._prov_attr2 = Provider.cattribute + self._prov_attr2 = Provider.cattribute # [no-member] self.set_later = 0 def set_set_later(self, value): @@ -32,12 +31,12 @@ class Client: def use_method(self): """use provider's method""" self._prov.hophop() - self._prov.hophophop() + self._prov.hophophop() # [no-member] def use_attr(self): """use provider's attr""" print self._prov.attr - print self._prov.attribute + print self._prov.attribute # [no-member] def debug(self): """print debug information""" @@ -49,21 +48,16 @@ class Client: def test_bt_types(self): """test access to unexistant member of builtin types""" lis = [] - lis.apppend(self) + lis.apppend(self) # [no-member] dic = {} - dic.set(self) + dic.set(self) # [no-member] tup = () - tup.append(self) + tup.append(self) # [no-member] string = 'toto' - print string.loower() - # unicode : moved to func_3k_removed_stuff_py_30.py - # + print string.loower() # [no-member] integer = 1 - print integer.whatever + print integer.whatever # [no-member] print object.__init__ print property.__init__ -print Client().set_later.lower() - -# should detect mixing new style / old style classes -Client.__bases__ += (object,) +print Client().set_later.lower() # [no-member] diff --git a/test/functional/member_checks.txt b/test/functional/member_checks.txt new file mode 100644 index 0000000..12fe6ee --- /dev/null +++ b/test/functional/member_checks.txt @@ -0,0 +1,9 @@ +no-member:24:Client.__init__:Class 'Provider' has no 'cattribute' member:INFERENCE +no-member:34:Client.use_method:Instance of 'Provider' has no 'hophophop' member:INFERENCE +no-member:39:Client.use_attr:Instance of 'Provider' has no 'attribute' member:INFERENCE +no-member:51:Client.test_bt_types:Instance of 'list' has no 'apppend' member:INFERENCE +no-member:53:Client.test_bt_types:Instance of 'dict' has no 'set' member:INFERENCE +no-member:55:Client.test_bt_types:Instance of 'tuple' has no 'append' member:INFERENCE +no-member:57:Client.test_bt_types:Instance of 'str' has no 'loower' member:INFERENCE +no-member:59:Client.test_bt_types:Instance of 'int' has no 'whatever' member:INFERENCE +no-member:63::Instance of 'int' has no 'lower' member:INFERENCE_FAILURE diff --git a/test/functional/missing_self_argument.txt b/test/functional/missing_self_argument.txt index 3f7f7f2..1deef18 100644 --- a/test/functional/missing_self_argument.txt +++ b/test/functional/missing_self_argument.txt @@ -1,4 +1,6 @@ no-method-argument:11:MyClass.method:Method has no argument -no-method-argument:13:MyClass.met:Method has no argument +no-method-argument:13:MyClass.met:"""Method has no argument +"" +" no-method-argument:14:MyClass.setup:Method has no argument undefined-variable:16:MyClass.setup:Undefined variable 'self' diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt index 4f0ae53..e81d27a 100644 --- a/test/functional/name_styles.txt +++ b/test/functional/name_styles.txt @@ -1,17 +1,17 @@ -invalid-name:6::Invalid constant name "bad_const_name" -invalid-name:9:BADFUNCTION_name:Invalid function name "BADFUNCTION_name" -invalid-name:11:BADFUNCTION_name:Invalid variable name "BAD_LOCAL_VAR" -invalid-name:15:func_bad_argname:Invalid argument name "NOT_GOOD" -invalid-name:25:bad_class_name:Invalid class name "bad_class_name" -invalid-name:36:CorrectClassName.__init__:Invalid attribute name "_Bad_AtTR_name" -invalid-name:37:CorrectClassName.__init__:Invalid attribute name "Bad_PUBLIC_name" -invalid-name:39:CorrectClassName:Invalid class attribute name "zz" -invalid-name:42:CorrectClassName.BadMethodName:Invalid method name "BadMethodName" -invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:Invalid method name "__DunDER_IS_not_free_for_all__" -invalid-name:78::Invalid class name "BAD_NAME_FOR_CLASS" -invalid-name:79::Invalid class name "NEXT_BAD_NAME_FOR_CLASS" -invalid-name:86::Invalid class name "NOT_CORRECT" -invalid-name:92:test_globals:Invalid constant name "AlsoCorrect" -invalid-name:105:FooClass.PROPERTY_NAME:Invalid attribute name "PROPERTY_NAME" -invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:Invalid attribute name "ABSTRACT_PROPERTY_NAME" -invalid-name:115:FooClass.PROPERTY_NAME_SETTER:Invalid attribute name "PROPERTY_NAME_SETTER" +invalid-name:6::"Invalid constant name ""bad_const_name""" +invalid-name:9:BADFUNCTION_name:"Invalid function name ""BADFUNCTION_name""" +invalid-name:11:BADFUNCTION_name:"Invalid variable name ""BAD_LOCAL_VAR""" +invalid-name:15:func_bad_argname:"Invalid argument name ""NOT_GOOD""" +invalid-name:25:bad_class_name:"Invalid class name ""bad_class_name""" +invalid-name:36:CorrectClassName.__init__:"Invalid attribute name ""_Bad_AtTR_name""" +invalid-name:37:CorrectClassName.__init__:"Invalid attribute name ""Bad_PUBLIC_name""" +invalid-name:39:CorrectClassName:"Invalid class attribute name ""zz""" +invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""":INFERENCE +invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""":INFERENCE +invalid-name:78::"Invalid class name ""BAD_NAME_FOR_CLASS""" +invalid-name:79::"Invalid class name ""NEXT_BAD_NAME_FOR_CLASS""" +invalid-name:86::"Invalid class name ""NOT_CORRECT""" +invalid-name:92:test_globals:"Invalid constant name ""AlsoCorrect""" +invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""":INFERENCE +invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""":INFERENCE +invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""":INFERENCE diff --git a/test/functional/namedtuple_member_inference.txt b/test/functional/namedtuple_member_inference.txt index 87d9da4..308336f 100644 --- a/test/functional/namedtuple_member_inference.txt +++ b/test/functional/namedtuple_member_inference.txt @@ -1,3 +1,3 @@ -no-member:15:test:Class 'Thing' has no 'x' member +no-member:15:test:Class 'Thing' has no 'x' member:INFERENCE protected-access:19:test:Access to a protected member _replace of a client class -no-member:21:test:Instance of 'Fantastic' has no 'foo' member +no-member:21:test:Instance of 'Fantastic' has no 'foo' member:INFERENCE diff --git a/test/functional/newstyle__slots__.txt b/test/functional/newstyle__slots__.txt index afc9117..4320390 100644 --- a/test/functional/newstyle__slots__.txt +++ b/test/functional/newstyle__slots__.txt @@ -1,2 +1,2 @@ old-style-class:10:OldStyleClass:Old-style class defined. -slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class +slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class:INFERENCE diff --git a/test/functional/newstyle_properties.txt b/test/functional/newstyle_properties.txt index f3a8dbb..a16686b 100644 --- a/test/functional/newstyle_properties.txt +++ b/test/functional/newstyle_properties.txt @@ -1,2 +1,2 @@ old-style-class:13:OldStyleClass:Old-style class defined. -property-on-old-class:15:OldStyleClass:Use of "property" on an old style class +property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class":INFERENCE diff --git a/test/functional/old_style_class_py27.py b/test/functional/old_style_class_py27.py new file mode 100644 index 0000000..2c3c126 --- /dev/null +++ b/test/functional/old_style_class_py27.py @@ -0,0 +1,17 @@ +""" Tests for old style classes. """
+# pylint: disable=no-init, too-few-public-methods, invalid-name
+
+class Old: # [old-style-class]
+ """ old style class """
+
+class Child(Old):
+ """ Old style class, but don't emit for it. """
+
+class NotOldStyle2:
+ """ Because I have a metaclass at class level. """
+ __metaclass__ = type
+
+__metaclass__ = type
+
+class NotOldStyle:
+ """ Because I have a metaclass at global level. """
diff --git a/test/functional/old_style_class_py27.rc b/test/functional/old_style_class_py27.rc new file mode 100644 index 0000000..a650233 --- /dev/null +++ b/test/functional/old_style_class_py27.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.0 diff --git a/test/functional/old_style_class_py27.txt b/test/functional/old_style_class_py27.txt new file mode 100644 index 0000000..c46a7b6 --- /dev/null +++ b/test/functional/old_style_class_py27.txt @@ -0,0 +1,2 @@ +old-style-class:4:Old:Old-style class defined. +old-style-class:7:Child:Old-style class defined. diff --git a/test/functional/statement_without_effect.txt b/test/functional/statement_without_effect.txt index 636a441..81fc51b 100644 --- a/test/functional/statement_without_effect.txt +++ b/test/functional/statement_without_effect.txt @@ -1,28 +1,60 @@ pointless-string-statement:5::String statement has no effect -pointless-statement:6::Statement seems to have no effect -pointless-statement:8::Statement seems to have no effect +pointless-statement:6::"""Statement seems to have no effect +"" +" +pointless-statement:8::"""Statement seems to have no effect +"" +" pointless-statement:9::Statement seems to have no effect pointless-statement:11::Statement seems to have no effect -pointless-statement:12::Statement seems to have no effect +pointless-statement:12::"""Statement seems to have no effect +"" +" pointless-statement:15::Statement seems to have no effect -pointless-string-statement:15::String statement has no effect -unnecessary-semicolon:17::Unnecessary semicolon +pointless-string-statement:15::"""String statement has no effect +"" +" +unnecessary-semicolon:17::"""Unnecessary semicolon +"" +" pointless-string-statement:18::String statement has no effect -unnecessary-semicolon:18::Unnecessary semicolon -expression-not-assigned:19::Expression "(list()) and (tuple())" is assigned to nothing -expression-not-assigned:20::Expression "(list()) and (tuple())" is assigned to nothing +unnecessary-semicolon:18::"""Unnecessary semicolon +"" +" +expression-not-assigned:19::"""Expression """"(list()) and (tuple())"""" is assigned to nothing +"" +" +expression-not-assigned:20::"""Expression """"(list()) and (tuple())"""" is assigned to nothing +"" +" unnecessary-semicolon:21::Unnecessary semicolon -expression-not-assigned:23::Expression "(list()) and (tuple())" is assigned to nothing -expression-not-assigned:26::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:27::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:28::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:29::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:30::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:32::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:33::Expression "to_be().title" is assigned to nothing -pointless-string-statement:54:ClassLevelAttributeTest.__init__:String statement has no effect -pointless-string-statement:55:ClassLevelAttributeTest.__init__:String statement has no effect +expression-not-assigned:23::"Expression ""(list()) and (tuple())"" is assigned to nothing" +expression-not-assigned:26::"""Expression """"ANSWER == to_be()"""" is assigned to nothing +"" +" +expression-not-assigned:27::"""Expression """"ANSWER == to_be()"""" is assigned to nothing +"" +" +expression-not-assigned:28::"""Expression """"(to_be()) or (not to_be())"""" is assigned to nothing +"" +" +expression-not-assigned:29::"""Expression """"(to_be()) or (not to_be())"""" is assigned to nothing +"" +" +expression-not-assigned:30::"Expression ""ANSWER == to_be()"" is assigned to nothing" +expression-not-assigned:32::"Expression ""(to_be()) or (not to_be())"" is assigned to nothing" +expression-not-assigned:33::"Expression ""to_be().title"" is assigned to nothing" +pointless-string-statement:54:ClassLevelAttributeTest.__init__:"""String statement has no effect +"" +" +pointless-string-statement:55:ClassLevelAttributeTest.__init__:"""String statement has no effect +"" +" pointless-string-statement:58:ClassLevelAttributeTest.__init__:String statement has no effect -pointless-string-statement:61:ClassLevelAttributeTest.test:String statement has no effect -pointless-string-statement:62:ClassLevelAttributeTest.test:String statement has no effect +pointless-string-statement:61:ClassLevelAttributeTest.test:"""String statement has no effect +"" +" +pointless-string-statement:62:ClassLevelAttributeTest.test:"""String statement has no effect +"" +" pointless-string-statement:65:ClassLevelAttributeTest.test:String statement has no effect diff --git a/test/functional/string_formatting.py b/test/functional/string_formatting.py index 594c870..8d9707c 100644 --- a/test/functional/string_formatting.py +++ b/test/functional/string_formatting.py @@ -119,3 +119,11 @@ def issue310(): """ Test a regression using duplicate manual position arguments. """
'{0} {1} {0}'.format(1, 2)
'{0} {1} {0}'.format(1) # [too-few-format-args]
+
+def issue322():
+ """ Test a regression using mixed manual position arguments
+ and attribute access arguments.
+ """
+ '{0}{1[FOO]}'.format(123, {'FOO': 456})
+ '{0}{1[FOO]}'.format(123, {'FOO': 456}, 321) # [too-many-format-args]
+ '{0}{1[FOO]}'.format(123) # [too-few-format-args]
diff --git a/test/functional/string_formatting.txt b/test/functional/string_formatting.txt index 5f27835..5a4bf66 100644 --- a/test/functional/string_formatting.txt +++ b/test/functional/string_formatting.txt @@ -16,7 +16,7 @@ missing-format-argument-key:71:pprint_bad:Missing keyword argument 'b' for forma missing-format-argument-key:72:pprint_bad:Missing keyword argument 'a' for format string missing-format-attribute:74:pprint_bad:Missing format attribute 'length' in format specifier 'a.ids.__len__.length' invalid-format-index:75:pprint_bad:Using invalid lookup key 400 in format specifier 'a.ids[3][400]' -invalid-format-index:76:pprint_bad:Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +invalid-format-index:76:pprint_bad:"Using invalid lookup key ""'string'"" in format specifier 'a.ids[3][""\'string\'""]'" invalid-format-index:77:pprint_bad:Using invalid lookup key 1 in format specifier '0[0][1]' invalid-format-index:78:pprint_bad:Using invalid lookup key 0 in format specifier '0[0][0]' missing-format-argument-key:80:pprint_bad:Missing keyword argument 'b' for format string @@ -34,3 +34,5 @@ too-many-format-args:114:nested_issue294:Too many arguments for format string missing-format-argument-key:115:nested_issue294:Missing keyword argument 'a' for format string missing-format-attribute:116:nested_issue294:Missing format attribute 'x' in format specifier 'a.x' too-few-format-args:121:issue310:Not enough arguments for format string +too-many-format-args:128:issue322:Too many arguments for format string +too-few-format-args:129:issue322:Not enough arguments for format string diff --git a/test/functional/super_checks.txt b/test/functional/super_checks.txt index 2d69098..79f6ae1 100644 --- a/test/functional/super_checks.txt +++ b/test/functional/super_checks.txt @@ -1,8 +1,8 @@ old-style-class:6:Aaaa:Old-style class defined. -super-on-old-class:8:Aaaa.hop:Use of super on an old style class -super-on-old-class:12:Aaaa.__init__:Use of super on an old style class -bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super() -missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super() -bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super() -bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super() -bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super() +super-on-old-class:8:Aaaa.hop:Use of super on an old style class:INFERENCE +super-on-old-class:12:Aaaa.__init__:Use of super on an old style class:INFERENCE +bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super():INFERENCE +missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super():INFERENCE +bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE +bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super():INFERENCE +bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE diff --git a/test/functional/too_many_branches.txt b/test/functional/too_many_branches.txt index 434dc3e..fbc82cc 100644 --- a/test/functional/too_many_branches.txt +++ b/test/functional/too_many_branches.txt @@ -1 +1 @@ -too-many-branches:3:wrong:Too many branches (13/12)
+too-many-branches:3:wrong:Too many branches (13/12) diff --git a/test/functional/undefined_variable.py b/test/functional/undefined_variable.py index 25f0145..003708c 100644 --- a/test/functional/undefined_variable.py +++ b/test/functional/undefined_variable.py @@ -1,5 +1,5 @@ """Test warnings about access to undefined variables.""" -# pylint: disable=too-few-public-methods, no-init, no-self-use +# pylint: disable=too-few-public-methods, no-init, no-self-use, old-style-class DEFINED = 1 diff --git a/test/input/func_noerror_access_attr_before_def_false_positive.py b/test/input/func_noerror_access_attr_before_def_false_positive.py index 3bf966b..7f8bbb4 100644 --- a/test/input/func_noerror_access_attr_before_def_false_positive.py +++ b/test/input/func_noerror_access_attr_before_def_false_positive.py @@ -1,4 +1,4 @@ -#pylint: disable=C0103,R0904,R0903,W0201 +#pylint: disable=C0103,R0904,R0903,W0201,old-style-class """ This module demonstrates a possible problem of pyLint with calling __init__ s from inherited classes. diff --git a/test/input/func_noerror_indirect_interface.py b/test/input/func_noerror_indirect_interface.py index a0f245a..56d9ddd 100644 --- a/test/input/func_noerror_indirect_interface.py +++ b/test/input/func_noerror_indirect_interface.py @@ -1,7 +1,7 @@ """shows a bug where pylint can't find interfaces when they are used indirectly. See input/indirect[123].py for details on the setup""" - +# pylint: disable=old-style-class __revision__ = None from input.indirect2 import AbstractToto diff --git a/test/input/func_w0233.py b/test/input/func_w0233.py index 857743a..ef5f205 100644 --- a/test/input/func_w0233.py +++ b/test/input/func_w0233.py @@ -34,3 +34,17 @@ class DDDD(AAAA): AAAA.__init__(self) else: AAAA.__init__(self) + +class Super(dict): + """ test late binding super() call """ + def __init__(self): + base = super() + base.__init__() + +class Super2(dict): + """ Using the same idiom as Super, but without calling + the __init__ method. + """ + def __init__(self): + base = super() + base.__woohoo__() diff --git a/test/messages/func_typecheck_getattr.txt b/test/messages/func_typecheck_getattr.txt deleted file mode 100644 index bd91228..0000000 --- a/test/messages/func_typecheck_getattr.txt +++ /dev/null @@ -1,10 +0,0 @@ -C: 19:Client: Old-style class defined. -E: 25:Client.__init__: Class 'Provider' has no 'cattribute' member -E: 35:Client.use_method: Instance of 'Provider' has no 'hophophop' member -E: 40:Client.use_attr: Instance of 'Provider' has no 'attribute' member -E: 52:Client.test_bt_types: Instance of 'list' has no 'apppend' member -E: 54:Client.test_bt_types: Instance of 'dict' has no 'set' member -E: 56:Client.test_bt_types: Instance of 'tuple' has no 'append' member -E: 58:Client.test_bt_types: Instance of 'str' has no 'loower' member -E: 62:Client.test_bt_types: Instance of 'int' has no 'whatever' member -E: 66: Instance of 'int' has no 'lower' member (but some types could not be inferred) diff --git a/test/messages/func_w0233.txt b/test/messages/func_w0233.txt index 614ceac..157b270 100644 --- a/test/messages/func_w0233.txt +++ b/test/messages/func_w0233.txt @@ -2,3 +2,4 @@ E: 22:CCC: Module 'input.func_w0233' has no 'BBBB' member E: 27:CCC.__init__: Module 'input.func_w0233' has no 'BBBB' member F: 20: Unable to import 'nonexistant' W: 12:AAAA.__init__: __init__ method from a non direct base class 'BBBBMixin' is called +W: 48:Super2.__init__: __init__ method from base class 'dict' is not called
\ No newline at end of file diff --git a/test/test_functional.py b/test/test_functional.py index fb495e0..c2467b3 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -1,5 +1,6 @@ """Functional full-module tests for PyLint.""" from __future__ import unicode_literals +import csv import collections import ConfigParser import io @@ -9,14 +10,31 @@ import re import sys import unittest +from pylint import checkers +from pylint import interfaces from pylint import lint from pylint import reporters -from pylint import checkers +from pylint import utils + +class test_dialect(csv.excel): + if sys.version_info[0] < 3: + delimiter = b':' + lineterminator = b'\n' + else: + delimiter = ':' + lineterminator = '\n' + + +csv.register_dialect('test', test_dialect) class NoFileError(Exception): pass +# Notes: +# - for the purpose of this test, the confidence levels HIGH and UNDEFINED +# are treated as the same. + # TODOs # - implement exhaustivity tests @@ -24,10 +42,24 @@ class NoFileError(Exception): UPDATE = False class OutputLine(collections.namedtuple('OutputLine', - ['symbol', 'lineno', 'object', 'msg'])): + ['symbol', 'lineno', 'object', 'msg', 'confidence'])): @classmethod def from_msg(cls, msg): - return cls(msg.symbol, msg.line, msg.obj or '', msg.msg + '\n') + return cls( + msg.symbol, msg.line, msg.obj or '', msg.msg, + msg.confidence.name + if msg.confidence != interfaces.UNDEFINED else interfaces.HIGH.name) + + @classmethod + def from_csv(cls, row): + confidence = row[4] if len(row) == 5 else interfaces.HIGH.name + return cls(row[0], int(row[1]), row[2], row[3], confidence) + + def to_csv(self): + if self.confidence == interfaces.HIGH.name: + return self[:-1] + else: + return self # Common sub-expressions. @@ -48,8 +80,8 @@ def parse_python_version(str): class TestReporter(reporters.BaseReporter): - def add_message(self, msg_id, location, msg): - self.messages.append(reporters.Message(self, msg_id, location, msg)) + def handle_message(self, msg): + self.messages.append(msg) def on_set_current_module(self, module, filepath): self.messages = [] @@ -123,16 +155,7 @@ _OPERATORS = { } def parse_expected_output(stream): - lines = [] - for line in stream: - parts = line.split(':', 3) - if len(parts) != 4: - symbol, lineno, obj, msg = lines.pop() - lines.append(OutputLine(symbol, lineno, obj, msg + line)) - else: - linenum = int(parts[1]) - lines.append(OutputLine(parts[0], linenum, parts[2], parts[3])) - return lines + return [OutputLine.from_csv(row) for row in csv.reader(stream, 'test')] def get_expected_messages(stream): @@ -284,7 +307,7 @@ class LintModuleOutputUpdate(LintModuleTest): try: return super(LintModuleOutputUpdate, self)._open_expected_file() except IOError: - return contextlib.closing(cStringIO.StringIO()) + return io.StringIO() def _check_output_text(self, expected_messages, expected_lines, received_lines): @@ -295,9 +318,9 @@ class LintModuleOutputUpdate(LintModuleTest): remaining.extend(received_lines) remaining.sort(key=lambda m: (m[1], m[0], m[3])) with open(self._test_file.expected_output, 'w') as fobj: + writer = csv.writer(fobj, dialect='test') for line in remaining: - fobj.write('{0}:{1}:{2}:{3}'.format(*line)) - + writer.writerow(line.to_csv()) def suite(): input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), diff --git a/test/test_self.py b/test/test_self.py index 4935f8a..7eb3261 100644 --- a/test/test_self.py +++ b/test/test_self.py @@ -33,9 +33,9 @@ class MultiReporter(BaseReporter): for rep in self._reporters: rep.on_set_current_module(*args, **kwargs) - def add_message(self, *args, **kwargs): + def handle_message(self, msg): for rep in self._reporters: - rep.add_message(*args, **kwargs) + rep.handle_message(msg) def display_results(self, layout): pass diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py index fe0dbca..adf50d8 100644 --- a/test/unittest_checker_base.py +++ b/test/unittest_checker_base.py @@ -82,8 +82,6 @@ class NameCheckerTest(CheckerTestCase): @set_config(attr_rgx=re.compile('[A-Z]+')) def test_property_names(self): - if sys.version_info < (2, 6): - self.skip('abc module does not exist on 2.5') # If a method is annotated with @property, it's name should # match the attr regex. Since by default the attribute regex is the same # as the method regex, we override it here. @@ -161,18 +159,19 @@ class MultiNamingStyleTest(CheckerTestCase): MULTI_STYLE_RE = re.compile('(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$') @set_config(class_rgx=MULTI_STYLE_RE) - def test_multi_name_detection_first(self): + def test_multi_name_detection_majority(self): classes = test_utils.extract_node(""" - class CLASSA(object): #@ - pass class classb(object): #@ pass + class CLASSA(object): #@ + pass class CLASSC(object): #@ pass """) - with self.assertAddsMessages(Message('invalid-name', node=classes[1], args=('class', 'classb', ''))): + with self.assertAddsMessages(Message('invalid-name', node=classes[0], args=('class', 'classb', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(class_rgx=MULTI_STYLE_RE) def test_multi_name_detection_first_invalid(self): @@ -188,6 +187,7 @@ class MultiNamingStyleTest(CheckerTestCase): Message('invalid-name', node=classes[2], args=('class', 'CLASSC', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(method_rgx=MULTI_STYLE_RE, function_rgx=MULTI_STYLE_RE, @@ -204,6 +204,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[1], args=('function', 'FUNC', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) @set_config(function_rgx=re.compile('(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$')) def test_multi_name_detection_exempt(self): @@ -220,6 +221,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[3], args=('function', 'UPPER', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) if __name__ == '__main__': diff --git a/test/unittest_lint.py b/test/unittest_lint.py index b2022cc..91525e3 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -26,12 +26,13 @@ from logilab.common.compat import reload from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError -from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ +from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, MSG_STATE_CONFIDENCE, \ MessagesStore, PyLintASTWalker, MessageDefinition, FileState, \ build_message_def, tokenize_module from pylint.testutils import TestReporter -from pylint.reporters import text +from pylint.reporters import text, html from pylint import checkers +from pylint import interfaces if sys.platform == 'win32': HOME = 'USERPROFILE' @@ -139,19 +140,26 @@ class PyLinterTC(unittest.TestCase): self.assertTrue(linter.is_message_enabled('C0121', line=1)) def test_message_state_scope(self): + class FakeConfig(object): + confidence = ['HIGH'] + linter = self.init_linter() - fs = linter.file_state linter.disable('C0121') self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) linter.disable('W0101', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0101', 3)) + linter.get_message_state_scope('W0101', 3)) linter.enable('W0102', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0102', 3)) + linter.get_message_state_scope('W0102', 3)) + linter.config = FakeConfig() + self.assertEqual( + MSG_STATE_CONFIDENCE, + linter.get_message_state_scope('this-is-bad', + confidence=interfaces.INFERENCE)) def test_enable_message_block(self): linter = self.init_linter() @@ -359,6 +367,15 @@ class PyLinterTC(unittest.TestCase): ['C: 2: Line too long (175/80)'], self.linter.reporter.messages) + def test_html_reporter_missing_files(self): + output = cStringIO.StringIO() + self.linter.set_reporter(html.HTMLReporter(output)) + self.linter.set_option('output-format', 'html') + self.linter.check('troppoptop.py') + value = output.getvalue() + self.assertIn('troppoptop.py', value) + self.assertIn('fatal', value) + class ConfigTC(unittest.TestCase): diff --git a/test/unittest_utils.py b/test/unittest_utils.py index ef0cda2..d631dbb 100644 --- a/test/unittest_utils.py +++ b/test/unittest_utils.py @@ -15,6 +15,7 @@ import unittest from astroid import test_utils from pylint import utils +from pylint import interfaces from pylint.checkers.utils import check_messages @@ -59,4 +60,3 @@ class PyLintASTWalkerTest(unittest.TestCase): if __name__ == '__main__': unittest.main() - diff --git a/testutils.py b/testutils.py index ef21211..86539ac 100644 --- a/testutils.py +++ b/testutils.py @@ -141,7 +141,7 @@ class UnittestLinter(object): finally: self._messages = [] - def add_message(self, msg_id, line=None, node=None, args=None): + 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): @@ -245,6 +245,7 @@ class LintTestUsingModule(unittest.TestCase): depends = None output = None _TEST_TYPE = 'module' + maxDiff = None def shortDescription(self): values = {'mode' : self._TEST_TYPE, @@ -16,12 +16,15 @@ """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 os -from warnings import warn +import warnings + from os.path import dirname, basename, splitext, exists, isdir, join, normpath from logilab.common.interface import implements @@ -33,7 +36,7 @@ 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 +from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED class UnknownMessage(Exception): @@ -51,7 +54,7 @@ MSG_TYPES = { 'E' : 'error', 'F' : 'fatal' } -MSG_TYPES_LONG = dict([(v, k) for k, v in MSG_TYPES.iteritems()]) +MSG_TYPES_LONG = {v: k for k, v in MSG_TYPES.iteritems()} MSG_TYPES_STATUS = { 'I' : 0, @@ -65,6 +68,7 @@ MSG_TYPES_STATUS = { _MSG_ORDER = 'EWRCIF' MSG_STATE_SCOPE_CONFIG = 0 MSG_STATE_SCOPE_MODULE = 1 +MSG_STATE_CONFIDENCE = 2 OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') @@ -75,6 +79,29 @@ class WarningScope(object): LINE = 'line-based-msg' NODE = 'node-based-msg' +_MsgBase = collections.namedtuple( + '_MsgBase', + ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence', + 'abspath', '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""" @@ -124,8 +151,8 @@ def build_message_def(checker, msgid, msg_tuple): # messages should have a symbol, but for backward compatibility # they may not. (msg, descr) = msg_tuple - warn("[pylint 0.26] description of message %s doesn't include " - "a symbolic name" % msgid, DeprecationWarning) + 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) @@ -278,12 +305,25 @@ class MessagesHandlerMixIn(object): # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - def is_message_enabled(self, msg_descr, line=None): + 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: @@ -298,7 +338,7 @@ class MessagesHandlerMixIn(object): except KeyError: return self._msgs_state.get(msgid, True) - def add_message(self, msg_descr, line=None, node=None, args=None): + 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 @@ -328,8 +368,10 @@ class MessagesHandlerMixIn(object): else: col_offset = None # should this message be displayed - if not self.is_message_enabled(msgid, line): - self.file_state.handle_ignored_message(msgid, line, node, args) + 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]] @@ -352,7 +394,9 @@ class MessagesHandlerMixIn(object): module, obj = get_module_and_frameid(node) path = node.root().file # add the message - self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg) + self.reporter.handle_message( + Message(msgid, symbol, + (path, module, obj, line or 1, col_offset or 0), msg, confidence)) def print_full_documentation(self): """output a full documentation in ReST format""" @@ -360,18 +404,18 @@ class MessagesHandlerMixIn(object): for checker in self.get_checkers(): if checker.name == 'master': prefix = 'Main ' - print "Options" - print '-------\n' + print("Options") + print('-------\n') 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) + print(title) + print('~' * len(title)) rest_format_section(sys.stdout, None, options) - print + print() else: try: by_checker[checker.name][0] += checker.options_and_values() @@ -384,32 +428,32 @@ class MessagesHandlerMixIn(object): for checker, (options, msgs, reports) in by_checker.iteritems(): prefix = '' title = '%s checker' % checker - print title - print '-' * len(title) - print + print(title) + print('-' * len(title)) + print() if options: title = 'Options' - print title - print '~' * len(title) + print(title) + print('~' * len(title)) rest_format_section(sys.stdout, None, options) - print + print() if msgs: title = ('%smessages' % prefix).capitalize() - print title - print '~' * len(title) + print(title) + print('~' * len(title)) for msgid, msg in sorted(msgs.iteritems(), - key=lambda (k, v): (_MSG_ORDER.index(k[0]), k)): + 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 + print(msg.format_help(checkerref=False)) + print() if reports: title = ('%sreports' % prefix).capitalize() - print title - print '~' * len(title) + print(title) + print('~' * len(title)) for report in reports: - print ':%s: %s' % report[:2] - print - print + print(':%s: %s' % report[:2]) + print() + print() class FileState(object): @@ -493,14 +537,13 @@ class FileState(object): except KeyError: self._module_msgs_state[msg.msgid] = {line: status} - def handle_ignored_message(self, msgid, line, node, args): + def handle_ignored_message(self, state_scope, msgid, line, node, args, confidence): """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. """ - state_scope = self._message_state_scope(msgid, line) if state_scope == MSG_STATE_SCOPE_MODULE: try: orig_line = self._suppression_mapping[(msgid, line)] @@ -508,14 +551,6 @@ class FileState(object): except KeyError: pass - def _message_state_scope(self, msgid, line=None): - """Returns the scope at which a message was enabled/disabled.""" - try: - if line in self._module_msgs_state[msgid]: - return MSG_STATE_SCOPE_MODULE - except KeyError: - return MSG_STATE_SCOPE_CONFIG - def iter_spurious_suppression_messages(self, msgs_store): for warning, lines in self._raw_module_msgs_state.iteritems(): for line, enable in lines.iteritems(): @@ -615,11 +650,11 @@ class MessagesStore(object): """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, ex: - print ex - print + print(self.check_message_id(msgid).format_help(checkerref=True)) + print() + except UnknownMessage as ex: + print(ex) + print() continue def list_messages(self): @@ -628,8 +663,8 @@ class MessagesStore(object): for msg in msgs: if not msg.may_be_emitted(): continue - print msg.format_help(checkerref=False) - print + print(msg.format_help(checkerref=False)) + print() class ReportsHandlerMixIn(object): @@ -640,6 +675,12 @@ class ReportsHandlerMixIn(object): self._reports = {} 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 @@ -671,7 +712,7 @@ class ReportsHandlerMixIn(object): """render registered reports""" sect = Section('Report', '%s statements analysed.'% (self.stats['statement'])) - for checker in self._reports: + for checker in self.report_order(): for reportid, r_title, r_cb in self._reports[checker]: if not self.report_is_enabled(reportid): continue @@ -721,7 +762,7 @@ def expand_modules(files_or_modules, black_list): if filepath is None: errors.append({'key' : 'ignored-builtin-module', 'mod': modname}) continue - except (ImportError, SyntaxError), ex: + 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}) @@ -824,7 +865,7 @@ def register_plugins(linter, directory): except ValueError: # empty module name (usually emacs auto-save files) continue - except ImportError, exc: + except ImportError as exc: print >> sys.stderr, "Problem importing module %s: %s" % (filename, exc) else: if hasattr(module, 'register'): |