[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( 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

   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.



# Copyright (c) 2009 Arista Networks, Inc.
# This program is free software; you can redistribute it and/or modify it
# the terms of the GNU General Public License as published by the Free
# 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
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# You should have received a copy of the GNU General Public License along
# 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 \
    '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
    '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
    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
        # 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)
                numPositionalArgs += 1

        func = safe_infer(node.func)
        # Note that BoundMethod is a subclass of UnboundMethod (huh?), so
        # 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'
                        numPositionalArgs += 1
        elif isinstance(func, astng.Function) or isinstance(func,

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

        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.

        # Analyze the list of formal parameters.
        numMandatoryParameters = len(func.args.args) -
        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
                # are not assignable from keyword arguments.
                if isinstance(arg, astng.Keyword):
                    name = arg.arg
                    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]
                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
                # parameter.
                # Too many positional arguments.
                self.add_message('E9701', node=node)

        # 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)
                    parameters[i][1] = True
            elif func.args.kwarg is not None:
                # The keyword argument gets assigned to the **kwargs
                # 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
        #    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
                # the positional arguments but before the first parameter
                # assigned by the keyword arguments.  This is the best we
                # get without generating any false positives.
                if (defval is not None) or assigned:
                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
                    # **kwargs can't assign to tuples.

        # 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
                self.add_message('E9700', node=node, args=displayName)

def register(linter):
    """required method to auto register this checker """
-------------- 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