importtools.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. """Tools to assist importing optional external modules."""
  2. import sys
  3. import re
  4. # Override these in the module to change the default warning behavior.
  5. # For example, you might set both to False before running the tests so that
  6. # warnings are not printed to the console, or set both to True for debugging.
  7. WARN_NOT_INSTALLED = None # Default is False
  8. WARN_OLD_VERSION = None # Default is True
  9. def __sympy_debug():
  10. # helper function from sympy/__init__.py
  11. # We don't just import SYMPY_DEBUG from that file because we don't want to
  12. # import all of SymPy just to use this module.
  13. import os
  14. debug_str = os.getenv('SYMPY_DEBUG', 'False')
  15. if debug_str in ('True', 'False'):
  16. return eval(debug_str)
  17. else:
  18. raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" %
  19. debug_str)
  20. if __sympy_debug():
  21. WARN_OLD_VERSION = True
  22. WARN_NOT_INSTALLED = True
  23. _component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
  24. def version_tuple(vstring):
  25. # Parse a version string to a tuple e.g. '1.2' -> (1, 2)
  26. # Simplified from distutils.version.LooseVersion which was deprecated in
  27. # Python 3.10.
  28. components = []
  29. for x in _component_re.split(vstring):
  30. if x and x != '.':
  31. try:
  32. x = int(x)
  33. except ValueError:
  34. pass
  35. components.append(x)
  36. return tuple(components)
  37. def import_module(module, min_module_version=None, min_python_version=None,
  38. warn_not_installed=None, warn_old_version=None,
  39. module_version_attr='__version__', module_version_attr_call_args=None,
  40. import_kwargs={}, catch=()):
  41. """
  42. Import and return a module if it is installed.
  43. If the module is not installed, it returns None.
  44. A minimum version for the module can be given as the keyword argument
  45. min_module_version. This should be comparable against the module version.
  46. By default, module.__version__ is used to get the module version. To
  47. override this, set the module_version_attr keyword argument. If the
  48. attribute of the module to get the version should be called (e.g.,
  49. module.version()), then set module_version_attr_call_args to the args such
  50. that module.module_version_attr(*module_version_attr_call_args) returns the
  51. module's version.
  52. If the module version is less than min_module_version using the Python <
  53. comparison, None will be returned, even if the module is installed. You can
  54. use this to keep from importing an incompatible older version of a module.
  55. You can also specify a minimum Python version by using the
  56. min_python_version keyword argument. This should be comparable against
  57. sys.version_info.
  58. If the keyword argument warn_not_installed is set to True, the function will
  59. emit a UserWarning when the module is not installed.
  60. If the keyword argument warn_old_version is set to True, the function will
  61. emit a UserWarning when the library is installed, but cannot be imported
  62. because of the min_module_version or min_python_version options.
  63. Note that because of the way warnings are handled, a warning will be
  64. emitted for each module only once. You can change the default warning
  65. behavior by overriding the values of WARN_NOT_INSTALLED and WARN_OLD_VERSION
  66. in sympy.external.importtools. By default, WARN_NOT_INSTALLED is False and
  67. WARN_OLD_VERSION is True.
  68. This function uses __import__() to import the module. To pass additional
  69. options to __import__(), use the import_kwargs keyword argument. For
  70. example, to import a submodule A.B, you must pass a nonempty fromlist option
  71. to __import__. See the docstring of __import__().
  72. This catches ImportError to determine if the module is not installed. To
  73. catch additional errors, pass them as a tuple to the catch keyword
  74. argument.
  75. Examples
  76. ========
  77. >>> from sympy.external import import_module
  78. >>> numpy = import_module('numpy')
  79. >>> numpy = import_module('numpy', min_python_version=(2, 7),
  80. ... warn_old_version=False)
  81. >>> numpy = import_module('numpy', min_module_version='1.5',
  82. ... warn_old_version=False) # numpy.__version__ is a string
  83. >>> # gmpy does not have __version__, but it does have gmpy.version()
  84. >>> gmpy = import_module('gmpy', min_module_version='1.14',
  85. ... module_version_attr='version', module_version_attr_call_args=(),
  86. ... warn_old_version=False)
  87. >>> # To import a submodule, you must pass a nonempty fromlist to
  88. >>> # __import__(). The values do not matter.
  89. >>> p3 = import_module('mpl_toolkits.mplot3d',
  90. ... import_kwargs={'fromlist':['something']})
  91. >>> # matplotlib.pyplot can raise RuntimeError when the display cannot be opened
  92. >>> matplotlib = import_module('matplotlib',
  93. ... import_kwargs={'fromlist':['pyplot']}, catch=(RuntimeError,))
  94. """
  95. # keyword argument overrides default, and global variable overrides
  96. # keyword argument.
  97. warn_old_version = (WARN_OLD_VERSION if WARN_OLD_VERSION is not None
  98. else warn_old_version or True)
  99. warn_not_installed = (WARN_NOT_INSTALLED if WARN_NOT_INSTALLED is not None
  100. else warn_not_installed or False)
  101. import warnings
  102. # Check Python first so we don't waste time importing a module we can't use
  103. if min_python_version:
  104. if sys.version_info < min_python_version:
  105. if warn_old_version:
  106. warnings.warn("Python version is too old to use %s "
  107. "(%s or newer required)" % (
  108. module, '.'.join(map(str, min_python_version))),
  109. UserWarning, stacklevel=2)
  110. return
  111. # PyPy 1.6 has rudimentary NumPy support and importing it produces errors, so skip it
  112. if module == 'numpy' and '__pypy__' in sys.builtin_module_names:
  113. return
  114. try:
  115. mod = __import__(module, **import_kwargs)
  116. ## there's something funny about imports with matplotlib and py3k. doing
  117. ## from matplotlib import collections
  118. ## gives python's stdlib collections module. explicitly re-importing
  119. ## the module fixes this.
  120. from_list = import_kwargs.get('fromlist', tuple())
  121. for submod in from_list:
  122. if submod == 'collections' and mod.__name__ == 'matplotlib':
  123. __import__(module + '.' + submod)
  124. except ImportError:
  125. if warn_not_installed:
  126. warnings.warn("%s module is not installed" % module, UserWarning,
  127. stacklevel=2)
  128. return
  129. except catch as e:
  130. if warn_not_installed:
  131. warnings.warn(
  132. "%s module could not be used (%s)" % (module, repr(e)),
  133. stacklevel=2)
  134. return
  135. if min_module_version:
  136. modversion = getattr(mod, module_version_attr)
  137. if module_version_attr_call_args is not None:
  138. modversion = modversion(*module_version_attr_call_args)
  139. if version_tuple(modversion) < version_tuple(min_module_version):
  140. if warn_old_version:
  141. # Attempt to create a pretty string version of the version
  142. if isinstance(min_module_version, str):
  143. verstr = min_module_version
  144. elif isinstance(min_module_version, (tuple, list)):
  145. verstr = '.'.join(map(str, min_module_version))
  146. else:
  147. # Either don't know what this is. Hopefully
  148. # it's something that has a nice str version, like an int.
  149. verstr = str(min_module_version)
  150. warnings.warn("%s version is too old to use "
  151. "(%s or newer required)" % (module, verstr),
  152. UserWarning, stacklevel=2)
  153. return
  154. return mod