Source code for africanus.util.requirements
# -*- coding: utf-8 -*-
import importlib
from decorator import decorate
from africanus.util.docs import on_rtd
from africanus.util.testing import in_pytest, force_missing_pkg_exception
def _missing_packages(fn, packages, import_errors):
if len(import_errors) > 0:
import_err_str = "\n".join((str(e) for e in import_errors))
return "%s requires installation of " "the following packages: %s.\n" "%s" % (
fn,
packages,
import_err_str,
)
else:
return "%s requires installation of the following packages: %s. " % (
fn,
tuple(packages),
)
class MissingPackageException(Exception):
pass
[docs]
def requires_optional(*requirements):
"""
Decorator returning either the original function,
or a dummy function raising a
:class:`MissingPackageException` when called,
depending on whether the supplied ``requirements``
are present.
If packages are missing and called within a test, the
dummy function will call :func:`pytest.skip`.
Used in the following way:
.. code-block:: python
try:
from scipy import interpolate
except ImportError as e:
# https://stackoverflow.com/a/29268974/1611416, pep 3110 and 344
scipy_import_error = e
else:
scipy_import_error = None
@requires_optional('scipy', scipy_import_error)
def function(*args, **kwargs):
return interpolate(...)
Parameters
----------
requirements : iterable of string, None or ImportError
Sequence of package names required by the decorated function.
ImportError exceptions (or None, indicating their absence)
may also be supplied and will be immediately re-raised within
the decorator. This is useful for tracking down problems
in user import logic.
Returns
-------
callable
Either the original function if all ``requirements``
are available or a dummy function that throws
a :class:`MissingPackageException` or skips a pytest.
"""
# Return a bare wrapper if we're on readthedocs
if on_rtd():
def _function_decorator(fn):
def _wrapper(*args, **kwargs):
pass
return decorate(fn, _wrapper)
return _function_decorator
have_requirements = True
missing_requirements = []
honour_pytest_marker = True
actual_imports = []
import_errors = []
# Try imports
for requirement in requirements:
# Ignore
if requirement is None:
continue
# Reraise any supplied ImportErrors
elif isinstance(requirement, ImportError):
import_errors.append(requirement)
# An actual package, try to import it
elif isinstance(requirement, str):
try:
importlib.import_module(requirement)
except ImportError:
missing_requirements.append(requirement)
have_requirements = False
else:
actual_imports.append(requirement)
# We should force exceptions, even if we're in a pytest test case
elif requirement == force_missing_pkg_exception:
honour_pytest_marker = False
# Just wrong
else:
raise TypeError(
"requirements must be "
"None, strings or ImportErrors. "
"Received %s" % requirement
)
# Requested requirement import succeeded, but there were user
# import errors that we now re-raise
if have_requirements and len(import_errors) > 0:
raise ImportError(
"Successfully imported %s "
"but the following user-supplied "
"ImportErrors ocurred: \n%s"
% (actual_imports, "\n".join((str(e) for e in import_errors)))
)
def _function_decorator(fn):
# We have requirements, return the original function
if have_requirements:
return fn
# We don't have requirements, produce a failing wrapper
def _wrapper(*args, **kwargs):
"""Empty docstring"""
# We're running test cases
if honour_pytest_marker and in_pytest():
try:
import pytest
except ImportError as e:
raise ImportError(
"Marked as in a pytest "
"test case, but pytest cannot "
"be imported! %s" % str(e)
)
else:
msg = _missing_packages(
fn.__name__, missing_requirements, import_errors
)
pytest.skip(msg)
# Raise the exception
else:
msg = _missing_packages(
fn.__name__, missing_requirements, import_errors
)
raise MissingPackageException(msg)
return decorate(fn, _wrapper)
return _function_decorator