Source code for bronx.syntax.externalcode
# -*- coding: utf-8 -*-
"""
A handy class that checks that an external code import worked properly.
If not, a decorator is provided to disable portions of code.
Example of a simple use::
from bronx.syntax.externalcode import ExternalCodeImportChecker
echecker = ExternalCodeImportChecker('anypackage')
with echecker:
import anypackage
print('Did it work ? {!s}'.format(echecker.is_available()))
@echecker.disabled_if_unavailable
def my_function_based_on_anypackage()
return anypackage.sayhi()
@echecker.disabled_if_unavailable
class MyClassBasedOnAnypackage(object)
def sayhi(self)
return anypackage.sayhi()
# A call to my_function_based_on_anypackage or any attempt to create a
# MyClassBasedOnAnypackage object will lead to a ExternalCodeUnavailableError
# error if the import statement failed.
Example of that also checks a version number::
from bronx.syntax.externalcode import ExternalCodeImportChecker
echecker = ExternalCodeImportChecker('anypackage')
with echecker as echecker_register:
import anypackage
echecker_register.update(version=anypackage.__version__,
otherthing=anypackage.otherthing)
# Ensure that the package is here, that version >= 1.0.0 and otherthings==1
@echecker.disabled_if_unavailable(version='1.0.0', otherthing=1)
def my_function_based_on_anypackage()
return anypackage.sayhi()
"""
from __future__ import print_function, absolute_import, unicode_literals, division
import re
import sys
import traceback
import types
from bronx.fancies import loggers
from bronx.fancies.display import join_list_in_proper_english
logger = loggers.getLogger(__name__)
if (sys.version_info.major == 2 or
(sys.version_info.major == 3 and sys.version_info.minor < 10)):
from distutils.version import LooseVersion as version_cb
else:
# distutils is now deprecated
try:
from packaging.version import parse as version_cb
except ImportError:
def version_cb(version1):
"""Crude version processing."""
return tuple([int(x)
for x in re.sub(r'(\.0+)*$', '', version1).split(".")
if x.isdigit()])
[docs]class ExternalCodeUnavailableError(Exception):
"""Raised by the decorated function/class whenever the import did not succeed."""
pass
[docs]class ExternalCodeImportChecker(object):
"""
Catches any import error and allow for the developer to test whether it
succeeded or not.
See the example above.
"""
def __init__(self, nickname='external'):
"""
:param str nickname: The name of the external code to be imported
"""
self.nickname = nickname
self._checked_out = None
self._register = dict()
def _version_check(self, minimal_version):
"""Check the imported package's version."""
if 'version' not in self._register:
raise RuntimeError('No version registered for the {!s} package.'
.format(self.nickname))
return version_cb(self._register['version']) >= version_cb(minimal_version)
def _item_check(self, itemname, value):
"""Check the imported package's info fits."""
if itemname not in self._register:
raise RuntimeError('No {:s} registered for the {!s} package.'
.format(itemname, self.nickname))
return self._register[itemname] == value
def _kwargs_check(self, kwargs):
"""Check that the kwargs dictionary fits !"""
accumulate = True
for k, v in kwargs.items():
if k in ('version', 'v'):
accumulate = accumulate and self._version_check(v)
else:
accumulate = accumulate and self._item_check(k, v)
return accumulate
def __enter__(self):
return self._register
def __exit__(self, exc_type, exc_value, exc_traceback):
"""Catch any ImportError and deal with it !"""
if exc_type is None:
self._checked_out = True
else:
if issubclass(exc_type, ImportError):
logger.warning('The %s package is unavailable.', str(self.nickname))
logger.info('Associated ' +
''.join(traceback.format_exception(exc_type, exc_value, exc_traceback)))
self._checked_out = False
return True
[docs] def is_available(self, **kwargs):
"""Is it ok ?
:param kwargs: A dictionary of requirements to check for
"""
if self._checked_out is None:
raise RuntimeError('No import was attempted yet for package {!s}.'
.format(self.nickname))
return self._checked_out and self._kwargs_check(kwargs)
def _format_exception_message(self, kwargs):
"""Return proper error messages."""
if not kwargs:
return 'The {!s} package is unavailable.'.format(self.nickname)
else:
requirements = [('{!s}>={!s}' if k in ('version', 'v') else '{!s}=={!s}').format(k, v)
for k, v in sorted(kwargs.items())]
return 'The {!s} package is unavailable (with {:s}).'.format(self.nickname,
join_list_in_proper_english(requirements))
[docs] def disabled_if_unavailable(self, *kargs, **kwargs):
"""This decorator disables the provided object if the external code is unavailable.
:param kwargs: A dictionary of requirements to check for
"""
direct_deco = False
if kargs:
if callable(kargs[0]):
direct_deco = True
else:
raise ValueError("kargs needs to be a callable")
available = self.is_available(** kwargs)
def actual_disabled_if_unavailable(func_or_cls):
isfunction = isinstance(func_or_cls, types.FunctionType)
if not isfunction:
if not hasattr(func_or_cls, '__new__'):
raise TypeError('Old-Style classes are not supported by this module.')
if available:
return func_or_cls
else:
excmsg = self._format_exception_message(kwargs)
if isfunction:
def error_wrap(*args, **kw):
raise ExternalCodeUnavailableError(excmsg)
error_wrap.__name__ = func_or_cls.__name__
error_wrap.__doc__ = func_or_cls.__doc__
return error_wrap
else:
def error_new(*args, **kw):
raise ExternalCodeUnavailableError(excmsg)
error_new.__name__ = str('__new__')
error_new.__doc__ = func_or_cls.__new__.__doc__
func_or_cls.__new__ = classmethod(error_new)
return func_or_cls
if direct_deco:
return actual_disabled_if_unavailable(kargs[0])
return actual_disabled_if_unavailable