Welcome, guest | Sign In | My Account | Store | Cart

Check if needed modules imported before run method

Example::

    @require_module(['time'],exception=Exception)
    def get_time():
        return time.time()
Python, 32 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys

def require_module(names,exception=None):
    """
    Check if needed modules imported before run method

    Example::

        @require_module(['time'],exception=Exception)
        def get_time():
            return time.time()
    """
    def check_module(f):
        def new_f(*args, **kwds):
            for module_name in names:
                if module_name not in sys.modules.keys():
                    if exception:
                        raise exception('Module %s is required for %s' % (module_name,f.func_name))
                    else:
                        return None
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_module


@require_module(['time'],exception=Exception)
def aaa():
    print time.time()


aaa()

4 comments

Steven D'Aprano 7 years, 7 months ago  # | flag

I don't understand the need for this decorator. If you haven't already imported a module, Python will automatically give you a NameError when you try to use it:

py> time.time()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'time' is not defined

This works no matter how you import the module:

import time
import my_custom_module as time

and gives you a standard exception that any experienced Python programmer should be able to recognize and debug in a second, rather than some arbitrary exception like Exception. As far as I can see, all your decorator does is shift the check from when the function is used, to when the function is defined.

Steven D'Aprano 7 years, 7 months ago  # | flag

Oh, I was mistaken about that last point -- the decorator doesn't move the check from when the function is called to when it is defined. The check still occurs when the function is called. That actually makes it worse, it means that perfectly good functions like this will fail:

py> @require_module(['math'], exception=Exception)
... def get_pi():
...     import math  # Make sure math is available before you use it.
...     return math.pi
...
py> get_pi()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 16, in new_f
Exception: Module math is required for get_pi

This decorator also fails to detect functions that won't work. Using your example:

py> @require_module(['time'],exception=Exception)
... def aaa():
...     print time.time()
...
py> aaa()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 19, in new_f
  File "<stdin>", line 3, in aaa
NameError: global name 'time' is not defined

Why did the decorator wrongly pass, but the function call fail? The decorator passed because something else has already imported the time module, so time is cached in sys.modules. But just because the time module has been cached doesn't mean I can use it! It still needs to be imported, and I failed to import time before use. Because the decorator only looks in the cache, it fails to notice that the module hasn't been imported and so cannot be used.

Steven D'Aprano 7 years, 7 months ago  # | flag

One last comment: by default, your decorator turns immediate, obvious, easy to debug exceptions into delayed, confusing, hard to debug silent errors. For example, define a function using the default setting for the exception:

py> @require_module(['math'])
... def test():
...     return math.pi * 2
...
py> num = test()

That looks promising: I have no errors, so I think everything must be good. Then, sometime later, perhaps a couple of lines away, perhaps hundreds of lines, in a different function or even a different module:

py> num+1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

Arrgggh!!! How did the variable "num" end up having the value None instead of a number, like I expected? I now have to try and work backwards, finding out where num came from ("oh, it came from module 'foo', where I called function test") and try to understand why it has the value None instead of the expected value 6.283...

If I had skipped the decorator, and just written the function, I would have had an immediate exception as soon as I tried to use it:

py> def test():
...     return math.pi * 2
...
py> num = test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in test
NameError: global name 'math' is not defined

So instead of helping, by default require_module actually makes debugging code harder.

Andrey Nikishaev (author) 7 years, 7 months ago  # | flag

Thanks for your bug report, i will fix it soon and give you know.