123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- """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
- # PyPy 1.6 has rudimentary NumPy support and importing it produces errors, so skip it
- if module == 'numpy' and '__pypy__' in sys.builtin_module_names:
- 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', tuple())
- 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
|