Search code examples
pythonpython-3.4

How to list all exceptions a function could raise in Python 3?


Is there a programmatic way to get a list of all exceptions a function could raise?

I know for example that os.makedirs(path[, mode]) can raise PermissionError (and maybe others), but the documentation only mentions OSError. (This is just an example - maybe even a bad one; I am not especially interested in this function - more in the problem in general).

Is there a programmatic way to find all the possible exceptions when they are not/poorly documented? This may be especially useful in 3rd-party libraries and libraries that do not ship with Python source code.

The solution presented in "Python: How can I know which exceptions might be thrown from a method call" does not work in Python 3; there is no compiler package.


Solution

  • You can't get reliable results for some (if not most) functions. Some examples:

    • functions that execute arbitrary code (e.g. exec(')(rorrEeulaV esiar'[::-1]) raises ValueError)

    • functions that aren't written in Python

    • functions that call other functions that can propagate errors to the caller

    • functions re-raising active exceptions in the except: block

    Unfortunately, this list is incomplete.

    E.g. os.makedirs is written in Python and you can see its source:

    ...
    try:
        mkdir(name, mode)
    except OSError as e:
        if not exist_ok or e.errno != errno.EEXIST or not path.isdir(name):
            raise
    

    Bare raise re-raises the last active exception (OSError or one of its subclasses). Here's the class hierarchy for OSError:

    +-- OSError
    |    +-- BlockingIOError
    |    +-- ChildProcessError
    |    +-- ConnectionError
    |    |    +-- BrokenPipeError
    |    |    +-- ConnectionAbortedError
    |    |    +-- ConnectionRefusedError
    |    |    +-- ConnectionResetError
    |    +-- FileExistsError
    |    +-- FileNotFoundError
    |    +-- InterruptedError
    |    +-- IsADirectoryError
    |    +-- NotADirectoryError
    |    +-- PermissionError
    |    +-- ProcessLookupError
    |    +-- TimeoutError
    

    To get the exact exception types you'll need to look into mkdir, functions it calls, functions those functions call etc.

    So, getting possible exceptions without running the function is very hard and you really should not do it.


    However for simple cases like

    raise Exception # without arguments
    raise Exception('abc') # with arguments
    

    a combination of ast module functionality and inspect.getclosurevars (to get exception classes, was introduced in Python 3.3) can produce quite accurate results:

    from inspect import getclosurevars, getsource
    from collections import ChainMap
    from textwrap import dedent
    import ast, os
    
    class MyException(Exception):
        pass
    
    def g():
        raise Exception
    
    class A():
        def method():
            raise OSError
    
    def f(x):
        int()
        A.method()
        os.makedirs()
        g()
        raise MyException
        raise ValueError('argument')
    
    
    def get_exceptions(func, ids=set()):
        try:
            vars = ChainMap(*getclosurevars(func)[:3])
            source = dedent(getsource(func))
        except TypeError:
            return
    
        class _visitor(ast.NodeTransformer):
            def __init__(self):
                self.nodes = []
                self.other = []
    
            def visit_Raise(self, n):
                self.nodes.append(n.exc)
    
            def visit_Expr(self, n):
                if not isinstance(n.value, ast.Call):
                    return
                c, ob = n.value.func, None
                if isinstance(c, ast.Attribute):
                    parts = []
                    while getattr(c, 'value', None):
                        parts.append(c.attr)
                        c = c.value
                    if c.id in vars:
                        ob = vars[c.id]
                        for name in reversed(parts):
                            ob = getattr(ob, name)
    
                elif isinstance(c, ast.Name):
                    if c.id in vars:
                        ob = vars[c.id]
    
                if ob is not None and id(ob) not in ids:
                    self.other.append(ob)
                    ids.add(id(ob))
    
        v = _visitor()
        v.visit(ast.parse(source))
        for n in v.nodes:
            if isinstance(n, (ast.Call, ast.Name)):
                name = n.id if isinstance(n, ast.Name) else n.func.id
                if name in vars:
                    yield vars[name]
    
        for o in v.other:
            yield from get_exceptions(o)
    
    
    for e in get_exceptions(f):
        print(e)
    

    prints

    <class '__main__.MyException'>
    <class 'ValueError'>
    <class 'OSError'>
    <class 'Exception'>
    

    Keep in mind that this code only works for functions written in Python.