Coverage for /usr/lib/python3/dist-packages/sympy/core/decorators.py: 80%

70 statements  

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

1""" 

2SymPy core decorators. 

3 

4The purpose of this module is to expose decorators without any other 

5dependencies, so that they can be easily imported anywhere in sympy/core. 

6""" 

7 

8from functools import wraps 

9from .sympify import SympifyError, sympify 

10 

11 

12def _sympifyit(arg, retval=None): 

13 """ 

14 decorator to smartly _sympify function arguments 

15 

16 Explanation 

17 =========== 

18 

19 @_sympifyit('other', NotImplemented) 

20 def add(self, other): 

21 ... 

22 

23 In add, other can be thought of as already being a SymPy object. 

24 

25 If it is not, the code is likely to catch an exception, then other will 

26 be explicitly _sympified, and the whole code restarted. 

27 

28 if _sympify(arg) fails, NotImplemented will be returned 

29 

30 See also 

31 ======== 

32 

33 __sympifyit 

34 """ 

35 def deco(func): 

36 return __sympifyit(func, arg, retval) 

37 

38 return deco 

39 

40 

41def __sympifyit(func, arg, retval=None): 

42 """Decorator to _sympify `arg` argument for function `func`. 

43 

44 Do not use directly -- use _sympifyit instead. 

45 """ 

46 

47 # we support f(a,b) only 

48 if not func.__code__.co_argcount: 

49 raise LookupError("func not found") 

50 # only b is _sympified 

51 assert func.__code__.co_varnames[1] == arg 

52 if retval is None: 

53 @wraps(func) 

54 def __sympifyit_wrapper(a, b): 

55 return func(a, sympify(b, strict=True)) 

56 

57 else: 

58 @wraps(func) 

59 def __sympifyit_wrapper(a, b): 

60 try: 

61 # If an external class has _op_priority, it knows how to deal 

62 # with SymPy objects. Otherwise, it must be converted. 

63 if not hasattr(b, '_op_priority'): 

64 b = sympify(b, strict=True) 

65 return func(a, b) 

66 except SympifyError: 

67 return retval 

68 

69 return __sympifyit_wrapper 

70 

71 

72def call_highest_priority(method_name): 

73 """A decorator for binary special methods to handle _op_priority. 

74 

75 Explanation 

76 =========== 

77 

78 Binary special methods in Expr and its subclasses use a special attribute 

79 '_op_priority' to determine whose special method will be called to 

80 handle the operation. In general, the object having the highest value of 

81 '_op_priority' will handle the operation. Expr and subclasses that define 

82 custom binary special methods (__mul__, etc.) should decorate those 

83 methods with this decorator to add the priority logic. 

84 

85 The ``method_name`` argument is the name of the method of the other class 

86 that will be called. Use this decorator in the following manner:: 

87 

88 # Call other.__rmul__ if other._op_priority > self._op_priority 

89 @call_highest_priority('__rmul__') 

90 def __mul__(self, other): 

91 ... 

92 

93 # Call other.__mul__ if other._op_priority > self._op_priority 

94 @call_highest_priority('__mul__') 

95 def __rmul__(self, other): 

96 ... 

97 """ 

98 def priority_decorator(func): 

99 @wraps(func) 

100 def binary_op_wrapper(self, other): 

101 if hasattr(other, '_op_priority'): 

102 if other._op_priority > self._op_priority: 

103 f = getattr(other, method_name, None) 

104 if f is not None: 

105 return f(self) 

106 return func(self, other) 

107 return binary_op_wrapper 

108 return priority_decorator 

109 

110 

111def sympify_method_args(cls): 

112 '''Decorator for a class with methods that sympify arguments. 

113 

114 Explanation 

115 =========== 

116 

117 The sympify_method_args decorator is to be used with the sympify_return 

118 decorator for automatic sympification of method arguments. This is 

119 intended for the common idiom of writing a class like : 

120 

121 Examples 

122 ======== 

123 

124 >>> from sympy import Basic, SympifyError, S 

125 >>> from sympy.core.sympify import _sympify 

126 

127 >>> class MyTuple(Basic): 

128 ... def __add__(self, other): 

129 ... try: 

130 ... other = _sympify(other) 

131 ... except SympifyError: 

132 ... return NotImplemented 

133 ... if not isinstance(other, MyTuple): 

134 ... return NotImplemented 

135 ... return MyTuple(*(self.args + other.args)) 

136 

137 >>> MyTuple(S(1), S(2)) + MyTuple(S(3), S(4)) 

138 MyTuple(1, 2, 3, 4) 

139 

140 In the above it is important that we return NotImplemented when other is 

141 not sympifiable and also when the sympified result is not of the expected 

142 type. This allows the MyTuple class to be used cooperatively with other 

143 classes that overload __add__ and want to do something else in combination 

144 with instance of Tuple. 

145 

146 Using this decorator the above can be written as 

147 

148 >>> from sympy.core.decorators import sympify_method_args, sympify_return 

149 

150 >>> @sympify_method_args 

151 ... class MyTuple(Basic): 

152 ... @sympify_return([('other', 'MyTuple')], NotImplemented) 

153 ... def __add__(self, other): 

154 ... return MyTuple(*(self.args + other.args)) 

155 

156 >>> MyTuple(S(1), S(2)) + MyTuple(S(3), S(4)) 

157 MyTuple(1, 2, 3, 4) 

158 

159 The idea here is that the decorators take care of the boiler-plate code 

160 for making this happen in each method that potentially needs to accept 

161 unsympified arguments. Then the body of e.g. the __add__ method can be 

162 written without needing to worry about calling _sympify or checking the 

163 type of the resulting object. 

164 

165 The parameters for sympify_return are a list of tuples of the form 

166 (parameter_name, expected_type) and the value to return (e.g. 

167 NotImplemented). The expected_type parameter can be a type e.g. Tuple or a 

168 string 'Tuple'. Using a string is useful for specifying a Type within its 

169 class body (as in the above example). 

170 

171 Notes: Currently sympify_return only works for methods that take a single 

172 argument (not including self). Specifying an expected_type as a string 

173 only works for the class in which the method is defined. 

174 ''' 

175 # Extract the wrapped methods from each of the wrapper objects created by 

176 # the sympify_return decorator. Doing this here allows us to provide the 

177 # cls argument which is used for forward string referencing. 

178 for attrname, obj in cls.__dict__.items(): 

179 if isinstance(obj, _SympifyWrapper): 

180 setattr(cls, attrname, obj.make_wrapped(cls)) 

181 return cls 

182 

183 

184def sympify_return(*args): 

185 '''Function/method decorator to sympify arguments automatically 

186 

187 See the docstring of sympify_method_args for explanation. 

188 ''' 

189 # Store a wrapper object for the decorated method 

190 def wrapper(func): 

191 return _SympifyWrapper(func, args) 

192 return wrapper 

193 

194 

195class _SympifyWrapper: 

196 '''Internal class used by sympify_return and sympify_method_args''' 

197 

198 def __init__(self, func, args): 

199 self.func = func 

200 self.args = args 

201 

202 def make_wrapped(self, cls): 

203 func = self.func 

204 parameters, retval = self.args 

205 

206 # XXX: Handle more than one parameter? 

207 [(parameter, expectedcls)] = parameters 

208 

209 # Handle forward references to the current class using strings 

210 if expectedcls == cls.__name__: 

211 expectedcls = cls 

212 

213 # Raise RuntimeError since this is a failure at import time and should 

214 # not be recoverable. 

215 nargs = func.__code__.co_argcount 

216 # we support f(a, b) only 

217 if nargs != 2: 

218 raise RuntimeError('sympify_return can only be used with 2 argument functions') 

219 # only b is _sympified 

220 if func.__code__.co_varnames[1] != parameter: 

221 raise RuntimeError('parameter name mismatch "%s" in %s' % 

222 (parameter, func.__name__)) 

223 

224 @wraps(func) 

225 def _func(self, other): 

226 # XXX: The check for _op_priority here should be removed. It is 

227 # needed to stop mutable matrices from being sympified to 

228 # immutable matrices which breaks things in quantum... 

229 if not hasattr(other, '_op_priority'): 

230 try: 

231 other = sympify(other, strict=True) 

232 except SympifyError: 

233 return retval 

234 if not isinstance(other, expectedcls): 

235 return retval 

236 return func(self, other) 

237 

238 return _func