"""Tools to assist importing optional external modules.""" import sys import re # Override these in the module to change the default warning behavior. # For example, you might set both to False before running the tests so that # warnings are not printed to the console, or set both to True for debugging. WARN_NOT_INSTALLED = None # Default is False WARN_OLD_VERSION = None # Default is True def __sympy_debug(): # helper function from sympy/__init__.py # We don't just import SYMPY_DEBUG from that file because we don't want to # import all of SymPy just to use this module. import os debug_str = os.getenv('SYMPY_DEBUG', 'False') if debug_str in ('True', 'False'): return eval(debug_str) else: raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" % debug_str) if __sympy_debug(): WARN_OLD_VERSION = True WARN_NOT_INSTALLED = True _component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) def version_tuple(vstring): # Parse a version string to a tuple e.g. '1.2' -> (1, 2) # Simplified from distutils.version.LooseVersion which was deprecated in # Python 3.10. components = [] for x in _component_re.split(vstring): if x and x != '.': try: x = int(x) except ValueError: pass components.append(x) return tuple(components) def import_module(module, min_module_version=None, min_python_version=None, warn_not_installed=None, warn_old_version=None, module_version_attr='__version__', module_version_attr_call_args=None, import_kwargs={}, catch=()): """ Import and return a module if it is installed. If the module is not installed, it returns None. A minimum version for the module can be given as the keyword argument min_module_version. This should be comparable against the module version. By default, module.__version__ is used to get the module version. To override this, set the module_version_attr keyword argument. If the attribute of the module to get the version should be called (e.g., module.version()), then set module_version_attr_call_args to the args such that module.module_version_attr(*module_version_attr_call_args) returns the module's version. If the module version is less than min_module_version using the Python < comparison, None will be returned, even if the module is installed. You can use this to keep from importing an incompatible older version of a module. You can also specify a minimum Python version by using the min_python_version keyword argument. This should be comparable against sys.version_info. If the keyword argument warn_not_installed is set to True, the function will emit a UserWarning when the module is not installed. If the keyword argument warn_old_version is set to True, the function will emit a UserWarning when the library is installed, but cannot be imported because of the min_module_version or min_python_version options. Note that because of the way warnings are handled, a warning will be emitted for each module only once. You can change the default warning behavior by overriding the values of WARN_NOT_INSTALLED and WARN_OLD_VERSION in sympy.external.importtools. By default, WARN_NOT_INSTALLED is False and WARN_OLD_VERSION is True. This function uses __import__() to import the module. To pass additional options to __import__(), use the import_kwargs keyword argument. For example, to import a submodule A.B, you must pass a nonempty fromlist option to __import__. See the docstring of __import__(). This catches ImportError to determine if the module is not installed. To catch additional errors, pass them as a tuple to the catch keyword argument. Examples ======== >>> from sympy.external import import_module >>> numpy = import_module('numpy') >>> numpy = import_module('numpy', min_python_version=(2, 7), ... warn_old_version=False) >>> numpy = import_module('numpy', min_module_version='1.5', ... warn_old_version=False) # numpy.__version__ is a string >>> # gmpy does not have __version__, but it does have gmpy.version() >>> gmpy = import_module('gmpy', min_module_version='1.14', ... module_version_attr='version', module_version_attr_call_args=(), ... warn_old_version=False) >>> # To import a submodule, you must pass a nonempty fromlist to >>> # __import__(). The values do not matter. >>> p3 = import_module('mpl_toolkits.mplot3d', ... import_kwargs={'fromlist':['something']}) >>> # matplotlib.pyplot can raise RuntimeError when the display cannot be opened >>> matplotlib = import_module('matplotlib', ... import_kwargs={'fromlist':['pyplot']}, catch=(RuntimeError,)) """ # keyword argument overrides default, and global variable overrides # keyword argument. warn_old_version = (WARN_OLD_VERSION if WARN_OLD_VERSION is not None else warn_old_version or True) warn_not_installed = (WARN_NOT_INSTALLED if WARN_NOT_INSTALLED is not None else warn_not_installed or False) import warnings # Check Python first so we don't waste time importing a module we can't use if min_python_version: if sys.version_info < min_python_version: if warn_old_version: warnings.warn("Python version is too old to use %s " "(%s or newer required)" % ( module, '.'.join(map(str, min_python_version))), UserWarning, stacklevel=2) return try: mod = __import__(module, **import_kwargs) ## there's something funny about imports with matplotlib and py3k. doing ## from matplotlib import collections ## gives python's stdlib collections module. explicitly re-importing ## the module fixes this. from_list = import_kwargs.get('fromlist', ()) for submod in from_list: if submod == 'collections' and mod.__name__ == 'matplotlib': __import__(module + '.' + submod) except ImportError: if warn_not_installed: warnings.warn("%s module is not installed" % module, UserWarning, stacklevel=2) return except catch as e: if warn_not_installed: warnings.warn( "%s module could not be used (%s)" % (module, repr(e)), stacklevel=2) return if min_module_version: modversion = getattr(mod, module_version_attr) if module_version_attr_call_args is not None: modversion = modversion(*module_version_attr_call_args) if version_tuple(modversion) < version_tuple(min_module_version): if warn_old_version: # Attempt to create a pretty string version of the version if isinstance(min_module_version, str): verstr = min_module_version elif isinstance(min_module_version, (tuple, list)): verstr = '.'.join(map(str, min_module_version)) else: # Either don't know what this is. Hopefully # it's something that has a nice str version, like an int. verstr = str(min_module_version) warnings.warn("%s version is too old to use " "(%s or newer required)" % (module, verstr), UserWarning, stacklevel=2) return return mod