Ok, so I found a solution which should also work in case a class is not under my control. This solution only targets the AttributeError
but should be extendable in case other Errors need to be caught.
We still have the same test function and the same Dummy class
def test(first, second):
print("My name is " + first.age + " and I am here with " + second.age)
class Dummy(object):
def __init__(self):
pass
We can use a Proxy object to wrap each value we pass to the test function.
This proxy object records if it sees an AttributeError
by setting the _had_exception
flag.
class Proxy(object):
def __init__(self, object_a):
self._object_a = object_a
self._had_exception: bool = False
def __getattribute__(self, name):
if name == "_had_exception":
return object.__getattribute__(self, name)
obj = object.__getattribute__(self, '_object_a')
try:
return getattr(obj, name)
except AttributeError as e:
# Flag this object as a cause for an exception
self._had_exception = True
raise e
And the call to the function looks as follows
d = Dummy()
d.__setattr__("age", "25")
p1 = Proxy(d)
p2 = Proxy(Dummy())
try:
test(p1, p2)
except AttributeError as e:
# Get the local variables from when the Error happened
locals = e.__traceback__.tb_next.tb_frame.f_locals
offender_names = []
# Check if one of the local items is the same
# as one of our inputs that caused an Error
for key, val in locals.items():
if p1._had_exception:
if p1 is val:
offender_names.append(key)
if p2._had_exception:
if p2 is val:
offender_names.append(key)
print(offender_names) # ['second']
The end result is a list with all local variable names -- used in the called function -- which correspond to our wrapped inputs, that caused an exception.