Tue 02 November 2010

Emulating Ruby's "method_missing" in Python

I don't pretend to be a huge fan of Ruby. That said, I can respect when a language has a feature that's pretty damn neat and useful. For the uninformed, method_missing in Ruby is something like the following:

# Basic class, no methods, but method_missing love
class Rapture
    def initialize
        @missing_msg = "Oh god, calling method_missing"
    end

    # Called when a non-existent method call on an instance
    # of this class is made.
    def method_missing(method_name, *args, &block)
        puts @missing_msg
    end
end

bioshock = Rapture.new
bioshock.play

# play doesn't exist, so this will output
# "Oh god, calling method_missing"

Obviously, this is a trick that should be used with caution. It can make for some unmaintainable code, as a class with many methods could get difficult to trace through and figure out just what the hell is happening. It can be put to good use, though - take an API wrapper, for instance. What's it consist of? Generally, nothing more than the same function calls made over and over to various service endpoints.

Cool, let's use this in Python!

I recently rewrote Twython to support OAuth authentication with Twitter (as of Twython 1.3). It ships with an example Django application to get people up and running quickly, and the adoption has been pretty awesome so far.

The funny thing about the Twython 1.3.0 release is that it was largely a rewrite of the entire library. It had become somewhat unwieldy, some odd two thousand lines of code with each API endpoint getting its own method definition. The only differing aspect of these calls is the endpoint URL itself. This is a perfect case for a method_missing setup - let's catch the calls to non-existent methods, and grab them out of a dictionary mapping to every endpoint.

method_dictionary = {
    "getPublicTimeline": {
        "endpoint": "http://...",
    },
}

class Twython(object):
    def __init__(self, params):
        """
            Store params and junk. Ordinarily more verbose.
        """
        self.params = params
    
    def __getattr__(self, method_name):
        """
            This is called every time a class method or property 
            is checked and/or called.
            
            In here we'll return a new function to handle what we
            want to do.
        """
        def get(self, **kwargs):
            # Make our API calls, return data, etc
        
        if method_name in method_dictionary:
            return get.__get__(self)
        else:
            # If the method isn't in our dictionary, act normal.
            raise AttributeError, method_name

# Instantiate...
twitter = Twython()

# Call an arbitrary method.
twitter.getPublicTimeline()

The source above is fairly well commented, but feel free to ask in the comments if you need further explanation. This resulted in a much more maintainable version of Twython - for each function that's listed in a hash table, we can now just take any named parameter and url-encode/combine it. This makes Twython pretty API-change agnostic of the entire Twitter API. Pretty awesome sauce, no?

Ryan around the Web