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.