Coverage for src/decopatch/main.py: 91%

67 statements  

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

1from makefun import with_signature, add_signature_parameters 

2from decopatch.utils_modes import SignatureInfo, make_decorator_spec 

3from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \ 

4 DecoratorUsageInfo, can_arg_be_a_decorator_target 

5from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode 

6 

7try: # python 3.3+ 

8 from inspect import signature, Parameter 

9except ImportError: 

10 from funcsigs import signature, Parameter 

11 

12try: # python 3.5+ 

13 from typing import Callable, Any, Optional 

14except ImportError: 

15 pass 

16 

17 

18def function_decorator(enable_stack_introspection=False, # type: bool 

19 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] 

20 flat_mode_decorated_name=None, # type: Optional[str] 

21 ): 

22 """ 

23 A decorator to create function decorators. 

24 Equivalent to 

25 

26 decorator(is_function_decorator=True, is_class_decorator=False) 

27 

28 :param enable_stack_introspection: 

29 :param custom_disambiguator: 

30 :param flat_mode_decorated_name: 

31 :return: 

32 """ 

33 if callable(enable_stack_introspection): 

34 # no-parenthesis call 

35 f = enable_stack_introspection 

36 return decorator(is_function_decorator=True, 

37 is_class_decorator=False)(f) 

38 else: 

39 return decorator(is_function_decorator=True, 

40 is_class_decorator=False, 

41 enable_stack_introspection=enable_stack_introspection, 

42 custom_disambiguator=custom_disambiguator, 

43 flat_mode_decorated_name=flat_mode_decorated_name) 

44 

45 

46def class_decorator(enable_stack_introspection=False, # type: bool 

47 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] 

48 flat_mode_decorated_name=None, # type: Optional[str] 

49 ): 

50 """ 

51 A decorator to create class decorators 

52 Equivalent to 

53 

54 decorator(is_function_decorator=False, is_class_decorator=True) 

55 

56 :param enable_stack_introspection: 

57 :param custom_disambiguator: 

58 :param flat_mode_decorated_name: 

59 :return: 

60 """ 

61 if callable(enable_stack_introspection): 

62 # no-parenthesis call 

63 f = enable_stack_introspection 

64 return decorator(is_function_decorator=False, 

65 is_class_decorator=True)(f) 

66 else: 

67 return decorator(is_function_decorator=False, 

68 is_class_decorator=True, 

69 enable_stack_introspection=enable_stack_introspection, 

70 custom_disambiguator=custom_disambiguator, 

71 flat_mode_decorated_name=flat_mode_decorated_name) 

72 

73 

74def decorator(is_function_decorator=True, # type: bool 

75 is_class_decorator=True, # type: bool 

76 enable_stack_introspection=False, # type: bool 

77 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] 

78 use_signature_trick=True, # type: bool 

79 flat_mode_decorated_name=None, # type: str 

80 ): 

81 """ 

82 A decorator to create decorators. 

83 

84 It support two main modes: "nested", and "flat". 

85 

86 In "flat" mode your implementation is flat: 

87 

88 ```python 

89 def my_decorator(a, b, f=DECORATED): 

90 # ... 

91 return <replacement for f> 

92 ``` 

93 

94 For this mode to be automatically detected, your implementation has to have an argument with default value 

95 `DECORATED`, or a non-None `decorated` argument name should be provided. This argument will be injected with the 

96 decorated target when your decorator is used. 

97 

98 Otherwise the "nested" mode is activated. In this mode your implementation is nested, as usual in python: 

99 

100 ```python 

101 def my_decorator(a, b): 

102 def replace_f(f): 

103 # ... 

104 return <replacement for f> 

105 return replace_f 

106 ``` 

107 

108 In both modes, because python language does not redirect no-parenthesis usages (@my_decorator) to no-args usages 

109 (@my_decorator()), `decopatch` tries to disambiguate automatically the type of call. See documentation for details. 

110 

111 Finally you can use this function to directly create a "signature preserving function wrapper" decorator. This mode 

112 is called "double flat" because it saves you from 2 levels of nesting. For this, use the `WRAPPED` default value 

113 instead of `DECORATED`, and include two arguments with default values `F_ARGS` and `F_KWARGS`: 

114 

115 ```python 

116 @function_decorator 

117 def say_hello(person="world", f=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS): 

118 ''' 

119 This decorator wraps the decorated function so that a nice hello 

120 message is printed before each call. 

121 

122 :param person: the person name in the print message. Default = "world" 

123 ''' 

124 print("hello, %s !" % person) # say hello 

125 return f(*f_args, **f_kwargs) # call f 

126 ``` 

127 

128 :param is_function_decorator: 

129 :param is_class_decorator: 

130 :param enable_stack_introspection: 

131 :param custom_disambiguator: 

132 :param use_signature_trick: if set to `True`, generated decorators will have a generic signature but the `help` 

133 and `signature` modules will still think that they have the specific signature, because by default they 

134 follow the `__wrapped__` attribute if it is set. See 

135 https://docs.python.org/3/library/inspect.html#inspect.signature for details. 

136 :param flat_mode_decorated_name: 

137 :return: 

138 """ 

139 

140 if callable(is_function_decorator): 

141 # called without argument: the first argument is actually the decorated function 

142 f = is_function_decorator 

143 return create_decorator(f) 

144 else: 

145 # called with argument. Return a decorator function 

146 def _apply_on(f): 

147 return create_decorator(f, 

148 is_function_decorator=is_function_decorator, 

149 is_class_decorator=is_class_decorator, 

150 enable_stack_introspection=enable_stack_introspection, 

151 custom_disambiguator=custom_disambiguator, 

152 flat_mode_decorated_name=flat_mode_decorated_name, 

153 use_signature_trick=use_signature_trick) 

154 return _apply_on 

155 

156 

157def create_decorator(impl_function, 

158 is_function_decorator=True, # type: bool 

159 is_class_decorator=True, # type: bool 

160 enable_stack_introspection=False, # type: bool 

161 custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] 

162 use_signature_trick=True, # type: bool 

163 flat_mode_decorated_name=None, # type: Optional[str] 

164 ): 

165 """ 

166 Main function to create a decorator implemented with the `decorator_function` implementation. 

167 

168 :param impl_function: 

169 :param is_function_decorator: 

170 :param is_class_decorator: 

171 :param enable_stack_introspection: 

172 :param custom_disambiguator: 

173 :param use_signature_trick: 

174 :param flat_mode_decorated_name: 

175 :return: 

176 """ 

177 # input checks 

178 if not is_function_decorator and not is_class_decorator: 178 ↛ 179line 178 didn't jump to line 179, because the condition on line 178 was never true

179 raise ValueError("At least one of `is_function_decorator` and `is_class_decorator` must be True") 

180 

181 # (1) --- Detect mode and prepare signature to generate -------- 

182 sig_info, f_for_metadata, nested_impl_function = make_decorator_spec(impl_function, flat_mode_decorated_name) 

183 sig_info.use_signature_trick = use_signature_trick 

184 

185 # (2) --- Generate according to the situation-------- 

186 # check if the resulting function has any parameter at all 

187 if len(sig_info.exposed_signature.parameters) == 0: 

188 # (A) no argument at all. Special handling. 

189 return create_no_args_decorator(nested_impl_function, function_for_metadata=f_for_metadata) 

190 

191 else: 

192 # (B) general case: at least one argument 

193 

194 # if the decorator has at least 1 mandatory argument, we allow it to be created but its default behaviour 

195 # is to raise errors only on ambiguous cases. Usually ambiguous cases are rare (not nominal cases) 

196 disambiguator = create_single_arg_callable_or_class_disambiguator(nested_impl_function, 

197 is_function_decorator, 

198 is_class_decorator, 

199 custom_disambiguator, 

200 enable_stack_introspection, 

201 signature_knowledge=sig_info) 

202 

203 if sig_info.is_first_arg_keyword_only: 

204 # in this case the decorator *can* be used without arguments but *cannot* with one positional argument, 

205 # which will happen in the no-parenthesis case. We have to modify the signature to allow no-parenthesis 

206 return create_kwonly_decorator(sig_info, nested_impl_function, disambiguator, 

207 function_for_metadata=f_for_metadata) 

208 

209 # general case 

210 return create_general_case_decorator(sig_info, nested_impl_function, disambiguator, 

211 function_for_metadata=f_for_metadata) 

212 

213 

214def create_no_args_decorator(decorator_function, 

215 function_for_metadata=None, 

216 ): 

217 """ 

218 Utility method to create a decorator that has no arguments at all and is implemented by `decorator_function`, in 

219 implementation-first mode or usage-first mode. 

220 

221 The created decorator is a function with var-args. When called it checks the length 

222 (0=called with parenthesis, 1=called without, 2=error). 

223 

224 Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for 

225 the decorator's help. 

226 

227 :param decorator_function: 

228 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the 

229 generated function 

230 :return: 

231 """ 

232 if function_for_metadata is None: 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true

233 function_for_metadata = decorator_function 

234 

235 @with_signature(None, 

236 func_name=function_for_metadata.__name__, 

237 doc=function_for_metadata.__doc__, 

238 module_name=function_for_metadata.__module__) 

239 def new_decorator(*_): 

240 """ 

241 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis 

242 """ 

243 if len(_) == 0: 

244 # called with no args BUT parenthesis: @foo_decorator(). 

245 return with_parenthesis_usage(decorator_function, *_) 

246 

247 elif len(_) == 1: 

248 first_arg_value = _[0] 

249 if can_arg_be_a_decorator_target(first_arg_value): 

250 # called with no arg NOR parenthesis: @foo_decorator 

251 return no_parenthesis_usage(decorator_function, first_arg_value) 

252 

253 # more than 1 argument or non-decorable argument: not possible 

254 raise TypeError("Decorator function '%s' does not accept any argument." 

255 "" % decorator_function.__name__) 

256 

257 return new_decorator 

258 

259 

260_GENERATED_VARPOS_NAME = '_' 

261 

262 

263def create_kwonly_decorator(sig_info, # type: SignatureInfo 

264 decorator_function, 

265 disambiguator, 

266 function_for_metadata, 

267 ): 

268 """ 

269 Utility method to create a decorator that has only keyword arguments and is implemented by `decorator_function`, in 

270 implementation-first mode or usage-first mode. 

271 

272 When the decorator to create has a mandatory argument, it is exposed "as-is" because it is directly protected. 

273 

274 Otherwise (if all arguments are optional and keyword-only), we modify the created decorator's signature to add a 

275 leading var-args, so that users will be able to call the decorator without parenthesis. 

276 When called it checks the length of the var-positional received: 

277 - 0 positional=called with parenthesis, 

278 - 1 and the positional argument is not a callable/class : called with parenthesis 

279 - 1 and the positional argument is a callable/class: disambiguation is required to know if this is without 

280 parenthesis or with positional arg 

281 - 2 positional=error). 

282 

283 Note: we prefer to use this var-arg signature rather than a "(_=None)" signature, because it is more readable for 

284 the decorator's help. 

285 

286 :param sig_info: 

287 :param decorator_function: 

288 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the 

289 generated function 

290 :return: 

291 """ 

292 if sig_info.is_first_arg_mandatory: 

293 # The first argument is mandatory AND keyword. So we do not need to change the signature to be fully protected 

294 # indeed python will automatically raise a `TypeError` when users will use this decorator without parenthesis 

295 # or with positional arguments. 

296 @with_signature(sig_info.exposed_signature, 

297 func_name=function_for_metadata.__name__, 

298 doc=function_for_metadata.__doc__, 

299 modulename=function_for_metadata.__module__) 

300 def new_decorator(*no_args, **kwargs): 

301 """ 

302 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis 

303 """ 

304 # this is a parenthesis call, because otherwise a `TypeError` would already have been raised by python. 

305 return with_parenthesis_usage(decorator_function, *no_args, **kwargs) 

306 

307 return new_decorator 

308 elif sig_info.use_signature_trick: 308 ↛ 313line 308 didn't jump to line 313, because the condition on line 308 was never false

309 # no need to modify the signature, we will expose *args, **kwargs 

310 pass 

311 else: 

312 # modify the signature to add a var-positional first 

313 gen_varpos_param = Parameter(_GENERATED_VARPOS_NAME, kind=Parameter.VAR_POSITIONAL) 

314 sig_info.exposed_signature = add_signature_parameters(sig_info.exposed_signature, first=[gen_varpos_param]) 

315 

316 # we can fallback to the same case than varpositional 

317 return create_general_case_decorator(sig_info, decorator_function, disambiguator, 

318 function_for_metadata=function_for_metadata) 

319 

320 

321def create_general_case_decorator(sig_info, # type: SignatureInfo 

322 impl_function, 

323 disambiguator, 

324 function_for_metadata, 

325 ): 

326 """ 

327 This method supports both with-trick and without-trick 

328 

329 :param sig_info: 

330 :param impl_function: 

331 :param disambiguator: 

332 :param function_for_metadata: an alternate function to use for the documentation and module metadata of the 

333 generated function 

334 :return: 

335 """ 

336 # Note: since we expose a decorator with a preserved signature and not (*args, **kwargs) 

337 # we lose the information about the number of arguments *actually* provided. 

338 # `@with_signature` will send us all arguments, including the defaults (because it has no way to 

339 # determine what was actually provided by the user and what is just the default). So in this decorator we may 

340 # receive several kwargs 

341 # - even if user did not provide them 

342 # - and even if user provided them as positional !! (except for var-positional and fuutre positional-only args) 

343 

344 @with_signature(None if sig_info.use_signature_trick else sig_info.exposed_signature, 

345 func_name=function_for_metadata.__name__, 

346 doc=function_for_metadata.__doc__, 

347 module_name=function_for_metadata.__module__) 

348 def new_decorator(*args, **kwargs): 

349 """ 

350 Code for your decorator, generated by decopatch to handle the case when it is called without parenthesis 

351 """ 

352 # disambiguate 

353 dk = DecoratorUsageInfo(sig_info, args, kwargs) 

354 disambiguation_result = disambiguate_call(dk, disambiguator) 

355 

356 # call 

357 return call_in_appropriate_mode(impl_function, dk, disambiguation_result) 

358 

359 # trick to declare that our true signature is different than our actual one 

360 if sig_info.use_signature_trick: 360 ↛ 367line 360 didn't jump to line 367, because the condition on line 360 was never false

361 # thanks to setting this field, python help() and signature() will be tricked without compromising the 

362 # actual code signature (so, no dynamic function creation in @with_signature above). 

363 # Indeed by default they follow the `__wrapped__` attribute if it is set. See 

364 # https://docs.python.org/3/library/inspect.html#inspect.signature for details. 

365 new_decorator.__wrapped__ = impl_function 

366 

367 return new_decorator