Commits

Sylvain Thénault committed 20d4be6 Merge

merge

Comments (0)

Files changed (38)

 ChangeLog for PyLint
 ====================
 
+--
+    * #69738: add regular expressions support for "generated-members"
+
+    * ids of logging and string_format checkers have been changed:
+      logging: 65 -> 12, string_format: 99 -> 13
+      Also add documentation to say that ids of range 1-50 shall be reserved
+      to pylint internal checkers
+
+    * #69993: Additional string format checks for logging module:
+      check for missing arguments, too many arguments, or invalid string
+      formats in the logging checker module. Contributed by Daniel Arena
+
+    * #69220: add column offset to the reports. If you've a custom reporter,
+      this change may break it has now location gain a new item giving the
+      column offset.
+
+    * #60828: Fix false positive in reimport check
+
+    * #70495: absolute imports fail depending on module path (patch by Jacek Konieczny)
+
+    * #22273: Fix --ignore option documentation to match reality
+
+
 2011-01-11  --  0.23.0
     * documentation update, add manpages
 

checkers/__init__.py

 09: design_analysis
 10: newstyle
 11: typecheck
+12: logging
+13: string_format
+14-50: not yet used: reserved for future internal checkers.
+51-99: perhaps used: reserved for external checkers
 
 The raw_metrics checker has no number associated since it doesn't emit any
 messages nor reports. XXX not true, emit a 07 report !
+
 """
 
 import tokenize

checkers/imports.py

     """return the node where [base.]<name> is imported or None if not found
     """
     first = None
+    found = False
     for first in context.values():
         if isinstance(first, astng.Import):
             if name in [iname[0] for iname in first.names]:
+                found = True
                 break
         elif isinstance(first, astng.From):
             if base == first.modname and level == first.level and \
                    name in [iname[0] for iname in first.names]:
+                found = True
                 break
-    if first is not node and not are_exclusive(first, node):
+    if found and first is not node and not are_exclusive(first, node):
         return first
 
 # utilities to represents import dependencies as tree and dot graph ###########
 # the import checker itself ###################################################
 
 MSGS = {
-    'F0401': ('Unable to import %r',
+    'F0401': ('Unable to import %s',
               'Used when pylint has been unable to import a module.'),
     'R0401': ('Cyclic import (%s)',
               'Used when a cyclic import between two or more modules is \
         try:
             return importnode.do_import_module(modname)
         except astng.InferenceError, ex:
-            self.add_message("F0401", args=modname, node=importnode)
+            if str(ex) != modname:
+                args = '%r (%s)' % (modname, ex)
+            else:
+                args = repr(modname)
+            self.add_message("F0401", args=args, node=importnode)
 
     def _check_relative_import(self, modnode, importnode, importedmodnode,
                                importedasname):

checkers/logging.py

 from logilab import astng
 from pylint import checkers
 from pylint import interfaces
+from pylint.checkers import utils
 
-EAGER_STRING_INTERPOLATION = 'W6501'
 
-CHECKED_CONVENIENCE_FUNCTIONS = set([
-    'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn',
-    'warning'])
-
-
-class LoggingChecker(checkers.BaseChecker):
-    """Checks use of the logging module."""
-
-    __implements__ = interfaces.IASTNGChecker
-
-    name = 'logging'
-
-    msgs = {EAGER_STRING_INTERPOLATION:
-            ('Specify string format arguments as logging function parameters',
+MSGS = {
+    'W1201': ('Specify string format arguments as logging function parameters',
              'Used when a logging statement has a call form of '
              '"logging.<logging method>(format_string % (format_args...))". '
              'Such calls should leave string interpolation to the logging '
              'so that the program may avoid incurring the cost of the '
              'interpolation in those cases in which no message will be '
              'logged. For more, see '
-             'http://www.python.org/dev/peps/pep-0282/.')
-            }
+             'http://www.python.org/dev/peps/pep-0282/.'),
+    'E1200': ('Unsupported logging format character %r (%#02x) at index %d',
+              'Used when an unsupported format character is used in a logging\
+              statement format string.'),
+    'E1201': ('Logging format string ends in middle of conversion specifier',
+              'Used when a logging statement format string terminates before\
+              the end of a conversion specifier.'),
+    'E1205': ('Too many arguments for logging format string',
+              'Used when a logging format string is given too few arguments.'),
+    'E1206': ('Not enough arguments for logging format string',
+              'Used when a logging format string is given too many arguments'),
+    }
+
+
+CHECKED_CONVENIENCE_FUNCTIONS = set([
+    'critical', 'debug', 'error', 'exception', 'fatal', 'info', 'warn',
+    'warning'])
+
+
+class LoggingChecker(checkers.BaseChecker):
+    """Checks use of the logging module."""
+
+    __implements__ = interfaces.IASTNGChecker
+    name = 'logging'
+    msgs = MSGS
 
     def visit_module(self, unused_node):
         """Clears any state left in this checker from last module checked."""
             or not isinstance(node.func.expr, astng.Name)
             or node.func.expr.name != self._logging_name):
             return
-        self._CheckConvenienceMethods(node)
-        self._CheckLogMethod(node)
+        self._check_convenience_methods(node)
+        self._check_log_methods(node)
 
-    def _CheckConvenienceMethods(self, node):
+    def _check_convenience_methods(self, node):
         """Checks calls to logging convenience methods (like logging.warn)."""
         if node.func.attrname not in CHECKED_CONVENIENCE_FUNCTIONS:
             return
-        if not node.args:
-            # Either no args, or star args, or double-star args. Beyond the
-            # scope of this checker in any case.
+        if node.starargs or node.kwargs or not node.args:
+            # Either no args, star args, or double-star args. Beyond the
+            # scope of this checker.
             return
         if isinstance(node.args[0], astng.BinOp) and node.args[0].op == '%':
-            self.add_message(EAGER_STRING_INTERPOLATION, node=node)
+            self.add_message('W1201', node=node)
+        elif isinstance(node.args[0], astng.Const):
+            self._check_format_string(node, 0)
 
-    def _CheckLogMethod(self, node):
+    def _check_log_methods(self, node):
         """Checks calls to logging.log(level, format, *format_args)."""
         if node.func.attrname != 'log':
             return
-        if len(node.args) < 2:
-            # Either a malformed call or something with crazy star args or
-            # double-star args magic. Beyond the scope of this checker.
+        if node.starargs or node.kwargs or len(node.args) < 2:
+            # Either a malformed call, star args, or double-star args. Beyond
+            # the scope of this checker.
             return
         if isinstance(node.args[1], astng.BinOp) and node.args[1].op == '%':
-            self.add_message(EAGER_STRING_INTERPOLATION, node=node)
+            self.add_message('W1201', node=node)
+        elif isinstance(node.args[1], astng.Const):
+            self._check_format_string(node, 1)
+
+    def _check_format_string(self, node, format_arg):
+        """Checks that format string tokens match the supplied arguments.
+
+        Args:
+          node: AST node to be checked.
+          format_arg: Index of the format string in the node arguments.
+        """
+        num_args = self._count_supplied_tokens(node.args[format_arg + 1:])
+        if not num_args:
+            # If no args were supplied, then all format strings are valid -
+            # don't check any further.
+            return
+        format_string = node.args[format_arg].value
+        if not isinstance(format_string, basestring):
+            # If the log format is constant non-string (e.g. logging.debug(5)),
+            # ensure there are no arguments.
+            required_num_args = 0
+        else:
+            try:
+                keyword_args, required_num_args = \
+                    utils.parse_format_string(format_string)
+                if keyword_args:
+                    # Keyword checking on logging strings is complicated by
+                    # special keywords - out of scope.
+                    return
+            except utils.UnsupportedFormatCharacter, e:
+                c = format_string[e.index]
+                self.add_message('E1200', node=node, args=(c, ord(c), e.index))
+                return
+            except utils.IncompleteFormatString:
+                self.add_message('E1201', node=node)
+                return
+        if num_args > required_num_args:
+            self.add_message('E1205', node=node)
+        elif num_args < required_num_args:
+            self.add_message('E1206', node=node)
+
+    def _count_supplied_tokens(self, args):
+        """Counts the number of tokens in an args list.
+
+        The Python log functions allow for special keyword arguments: func,
+        exc_info and extra. To handle these cases correctly, we only count
+        arguments that aren't keywords.
+
+        Args:
+          args: List of AST nodes that are arguments for a log format string.
+
+        Returns:
+          Number of AST nodes that aren't keywords.
+        """
+        return sum(1 for arg in args if not isinstance(arg, astng.Keyword))
+
 
 def register(linter):
     """Required method to auto-register this checker."""

checkers/string_format.py

 from logilab import astng
 from pylint.interfaces import IASTNGChecker
 from pylint.checkers import BaseChecker
+from pylint.checkers import utils
+
 
 MSGS = {
-    'E9900': ("Unsupported format character %r (%#02x) at index %d",
+    'E1300': ("Unsupported format character %r (%#02x) at index %d",
               "Used when a unsupported format character is used in a format\
               string."),
-    'E9901': ("Format string ends in middle of conversion specifier",
+    'E1301': ("Format string ends in middle of conversion specifier",
               "Used when a format string terminates before the end of a \
               conversion specifier."),
-    'E9902': ("Mixing named and unnamed conversion specifiers in format string",
+    'E1302': ("Mixing named and unnamed conversion specifiers in format string",
               "Used when a format string contains both named (e.g. '%(foo)d') \
               and unnamed (e.g. '%d') conversion specifiers.  This is also \
               used when a named conversion specifier contains * for the \
               minimum field width and/or precision."),
-    'E9903': ("Expected mapping for format string, not %s",
+    'E1303': ("Expected mapping for format string, not %s",
               "Used when a format string that uses named conversion specifiers \
               is used with an argument that is not a mapping."),
-    'W9900': ("Format string dictionary key should be a string, not %s",
+    'W1300': ("Format string dictionary key should be a string, not %s",
               "Used when a format string that uses named conversion specifiers \
               is used with a dictionary whose keys are not all strings."),
-    'W9901': ("Unused key %r in format string dictionary",
+    'W1301': ("Unused key %r in format string dictionary",
               "Used when a format string that uses named conversion specifiers \
               is used with a dictionary that conWtains keys not required by the \
               format string."),
-    'E9904': ("Missing key %r in format string dictionary",
+    'E1304': ("Missing key %r in format string dictionary",
               "Used when a format string that uses named conversion specifiers \
               is used with a dictionary that doesn't contain all the keys \
               required by the format string."),
-    'E9905': ("Too many arguments for format string",
+    'E1305': ("Too many arguments for format string",
               "Used when a format string that uses unnamed conversion \
               specifiers is given too few arguments."),
-    'E9906': ("Not enough arguments for format string",
+    'E1306': ("Not enough arguments for format string",
               "Used when a format string that uses unnamed conversion \
               specifiers is given too many arguments"),
     }
 
-class IncompleteFormatString(Exception):
-    """A format string ended in the middle of a format specifier."""
-    pass
-
-class UnsupportedFormatCharacter(Exception):
-    """A format character in a format string is not one of the supported
-    format characters."""
-    def __init__(self, index):
-        Exception.__init__(self, index)
-        self.index = index
-
-def parse_format_string(format_string):
-    """Parses a format string, returning a tuple of (keys, num_args), where keys
-    is the set of mapping keys in the format string, and num_args is the number
-    of arguments required by the format string.  Raises
-    IncompleteFormatString or UnsupportedFormatCharacter if a
-    parse error occurs."""
-    keys = set()
-    num_args = 0
-    def next_char(i):
-        i += 1
-        if i == len(format_string):
-            raise IncompleteFormatString
-        return (i, format_string[i])
-    i = 0
-    while i < len(format_string):
-        c = format_string[i]
-        if c == '%':
-            i, c = next_char(i)
-            # Parse the mapping key (optional).
-            key = None
-            if c == '(':
-                depth = 1
-                i, c = next_char(i)
-                key_start = i
-                while depth != 0:
-                    if c == '(':
-                        depth += 1
-                    elif c == ')':
-                        depth -= 1
-                    i, c = next_char(i)
-                key_end = i - 1
-                key = format_string[key_start:key_end]
-
-            # Parse the conversion flags (optional).
-            while c in '#0- +':
-                i, c = next_char(i)
-            # Parse the minimum field width (optional).
-            if c == '*':
-                num_args += 1
-                i, c = next_char(i)
-            else:
-                while c in string.digits:
-                    i, c = next_char(i)
-            # Parse the precision (optional).
-            if c == '.':
-                i, c = next_char(i)
-                if c == '*':
-                    num_args += 1
-                    i, c = next_char(i)
-                else:
-                    while c in string.digits:
-                        i, c = next_char(i)
-            # Parse the length modifier (optional).
-            if c in 'hlL':
-                i, c = next_char(i)
-            # Parse the conversion type (mandatory).
-            if c not in 'diouxXeEfFgGcrs%':
-                raise UnsupportedFormatCharacter(i)
-            if key:
-                keys.add(key)
-            elif c != '%':
-                num_args += 1
-        i += 1
-    return keys, num_args
-
 OTHER_NODES = (astng.Const, astng.List, astng.Backquote,
                astng.Lambda, astng.Function,
                astng.ListComp, astng.SetComp, astng.GenExpr)
         format_string = left.value
         try:
             required_keys, required_num_args = \
-                parse_format_string(format_string)
-        except UnsupportedFormatCharacter, e:
+                utils.parse_format_string(format_string)
+        except utils.UnsupportedFormatCharacter, e:
             c = format_string[e.index]
-            self.add_message('E9900', node=node, args=(c, ord(c), e.index))
+            self.add_message('E1300', node=node, args=(c, ord(c), e.index))
             return
-        except IncompleteFormatString:
-            self.add_message('E9901', node=node)
+        except utils.IncompleteFormatString:
+            self.add_message('E1301', node=node)
             return
         if required_keys and required_num_args:
             # The format string uses both named and unnamed format
             # specifiers.
-            self.add_message('E9902', node=node)
+            self.add_message('E1302', node=node)
         elif required_keys:
             # The format string uses only named format specifiers.
             # Check that the RHS of the % operator is a mapping object
                         if isinstance(key, basestring):
                             keys.add(key)
                         else:
-                            self.add_message('W9900', node=node, args=key)
+                            self.add_message('W1300', node=node, args=key)
                     else:
                         # One of the keys was something other than a
                         # constant.  Since we can't tell what it is,
                 if not unknown_keys:
                     for key in required_keys:
                         if key not in keys:
-                            self.add_message('E9904', node=node, args=key)
+                            self.add_message('E1304', node=node, args=key)
                 for key in keys:
                     if key not in required_keys:
-                        self.add_message('W9901', node=node, args=key)
+                        self.add_message('W1301', node=node, args=key)
             elif isinstance(args, OTHER_NODES + (astng.Tuple,)):
                 type_name = type(args).__name__
-                self.add_message('E9903', node=node, args=type_name)
+                self.add_message('E1303', node=node, args=type_name)
             # else:
                 # The RHS of the format specifier is a name or
                 # expression.  It may be a mapping object, so
                 num_args = None
             if num_args is not None:
                 if num_args > required_num_args:
-                    self.add_message('E9905', node=node)
+                    self.add_message('E1305', node=node)
                 elif num_args < required_num_args:
-                    self.add_message('E9906', node=node)
+                    self.add_message('E1306', node=node)
 
 
 def register(linter):

checkers/typecheck.py

 """try to find more bugs in the code using astng inference capabilities
 """
 
+import re
+import shlex
+
 from logilab import astng
 from logilab.astng import InferenceError, NotFoundError, YES, Instance
 
                ('generated-members',
                 {'default' : (
         'REQUEST', 'acl_users', 'aq_parent'),
-                 'type' : 'csv',
+                 'type' : 'string',
                  'metavar' : '<members names>',
                  'help' : 'List of members which are set dynamically and \
 missed by pylint inference system, and so shouldn\'t trigger E0201 when \
-accessed.'}
+accessed. Python regular expressions are accepted.'}
                 ),
         )
 
 
         function/method, super call and metaclasses are ignored
         """
-        if node.attrname in self.generated_members:
+        # generated_members may containt regular expressions
+        # (surrounded by quote `"` and followed by a comma `,`)
+        # REQUEST,aq_parent,"[a-zA-Z]+_set{1,2}"' =>
+        # ('REQUEST', 'aq_parent', '[a-zA-Z]+_set{1,2}')
+        if isinstance(self.config.generated_members, str):
+            gen = shlex.shlex(self.config.generated_members)
+            gen.whitespace += ','
+            self.config.generated_members = tuple(tok.strip('"') for tok in gen)
+        for pattern in self.config.generated_members:
             # attribute is marked as generated, stop here
-            return
+            if re.match(pattern, node.attrname):
+                return
         try:
             infered = list(node.expr.infer())
         except InferenceError:

checkers/utils.py

 """some functions that may be useful for various checkers
 """
 
+import string
 from logilab import astng
 from logilab.common.compat import builtins
 BUILTINS_NAME = builtins.__name__
         return func
     return store_messages
 
+class IncompleteFormatString(Exception):
+    """A format string ended in the middle of a format specifier."""
+    pass
+
+class UnsupportedFormatCharacter(Exception):
+    """A format character in a format string is not one of the supported
+    format characters."""
+    def __init__(self, index):
+        Exception.__init__(self, index)
+        self.index = index
+
+def parse_format_string(format_string):
+    """Parses a format string, returning a tuple of (keys, num_args), where keys
+    is the set of mapping keys in the format string, and num_args is the number
+    of arguments required by the format string.  Raises
+    IncompleteFormatString or UnsupportedFormatCharacter if a
+    parse error occurs."""
+    keys = set()
+    num_args = 0
+    def next_char(i):
+        i += 1
+        if i == len(format_string):
+            raise IncompleteFormatString
+        return (i, format_string[i])
+    i = 0
+    while i < len(format_string):
+        c = format_string[i]
+        if c == '%':
+            i, c = next_char(i)
+            # Parse the mapping key (optional).
+            key = None
+            if c == '(':
+                depth = 1
+                i, c = next_char(i)
+                key_start = i
+                while depth != 0:
+                    if c == '(':
+                        depth += 1
+                    elif c == ')':
+                        depth -= 1
+                    i, c = next_char(i)
+                key_end = i - 1
+                key = format_string[key_start:key_end]
+
+            # Parse the conversion flags (optional).
+            while c in '#0- +':
+                i, c = next_char(i)
+            # Parse the minimum field width (optional).
+            if c == '*':
+                num_args += 1
+                i, c = next_char(i)
+            else:
+                while c in string.digits:
+                    i, c = next_char(i)
+            # Parse the precision (optional).
+            if c == '.':
+                i, c = next_char(i)
+                if c == '*':
+                    num_args += 1
+                    i, c = next_char(i)
+                else:
+                    while c in string.digits:
+                        i, c = next_char(i)
+            # Parse the length modifier (optional).
+            if c in 'hlL':
+                i, c = next_char(i)
+            # Parse the conversion type (mandatory).
+            if c not in 'diouxXeEfFgGcrs%':
+                raise UnsupportedFormatCharacter(i)
+            if key:
+                keys.add(key)
+            elif c != '%':
+                num_args += 1
+        i += 1
+    return keys, num_args

checkers/variables.py

     def visit_from(self, node):
         """check modules attribute accesses"""
         name_parts = node.modname.split('.')
+        level = getattr(node, 'level', None)
         try:
-            module = node.root().import_module(name_parts[0])
+            module = node.root().import_module(name_parts[0], level=level)
         except ASTNGBuildingException:
             return
         except Exception, exc:
 
 Messages
 ~~~~~~~~
-:W6501: *Specify string format arguments as logging function parameters*
+:W1201: *Specify string format arguments as logging function parameters*
   Used when a logging statement has a call form of "logging.<logging
   method>(format_string % (format_args...))". Such calls should leave string
   interpolation to the logging method itself and be written "logging.<logging
   incurring the cost of the interpolation in those cases in which no message
   will be logged. For more, see http://www.python.org/dev/peps/pep-0282/.
 
+:E1200: *Unsupported logging format character %r (%#02x) at index %d*
+  Used when an unsupported format character is used in a logging statement 
+  format string.
+
+:E1201: *'Logging format string ends in middle of conversion specifier*
+  Used when a logging statement format string terminates before the end of a
+  conversion specifier.
+
+:E1205: *Too many arguments for logging format string*
+  Used when a logging format string is given too few arguments.
+
+:E1206: *Not enough arguments for logging format string*
+  Used when a logging format string is given too many arguments.
 
 similarities checker
 --------------------
 
 Messages
 ~~~~~~~~
-:E9900: *Unsupported format character %r (%#02x) at index %d*
+:E1300: *Unsupported format character %r (%#02x) at index %d*
   Used when a unsupported format character is used in a format string.
-:E9901: *Format string ends in middle of conversion specifier*
+:E1301: *Format string ends in middle of conversion specifier*
   Used when a format string terminates before the end of a conversion specifier.
-:E9902: *Mixing named and unnamed conversion specifiers in format string*
+:E1302: *Mixing named and unnamed conversion specifiers in format string*
   Used when a format string contains both named (e.g. '%(foo)d') and unnamed
   (e.g. '%d') conversion specifiers. This is also used when a named conversion
   specifier contains * for the minimum field width and/or precision.
-:E9903: *Expected mapping for format string, not %s*
+:E1303: *Expected mapping for format string, not %s*
   Used when a format string that uses named conversion specifiers is used with
   an argument that is not a mapping.
-:E9904: *Missing key %r in format string dictionary*
+:E1304: *Missing key %r in format string dictionary*
   Used when a format string that uses named conversion specifiers is used with a
   dictionary that doesn't contain all the keys required by the format string.
-:E9905: *Too many arguments for format string*
+:E1305: *Too many arguments for format string*
   Used when a format string that uses unnamed conversion specifiers is given too
   few arguments.
-:E9906: *Not enough arguments for format string*
+:E1306: *Not enough arguments for format string*
   Used when a format string that uses unnamed conversion specifiers is given too
   many arguments
-:W9900: *Format string dictionary key should be a string, not %s*
+:W1300: *Format string dictionary key should be a string, not %s*
   Used when a format string that uses named conversion specifiers is used with a
   dictionary whose keys are not all strings.
-:W9901: *Unused key %r in format string dictionary*
+:W1301: *Unused key %r in format string dictionary*
   Used when a format string that uses named conversion specifiers is used with a
   dictionary that conWtains keys not required by the format string.
 
   This is a particular case of W0104 with its own message so you can easily
   disable it if you're using those strings as documentation, instead of
   comments.
+:W0106: *Expression "%s" is assigned to nothing',*
+  Used when an expression that is not a function call is assigned to nothing. 
+  Probably something else was intended.
 :W0107: *Unnecessary pass statement*
   Used when a "pass" statement that can be avoided is encountered.)
 :W0108: *Lambda may not be necessary*

examples/pylintrc

 zope=no
 
 # List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E0201 when accessed.
-generated-members=REQUEST,acl_users,aq_parent
+# system, and so shouldn't trigger E0201 when accessed. Note that regular
+# expressions are accepted (surrounded by quote `"` and followed by a comma `,`)
+generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",
 
 
 [BASIC]
     to:
     * handle message activation / deactivation at the module level
     * handle some basic but necessary stats'data (number of classes, methods...)
+
+    IDE plugins developpers: you may have to call
+    `logilab.astng.builder.MANAGER.astng_cache.clear()` accross run if you want
+    to ensure the latest code version is actually checked.
     """
 
     __implements__ = (ILinter, IRawChecker)
     may_be_disabled = False
 
     options = (('ignore',
-                {'type' : 'csv', 'metavar' : '<file>',
+                {'type' : 'csv', 'metavar' : '<file>[,<file>...]',
                  'dest' : 'black_list', 'default' : ('CVS',),
-                 'help' : 'Add <file or directory> to the black list. It \
-should be a base name, not a path. You may set this option multiple times.'}),
-
+                 'help' : 'Add files or directories to the blacklist. \
+They should be base names, not paths.'}),
                ('persistent',
                 {'default': True, 'type' : 'yn', 'metavar' : '<y_or_n>',
                  'level': 1,
 Python code to execute, usually for sys.path manipulation such as pygtk.require().
 .IP "--errors-only, -E"
 In error mode, checkers without error messages are disabled and for others, only the ERROR messages are displayed, and no reports are done by default
-.IP "--ignore=<file>"
-Add <file or directory> to the black list. It should be a base name, not a path. You may set this option multiple times. [current: CVS]
+.IP "--ignore=<file>[,<file>...]"
+Add files or directories to the blacklist. They should be base names, not paths.
 .IP "--persistent=<y_or_n>"
 Pickle collected data for later comparisons. [current: yes]
 .IP "--load-plugins=<modules>"

reporters/html.py

 
     def add_message(self, msg_id, location, msg):
         """manage message of different type and in the context of path"""
-        module, obj, line = location[1:]
+        module, obj, line, col_offset = location[1:]
         if self.include_ids:
             sigle = msg_id
         else:
         """
         if self.msgs:
             # add stored messages to the layout
-            msgs = ['type', 'module', 'object', 'line', 'message']
+            msgs = ['type', 'module', 'object', 'line', 'col_offset', 'message']
             msgs += self.msgs
             sect = Section('Messages')
             layout.append(sect)

reporters/text.py

 # Copyright (c) 2003-2007 Sylvain Thenault (thenault@gmail.com).
-# Copyright (c) 2003-2007 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
 # Foundation; either version 2 of the License, or (at your option) any later
 :text: the default one grouping messages by module
 :parseable:
   standard parseable output with full module path on each message (for
-  editor integration) 
+  editor integration)
 :colorized: an ANSI colorized text reporter
 
 """
 class TextReporter(BaseReporter):
     """reports messages and layouts in plain text
     """
-    
+
     __implements__ = IReporter
     extension = 'txt'
-    
+
     def __init__(self, output=sys.stdout):
         BaseReporter.__init__(self, output)
         self._modules = {}
 
     def add_message(self, msg_id, location, msg):
         """manage message of different type and in the context of path"""
-        module, obj, line = location[1:]
+        module, obj, line, col_offset = location[1:]
         if module not in self._modules:
             if module:
                 self.writeln('************* Module %s' % module)
             sigle = msg_id
         else:
             sigle = msg_id[0]
-        self.writeln('%s:%3s%s: %s' % (sigle, line, obj, msg))
+        self.writeln('%s:%3s,%s%s: %s' % (sigle, line, col_offset, obj, msg))
 
     def _display(self, layout):
         """launch layouts display"""
-        print >> self.out 
+        print >> self.out
         TextWriter().format(layout, self.out)
 
 
 class ParseableTextReporter(TextReporter):
     """a reporter very similar to TextReporter, but display messages in a form
     recognized by most text editors :
-    
+
     <filename>:<linenum>:<msg>
     """
     line_format = '%(path)s:%(line)s: [%(sigle)s%(obj)s] %(msg)s'
-    
+
     def __init__(self, output=sys.stdout, relative=True):
         TextReporter.__init__(self, output)
         if relative:
 
     def add_message(self, msg_id, location, msg):
         """manage message of different type and in the context of path"""
-        path, _, obj, line = location
+        path, _, obj, line, _ = location
         if obj:
             obj = ', %s' % obj
         if self.include_ids:
         if self._prefix:
             path = path.replace(self._prefix, '')
         self.writeln(self.line_format % locals())
-    
+
 class VSTextReporter(ParseableTextReporter):
     """Visual studio text reporter"""
     line_format = '%(path)s(%(line)s): [%(sigle)s%(obj)s] %(msg)s'
-    
+
 class ColorizedTextReporter(TextReporter):
     """Simple TextReporter that colorizes text output"""
 
         TextReporter.__init__(self, output)
         self.color_mapping = color_mapping or \
                              dict(ColorizedTextReporter.COLOR_MAPPING)
-       
+
 
     def _get_decoration(self, msg_id):
         """Returns the tuple color, style associated with msg_id as defined
         """manage message of different types, and colorize output
         using ansi escape codes
         """
-        module, obj, line = location[1:]
+        module, obj, line, _ = location[1:]
         if module not in self._modules:
             color, style = self._get_decoration('S')
             if module:
         msg = colorize_ansi(msg, color, style)
         sigle = colorize_ansi(sigle, color, style)
         self.writeln('%s:%3s%s: %s' % (sigle, line, obj, msg))
- 

test/input/func_e12xx.py

+# pylint: disable=E1101
+"""Test checking of log format strings
+"""
+
+__revision__ = ''
+
+import logging
+
+def pprint():
+    """Test string format in logging statements.
+    """
+    # These should all emit lint errors:
+    logging.info(0, '') # 1205
+    logging.info('', '') # 1205
+    logging.info('%s%', '') # 1201
+    logging.info('%s%s', '') # 1206
+    logging.info('%s%a', '', '') # 1200
+    logging.info('%s%s', '', '', '') # 1205
+
+    # These should be okay:
+    logging.info(1)
+    logging.info(True)
+    logging.info('')
+    logging.info('%s%')
+    logging.info('%s', '')
+    logging.info('%s%%', '')
+    logging.info('%s%s', '', '')

test/input/func_e13xx.py

+"""test string format error
+"""
+
+__revision__ = 1
+
+PARG_1 = PARG_2 = PARG_3 = 1
+
+def pprint():
+    """Test string format
+    """
+    print "%s %s" % {'PARG_1': 1, 'PARG_2': 2} # E1306
+    print "%s" % (PARG_1, PARG_2) # E1305
+    print "%(PARG_1)d %d" % {'PARG_1': 1, 'PARG_2': 2} # E1302
+    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1} # E1304
+    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1, 'PARG_2':2, 'PARG_3':3}#W1301
+    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1, 2:3} # W1300 E1304
+    print "%(PARG_1)d %(PARG_2)d" % (2, 3) # 1303
+    print "%(PARG_1)d %(PARG_2)d" % [2, 3] # 1303
+    print "%2z" % PARG_1
+    print "strange format %2" % PARG_2
+

test/input/func_e99xx.py

-"""test string format error
-"""
-
-__revision__ = 1
-
-PARG_1 = PARG_2 = PARG_3 = 1
-
-def pprint():
-    """Test string format
-    """
-    print "%s %s" % {'PARG_1': 1, 'PARG_2': 2} # E9906
-    print "%s" % (PARG_1, PARG_2) # E9905
-    print "%(PARG_1)d %d" % {'PARG_1': 1, 'PARG_2': 2} # E9902
-    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1} # E9904
-    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1, 'PARG_2':2, 'PARG_3':3}#W9901
-    print "%(PARG_1)d %(PARG_2)d" % {'PARG_1': 1, 2:3} # W9900 E9904
-    print "%(PARG_1)d %(PARG_2)d" % (2, 3) # 9903
-    print "%(PARG_1)d %(PARG_2)d" % [2, 3] # 9903
-    print "%2z" % PARG_1
-    print "strange format %2" % PARG_2
-

test/input/func_import_syntax_error.py

+import syntax_error

test/input/func_w0404.py

+"""pylint ticket #60828"""
+
+__revision__ = 0
+
+def reimport():
+    """docstring"""
+    import os

test/input/func_w1201.py

+# pylint: disable=E1101
+"""test checking use of the logging module
+"""
+
+__revision__ = ''
+
+# Muck up the names in an effort to confuse...
+import logging as renamed_logging
+import os as logging
+
+# Statements that should be flagged:
+renamed_logging.warn('%s, %s' % (4, 5))
+renamed_logging.exception('%s' % 'Exceptional!')
+renamed_logging.log(renamed_logging.INFO, 'msg: %s' % 'Run!')
+
+# Statements that should not be flagged:
+renamed_logging.warn('%s, %s', 4, 5)
+renamed_logging.log(renamed_logging.INFO, 'msg: %s', 'Run!')
+renamed_logging.warn('%s' + ' the rest of a single string')
+logging.warn('%s, %s' % (4, 5))
+logging.log(logging.INFO, 'msg: %s' % 'Run!')

test/input/func_w6501.py

-# pylint: disable=E1101
-"""test checking use of the logging module
-"""
-
-__revision__ = ''
-
-# Muck up the names in an effort to confuse...
-import logging as renamed_logging
-import os as logging
-
-# Statements that should be flagged:
-renamed_logging.warn('%s, %s' % (4, 5))
-renamed_logging.exception('%s' % 'Exceptional!')
-renamed_logging.log(renamed_logging.INFO, 'msg: %s' % 'Run!')
-
-# Statements that should not be flagged:
-renamed_logging.warn('%s, %s', 4, 5)
-renamed_logging.log(renamed_logging.INFO, 'msg: %s', 'Run!')
-renamed_logging.warn('%s' + ' the rest of a single string')
-logging.warn('%s, %s' % (4, 5))
-logging.log(logging.INFO, 'msg: %s' % 'Run!')

test/input/syntax_error.py

+if False:
+print 'hop'

test/messages/func_e12xx.txt

+E: 13:pprint: Too many arguments for logging format string
+E: 14:pprint: Too many arguments for logging format string
+E: 15:pprint: Logging format string ends in middle of conversion specifier
+E: 16:pprint: Not enough arguments for logging format string
+E: 17:pprint: Unsupported logging format character 'a' (0x61) at index 3
+E: 18:pprint: Too many arguments for logging format string

test/messages/func_e13xx.txt

+E: 11:pprint: Not enough arguments for format string
+E: 12:pprint: Too many arguments for format string
+E: 13:pprint: Mixing named and unnamed conversion specifiers in format string
+E: 14:pprint: Missing key 'PARG_2' in format string dictionary
+E: 16:pprint: Missing key 'PARG_2' in format string dictionary
+E: 17:pprint: Expected mapping for format string, not Tuple
+E: 18:pprint: Expected mapping for format string, not List
+E: 19:pprint: Unsupported format character 'z' (0x7a) at index 2
+E: 20:pprint: Format string ends in middle of conversion specifier
+W: 15:pprint: Unused key 'PARG_3' in format string dictionary
+W: 16:pprint: Format string dictionary key should be a string, not 2
+

test/messages/func_e99xx.txt

-E: 11:pprint: Not enough arguments for format string
-E: 12:pprint: Too many arguments for format string
-E: 13:pprint: Mixing named and unnamed conversion specifiers in format string
-E: 14:pprint: Missing key 'PARG_2' in format string dictionary
-E: 16:pprint: Missing key 'PARG_2' in format string dictionary
-E: 17:pprint: Expected mapping for format string, not Tuple
-E: 18:pprint: Expected mapping for format string, not List
-E: 19:pprint: Unsupported format character 'z' (0x7a) at index 2
-E: 20:pprint: Format string ends in middle of conversion specifier
-W: 15:pprint: Unused key 'PARG_3' in format string dictionary
-W: 16:pprint: Format string dictionary key should be a string, not 2
-

test/messages/func_import_syntax_error.txt

+C:  1: Missing docstring
+C:  1: Missing required attribute "__revision__"
+F:  1: Unable to import 'syntax_error' (expected an indented block (<string>, line 2))
+W:  1: Unused import syntax_error

test/messages/func_syntax_error.txt

 E:  1: invalid syntax
+E:  2: expected an indented block
 

test/messages/func_w0404.txt

+W:  7:reimport: Unused variable 'os'

test/messages/func_w1201.txt

+W: 12: Specify string format arguments as logging function parameters
+W: 13: Specify string format arguments as logging function parameters
+W: 14: Specify string format arguments as logging function parameters

test/messages/func_w6501.txt

-W: 12: Specify string format arguments as logging function parameters
-W: 13: Specify string format arguments as logging function parameters
-W: 14: Specify string format arguments as logging function parameters

test/regrtest_data/absimp/__init__.py

+"""a package with absolute import activated
+"""
+
+from __future__ import absolute_import
+

test/regrtest_data/absimp/string.py

+"""
+http://www.logilab.org/ticket/70495
+http://www.logilab.org/ticket/70565
+"""
+from __future__ import absolute_import
+import string
+print string

test/smoketest.py

     def _runtest(self, args, reporter=None, out=None, code=28):
         if out is None:
             out = StringIO()
+        if args and args[-1].startswith('pylint.lint'):
+            try:
+                import cProfile, pstats
+            except ImportError:
+                code += 1
         try:
             sys.stderr = sys.stdout = out
             try:

test/test_format.py

 # You should have received a copy of the GNU General Public License along with
 # this program; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-""" Copyright (c) 2000-2003 LOGILAB S.A. (Paris, FRANCE).
+""" Copyright (c) 2000-2011 LOGILAB S.A. (Paris, FRANCE).
  http://www.logilab.fr/ -- mailto:contact@logilab.fr
 
 Check format checker helper functions
 """
 
-__revision__ = '$Id: test_format.py,v 1.13 2005-11-02 09:22:06 syt Exp $'
-
-from logilab.common.testlib import TestCase
 import sys
 import re
 from os import linesep
 
+from logilab.common.testlib import TestCase, unittest_main
+
 from pylint.checkers.format import *
+
 from utils import TestReporter
 
 REPORTER = TestReporter()
         self.assertEqual(check_line("print '''<a='=')\n'''"), None)
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest_main()

test/test_regr.py

     def test_try_finally_disable_msg_crash(self):
         linter.check(join(REGR_DATA, 'try_finally_disable_msg_crash'))
 
-
     def test___path__(self):
         linter.check('pylint.checkers.__init__')
         messages = linter.reporter.finalize().strip()
         self.failIf('__path__' in messages, messages)
 
+    def test_absolute_import(self):
+        linter.check(join(REGR_DATA, 'absimp', 'string.py'))
+        got = linter.reporter.finalize().strip()
+        self.failUnlessEqual(got, "W:  6: Uses of a deprecated module 'string'")
 
 if __name__ == '__main__':
     unittest_main()

test/unittest_lint.py

         self.linter.error_mode()
         checkers = self.linter.prepare_checkers()
         checker_names = tuple(c.name for c in checkers)
-        should_not = ('design', 'format', 'imports', 'logging', 'metrics',
+        should_not = ('design', 'format', 'imports', 'metrics',
                       'miscellaneous', 'similarities')
         self.failIf(any(name in checker_names for name in should_not))
 
         # FIXME should it be necessary to explicitly desactivate failures ?
         self.linter.set_option('disable', 'R,C,W')
         checker_names = [c.name for c in self.linter.prepare_checkers()]
-        should_not = ('design', 'logging', 'metrics', 'similarities')
+        should_not = ('design', 'metrics', 'similarities')
         rest = [name for name in checker_names if name in should_not]
         self.assertListEqual(rest, [])
         self.linter.set_option('disable', 'R,C,W,F')
         
     def add_message(self, msg_id, location, msg):
         """manage message of different type and in the context of path """
-        fpath, module, object, line = location
+        fpath, module, object, line, _ = location
         self.message_ids[msg_id] = 1
         if object:
             object = ':%s' % object
         """
         if line is None and node is not None:
             line = node.fromlineno
+        if hasattr(node, 'col_offset'):
+            col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
+        else:
+            col_offset = None
         # should this message be displayed
         if not self.is_message_enabled(msgid, line):
             return
             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), msg)
+        self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg)
 
     def help_message(self, msgids):
         """display help messages for the given message identifiers"""