31

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.

Community
  • 1
  • 1
hiro protagonist
  • 44,693
  • 14
  • 86
  • 111
  • 3
    possible duplicate of [Python: How can I know which exceptions might be thrown from a method call](http://stackoverflow.com/questions/1591319/python-how-can-i-know-which-exceptions-might-be-thrown-from-a-method-call) – Simon Gibbons Sep 14 '15 at 08:32
  • Do you want to know this at coding time, or run time? – J Richard Snape Sep 14 '15 at 08:46
  • 1
    "there is no [`compiler`](https://docs.python.org/2/library/compiler.html) package." Then as suggested into your link use the [`parser`](https://docs.python.org/2/library/parser.html#module-parser) one. Or the [`compile`](https://docs.python.org/3/library/functions.html?highlight=compile#compile) builtin or the [`ast.parse`](https://docs.python.org/2/library/ast.html#ast.parse) function – 301_Moved_Permanently Sep 14 '15 at 08:55
  • 1
    in order to use the method described there i'd need to have the classes `Name`, `Raise`, `CallFunc`, `Const`, `Getattr` from the `compiler` package. where would i find those? – hiro protagonist Sep 14 '15 at 08:59
  • i found `Call`, `Name` and `Raise` in the `_ast` module. i'll see what i can do from there. – hiro protagonist Sep 14 '15 at 09:36
  • 2
    I don't think you _can_ get a guaranteed-complete list for arbitrary code, unless that list can include some kind of wildcard value. Nothing stops a function from executing a statement like `raise user_provided_callable('Ouch!')` or `raise CONFIG['exceptions'].get('nitpick', ValueError)('Whoops!')`. That kind of thing might even be a good idea in some situations, although I'm struggling to think of any beyond, "Because I could!". – Kevin J. Chase Sep 14 '15 at 09:52
  • 2
    @KevinJ.Chase not to mention built-in exceptions like NameError, KeyError or any other exception that propagates from a function call. Finding the complete list of a function is very likely to be reduceable to halting problem. – mike3996 Sep 14 '15 at 10:00
  • 1
    I am a little confused by the question - you say that `os.makedirs` can raise `FileExistsError` - but as far as I can see it actually swallows `FileExistsError` [here](https://github.com/python/cpython/blob/master/Lib/os.py#L232). – J Richard Snape Sep 14 '15 at 11:09
  • you are right `FileExistsError` does not belong to the exceptions `makedirs` can rasie. my mistake. i corrected it in the question. – hiro protagonist Sep 15 '15 at 06:52

3 Answers3

19

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.

vaultah
  • 44,105
  • 12
  • 114
  • 143
  • 1
    this looks very nice! i tried `get_exceptions(os.makedirs)` - there it returns nothing. – hiro protagonist Sep 15 '15 at 08:29
  • @hiroprotagonist This is because of the implementation of `os.makedirs`. It uses a bare `raise` in a [block where `OSError` is caught](https://github.com/python/cpython/blob/master/Lib/os.py#L242). Because of the bare `raise` (I think), you never get `name == 'OSError'` in the code above. `OSError` is actually in the `vars` ChainMap in the code above), but not the actual subclasses of `OSError` that might be thrown, I think. Possibly, you could recurse using `getclosurevars` as you visit the `ast` tree, but I'm not even sure that would get everything. – J Richard Snape Sep 15 '15 at 10:40
  • @vaultah In python3.11, `OSError: could not get source code` occurs, where it is supposed to be a ``. – Constantin Hong Jul 15 '23 at 23:34
4

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.

Community
  • 1
  • 1
Cyrbil
  • 6,341
  • 1
  • 24
  • 40
  • 'cannot parse built-in function' i was afraid of that; `PermissionError` is nowhere to be found in `os.py`. i will have a look at your solution. thanks! – hiro protagonist Sep 14 '15 at 09:38
  • Notice that `PermissionError` and `FileExistError` exceptions are subclasses of `OSError`. So you should be fine **only** catching `OSError` and filtering after if needed. – Cyrbil Sep 14 '15 at 09:41
  • true; but i'd still need to know about the existence of these exceptions. – hiro protagonist Sep 14 '15 at 09:43
  • I added an example to find exception form doc. This is not highly accurate but its a good start... – Cyrbil Sep 14 '15 at 09:51
1

I needed to do something similar and found this post. I decided I would write a little library to help.

Say hello to Deep-AST. It's very early alpha but it is pip installable. It has all of the limitations mentioned in this post and some additional ones but its already off to a really good start.

For example when parsing HTTPConnection.getresponse() from http.client it parses 24489 AST Nodes. It finds 181 total raised Exceptions (this includes duplicates) and 8 unique Exceptions were raised. A working code example.

The biggest flaw is this it currently does work with a bare raise:

def foo():

    try:
        bar()
    except TypeError:
        raise

But I think this will be easy to solve and I plan on fixing it.

The library can handle more than just figuring out exceptions, what about listing all Parent classes? It can handle that too!

Levi
  • 455
  • 8
  • 18