Writing an IronPython Debugger: Command Routing

At this point, ipydbg support seven commands: Continue, Quit, Show Stack Trace, Show Locals, Step Over, Step In, and Step Out. All these commands are invoked by a single keystroke. I’m using Console.ReadKey in an attempt to cut down on the number of keystrokes needed for interacting with the debugger. If I only type ‘s’ instead of ‘s <enter>’ to step, I figure I’ll be twice as productive! 😄

If I was writing ipydbg in C#, I could use switch statement to dispatch commands in the _input method based on user keystrokes. However, Python doesn’t have a switch statement so I’ve been using a cascading set of if/elif/else statements instead. When you get up to seven if/elif clauses plus an else clause, the code smell is pretty overwhelming.

# Only has three if/elif clauses,but it's already a little smelly

val = Console.ReadKey()
if val.Key == 'a':  
  result = 'a'  
elif val.Key == 'b'  
  result = 'b'  
elif val.Key == 'c'  
  result = 'c'  
else:  
  print "unknown key"

Python might not have a switch statement, but it does have first-order functions so you can get the effects of a switch by using a dictionary.

def do_a():
  return 'a'
def do_b():
  return 'b'
def do_c():
  return 'c'
_switch = {'a':do_a, 'b':do_b, 'c':do_c}

val = Console.ReadKey()
if val in _switch:
  result = _switch[val.Key]()
else:
  print "unknown key"

I like this approach much better. Individual if/elif blocks are now broken out into separate functions, which smells better than embedding them in one big function. Also, I like that my pseduo-switch statement is completely separate from the how the _switch dictionary is initialized. However, this approach also separates the pseudo-case statement functions from the _switch dictionary as well. That’s not a good thing. You can easily imagine screwing up by adding a new function but forgetting to manually update the _switch dictionary.

What I need is a way to declaratively associate the switch function with the dictionary lookup key that’s associated with it. Luckily, Python Decorators provides a very clean way to do this.

_switch = {}

@inputcmd(_switch, 'a')
def do_a():
  return 'a'
@inputcmd(_switch, 'b')
def do_b():
  return 'b'
@inputcmd(_switch, 'c')
def do_c():
  return 'c'

val = Console.ReadKey()
if val in _switch:  
  result = _switch[val.Key]()  
else:  
  print "unknown key"

I’ve blogged about decorators before when I wanted to automatically invoke operations on the right thread in my WPF photo viewing app. The @inputcmd decorator is a bit more complicated than the @BGThread and @UIThread decorators since @inputcmd decorator accepts arguments. Each of the @input command decorators in the code above is the equivalent to this code:

def do_a():
  return 'a'

_tmp = inputcmd(_switch, 'a')
do_a = _tmp(do_a)

As you can see, the inputcmd function returns the decorator that wraps do_a, rather than being the decorator itself. This function that returns a function that returns a function is kinda confusing at first. But this approach allows you to configure the decorator for a specific purpose via the arguments – in this case, specifying which dictionary and which console key this function is associated with.

Also unlike @BGThread and @UIThread, I don’t actually want to modify the behavior of the methods decorated with @inputcmd. I only want to store a reference to them in the passed in dictionary. So implementing this decorator is very easy:

def inputcmd(cmddict, key):
    def deco(f):
        cmddict[key] = f
        return f  
    return deco

The decorator simply inserts the function into the passed-in dictionary using the passed in key. It then returns the function as is, so it’s not really rebinding the symbol to a new method (technically, it’s rebinding the symbol to the same function it’s currently bound to). If I wanted also wrap the passed in function to provide additional functionality, I could do that with a second locally defined function inside the deco function.

The latest version of ipydbg as been refactored to use @inputcmd instead of set of a cascading if/elif statement blocks. Now that that’s done, I can start working on multi-key commands.

Comments:

What would be the tradeoffs if that _switch were built with lambdas instead of named functions, like this: _switch = { 'a': lambda: 'A' 'b': lambda: 'B' 'c': lambda: 'C' } # I can't get the indents right...whitespace got snipped
Lambdas would work just fine, but they are limited to be expressions only. If you can do everything you need in a single expression, using a lambda would be fine.
Oops, I forgot about the expression limitation...was having too much fun with Y combinators in Python :p Thanks for the answer
Seeing this use of decorators in IPy was most interesting. But reading this blog, I found myself thinking "I'll stick to F#". I realize it isn't aimed at the same language design space, but so far I'm willing to do more up-front work to prepare the functionality I want, in exchange for language depth. I can see that if I took the time to learn IPy, it would be handy for interactive scripting. Just sharing...