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
« 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
5class _cache(list):
6 """ List of cached functions """
8 def print_cache(self):
9 """print cache info"""
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
23 print(name, info)
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__
37# global cache registry:
38CACHE = _cache()
39# make clear and print methods available
40print_cache = CACHE.print_cache
41clear_cache = CACHE.clear_cache
43from functools import lru_cache, wraps
45def __cacheit(maxsize):
46 """caching decorator.
48 important: the result of cached function must be *immutable*
51 Examples
52 ========
54 >>> from sympy import cacheit
55 >>> @cacheit
56 ... def f(a, b):
57 ... return a+b
59 >>> @cacheit
60 ... def f(a, b): # noqa: F811
61 ... return [a, b] # <-- WRONG, returns mutable object
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)
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
79 wrapper.cache_info = cfunc.cache_info
80 wrapper.cache_clear = cfunc.cache_clear
82 CACHE.append(wrapper)
83 return wrapper
85 return func_wrapper
86########################################
89def __cacheit_nocache(func):
90 return func
93def __cacheit_debug(maxsize):
94 """cacheit + code to check cache consistency"""
95 def func_wrapper(func):
96 cfunc = __cacheit(maxsize)(func)
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)
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)
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
123def _getenv(key, default=None):
124 from os import getenv
125 return getenv(key, default)
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)
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)
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)
168def lazy_function(module : str, name : str) -> Callable:
169 """Create a lazy proxy for a function in a module.
171 The module containing the function is not imported until the function is used.
173 """
174 func = None
176 def _get_function():
177 nonlocal func
178 if func is None:
179 func = getattr(import_module(module), name)
180 return func
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
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)
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
204 def __str__(self):
205 return _get_function().__str__()
207 def __repr__(self):
208 return f"<{__class__.__name__} object at 0x{id(self):x}>: wrapping '{module}.{name}'"
210 return LazyFunction()