Friday, September 16, 2011

Confusing plus signs

In user interaction, the plus sign has at least two common functions: "add to list" and "expand existing list".



I saw this link while browsing movie times on Fandango, and, even though I clearly read "add to my theaters", it took all my mental strength to not click that plus button. The design of that tiny little icon simply screamed "there's more info here!"

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)