For the sake of posterity, this is what I came up with:
class BaseExplainedValue(object):
def __init__(self, value, reason):
self.value = value
self.reason = reason
def __getattribute__(self, name):
if name in ('value', 'reason'):
return object.__getattribute__(self, name)
value = object.__getattribute__(self, 'value')
return object.__getattribute__(value, name)
def __str__(self):
return "<'%s' explained by '%s'>" % (
str(self.value),
str(self.reason))
def __unicode__(self):
return u"<'%s' explained by '%s'>" % (
unicode(self.value),
unicode(self.reason))
def __repr__(self):
return "ExplainedValue(%s, %s)" % (
repr(self.value),
repr(self.reason))
force_special_methods = set(
"__%s__" % name for name in (
'lt le eq ne gt ge cmp rcmp nonzero call len getitem setitem delitem iter reversed contains getslice setslice delslice' + \
'add sub mul floordiv mod divmod pow lshift rshift and xor or div truediv' + \
'radd rsub rmul rdiv rtruediv rfloordiv rmod rdivmod rpow rlshift rrshift rand rxor ror' + \
'iadd isub imul idiv itruediv ifloordiv imod ipow ilshift irshift iand ixor ior' + \
'neg pos abs invert complex int long float oct hex index coerce' + \
'enter exit').split(),
)
def make_special_method_wrapper(method_name):
def wrapper(self, *args, **kwargs):
return getattr(self, method_name)(*args, **kwargs)
wrapper.__name__ = method_name
return wrapper
def EXP(obj, reason="no reason provided"):
if isinstance(obj, BaseExplainedValue):
return obj
class ThisExplainedValue(BaseExplainedValue):
pass
#special-case the 'special' (underscore) methods we want
obj_class = obj.__class__
for method_name in dir(obj_class):
if not (method_name.startswith("__") and method_name.endswith("__")): continue
method = getattr(obj_class, method_name)
if method_name in force_special_methods:
setattr(ThisExplainedValue, method_name, make_special_method_wrapper(method_name))
ThisExplainedValue.__name__ = "%sExplainedValue" % (obj_class.__name__,)
return ThisExplainedValue(obj, reason)
Usage:
>>> success = EXP(True, "it went ok")
>>> if success:
print 'we did it!'
we did it!
>>> success = EXP(False, "Server was on fire")
>>> if not success:
print "We failed: %s" % (EXP(success).reason,)
We failed: Server was on fire
The explained values can be used interchangeably with those which they wrap:
>>> numbers = EXP([1, 2, 3, 4, 5], "method worked ok")
>>> numbers
ExplainedValue([1, 2, 3, 4, 5], 'method worked ok')
>>> numbers[3]
4
>>> del numbers[3]
>>> numbers
ExplainedValue([1, 2, 3, 5], 'method worked ok')
It even fools isinstance
(explanation here):
>>> isinstance(EXP(False), bool)
True
>>> isinstance(EXP([]), list)
True