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

前端 未结 2 878
粉色の甜心
粉色の甜心 2021-01-03 22:32

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

相关标签:
2条回答
  • 2021-01-03 22:51

    Finding Exception in non built-in source code:

    As said in the topic Python: How can I know which exceptions might be thrown from a method call, you can get the Abstract Syntax Tree and search for raised exceptions.

    import ast
    
    def find_raise(body):
        raises = []
        for ast_ in body:
            if isinstance(ast_, ast.Raise):
                raises.append(ast_)
            if hasattr(ast_, 'body'):
                raises += find_raise(ast_.body)
        return list(set(raises))
    
    
    test = '''
    def f(arg):
        raise OSError(arg)
    '''
    
    raises = find_raise(ast.parse(test).body)
    print [i.type.func.id for i in raises] # print ['OSError']
    

    This method works for every piece of code that you have written.


    Finding Exception in Built-in methods

    You cannot parse built-in function like os.makedirs.

    Two alternatives:

    • You can have a look at the tests included in your python distribution (ex with cpython)
    • and if your target method offers python source code, you can parse it like previously (the code would be in /usr/lib/python3/*.py)

    For all native C methods, you are stuck with the documentation and should trust it. When os.makedirs says it only returns OSError, it is true, since PermissionError and FileExistError exceptions are subclasses of OSError.

    To find Errors programmatically for built-in you can use this example:

    >>> import re
    >>> re.findall(r'\w+Error', open.__doc__)
    ['IOError', 'FileExistsError', 'ValueError']
    >>> re.findall(r'\w+Error', os.makedirs.__doc__)
    ['OSError']
    

    It catches all exceptions with a name ending with 'Error', it surely can be extended to find all standard exceptions.

    0 讨论(0)
  • 2021-01-03 22:52

    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.

    0 讨论(0)
提交回复
热议问题