Monday, April 11, 2011

Dynamic exception classes

I'm in the midst of (re-)writing a python library to interface with Authorize.net's CIM (Customer Information Manager) API. The CIM API uses a XML-based, RPC-ish protocol, and on error it returns a (code, detail) pair. The code takes the format "E000XX" -- E00045 is an XML namespace error, E00039 is a "profile duplicate" error, etc.

The standard python behavior for error conditions is to raise Exceptions, with a given library hopefully implementing both generic and specific exception subclasses (making it easier to except: and handle specific cases, or catch a generic error case that's specific to a library). So naturally, I wanted to have a base CIMError exception, with specific per-code exceptions (NSError and Duplicate, for example) that subclass CIMError.

What I didn't want, however, was every API method to have code with huge if-elif-else blocks at the end:


if code == 'E00039':
raise Duplicate()
elif code == 'E00045':
raise NSError()
# etc
else:
raise CIMError()


Better, but still not desirable, is to have a map of codes to exceptions:


exc = {
'E00039': Duplicate,
'E00045': NSError,
# etc
}
if code in exc:
raise exc[code]()
else:
raise CIMError()


What I'd rather do is just raise CIMError() and, if there's a more specific subclass, let CIMError "morph" into that class. While that idea in and of itself isn't possible (classes don't "morph"...), what we can do is have CIMError's __new__ method return an appropriate subclass based on the code passed to it:


class CIMError(Exception):
def __new__(cls, code, text, *args, **kwargs):
# If the code matches a specialized Exception, use that instead
for subclass in cls.__subclasses__():
if subclass.code == code:
cls = subclass
break
return super(CIMError, cls).__new__(cls, code, text, *args, **kwargs)

def __init__(self, code, text, *args, **kwargs):
self.code = code
self.text = text
super(CIMError, self).__init__(code, text, *args, **kwargs)

def __str__(self):
return '%s: %s %s' % (self.__class__.__name__, self.code, self.text)

class Duplicate(CIMError):
code = 'E00039'

class NSError(CIMError):
code = 'E00045'


CIMError's __new__ now takes the code from CIM, searches the subclasses for that code, and, if a match is found, returns that class instead. If no match is found, the generic CIMError is returned.


>>> CIMError('E00011', '')
CIMError('E00011', '')
>>> CIMError('E00039', '')
Duplicate('E00039', '')
>>> CIMError('E00045', '')
NSError('E00045', '')


This allows my API methods' error code to be as simple as:


raise CIMError(code, detail)