Diagnosing errors when duck-typing fails

I have a "factory" that creates instances of classes using a tuple of arguments:

def factory(cls, arg):
  return cls(*arg)

class Foo(object):
  def __init__(self, a, b):
    pass

a = (1,2)
f = factory(Foo, a)

This works well, so then I decided (for various reasons beyond the scope of this question) to add support for classes that don't take any arguments as a fallback so that this can be used more widely within existing code. So I needed to detect the availability of a 2-arg vs 0-arg constructor and fallback appropriately. Duck-typing seems like the "Pythonic" answer and works beautifully:

def factory(cls, arg):
  try:
    return cls(*arg)
  except TypeError:
    print("Missing 2-arg, falling back to 0-arg ctor")
    return cls()

class Foo(object):
  def __init__(self,a,b):
    pass

class Bar(object):
  def __init__(self,a):
    pass

a = (1,2)
f = factory(Foo, a)
b = factory(Bar, a)

The problem comes however when there's an error inside one of the __init__ functions. I tried to be helpful and warn when neither the 0-arg nor 2-arg __init__ exist:

def factory(cls, arg):
  try:
    return cls(*arg)
  except TypeError:
    print("%s Missing 2-arg, falling back to 0-arg ctor" % str(cls))
    return cls()

class Foo(object):
  def __init__(self,a,b):
    iter(a) # Oops, TypeError

a = (1,2)
try:
  f = factory(Foo, a)
except TypeError: # 0-arg must be missing too
  print("Neither 2-arg nor 0-arg ctor exist, that's all we support, sorry")

But there's a fatal flaw here - I can't distinguish between a TypeError that was raised because no appropriate __init__ existed and a TypeError that was raised because of a problem deeper in the __init__.

I'm not looking for ways to fix Foo or Bar, rather ways to understand the failures more precicely at the level of the "factory" or its callers.

How can I programatically tell if the TypeError was the result of no overload matching, or a failure elsewhere? The best idea I have right now is programatically walking the stack trace and looking at line numbers, which is hideous at best.

Upvotes: 3

Views: 105

Answers (1)

dmcauslan
dmcauslan

Reputation: 398

Testing the number of arguments required by the __init__ method would let you separate the two failure modes.

class Foo(object):
    def __init__(self,a,b):
        pass

class Bar(object):
    def __init__(self,a):
        pass

class Baz(object):
    def __init__(self):
        pass

def ctr_arg_count(cls):
    fn = getattr(cls, '__init__')
    return len(inspect.getargspec(fn).args) - 1

def factory(cls, *args):
    if len(args) != ctr_arg_count(cls):
        print("Can't initialize %s - wrong number of arguments" % cls)
        return None
    return cls(*args)

print factory(Foo, 1, 2)
print factory(Bar, 1)
print factory(Baz)
print factory(Foo, 1)

>>> <__main__.Foo object at 0x100483d10>
    <__main__.Bar object at 0x100483d10>
    <__main__.Baz object at 0x100483d10>
    Can't initialize <class '__main__.Foo'> - wrong number of arguments
    None

Upvotes: 1

Related Questions