[Python-projects] Pylint checker for function arguments

James Lingard jchl at aristanetworks.com
Fri Nov 20 03:22:52 CET 2009


Below is a new pylint checker that checks that the arguments passed to a
function call match the function's formal parameters.  For example, on the
following file:

   def f( a, b=1 ):
      print a, b
   f()
   f( 1, 2, 3 )
   f( a=1, a=2 )
   f( 1, c=3 )
   f( 1, a=2 )

the checker produces the following output:

   ************* Module xyz
   E9700:  3: No value passed for parameter 'a' in function call
   E9701:  4: Two many positional arguments for function call
   E9702:  5: Duplicate keyword argument 'a' in function call
   E9703:  6: Passing unexpected keyword argument 'c' in function call
   E9704:  7: Multiple values passed for parameter 'a' in function call

It handles function definitions that use *args and **kwargs.  It handles
function calls that pass *args and/or **kwargs somewhat conservatively, only
warning if there are no possible values of the args and kwargs that will
make the call succeed -- it doesn't attempt to do any inference on the args
or kwargs.

The checker also checks calls to bound and unbound methods (including static
and class methods) and lambda functions.

I've tested this on a large body of code, and it's successfully found
several errors.  The only false positives I'm aware of are in the following
case:

   class Foo( object ):
      def f( self ): pass
      g = f

The checker will complain that a call to "Foo().g()" is missing the "self"
parameter, since inference claims that Foo().g is a function definition, not
a bound method.

As before, let me know if you're interested in incorporating this into
pylint.  If so, I'd be happy to work on some unit test cases, and I'd love
to hear any feedback.

Thanks,
James.

====================================

# Copyright (c) 2009 Arista Networks, Inc.
#
# This program is free software; you can redistribute it and/or modify it
under
# the terms of the GNU General Public License as published by the Free
Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
#
# You should have received a copy of the GNU General Public License along
with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Checker for function arguments.
"""

from logilab import astng
from pylint.interfaces import IASTNGChecker
from pylint.checkers import BaseChecker
from pylint.checkers.utils import safe_infer

MSGS = {
    'E9700': ("No value passed for parameter %s in function call",
              "Used when a function call passes too few arguments."),
    'E9701': ("Two many positional arguments for function call",
              "Used when a function call passes too many positional \
              arguments."),
    'E9702': ("Duplicate keyword argument %r in function call",
              "Used when a function call passes the same keyword argument \
              multiple times."),
    'E9703': ("Passing unexpected keyword argument %r in function call",
              "Used when a function call passes a keyword argument that \
              doesn't correspond to one of the function's parameter
names."),
    'E9704': ("Multiple values passed for parameter %r in function call",
              "Used when a function call would result in assigning multiple
\
              values to a function parameter, one value from a positional \
              argument and one from a keyword argument."),
    }

class ArgumentChecker(BaseChecker):
    """Checks that the arguments passed to a function or method call match
the
    parameters in the function's definition.
    """

    __implements__ = (IASTNGChecker,)

    name = 'arguments'
    msgs = MSGS

    def visit_callfunc(self, node):
        # Build the set of keyword arguments, checking for duplicate
keywords,
        # and count the positional arguments.
        keywordArgs = set()
        numPositionalArgs = 0
        for arg in node.args:
            if isinstance(arg, astng.Keyword):
                keyword = arg.arg
                if keyword in keywordArgs:
                    self.add_message('E9702', node=node, args=keyword)
                keywordArgs.add(keyword)
            else:
                numPositionalArgs += 1

        func = safe_infer(node.func)
        # Note that BoundMethod is a subclass of UnboundMethod (huh?), so
must
        # come first in this 'if..else'.
        if isinstance(func, astng.BoundMethod):
            # Bound methods have an extra implicit 'self' argument.
            numPositionalArgs += 1
        elif isinstance(func, astng.UnboundMethod):
            if func.decorators is not None:
                for d in func.decorators.nodes:
                    if d.name == 'classmethod':
                        # Class methods have an extra implicit 'cls'
argument.
                        numPositionalArgs += 1
                        break
        elif isinstance(func, astng.Function) or isinstance(func,
astng.Lambda):
            pass
        else:
            return

        if func.args.args is None:
            # Built-in functions have no argument information.
            return

        if len( func.argnames() ) != len( set( func.argnames() ) ):
            # Duplicate parameter name.  We can't really make sense
            # of the function call in this case, so just return.
            return

        # Analyze the list of formal parameters.
        numMandatoryParameters = len(func.args.args) -
len(func.args.defaults)
        parameters = []
        parameterNameToIndex = {}
        for i, arg in enumerate(func.args.args):
            if isinstance(arg, astng.Tuple):
                name = None
                # Don't store any parameter names within the tuple, since
those
                # are not assignable from keyword arguments.
            else:
                if isinstance(arg, astng.Keyword):
                    name = arg.arg
                else:
                    assert isinstance(arg, astng.AssName)
                    # This occurs with:
                    #    def f( (a), (b) ): pass
                    name = arg.name
                parameterNameToIndex[name] = i
            if i >= numMandatoryParameters:
                defval = func.args.defaults[i - numMandatoryParameters]
            else:
                defval = None
            parameters.append([(name, defval), False])

        # Match the supplied arguments against the function parameters.

        # 1. Match the positional arguments.
        for i in range(numPositionalArgs):
            if i < len(parameters):
                parameters[i][1] = True
            elif func.args.vararg is not None:
                # The remaining positional arguments get assigned to the
*args
                # parameter.
                break
            else:
                # Too many positional arguments.
                self.add_message('E9701', node=node)
                break

        # 2. Match the keyword arguments.
        for keyword in keywordArgs:
            if keyword in parameterNameToIndex:
                i = parameterNameToIndex[keyword]
                if parameters[i][1]:
                    # Duplicate definition of function parameter.
                    self.add_message('E9704', node=node, args=keyword)
                else:
                    parameters[i][1] = True
            elif func.args.kwarg is not None:
                # The keyword argument gets assigned to the **kwargs
parameter.
                pass
            else:
                # Unexpected keyword argument.
                self.add_message('E9703', node=node, args=keyword)

        # 3. Match the *args, if any.  Note that Python actually processes
        #    *args _before_ any keyword arguments, but we wait until after
        #    looking at the keyword arguments so as to make a more
conservative
        #    guess at how many values are in the *args sequence.
        if node.starargs is not None:
            for i in range(numPositionalArgs, len(parameters)):
                [(name, defval), assigned] = parameters[i]
                # Assume that *args provides just enough values for all
                # non-default parameters after the last parameter assigned
by
                # the positional arguments but before the first parameter
                # assigned by the keyword arguments.  This is the best we
can
                # get without generating any false positives.
                if (defval is not None) or assigned:
                    break
                parameters[i][1] = True

        # 4. Match the **kwargs, if any.
        if node.kwargs is not None:
            for i, [(name, defval), assigned] in enumerate(parameters):
                # Assume that *kwargs provides values for all remaining
                # unassigned named parameters.
                if name is not None:
                    parameters[i][1] = True
                else:
                    # **kwargs can't assign to tuples.
                    pass

        # Check that any parameters without a default have been assigned
        # values.
        for [(name, defval), assigned] in parameters:
            if (defval is None) and not assigned:
                displayName = repr(name) if (name is not None) else
'<tuple>'
                self.add_message('E9700', node=node, args=displayName)

def register(linter):
    """required method to auto register this checker """
    linter.register_checker(ArgumentChecker(linter))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.logilab.org/pipermail/python-projects/attachments/20091119/010fce6d/attachment.html>


More information about the Python-Projects mailing list