Coverage for /usr/lib/python3/dist-packages/sympy/core/cache.py: 47%

113 statements  

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

1""" Caching facility for SymPy """ 

2from importlib import import_module 

3from typing import Callable 

4 

5class _cache(list): 

6 """ List of cached functions """ 

7 

8 def print_cache(self): 

9 """print cache info""" 

10 

11 for item in self: 

12 name = item.__name__ 

13 myfunc = item 

14 while hasattr(myfunc, '__wrapped__'): 

15 if hasattr(myfunc, 'cache_info'): 

16 info = myfunc.cache_info() 

17 break 

18 else: 

19 myfunc = myfunc.__wrapped__ 

20 else: 

21 info = None 

22 

23 print(name, info) 

24 

25 def clear_cache(self): 

26 """clear cache content""" 

27 for item in self: 

28 myfunc = item 

29 while hasattr(myfunc, '__wrapped__'): 

30 if hasattr(myfunc, 'cache_clear'): 

31 myfunc.cache_clear() 

32 break 

33 else: 

34 myfunc = myfunc.__wrapped__ 

35 

36 

37# global cache registry: 

38CACHE = _cache() 

39# make clear and print methods available 

40print_cache = CACHE.print_cache 

41clear_cache = CACHE.clear_cache 

42 

43from functools import lru_cache, wraps 

44 

45def __cacheit(maxsize): 

46 """caching decorator. 

47 

48 important: the result of cached function must be *immutable* 

49 

50 

51 Examples 

52 ======== 

53 

54 >>> from sympy import cacheit 

55 >>> @cacheit 

56 ... def f(a, b): 

57 ... return a+b 

58 

59 >>> @cacheit 

60 ... def f(a, b): # noqa: F811 

61 ... return [a, b] # <-- WRONG, returns mutable object 

62 

63 to force cacheit to check returned results mutability and consistency, 

64 set environment variable SYMPY_USE_CACHE to 'debug' 

65 """ 

66 def func_wrapper(func): 

67 cfunc = lru_cache(maxsize, typed=True)(func) 

68 

69 @wraps(func) 

70 def wrapper(*args, **kwargs): 

71 try: 

72 retval = cfunc(*args, **kwargs) 

73 except TypeError as e: 

74 if not e.args or not e.args[0].startswith('unhashable type:'): 

75 raise 

76 retval = func(*args, **kwargs) 

77 return retval 

78 

79 wrapper.cache_info = cfunc.cache_info 

80 wrapper.cache_clear = cfunc.cache_clear 

81 

82 CACHE.append(wrapper) 

83 return wrapper 

84 

85 return func_wrapper 

86######################################## 

87 

88 

89def __cacheit_nocache(func): 

90 return func 

91 

92 

93def __cacheit_debug(maxsize): 

94 """cacheit + code to check cache consistency""" 

95 def func_wrapper(func): 

96 cfunc = __cacheit(maxsize)(func) 

97 

98 @wraps(func) 

99 def wrapper(*args, **kw_args): 

100 # always call function itself and compare it with cached version 

101 r1 = func(*args, **kw_args) 

102 r2 = cfunc(*args, **kw_args) 

103 

104 # try to see if the result is immutable 

105 # 

106 # this works because: 

107 # 

108 # hash([1,2,3]) -> raise TypeError 

109 # hash({'a':1, 'b':2}) -> raise TypeError 

110 # hash((1,[2,3])) -> raise TypeError 

111 # 

112 # hash((1,2,3)) -> just computes the hash 

113 hash(r1), hash(r2) 

114 

115 # also see if returned values are the same 

116 if r1 != r2: 

117 raise RuntimeError("Returned values are not the same") 

118 return r1 

119 return wrapper 

120 return func_wrapper 

121 

122 

123def _getenv(key, default=None): 

124 from os import getenv 

125 return getenv(key, default) 

126 

127# SYMPY_USE_CACHE=yes/no/debug 

128USE_CACHE = _getenv('SYMPY_USE_CACHE', 'yes').lower() 

129# SYMPY_CACHE_SIZE=some_integer/None 

130# special cases : 

131# SYMPY_CACHE_SIZE=0 -> No caching 

132# SYMPY_CACHE_SIZE=None -> Unbounded caching 

133scs = _getenv('SYMPY_CACHE_SIZE', '1000') 

134if scs.lower() == 'none': 

135 SYMPY_CACHE_SIZE = None 

136else: 

137 try: 

138 SYMPY_CACHE_SIZE = int(scs) 

139 except ValueError: 

140 raise RuntimeError( 

141 'SYMPY_CACHE_SIZE must be a valid integer or None. ' + \ 

142 'Got: %s' % SYMPY_CACHE_SIZE) 

143 

144if USE_CACHE == 'no': 

145 cacheit = __cacheit_nocache 

146elif USE_CACHE == 'yes': 

147 cacheit = __cacheit(SYMPY_CACHE_SIZE) 

148elif USE_CACHE == 'debug': 

149 cacheit = __cacheit_debug(SYMPY_CACHE_SIZE) # a lot slower 

150else: 

151 raise RuntimeError( 

152 'unrecognized value for SYMPY_USE_CACHE: %s' % USE_CACHE) 

153 

154 

155def cached_property(func): 

156 '''Decorator to cache property method''' 

157 attrname = '__' + func.__name__ 

158 _cached_property_sentinel = object() 

159 def propfunc(self): 

160 val = getattr(self, attrname, _cached_property_sentinel) 

161 if val is _cached_property_sentinel: 

162 val = func(self) 

163 setattr(self, attrname, val) 

164 return val 

165 return property(propfunc) 

166 

167 

168def lazy_function(module : str, name : str) -> Callable: 

169 """Create a lazy proxy for a function in a module. 

170 

171 The module containing the function is not imported until the function is used. 

172 

173 """ 

174 func = None 

175 

176 def _get_function(): 

177 nonlocal func 

178 if func is None: 

179 func = getattr(import_module(module), name) 

180 return func 

181 

182 # The metaclass is needed so that help() shows the docstring 

183 class LazyFunctionMeta(type): 

184 @property 

185 def __doc__(self): 

186 docstring = _get_function().__doc__ 

187 docstring += f"\n\nNote: this is a {self.__class__.__name__} wrapper of '{module}.{name}'" 

188 return docstring 

189 

190 class LazyFunction(metaclass=LazyFunctionMeta): 

191 def __call__(self, *args, **kwargs): 

192 # inline get of function for performance gh-23832 

193 nonlocal func 

194 if func is None: 

195 func = getattr(import_module(module), name) 

196 return func(*args, **kwargs) 

197 

198 @property 

199 def __doc__(self): 

200 docstring = _get_function().__doc__ 

201 docstring += f"\n\nNote: this is a {self.__class__.__name__} wrapper of '{module}.{name}'" 

202 return docstring 

203 

204 def __str__(self): 

205 return _get_function().__str__() 

206 

207 def __repr__(self): 

208 return f"<{__class__.__name__} object at 0x{id(self):x}>: wrapping '{module}.{name}'" 

209 

210 return LazyFunction()