5

When forgetting to pass certain arguments to a function, Python gives the only-somewhat-helpful message "myfunction() takes X arguments (Y given)". Is there a way to figure out the names of the missing arguments, and tell the user? Something like:

try:
    #begin blackbox
    def f(x,y):
        return x*y

    f(x=1)
    #end blackbox
except Exception as e:
    #figure out the missing keyword argument is called "y" and tell the user so

Assuming that the code between begin blackbox and end blackbox is unknown to the exception handler.

Edit: As its been pointed out to me below, Python 3 already has this functionality built in. Let me extend the question then, is there a (probably ugly and hacky) way to do this in Python 2.x?

CrazyCasta
  • 26,917
  • 4
  • 45
  • 72
marius
  • 1,352
  • 1
  • 13
  • 29
  • possible duplicate of [Getting method parameter names in python](http://stackoverflow.com/questions/218616/getting-method-parameter-names-in-python) – CrazyCasta Jan 10 '14 at 21:15
  • 1
    @CrazyCasta That’s not really answering the question… – poke Jan 10 '14 at 21:20
  • @poke I'm assuming that he knows what's being called and how it's being called. If so, then all he needs to know is the names of the parameters, hence the suggestion that it's a duplicate. – CrazyCasta Jan 10 '14 at 21:22
  • 3
    It's a helpful and related question, but not a duplicate. People are too trigger happy with the close votes on SO these days ... – wim Jan 10 '14 at 21:23
  • 1
    Which version of Python is this for? The answers are different for very early 2.x, later 2.x, very early 3.x, and later 3.x… – abarnert Jan 10 '14 at 21:25
  • @marius Maybe we need to clarify the question. Are you limiting the resources available to just the exception, or can we assume you know how the function is being called? – CrazyCasta Jan 10 '14 at 21:28
  • 1
    Note that current python 3 will give you this information. Upgrade! :) – wim Jan 10 '14 at 21:29
  • @CrazyCasta Ideally the only information you have is the Exception e and whatever else is available globally about the last thrown exception. – marius Jan 10 '14 at 21:34
  • So, given your edit, your actual intent is just to get more information from the exception? I.e. the code isn’t actually meant for production? – poke Jan 10 '14 at 21:34
  • @poke More or less yes, seeing as I'm not sure its possible cleanly if at all in 2.x this is more of an exercise at this point. – marius Jan 10 '14 at 21:43
  • 1
    @marius: If this is just an exercise… it may be a great time to start playing with tracebacks, frames, code objects, `inspect.getsource`, `dis.dis`, etc. But really, I think you might be better off first moving to Python 3.4, where all of this is a lot easier and cleaner and more fun to play around with than in 2.7, and the knowledge you gain will be valuable farther into the future. – abarnert Jan 10 '14 at 21:44
  • I think I made it work with a decorator below for all cases in 2x at least ... but im sure i missed some edge case – Joran Beasley Jan 10 '14 at 22:02
  • @JoranBeasley The function definition is part of OP’s black box. You cannot apply a decorator then. – poke Jan 10 '14 at 22:04
  • sure you can ... `new_fn = arged_spec(some_fn_blackbox)` then just call new_fn ... which is not all that different than `try:some_blackbox();except:whatever()` – Joran Beasley Jan 10 '14 at 22:05
  • @JoranBeasley: All of the code between `try` and `except` is a blackbox. That means you can't replace any of that code with anything different. You're replacing _all_ of that code with something different. – abarnert Jan 10 '14 at 22:12
  • ok /sigh I give up ... that was my best shot ... (with the ammount of time Im willing to invest in an answer) – Joran Beasley Jan 10 '14 at 22:17
  • @JoranBeasley: Showing how you could solve a similar problem if you can push the borders of the blackbox a bit isn't exactly useless, so I think your answer should help future searchers and deserves to be here and positive-voted (as it is), even if it isn't the answer to the OP's exact use case. – abarnert Jan 10 '14 at 22:22
  • @JoranBeasley It peeks in the the black box a little, yes, but still that code is VERY helpful to see. Thanks. – marius Jan 10 '14 at 22:22

4 Answers4

3

A much cleaner way to do this would be to wrap the function in another function, pass through the *args, **kwargs, and then use those values when you need them, instead of trying to reconstruct them after the fact. But if you don't want to do that…

In Python 3.x (except very early versions), this is easy, as poke's answer explains. Even easier with 3.3+, with inspect.signature, inspect.getargvalues, and inspect.Signature.bind_partial and friends.

In Python 2.x, there is no way to do this. The exception only has the string 'f() takes exactly 2 arguments (1 given)' in its args.

Except… in CPython 2.x specifically, it's possible with enough ugly and brittle hackery.

You've got a traceback, so you've got its tb_frame and tb_lineno… which is everything you need. So as long as the source is available, the inspect module makes it easy to get the actual function call expression. Then you just need to parse it (via ast) to get the arguments passed, and compare to the function's signature (which, unfortunately, isn't nearly as easy to get in 2.x as in 3.3+, but between f.func_defaults, f.func_code.co_argcount, etc., you can reconstruct it).

But what if the source isn't available? Well, between tb_frame.f_code and tb_lasti, you can find out where the function call was in the bytecode. And the dis module makes that relatively easy to parse. In particular, right before the call, the positional arguments and the name-value pairs for keyword arguments were all pushed on the stack, so you can easily see which names got pushed, and how many positional values, and reconstruct the function call that way. Which you compare to the signature in the same way.

Of course that relies on the some assumptions about how CPython's compiler builds bytecode. It would be perfectly legal to do things in all kinds of different orders as long as the stack ended up with the right values. So, it's pretty brittle. But I think there are already better reasons not to do it.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • @wim: I was originally planning to write some example code for this… but then I decided that if someone is doing this "as an exercise", it would be more fun for him to build it from scratch based on the links—see what is and isn't in the half-documented `code` object, what `ast` objects and/or disassembly look like, etc. So I rewrote the whole think, making it even later. – abarnert Jan 10 '14 at 21:46
2
except TypeError as e:
   import inspect
   got_args = int(re.search("\d+.*(\d+)",str(e)).groups()[0])
   print "missing args:",inspect.getargspec(f).args[got_args:]

a better method would be a decorator

def arg_decorator(fn):
   def func(*args,**kwargs):
       try: 
           return fn(*args,**kwargs)
       except TypeError:
           arg_spec = inspect.getargspec(fn)
           missing_named = [a for a in arg_spec.args if a not in kwargs]
           if arg_spec.defaults:
                    missing_args = missing_named[len(args): -len(arg_spec.defaults) ]
           else:
                    missing_args = missing_named[len(args):]
           print "Missing:",missing_args
   return func

@arg_decorator
def fn1(x,y,z):                      
      pass

def fn2(x,y):
    pass

arged_fn2 = arg_decorator(fn2)

fn1(5,y=2)
arged_fn2(x=1)
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
  • 2
    The number in the exeption message will not tell you *which* argument is missing, but just how many. – poke Jan 10 '14 at 21:21
  • try `f(y=5)` to see output so while technically yes its just how many ... you can use it to get the last index given – Joran Beasley Jan 10 '14 at 21:22
  • f(y=5) still gives me "missing args: ['y']" am I missing something? – marius Jan 10 '14 at 21:24
  • umm I get x,y when i do it in 2.6 it returns `aa() takes exactly 2 non-keyword arguments (0 given)` even though I called it with the second positional named argument – Joran Beasley Jan 10 '14 at 21:25
  • -1 this simply doesn't work. try with f(y=1) and it's still going to tell you that y is missing – wim Jan 10 '14 at 21:41
  • yeah I know ... but its the closest I could get to what he wants with 2.6 with a few minutes to think about it – Joran Beasley Jan 10 '14 at 21:47
  • it's useless and impossible to extend to 3 or more args – wim Jan 10 '14 at 21:49
  • Doesn’t work. The function definition is part of the blackbox. You cannot modify the function defined within the blackbox. And calling your decorator on the blackbox (assuming you would put that into a function) simply doesn’t work because the blackbox isn’t where the parameters are missing. – poke Jan 10 '14 at 22:13
2

I would argue that doing this doesn’t really make that much sense. Such an exception is thrown because the programmer missed specifying the argument. So if you knowingly catch the exception, then you could just as well just fix it in the first place.

That being said, in current Python 3 versions, the TypeError that is being thrown does mention which arguments are missing from the call:

"f() missing 1 required positional argument: 'y'"

Unfortunately, the argument name is not mentioned separately, so you would have to extract it from the string:

try:
    f(x=1)
except TypeError as e:
    if 'required positional argument' in e.args[0]:
        argumentNames = e.args[0].split("'")[1::2]
        print('Missing arguments are ' + argumentNames)
    else:
        raise # Re-raise other TypeErrors

As Joran Beasley pointed out in the comments, Python 2 does not tell you which arguments are missing but just how many are missing. So there is no way to tell from the exception which arguments were missing in the call.

poke
  • 369,085
  • 72
  • 557
  • 602
1

With purely the exception to deal with it is not possible to do what you want and handle keyword arguments. This is of course wrt Python 2.7.

The code that generates this message in Python is:

PyErr_Format(PyExc_TypeError,
    "%.200s() takes %s %d "
    "argument%s (%d given)",
    PyString_AsString(co->co_name),
    defcount ? "at most" : "exactly",
    co->co_argcount,
    co->co_argcount == 1 ? "" : "s",
    argcount + kwcount);

Taken from lines 3056-3063 from http://hg.python.org/cpython/file/0e5df5b62488/Python/ceval.c

As you can see, there is just not enough information given to the exception as to what arguments are missing. co in this context is the PyCodeObject being called. The only thing given is a string (which you could parse if you like) with the function name, whether or not there is a vararg, how many arguments are expected, and how many arguments were given. As has been pointed out, this does not give you sufficient information as to what argument(s) were not given (in the case of keyword arguments).

Something like inspect or the other debugging modules might be able to give you enough information as to what function was called and how it was called, from which you could figure out what arguments were not given.

I should also mention however that almost certainly, whatever solution you come up with will not be able to handle at least some extension module methods (those written in C) because they don't provide argument information as part of their object.

CrazyCasta
  • 26,917
  • 4
  • 45
  • 72
  • For the last paragraph, that's another thing that's (getting) better in 3.x. Functions defined with [Argument Clinic](http://www.python.org/dev/peps/pep-0436/) expose `inspect.Signature` objects. In 3.4b3+, that should be most of the builtins and stdlib. After 3.4, I believe the goal is to make it more usable for third-party extension modules, including those that work with earlier versions (though whether that means 3.1+ or 2.6+ I don't know). – abarnert Jan 10 '14 at 22:19