O.rka
O.rka

Reputation: 30767

How to properly use a decorator? (TypeError: wrapper() takes 0 positional arguments but 1 was given)

I'm trying to write a decorator that checks if specific packages are available before using a function.

In the example below, numpy should not throw an error but non_existent_test_package should inform the user that they need to install packages to use this functionality. The point of this is to reduce dependencies.

UPDATE based on @henry-harutyunyan suggestion


import numpy as np
import importlib

def check_available_packages(packages):
    if isinstance(packages,str):
        packages = [packages]
    packages = np.asarray(sorted(set(packages)))
    def wrapper(func):
        installed = list()
        for package in packages:
            try: 
                globals()[package] = importlib.import_module(package)
                installed.append(True)
            except ImportError:
                installed.append(False)
        installed = np.asarray(installed)
        assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(", ".join(map(lambda x: "'{}'".format(x), packages[~installed])))
        return func
    return wrapper

@check_available_packages(["numpy"])
def f():
    print("This worked")

@check_available_packages(["numpy", "non_existent_test_package"])
def f():
    print("This shouldn't work")

# ---------------------------------------------------------------------------
# AssertionError                            Traceback (most recent call last)
# <ipython-input-222-5e8224fb30bd> in <module>
#      23     print("This worked")
#      24 
# ---> 25 @check_available_packages(["numpy", "non_existent_test_package"])
#      26 def f():
#      27     print("This shouldn't work")

# <ipython-input-222-5e8224fb30bd> in wrapper(func)
#      15                 installed.append(False)
#      16         installed = np.asarray(installed)
# ---> 17         assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(", ".join(map(lambda x: "'{}'".format(x), packages[~installed])))
#      18         return func
#      19     return wrapper

# AssertionError: Please install the following packages to use this functionality:
# 'non_existent_test_package'

Now the decorator seems to be checking the packages exist at run time instead of when the function is actually called. How can I adjust this code?

Upvotes: 2

Views: 4815

Answers (2)

Blckknght
Blckknght

Reputation: 104852

If you want the check to happen when the underlying function is called, you need an extra level of wrapping around it:

import functools

def check_available_packages(packages):
    if isinstance(packages,str):
        packages = [packages]
    packages = sorted(set(packages))
    def decorator(func):                # we need an extra layer of wrapping
        @functools.wraps(func)          # the wrapper replaces func in the global namespace
        def wrapper(*args, **kwargs):   # so it needs to accept any arguments that func does
            missing_packages = []       # no need for fancy numpy array indexing, a list will do
            for package in packages:
                try: 
                    globals()[package] = importlib.import_module(package)
                except ImportError:
                    missing_packages.append(package)
            assert not missing_packages, "Please install the following packages to use this functionality:\n{}".format(", ".join(missing_packages))
            return func(*args, **kwargs)  # call the function after doing the library checking!
        return wrapper
    return decorator

I removed the reliance on numpy from the code, it seemed completely unnecessary to me, and especially if you're testing if numpy is installed, it doesn't make sense to require it for the checks to work.

Upvotes: 2

Henry Harutyunyan
Henry Harutyunyan

Reputation: 2425

This will work

import numpy as np
import importlib


def check_available_packages(packages):
    if isinstance(packages, str):
        packages = [packages]
    packages = np.asarray(sorted(set(packages)))

    def decorator(func):
        def wrapper():
            installed = list()
            for package in packages:
                try:
                    globals()[package] = importlib.import_module(package)
                    installed.append(True)
                except ImportError:
                    installed.append(False)
            installed = np.asarray(installed)
            assert np.all(installed), "Please install the following packages to use this functionality:\n{}".format(
                ", ".join(packages[~installed]))
            func()

        return wrapper

    return decorator


@check_available_packages(["numpy"])
def foo():
    print("This worked")


@check_available_packages(["numpy", "non_existent_test_package"])
def bar():
    print("This shouldn't work")


foo()
bar()

The issue is that the wrapper() function you have is taking an argument, while by definition it doesn't need any. So passing _ in this statement wrapper(_) will do the job.

_ is dummy, it can't be used, but it still is something. The IDEs won't complain about the unused variable either.

To execute the decorator only when the function is called you need to use the decorator factory as above. See this reference for more details.

Upvotes: 1

Related Questions