I'm working on python packages that implement scientific models and I'm wondering what is the best way to handle optional features. Here's the behavior I'd like: If some optional dependencies can't be imported (plotting module on a headless machine for example), I'd like to disable the functions using these modules in my classes, warn the user if he tries to use them and all that without breaking the execution. so the following script would work in any cases:
mymodel.dostuff()
mymodel.plot() <= only plots if possible, else display log an error
mymodel.domorestuff() <= get executed regardless of the result of the previous statement
So far the options I see are the following:
__init __.py
for available modules and keep a list of
them (but how to properly use it in the rest of the package?) try import ...
except ...
statement These options should work, but they all seem to be rather hacky and hard to maintain. what if we want to drop a dependency completely? or make it mandatory?
The easiest solution, of course, is to simply import the optional dependencies in the body of the function that requires them. But the always-right PEP 8
says:
Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.
Not wanting to go against the best wishes of the python masters, I take the following approach, which has several benefits...
Say one of my functions foo
needs numpy
, and I want to make it an optional dependency. At the top of the module, I put:
try:
import numpy as _numpy
except ImportError:
_has_numpy = False
else:
_has_numpy = True
Here (in the except block) would be the place to print a warning, preferably using the warnings
module.
What if the user calls foo
and doesn't have numpy? I throw the exception there and document this behaviour.
def foo(x):
"""Requires numpy."""
if not _has_numpy:
raise ImportError("numpy is required to do this.")
...
Alternatively you can use a decorator and apply it to any function requiring that dependency:
@requires_numpy
def foo(x):
...
This has the benefit of preventing code duplication.
If you're distributing code, look up how to add the extra dependency to the setup configuration. For example, with setuptools
, I can write:
install_requires = ["networkx"],
extras_require = {
"numpy": ["numpy"],
"sklearn": ["scikit-learn"]}
This specifies that networkx
is absolutely required at install time, but that the extra functionality of my module requires numpy
and sklearn
, which are optional.
Using this approach, here are the answers to your specific questions:
We can simply add our optional dependency to our setup tool's list of required dependencies. In the example above, we move numpy
to install_requires
. All of the code checking for the existence of numpy
can then be removed, but leaving it in won't cause your program to break.
Simply remove the check for the dependency in any function that previously required it. If you implemented the dependency check with a decorator, you could just change it so that it simply passes the original function through unchanged.
This approach has the benefit of placing all of the imports at the top of the module so that I can see at a glance what is required and what is optional.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With