3

I'm using a library that will return something like the following when calling one of their functions

result = getResult(foo)
print(result)

The result is a dict

{'errorMessage': "name 'foo' is not defined", 'errorType': 'NameError'}

I want to raise a NameError exception, but the following doesn't work

raise result['errorType'](result['errorMessage'])

It gives a new error

TypeError: 'str' object is not callable

How can I raise an exception who's type is defined from a string variable?

Steve Robbins
  • 13,672
  • 12
  • 76
  • 124
  • 1
    Why is it being printed with `"""` around it? That would be a string. – Barmar Apr 26 '22 at 20:18
  • 1
    Of course not, because `str` objects are not `Exception` objects. – juanpa.arrivillaga Apr 26 '22 at 20:25
  • I assume these are python exceptions being summarized in the dict. Are there many types of errors that can be raised? More specifically, would you expect exceptions that are not in `__builtins__` to be represented? – tdelaney Apr 26 '22 at 21:51

3 Answers3

7

The value of result["errorType"] is a string that names an error class, not the error class itself.

If the error type will always be a built-in type, you can look it up in __builtins__.

raise getattr(__builtins__, result['errorType'])(result['errorMessage'])
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • 3
    I was surprised to find this doesn't work. You get `KeyError: 'NameError'`. It is in builtins, so raise `getattr(__builtins__, result['errorType'])(result['errorMessage'])` does work. – tdelaney Apr 26 '22 at 20:28
1

You could use eval but carefully:

>>> raise eval(f"{result['errorType']}(\"{result['errorMessage']}\")")

NameError: name 'foo' is not defined
not_speshal
  • 22,093
  • 2
  • 15
  • 30
  • 1
    This does work, but since the result is coming from a 3rd party I wouldn't trust it in this use case – Steve Robbins Apr 26 '22 at 20:39
  • Sorry, I don't normally downvote when I write an answer of my own, but I feel it is necessary to point that this type of answer normalizes using `eval`. Which is very much a really, really, bad security practice. Pointing to the original question about what `eval` does isn't in itself sufficient to guard a naive user against using it. And whatever very few reasons for using eval (there's a safer alternative at `ast.literal_eval`) still remain, the OP's circumstances do not require it, as Barmar's answer shows. – JL Peyret Apr 27 '22 at 00:30
  • "Normalizes" - definitely not. As mentioned in the answer ("carefully"), have a look at the linked question for all potential problems with using `eval`. – not_speshal Apr 27 '22 at 13:00
1

This should cover most of the bases and allows for non-builtin exceptions. Basically, get the exception class name and look up the corresponding class object up from a pre-computed dictionary of classnames to Exception subclasses.

from django.core.exceptions import ImproperlyConfigured

class MyCustomExc(Exception):
    pass

def raiser(di : dict):

    di = di.copy()
    cls = None

    def get_excs(di : dict):
        di_exc = {k:v for k,v in di.items() if type(v) == type and issubclass(v, Exception)}    
        return di_exc

    di_exc = get_excs(vars(__builtins__))

    di_exc.update(**get_excs(globals()))

    dflt_exc = RuntimeError

    errorType = di.pop("errorType", dflt_exc.__name__)


    cls = di_exc.get(errorType)

    if not cls:
        di = di.copy()
        di["unknown_exception"] = errorType
        cls = dflt_exc

    raise cls(di)

    #the alternative version which does not raise
    return cls, di





inputs = [
    {'errorMessage': "name 'foo' is not defined", 'errorType': 'NameError'},
    {'errorMessage': "custom stuff", 'errorType': 'MyCustomExc'},
    {'errorMessage': "Django stuff", 'errorType': 'ImproperlyConfigured'},
    {'errorMessage': "Django stuff", 'errorType': 'UnknownExc'},
    ]

for inp in inputs:
    try:
        dummy = raiser(inp)
        print(dummy)
        
    except (Exception,) as got:
        # breakpoint()
        msg = f"{str(inp):60.60} => {repr(got)}"
        print(msg)

output:

{'errorMessage': "name 'foo' is not defined", 'errorType': ' => NameError({'errorMessage': "name 'foo' is not defined"})
{'errorMessage': 'custom stuff', 'errorType': 'MyCustomExc'} => MyCustomExc({'errorMessage': 'custom stuff'})
{'errorMessage': 'Django stuff', 'errorType': 'ImproperlyCon => ImproperlyConfigured({'errorMessage': 'Django stuff'})
{'errorMessage': 'Django stuff', 'errorType': 'UnknownExc'}  => RuntimeError({'errorMessage': 'Django stuff', 'unknown_exception': 'UnknownExc'})

One thing I would probably do however is to just get the bits you need from the function rather than raise-ing in it (that way the stacktrace shows the actual location of your call, not raiser). I would also pre-compute the exceptions dictionary from builtins and globals and pass it into raiser rather than re-computing each time.

result = getResult(foo)
cls_exc, exc_data = raiser(result)
if cls_exc:
  raise cls_exc(exc_data)

The conditional allows handling when you don't get an error.

JL Peyret
  • 10,917
  • 2
  • 54
  • 73