Multi-argument append() is illegal

Tim Peters tim_one at email.msn.com
Tue Feb 29 04:25:03 EST 2000


[Guido van Rossum]
> I've noticed that there is some code out there that creates a list of
> tuples and uses code like list.append(a,b,c) to add the tuple (a,b,c)
> to the list.  According to the documentation, this is illegal:
> append() only takes a single argument, and one should write
> list.append((a,b,c)).  However, the actual append() implementation
> didn't mind, and implemented list.append(a,b,c) as
> list.append((a,b,c)).  Many people are using this even though it's
> never been documented.
>
> I am going to rectify this in Python 1.6 -- people coming from other
> languages might well expect list.append(a, b, c) to mean the same as
> list.append(a); list.append(b); list.append(c), and it's always been
> my philosophy to make ambiguous syntax illegal rather than to pick one
> interpretation randomly.

Before anyone starts <wink>, protesting this appears to be as futile as
griping about whitespace:  The Dictator Has Spoken here.  It's been broken
forever and needs to get fixed.

> ...
> You can also grep through your sources for a pattern like
> "\. *append *\(.*," -- which doesn't find every occurrence, but is
> a good starting point.

Actually, it's a terrible starting point:  the sheer quantity of "false
hits" will drive you mad.  For example, note that the one above matches your
.append()s again *after* you've fixed them(!).  I've been thru this a few
times in my own code over the years, and never dreamed up a regexp that
didn't leave me with hours of manual drudge.  You really need to account for
bracket nesting levels to avoid spurious hits on "deep" commas, and to
ignore commas in strings and trailing comments, and to span lines too to
avoid missing the

    list.append(long_expression1...
                ...ending_here,
                and_another)

cases.

So, Python to the rescue.  What appears to be a very capable checker is
attached.  It's pretty slow, but not achingly so, and you shouldn't need to
use it often.  Read the docstring at the top for cautions.  A copy of this
(or a fixed version, if this proves to have bugs) may show up in the 1.6
distribution.  BTW, there's no chance of talking me into trying to extend
this to edit your files for you, so it's a good thing you didn't ask <wink>.

parsing-is-better-than-wild-ass-guessing-ly y'rs  - tim


#! /usr/bin/env python

# Released to the public domain, by Tim Peters, 28 February 2000.

"""checkappend.py -- search for multi-argument .append() calls.

Usage:  specify one or more file or directory paths:
    checkappend [-v] file_or_dir [file_or_dir] ...

Each file_or_dir is checked for multi-argument .append() calls.  When
a directory, all .py files in the directory, and recursively in its
subdirectories, are checked.

Use -v for status msgs.  Use -vv for more status msgs.

In the absence of -v, the only output is pairs of the form

    filename(linenumber):
    line containing the suspicious append

Note that this finds multi-argument append calls regardless of whether
they're attached to list objects.  If a module defines a class with an
append method that takes more than one argument, calls to that method
will be listed.

Note that this will not find multi-argument list.append calls made via a
bound method object.  For example, this is not caught:

    somelist = []
    push = somelist.append
    push(1, 2, 3)
"""

__version__ = 1, 0, 0

import os
import sys
import string
import getopt
import tokenize

verbose = 0

def errprint(*args):
    msg = string.join(args)
    sys.stderr.write(msg)
    sys.stderr.write("\n")

def main():
    args = sys.argv[1:]
    global verbose
    try:
        opts, args = getopt.getopt(sys.argv[1:], "v")
    except getopt.error, msg:
        errprint(msg + "\n\n" + __doc__)
        return
    for opt, optarg in opts:
        if opt == '-v':
            verbose = verbose + 1
    if not args:
        errprint(__doc__)
        return
    for arg in args:
        check(arg)

def check(file):
    if os.path.isdir(file) and not os.path.islink(file):
        if verbose:
            print "%s: listing directory" % `file`
        names = os.listdir(file)
        for name in names:
            fullname = os.path.join(file, name)
            if ((os.path.isdir(fullname) and
                 not os.path.islink(fullname))
                or os.path.normcase(name[-3:]) == ".py"):
                check(fullname)
        return

    try:
        f = open(file)
    except IOError, msg:
        errprint("%s: I/O Error: %s" % (`file`, str(msg)))
        return

    if verbose > 1:
        print "checking", `file`, "..."

    ok = AppendChecker(file, f).run()
    if verbose and ok:
        print "%s: Clean bill of health." % `file`

[FIND_DOT,
 FIND_APPEND,
 FIND_LPAREN,
 FIND_COMMA,
 FIND_STMT]   = range(5)

class AppendChecker:
    def __init__(self, fname, file):
        self.fname = fname
        self.file = file
        self.state = FIND_DOT
        self.nerrors = 0

    def run(self):
        try:
            tokenize.tokenize(self.file.readline, self.tokeneater)
        except tokenize.TokenError, msg:
            errprint("%s: Token Error: %s" % (`self.fname`, str(msg)))
            self.nerrors = self.nerrors + 1
        return self.nerrors == 0

    def tokeneater(self, type, token, start, end, line,
                NEWLINE=tokenize.NEWLINE,
                JUNK=(tokenize.COMMENT, tokenize.NL),
                OP=tokenize.OP,
                NAME=tokenize.NAME):

        state = self.state

        if type in JUNK:
            pass

        elif state is FIND_DOT:
            if type is OP and token == ".":
                state = FIND_APPEND

        elif state is FIND_APPEND:
            if type is NAME and token == "append":
                self.line = line
                self.lineno = start[0]
                state = FIND_LPAREN
            else:
                state = FIND_DOT

        elif state is FIND_LPAREN:
            if type is OP and token == "(":
                self.level = 1
                state = FIND_COMMA
            else:
                state = FIND_DOT

        elif state is FIND_COMMA:
            if type is OP:
                if token in ("(", "{", "["):
                    self.level = self.level + 1
                elif token in (")", "}", "]"):
                    self.level = self.level - 1
                    if self.level == 0:
                        state = FIND_DOT
                elif token == "," and self.level == 1:
                    self.nerrors = self.nerrors + 1
                    print "%s(%d):\n%s" % (self.fname, self.lineno,
                                           self.line)
                    # don't gripe about this stmt again
                    state = FIND_STMT

        elif state is FIND_STMT:
            if type is NEWLINE:
                state = FIND_DOT

        else:
            raise SystemError("unknown internal state '%s'" % `state`)

        self.state = state

if __name__ == '__main__':
    main()

# end of file






More information about the Python-list mailing list