Reputation: 30767
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.
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
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
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