Coverage for chebpy/core/decorators.py: 99%

73 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-07 10:30 +0000

1"""Decorator functions for the ChebPy package. 

2 

3This module provides various decorators used throughout the ChebPy package to 

4implement common functionality such as caching, handling empty objects, 

5pre- and post-processing of function inputs/outputs, and type conversion. 

6These decorators help reduce code duplication and ensure consistent behavior 

7across the package. 

8""" 

9 

10from functools import wraps 

11 

12import numpy as np 

13 

14 

15def cache(f: callable) -> callable: 

16 """Object method output caching mechanism. 

17 

18 This decorator caches the output of zero-argument methods to speed up repeated 

19 execution of relatively expensive operations such as .roots(). Cached computations 

20 are stored in a dictionary called _cache which is bound to self using keys 

21 corresponding to the method name. 

22 

23 Args: 

24 f (callable): The method to be cached. Must be a zero-argument method. 

25 

26 Returns: 

27 callable: A wrapped version of the method that implements caching. 

28 

29 Note: 

30 Can be used in principle on arbitrary objects. 

31 """ 

32 

33 # TODO: look into replacing this with one of the functools cache decorators 

34 @wraps(f) 

35 def wrapper(self): 

36 try: 

37 # f has been executed previously 

38 out = self._cache[f.__name__] 

39 except AttributeError: 

40 # f has not been executed previously and self._cache does not exist 

41 self._cache = {} 

42 out = self._cache[f.__name__] = f(self) 

43 except KeyError: # pragma: no cover 

44 # f has not been executed previously, but self._cache exists 

45 out = self._cache[f.__name__] = f(self) 

46 return out 

47 

48 return wrapper 

49 

50 

51def self_empty(resultif=None) -> callable: 

52 """Factory method to produce a decorator for handling empty objects. 

53 

54 This factory creates a decorator that checks whether the object whose method 

55 is being wrapped is empty. If the object is empty, it returns either the supplied 

56 resultif value or a copy of the object. Otherwise, it executes the wrapped method. 

57 

58 Args: 

59 resultif: Value to return when the object is empty. If None, returns a copy 

60 of the object instead. 

61 

62 Returns: 

63 callable: A decorator function that implements the empty-checking logic. 

64 

65 Note: 

66 This decorator is primarily used in chebtech.py. 

67 """ 

68 

69 # TODO: add unit test for this 

70 def decorator(f): 

71 @wraps(f) 

72 def wrapper(self, *args, **kwargs): 

73 if self.isempty: 

74 if resultif is not None: 

75 return resultif 

76 else: 

77 return self.copy() 

78 else: 

79 return f(self, *args, **kwargs) 

80 

81 return wrapper 

82 

83 return decorator 

84 

85 

86def preandpostprocess(f: callable) -> callable: 

87 """Decorator for pre- and post-processing tasks common to bary and clenshaw. 

88 

89 This decorator handles several edge cases for functions like bary and clenshaw: 

90 - Empty arrays in input arguments 

91 - Constant functions 

92 - NaN values in coefficients 

93 - Scalar vs. array inputs 

94 

95 Args: 

96 f (callable): The function to be wrapped. 

97 

98 Returns: 

99 callable: A wrapped version of the function with pre- and post-processing. 

100 """ 

101 

102 @wraps(f) 

103 def thewrapper(*args, **kwargs): 

104 xx, akfk = args[:2] 

105 # are any of the first two arguments empty arrays? 

106 if (np.asarray(xx).size == 0) | (np.asarray(akfk).size == 0): 

107 return np.array([]) 

108 # is the function constant? 

109 elif akfk.size == 1: 

110 if np.isscalar(xx): 

111 return akfk[0] 

112 else: 

113 return akfk * np.ones(xx.size) 

114 # are there any NaNs in the second argument? 

115 elif np.any(np.isnan(akfk)): 

116 return np.nan * np.ones(xx.size) 

117 # convert first argument to an array if it is a scalar and then 

118 # return the first (and only) element of the result if so 

119 else: 

120 args = list(args) 

121 args[0] = np.array([xx]) if np.isscalar(xx) else args[0] 

122 out = f(*args, **kwargs) 

123 return out[0] if np.isscalar(xx) else out 

124 

125 return thewrapper 

126 

127 

128def float_argument(f: callable) -> callable: 

129 """Decorator to ensure consistent input/output types for Chebfun __call__ method. 

130 

131 This decorator ensures that when a Chebfun object is called with a float input, 

132 it returns a float output, and when called with an array input, it returns an 

133 array output. It handles various input formats including scalars, numpy arrays, 

134 and nested arrays. 

135 

136 Args: 

137 f (callable): The __call__ method to be wrapped. 

138 

139 Returns: 

140 callable: A wrapped version of the method that ensures type consistency. 

141 """ 

142 

143 @wraps(f) 

144 def thewrapper(self, *args, **kwargs): 

145 x = args[0] 

146 xx = np.array([x]) if np.isscalar(x) else np.array(x) 

147 # discern between the array(0.1) and array([0.1]) cases 

148 if xx.size == 1: 

149 if xx.ndim == 0: 

150 xx = np.array([xx]) 

151 args = list(args) 

152 args[0] = xx 

153 out = f(self, *args, **kwargs) 

154 return out[0] if np.isscalar(x) else out 

155 

156 return thewrapper 

157 

158 

159def cast_arg_to_chebfun(f: callable) -> callable: 

160 """Decorator to cast the first argument to a chebfun object if needed. 

161 

162 This decorator attempts to convert the first argument to a chebfun object 

163 if it is not already one. Currently, only numeric types can be cast to chebfun. 

164 

165 Args: 

166 f (callable): The method to be wrapped. 

167 

168 Returns: 

169 callable: A wrapped version of the method that ensures the first argument 

170 is a chebfun object. 

171 """ 

172 

173 @wraps(f) 

174 def wrapper(self, *args, **kwargs): 

175 other = args[0] 

176 if not isinstance(other, self.__class__): 

177 fun = self.initconst(args[0], self.support) 

178 args = list(args) 

179 args[0] = fun 

180 return f(self, *args, **kwargs) 

181 

182 return wrapper 

183 

184 

185def cast_other(f: callable) -> callable: 

186 """Decorator to cast the first argument to the same type as self. 

187 

188 This generic decorator is applied to binary operator methods to ensure that 

189 the first positional argument (typically 'other') is cast to the same type 

190 as the object on which the method is called. 

191 

192 Args: 

193 f (callable): The binary operator method to be wrapped. 

194 

195 Returns: 

196 callable: A wrapped version of the method that ensures type consistency 

197 between self and the first argument. 

198 """ 

199 

200 @wraps(f) 

201 def wrapper(self, *args, **kwargs): 

202 cls = self.__class__ 

203 other = args[0] 

204 if not isinstance(other, cls): 

205 args = list(args) 

206 args[0] = cls(other) 

207 return f(self, *args, **kwargs) 

208 

209 return wrapper