Coverage for /usr/lib/python3/dist-packages/sympy/external/importtools.py: 36%

61 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1"""Tools to assist importing optional external modules.""" 

2 

3import sys 

4import re 

5 

6# Override these in the module to change the default warning behavior. 

7# For example, you might set both to False before running the tests so that 

8# warnings are not printed to the console, or set both to True for debugging. 

9 

10WARN_NOT_INSTALLED = None # Default is False 

11WARN_OLD_VERSION = None # Default is True 

12 

13 

14def __sympy_debug(): 

15 # helper function from sympy/__init__.py 

16 # We don't just import SYMPY_DEBUG from that file because we don't want to 

17 # import all of SymPy just to use this module. 

18 import os 

19 debug_str = os.getenv('SYMPY_DEBUG', 'False') 

20 if debug_str in ('True', 'False'): 

21 return eval(debug_str) 

22 else: 

23 raise RuntimeError("unrecognized value for SYMPY_DEBUG: %s" % 

24 debug_str) 

25 

26if __sympy_debug(): 

27 WARN_OLD_VERSION = True 

28 WARN_NOT_INSTALLED = True 

29 

30 

31_component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) 

32 

33def version_tuple(vstring): 

34 # Parse a version string to a tuple e.g. '1.2' -> (1, 2) 

35 # Simplified from distutils.version.LooseVersion which was deprecated in 

36 # Python 3.10. 

37 components = [] 

38 for x in _component_re.split(vstring): 

39 if x and x != '.': 

40 try: 

41 x = int(x) 

42 except ValueError: 

43 pass 

44 components.append(x) 

45 return tuple(components) 

46 

47 

48def import_module(module, min_module_version=None, min_python_version=None, 

49 warn_not_installed=None, warn_old_version=None, 

50 module_version_attr='__version__', module_version_attr_call_args=None, 

51 import_kwargs={}, catch=()): 

52 """ 

53 Import and return a module if it is installed. 

54 

55 If the module is not installed, it returns None. 

56 

57 A minimum version for the module can be given as the keyword argument 

58 min_module_version. This should be comparable against the module version. 

59 By default, module.__version__ is used to get the module version. To 

60 override this, set the module_version_attr keyword argument. If the 

61 attribute of the module to get the version should be called (e.g., 

62 module.version()), then set module_version_attr_call_args to the args such 

63 that module.module_version_attr(*module_version_attr_call_args) returns the 

64 module's version. 

65 

66 If the module version is less than min_module_version using the Python < 

67 comparison, None will be returned, even if the module is installed. You can 

68 use this to keep from importing an incompatible older version of a module. 

69 

70 You can also specify a minimum Python version by using the 

71 min_python_version keyword argument. This should be comparable against 

72 sys.version_info. 

73 

74 If the keyword argument warn_not_installed is set to True, the function will 

75 emit a UserWarning when the module is not installed. 

76 

77 If the keyword argument warn_old_version is set to True, the function will 

78 emit a UserWarning when the library is installed, but cannot be imported 

79 because of the min_module_version or min_python_version options. 

80 

81 Note that because of the way warnings are handled, a warning will be 

82 emitted for each module only once. You can change the default warning 

83 behavior by overriding the values of WARN_NOT_INSTALLED and WARN_OLD_VERSION 

84 in sympy.external.importtools. By default, WARN_NOT_INSTALLED is False and 

85 WARN_OLD_VERSION is True. 

86 

87 This function uses __import__() to import the module. To pass additional 

88 options to __import__(), use the import_kwargs keyword argument. For 

89 example, to import a submodule A.B, you must pass a nonempty fromlist option 

90 to __import__. See the docstring of __import__(). 

91 

92 This catches ImportError to determine if the module is not installed. To 

93 catch additional errors, pass them as a tuple to the catch keyword 

94 argument. 

95 

96 Examples 

97 ======== 

98 

99 >>> from sympy.external import import_module 

100 

101 >>> numpy = import_module('numpy') 

102 

103 >>> numpy = import_module('numpy', min_python_version=(2, 7), 

104 ... warn_old_version=False) 

105 

106 >>> numpy = import_module('numpy', min_module_version='1.5', 

107 ... warn_old_version=False) # numpy.__version__ is a string 

108 

109 >>> # gmpy does not have __version__, but it does have gmpy.version() 

110 

111 >>> gmpy = import_module('gmpy', min_module_version='1.14', 

112 ... module_version_attr='version', module_version_attr_call_args=(), 

113 ... warn_old_version=False) 

114 

115 >>> # To import a submodule, you must pass a nonempty fromlist to 

116 >>> # __import__(). The values do not matter. 

117 >>> p3 = import_module('mpl_toolkits.mplot3d', 

118 ... import_kwargs={'fromlist':['something']}) 

119 

120 >>> # matplotlib.pyplot can raise RuntimeError when the display cannot be opened 

121 >>> matplotlib = import_module('matplotlib', 

122 ... import_kwargs={'fromlist':['pyplot']}, catch=(RuntimeError,)) 

123 

124 """ 

125 # keyword argument overrides default, and global variable overrides 

126 # keyword argument. 

127 warn_old_version = (WARN_OLD_VERSION if WARN_OLD_VERSION is not None 

128 else warn_old_version or True) 

129 warn_not_installed = (WARN_NOT_INSTALLED if WARN_NOT_INSTALLED is not None 

130 else warn_not_installed or False) 

131 

132 import warnings 

133 

134 # Check Python first so we don't waste time importing a module we can't use 

135 if min_python_version: 

136 if sys.version_info < min_python_version: 

137 if warn_old_version: 

138 warnings.warn("Python version is too old to use %s " 

139 "(%s or newer required)" % ( 

140 module, '.'.join(map(str, min_python_version))), 

141 UserWarning, stacklevel=2) 

142 return 

143 

144 try: 

145 mod = __import__(module, **import_kwargs) 

146 

147 ## there's something funny about imports with matplotlib and py3k. doing 

148 ## from matplotlib import collections 

149 ## gives python's stdlib collections module. explicitly re-importing 

150 ## the module fixes this. 

151 from_list = import_kwargs.get('fromlist', ()) 

152 for submod in from_list: 

153 if submod == 'collections' and mod.__name__ == 'matplotlib': 

154 __import__(module + '.' + submod) 

155 except ImportError: 

156 if warn_not_installed: 

157 warnings.warn("%s module is not installed" % module, UserWarning, 

158 stacklevel=2) 

159 return 

160 except catch as e: 

161 if warn_not_installed: 

162 warnings.warn( 

163 "%s module could not be used (%s)" % (module, repr(e)), 

164 stacklevel=2) 

165 return 

166 

167 if min_module_version: 

168 modversion = getattr(mod, module_version_attr) 

169 if module_version_attr_call_args is not None: 

170 modversion = modversion(*module_version_attr_call_args) 

171 if version_tuple(modversion) < version_tuple(min_module_version): 

172 if warn_old_version: 

173 # Attempt to create a pretty string version of the version 

174 if isinstance(min_module_version, str): 

175 verstr = min_module_version 

176 elif isinstance(min_module_version, (tuple, list)): 

177 verstr = '.'.join(map(str, min_module_version)) 

178 else: 

179 # Either don't know what this is. Hopefully 

180 # it's something that has a nice str version, like an int. 

181 verstr = str(min_module_version) 

182 warnings.warn("%s version is too old to use " 

183 "(%s or newer required)" % (module, verstr), 

184 UserWarning, stacklevel=2) 

185 return 

186 

187 return mod