Coverage for src/decopatch/utils_modes.py: 87%

148 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-06 15:13 +0000

1from makefun import remove_signature_parameters, with_signature, wraps 

2 

3try: # python 3.3+ 

4 from inspect import signature, Parameter 

5 funcsigs_used = False 

6except ImportError: 

7 from funcsigs import signature, Parameter 

8 funcsigs_used = True 

9 

10 

11class _Symbol: 

12 """ 

13 Symbols used in your (double) flat-mode signatures to declare where the various objects should be injected 

14 These symbols have a nice representation. 

15 """ 

16 __slots__ = ('repr_', ) 

17 

18 def __init__(self, repr_): 

19 self.repr_ = repr_ 

20 

21 def __repr__(self): 

22 return self.repr_ 

23 

24 

25DECORATED = _Symbol('DECORATED') 

26# A symbol used in flat-mode signatures to declare where the decorated function should be injected 

27 

28 

29WRAPPED = _Symbol('WRAPPED') 

30# A symbol used in double flat-mode signatures to declare where the wrapped function should be injected 

31 

32 

33F_ARGS = _Symbol('F_ARGS') 

34# A symbol used in your double flat-mode signatures to declare where the wrapper args should be injected 

35 

36 

37F_KWARGS = _Symbol('F_KWARGS') 

38# A symbol used in your double flat-mode signatures to declare where the wrapper kwargs should be injected 

39 

40 

41def make_decorator_spec(impl_function, 

42 flat_mode_decorated_name=None # type: str 

43 ): 

44 """ 

45 Analyzes the implementation function 

46 

47 

48 If `flat_mode_decorated_name` is set, this is a shortcut for flat mode. In that case the implementation function 

49 is not analyzed. 

50 

51 :param impl_function: 

52 :param flat_mode_decorated_name: 

53 :return: sig_info, function_for_metadata, nested_impl_function 

54 """ 

55 # extract the implementation's signature 

56 implementors_signature = signature(impl_function) 

57 

58 # determine the mode (nested, flat, double-flat) and check signature 

59 mode, injected_name, contains_varpositional, injected_pos, \ 

60 injected_arg, f_args_name, f_kwargs_name = extract_mode_info(implementors_signature, flat_mode_decorated_name) 

61 

62 # create the signature of the decorator function to create, according to mode 

63 if mode is None: 

64 # *nested: keep the signature 'as is' 

65 exposed_signature = implementors_signature 

66 function_for_metadata = impl_function 

67 nested_impl_function = impl_function 

68 

69 elif mode is DECORATED: # flat mode 

70 # use the same signature, but remove the injected arg. 

71 exposed_signature = remove_signature_parameters(implementors_signature, injected_name) 

72 

73 # use the original function for the docstring/module metadata 

74 function_for_metadata = impl_function 

75 

76 # generate the corresponding nested decorator 

77 nested_impl_function = make_nested_impl_for_flat_mode(exposed_signature, impl_function, injected_name, 

78 injected_pos) 

79 

80 elif mode is WRAPPED: 80 ↛ 94line 80 didn't jump to line 94, because the condition on line 80 was never false

81 # *double-flat: the same signature, but we remove the injected args. 

82 args_to_remove = (injected_name,) + ((f_args_name,) if f_args_name is not None else ()) \ 

83 + ((f_kwargs_name,) if f_kwargs_name is not None else ()) 

84 exposed_signature = remove_signature_parameters(implementors_signature, *args_to_remove) 

85 

86 # use the original function for the docstring/module metadata 

87 function_for_metadata = impl_function 

88 

89 # generate the corresponding nested decorator 

90 nested_impl_function = make_nested_impl_for_doubleflat_mode(exposed_signature, impl_function, injected_name, 

91 f_args_name, f_kwargs_name, injected_pos) 

92 

93 else: 

94 raise ValueError("Unknown mode: %s" % mode) 

95 

96 # create an object to easily access the exposed signature information afterwards 

97 sig_info = SignatureInfo(exposed_signature, contains_varpositional, injected_pos) 

98 

99 return sig_info, function_for_metadata, nested_impl_function 

100 

101 

102def make_nested_impl_for_flat_mode(decorator_signature, user_provided_applier, injected_name, injected_pos): 

103 """ 

104 Creates the nested-mode decorator to be used when the implementation is provided in flat mode. 

105 

106 Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of 

107 exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator 

108 usage and does not impact the decorated object we can afford. 

109 

110 :param decorator_signature: 

111 :param user_provided_applier: 

112 :param injected_name: 

113 :param argnames_before_varpos_arg: 

114 :return: 

115 """ 

116 

117 @with_signature(decorator_signature) 

118 def _decorator(*args, **kwargs): 

119 """ The decorator. Its signature will be overriden by `generated_signature` """ 

120 

121 def _apply_decorator(decorated): 

122 """ This is called when the decorator is applied to an object `decorated` """ 

123 

124 # inject `decorated` under the correct name 

125 # fix in case of var-positional arguments 

126 if injected_pos >= 0: 

127 new_args = args[:injected_pos] + (decorated, ) + args[injected_pos:] 

128 else: 

129 new_args = args 

130 kwargs[injected_name] = decorated 

131 

132 return user_provided_applier(*new_args, **kwargs) 

133 

134 return _apply_decorator 

135 

136 return _decorator 

137 

138 

139def make_nested_impl_for_doubleflat_mode(decorator_signature, user_provided_wrapper, injected_name, 

140 f_args_name, f_kwargs_name, injected_pos): 

141 """ 

142 Creates the nested-mode decorator to be used when the implementation is provided in double-flat mode. 

143 

144 Note: we set the signature correctly so that this behaves exactly like a nested implementation in terms of 

145 exceptions raised when the arguments are incorrect. Since the external method is called only once per decorator 

146 usage and does not impact the decorated object / created wrappe, we can afford. 

147 

148 :param decorator_signature: 

149 :param user_provided_wrapper: 

150 :param injected_name: 

151 :param f_args_name: 

152 :param f_kwargs_name: 

153 :return: 

154 """ 

155 

156 @with_signature(decorator_signature) 

157 def _decorator(*args, **kwargs): 

158 """ The decorator. Its signature will be overriden by `generated_signature` """ 

159 

160 def _apply_decorator(decorated): 

161 """ This is called when the decorator is applied to an object `decorated` """ 

162 

163 # inject `decorated` under the correct name 

164 # fix in case of var-positional arguments 

165 if injected_pos >= 0: 

166 new_args = args[:injected_pos] + (decorated,) + args[injected_pos:] 

167 else: 

168 new_args = args 

169 kwargs[injected_name] = decorated 

170 

171 # create a signature-preserving wrapper using `makefun.wraps` 

172 @wraps(decorated) 

173 def wrapper(*f_args, **f_kwargs): 

174 # if the user wishes us to inject the actual args and kwargs, let's inject them 

175 # note: for these it is always keyword-based. 

176 if f_args_name is not None: 176 ↛ 178line 176 didn't jump to line 178, because the condition on line 176 was never false

177 kwargs[f_args_name] = f_args 

178 if f_kwargs_name is not None: 178 ↛ 182line 178 didn't jump to line 182, because the condition on line 178 was never false

179 kwargs[f_kwargs_name] = f_kwargs 

180 

181 # finally call the user-provided implementation 

182 return user_provided_wrapper(*new_args, **kwargs) 

183 

184 return wrapper 

185 

186 return _apply_decorator 

187 

188 return _decorator 

189 

190 

191class InvalidSignatureError(Exception): 

192 """ 

193 Exception raised when a decorator signature is invalid with respect to the selected mode. 

194 Typically when you use flat-mode or wrapped-mode symbols but your signature does not allow them to be safely 

195 injected as keyword because they are followed by a var-positional argument. 

196 """ 

197 pass 

198 

199 

200def extract_mode_info(impl_sig, # type: Signature 

201 flat_mode_decorated_name=None # type: str 

202 ): 

203 """ 

204 Returns the (name, Parameter) for the parameter with default value DECORATED 

205 

206 :param impl_sig: the implementing function's signature 

207 :param flat_mode_decorated_name: an optional name of decorated argument. If provided a "flat mode" is automatically 

208 set 

209 :return: 

210 """ 

211 mode = None 

212 injected = None 

213 injected_pos = None 

214 position_of_varpos = -1 

215 f_args = None 

216 f_kwargs = None 

217 

218 if flat_mode_decorated_name is not None: 

219 # validate that the 'decorated' parameter is a string representing a real parameter of the function 

220 if not isinstance(flat_mode_decorated_name, str): 220 ↛ 221line 220 didn't jump to line 221, because the condition on line 220 was never true

221 raise InvalidSignatureError("'flat_mode_decorated_name' argument should be a string with the argument name " 

222 "where the wrapped object should be injected") 

223 

224 mode = DECORATED 

225 

226 # analyze signature to detect injected arg and potentially varpositional 

227 for i, (k, p) in enumerate(impl_sig.parameters.items()): 

228 if k == flat_mode_decorated_name: 

229 # this is the injected parameter 

230 injected = p 

231 injected_pos = i 

232 elif p.kind is Parameter.VAR_POSITIONAL: 

233 position_of_varpos = i 

234 

235 if injected is None: 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true

236 return ValueError("Function '%s' does not have an argument named '%s'" % (impl_sig.__name__, 

237 flat_mode_decorated_name)) 

238 if injected.kind in {Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD}: 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true

239 raise InvalidSignatureError("`flat_mode_decorated_name` cannot correspond to a Var-pos nor Var-kw") 

240 else: 

241 # analyze signature to detect 

242 for i, (p_name, p) in enumerate(impl_sig.parameters.items()): 

243 if p.kind is Parameter.VAR_POSITIONAL: 

244 position_of_varpos = i 

245 if f_args is not None or f_kwargs is not None: 

246 raise InvalidSignatureError("f_args and f_kwargs can only be used *after* var-positional arguments") 

247 elif p.default is DECORATED: 

248 if mode is not None: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature") 

250 else: 

251 mode = DECORATED 

252 injected = p 

253 injected_pos = i 

254 elif p.default is WRAPPED: 

255 if mode is not None: 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true

256 raise InvalidSignatureError("only one of `DECORATED` or `WRAPPED` can be used in your signature") 

257 else: 

258 mode = WRAPPED 

259 injected = p 

260 injected_pos = i 

261 elif p.default is F_ARGS: 

262 f_args = p 

263 elif p.default is F_KWARGS: 

264 f_kwargs = p 

265 

266 if mode in {None, DECORATED} and (f_args is not None or f_kwargs is not None): 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true

267 raise InvalidSignatureError("`F_ARGS` or `F_KWARGS` should only be used if you use `WRAPPED`") 

268 

269 # argnames_before_varpos_arg = None 

270 # if position_of_varpos > 0: 

271 # # if there is a var-positional we will have to inject arguments before it manually 

272 # argnames_before_varpos_arg = tuple(k for k in list(impl_sig.parameters.keys())[0:position_of_varpos]) 

273 # 

274 # if argnames_before_varpos_arg is None: 

275 # argnames_before_varpos_arg = tuple() 

276 

277 contains_varpositional = position_of_varpos >= 0 

278 

279 if not contains_varpositional or (injected_pos is not None and position_of_varpos < injected_pos): 

280 # do not inject as positional but as keyword argument 

281 injected_pos = -1 

282 

283 return mode, (injected.name if injected is not None else None), contains_varpositional, injected_pos, \ 

284 injected, (f_args.name if f_args is not None else None), (f_kwargs.name if f_kwargs is not None else None) 

285 

286 

287# ----------- 

288 

289 

290class SignatureInfo(object): 

291 """ 

292 Represents the knowledge we have on the decorator signature. 

293 Provides handy properties to separate the code requirements from the implementation (and possibly cache). 

294 """ 

295 __slots__ = '_exposed_signature', 'first_arg_def', '_use_signature_trick', 'contains_varpositional', \ 

296 'injected_pos' 

297 

298 def __init__(self, decorator_signature, contains_varpositional, injected_pos): 

299 self._exposed_signature = decorator_signature 

300 _, self.first_arg_def = get_first_parameter(decorator_signature) 

301 self._use_signature_trick = False 

302 self.contains_varpositional = contains_varpositional 

303 self.injected_pos = injected_pos 

304 

305 # -- 

306 

307 @property 

308 def use_signature_trick(self): 

309 return self._use_signature_trick 

310 

311 @use_signature_trick.setter 

312 def use_signature_trick(self, use_signature_trick): 

313 # note: as of today python 2.7 backport does not handle it properly, but hopefully it will :) 

314 # see https://github.com/testing-cabal/funcsigs/issues/33. 

315 self._use_signature_trick = use_signature_trick and not funcsigs_used 

316 

317 # -- 

318 

319 @property 

320 def exposed_signature(self): 

321 return self._exposed_signature 

322 

323 @exposed_signature.setter 

324 def exposed_signature(self, new_sig): 

325 """ 

326 If the signature is changed then we should be careful.. 

327 :param new_sig: 

328 :return: 

329 """ 

330 # this currently only happen in a single specific case, control that to avoid future mistakes 

331 if self.first_arg_kind is not Parameter.VAR_KEYWORD or len(self._exposed_signature.parameters) != 1: 

332 raise NotImplementedError("This case should not happen") 

333 

334 self._exposed_signature = new_sig 

335 self.contains_varpositional = any(p.kind is Parameter.VAR_POSITIONAL for p in new_sig.parameters.values()) 

336 _, self.first_arg_def = get_first_parameter(new_sig) 

337 

338 # -- 

339 

340 @property 

341 def first_arg_name(self): 

342 return self.first_arg_def.name # if self.first_arg_def is not None else None 

343 

344 @property 

345 def first_arg_name_with_possible_star(self): 

346 return ('*' if self.is_first_arg_varpositional else '') + self.first_arg_name 

347 

348 @property 

349 def first_arg_kind(self): 

350 return self.first_arg_def.kind # if self.first_arg_def is not None else None 

351 

352 @property 

353 def is_first_arg_keyword_only(self): 

354 return self.first_arg_kind in {Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD} 

355 

356 @property 

357 def is_first_arg_varpositional(self): 

358 return self.first_arg_kind is Parameter.VAR_POSITIONAL 

359 

360 @property 

361 def is_first_arg_positional_only(self): 

362 return self.first_arg_kind is Parameter.POSITIONAL_ONLY 

363 

364 @property 

365 def is_first_arg_mandatory(self): 

366 return self.first_arg_def.default is Parameter.empty and self.first_arg_kind not in {Parameter.VAR_POSITIONAL, 

367 Parameter.VAR_KEYWORD} 

368 

369 

370def get_first_parameter(ds # type: Signature 

371 ): 

372 """ 

373 Returns the (name, Parameter) for the first parameter in the signature 

374 

375 :param ds: 

376 :return: 

377 """ 

378 try: 

379 return next(iter(ds.parameters.items())) 

380 except StopIteration: 

381 return None, None