Coverage for src/makefun/main.py: 83%

554 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-03-12 17:34 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-makefun> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-makefun/blob/master/LICENSE> 

5from __future__ import print_function 

6 

7import functools 

8import re 

9import sys 

10import itertools 

11from collections import OrderedDict 

12from copy import copy 

13from inspect import getsource 

14from keyword import iskeyword 

15from textwrap import dedent 

16from types import FunctionType 

17 

18 

19if sys.version_info >= (3, 0): 19 ↛ 22line 19 didn't jump to line 22, because the condition on line 19 was never false

20 is_identifier = str.isidentifier 

21else: 

22 def is_identifier(string): 

23 """ 

24 Replacement for `str.isidentifier` when it is not available (e.g. on Python 2). 

25 :param string: 

26 :return: 

27 """ 

28 if len(string) == 0 or string[0].isdigit(): 

29 return False 

30 return all([(len(s) == 0) or s.isalnum() for s in string.split("_")]) 

31 

32try: # python 3.3+ 

33 from inspect import signature, Signature, Parameter 

34except ImportError: 

35 from funcsigs import signature, Signature, Parameter 

36 

37try: 

38 from inspect import iscoroutinefunction 

39except ImportError: 

40 # let's assume there are no coroutine functions in old Python 

41 def iscoroutinefunction(f): 

42 return False 

43 

44try: 

45 from inspect import isgeneratorfunction 

46except ImportError: 

47 # assume no generator function in old Python versions 

48 def isgeneratorfunction(f): 

49 return False 

50 

51try: 

52 from inspect import isasyncgenfunction 

53except ImportError: 

54 # assume no generator function in old Python versions 

55 def isasyncgenfunction(f): 

56 return False 

57 

58try: # python 3.5+ 

59 from typing import Callable, Any, Union, Iterable, Dict, Tuple, Mapping 

60except ImportError: 

61 pass 

62 

63 

64PY2 = sys.version_info < (3,) 

65if not PY2: 65 ↛ 68line 65 didn't jump to line 68, because the condition on line 65 was never false

66 string_types = str, 

67else: 

68 string_types = basestring, # noqa 

69 

70 

71# macroscopic signature strings checker (we do not look inside params, `signature` will do it for us) 

72FUNC_DEF = re.compile( 

73 '(?s)^\\s*(?P<funcname>[_\\w][_\\w\\d]*)?\\s*' 

74 '\\(\\s*(?P<params>.*?)\\s*\\)\\s*' 

75 '(((?P<typed_return_hint>->\\s*[^:]+)?(?P<colon>:)?\\s*)|:\\s*#\\s*(?P<comment_return_hint>.+))*$' 

76) 

77 

78 

79def create_wrapper(wrapped, 

80 wrapper, 

81 new_sig=None, # type: Union[str, Signature] 

82 prepend_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

83 append_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

84 remove_args=None, # type: Union[str, Iterable[str]] 

85 func_name=None, # type: str 

86 inject_as_first_arg=False, # type: bool 

87 add_source=True, # type: bool 

88 add_impl=True, # type: bool 

89 doc=None, # type: str 

90 qualname=None, # type: str 

91 co_name=None, # type: str 

92 module_name=None, # type: str 

93 **attrs 

94 ): 

95 """ 

96 Creates a signature-preserving wrapper function. 

97 `create_wrapper(wrapped, wrapper, **kwargs)` is equivalent to `wraps(wrapped, **kwargs)(wrapper)`. 

98 

99 See `@makefun.wraps` 

100 """ 

101 return wraps(wrapped, new_sig=new_sig, prepend_args=prepend_args, append_args=append_args, remove_args=remove_args, 

102 func_name=func_name, inject_as_first_arg=inject_as_first_arg, add_source=add_source, 

103 add_impl=add_impl, doc=doc, qualname=qualname, module_name=module_name, co_name=co_name, 

104 **attrs)(wrapper) 

105 

106 

107def getattr_partial_aware(obj, att_name, *att_default): 

108 """ Same as getattr but recurses in obj.func if obj is a partial """ 

109 

110 val = getattr(obj, att_name, *att_default) 

111 if isinstance(obj, functools.partial) and \ 

112 (val is None or att_name == '__dict__' and len(val) == 0): 

113 return getattr_partial_aware(obj.func, att_name, *att_default) 

114 else: 

115 return val 

116 

117 

118def create_function(func_signature, # type: Union[str, Signature] 

119 func_impl, # type: Callable[[Any], Any] 

120 func_name=None, # type: str 

121 inject_as_first_arg=False, # type: bool 

122 add_source=True, # type: bool 

123 add_impl=True, # type: bool 

124 doc=None, # type: str 

125 qualname=None, # type: str 

126 co_name=None, # type: str 

127 module_name=None, # type: str 

128 **attrs): 

129 """ 

130 Creates a function with signature `func_signature` that will call `func_impl` when called. All arguments received 

131 by the generated function will be propagated as keyword-arguments to `func_impl` when it is possible (so all the 

132 time, except for var-positional or positional-only arguments that get passed as *args. Note that positional-only 

133 does not yet exist in python but this case is already covered because it is supported by `Signature` objects). 

134 

135 `func_signature` can be provided in different formats: 

136 

137 - as a string containing the name and signature without 'def' keyword, such as `'foo(a, b: int, *args, **kwargs)'`. 

138 In which case the name in the string will be used for the `__name__` and `__qualname__` of the created function 

139 by default 

140 - as a `Signature` object, for example created using `signature(f)` or handcrafted. Since a `Signature` object 

141 does not contain any name, in this case the `__name__` and `__qualname__` of the created function will be copied 

142 from `func_impl` by default. 

143 

144 All the other metadata of the created function are defined as follows: 

145 

146 - default `__name__` attribute (see above) can be overridden by providing a non-None `func_name` 

147 - default `__qualname__` attribute (see above) can be overridden by providing a non-None `qualname` 

148 - `__annotations__` attribute is created to match the annotations in the signature. 

149 - `__doc__` attribute is copied from `func_impl.__doc__` except if overridden using `doc` 

150 - `__module__` attribute is copied from `func_impl.__module__` except if overridden using `module_name` 

151 - `__code__.co_name` (see above) defaults to the same value as the above `__name__` attribute, except when that 

152 value is not a valid Python identifier, in which case it will be `<lambda>`. It can be overridden by providing 

153 a `co_name` that is either a valid Python identifier or `<lambda>`. 

154 

155 Finally two new attributes are optionally created 

156 

157 - `__source__` attribute: set if `add_source` is `True` (default), this attribute contains the source code of the 

158 generated function 

159 - `__func_impl__` attribute: set if `add_impl` is `True` (default), this attribute contains a pointer to 

160 `func_impl` 

161 

162 A lambda function will be created in the following cases: 

163 

164 - when `func_signature` is a `Signature` object and `func_impl` is itself a lambda function, 

165 - when the function name, either derived from a `func_signature` string, or given explicitly with `func_name`, 

166 is not a valid Python identifier, or 

167 - when the provided `co_name` is `<lambda>`. 

168 

169 :param func_signature: either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" or "(a, b: int)", 

170 or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature` 

171 backport. Note that these objects can be created manually too. If the signature is provided as a string and 

172 contains a non-empty name, this name will be used instead of the one of the decorated function. 

173 :param func_impl: the function that will be called when the generated function is executed. Its signature should 

174 be compliant with (=more generic than) `func_signature` 

175 :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of 

176 `func_impl`. This can be handy in case the implementation is shared between several facades and needs 

177 to know from which context it was called. Default=`False` 

178 :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this 

179 is `None` (default), the `__name__` will default to the one of `func_impl` if `func_signature` is a `Signature`, 

180 or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

181 :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function 

182 (default: True) 

183 :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function 

184 (default: True) 

185 :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated 

186 function. If None (default), the doc of func_impl will be used. 

187 :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will 

188 default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in 

189 `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

190 :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), 

191 the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the 

192 name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

193 :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), 

194 `func_impl.__module__` will be used. 

195 :param attrs: other keyword attributes that should be set on the function. Note that `func_impl.__dict__` is not 

196 automatically copied. 

197 :return: 

198 """ 

199 # grab context from the caller frame 

200 try: 

201 attrs.pop('_with_sig_') 

202 # called from `@with_signature` 

203 frame = _get_callerframe(offset=1) 

204 except KeyError: 

205 frame = _get_callerframe() 

206 evaldict, _ = extract_module_and_evaldict(frame) 

207 

208 # name defaults 

209 user_provided_name = True 

210 if func_name is None: 

211 # allow None, this will result in a lambda function being created 

212 func_name = getattr_partial_aware(func_impl, '__name__', None) 

213 user_provided_name = False 

214 

215 # co_name default 

216 user_provided_co_name = co_name is not None 

217 if not user_provided_co_name: 

218 if func_name is None: 218 ↛ 219line 218 didn't jump to line 219, because the condition on line 218 was never true

219 co_name = '<lambda>' 

220 else: 

221 co_name = func_name 

222 else: 

223 if not (_is_valid_func_def_name(co_name) 

224 or _is_lambda_func_name(co_name)): 

225 raise ValueError("Invalid co_name %r for created function. " 

226 "It is not possible to declare a function " 

227 "with the provided co_name." % co_name) 

228 

229 # qname default 

230 user_provided_qname = True 

231 if qualname is None: 

232 qualname = getattr_partial_aware(func_impl, '__qualname__', None) 

233 user_provided_qname = False 

234 

235 # doc default 

236 if doc is None: 

237 doc = getattr(func_impl, '__doc__', None) 

238 # note: as opposed to what we do in `@wraps`, we cannot easily generate a better doc for partials here. 

239 # Indeed the new signature may not easily match the one in the partial. 

240 

241 # module name default 

242 if module_name is None: 

243 module_name = getattr_partial_aware(func_impl, '__module__', None) 

244 

245 # input signature handling 

246 if isinstance(func_signature, str): 

247 # transform the string into a Signature and make sure the string contains ":" 

248 func_name_from_str, func_signature, func_signature_str = get_signature_from_string(func_signature, evaldict) 

249 

250 # if not explicitly overridden using `func_name`, the name in the string takes over 

251 if func_name_from_str is not None: 

252 if not user_provided_name: 252 ↛ 254line 252 didn't jump to line 254, because the condition on line 252 was never false

253 func_name = func_name_from_str 

254 if not user_provided_qname: 254 ↛ 256line 254 didn't jump to line 256, because the condition on line 254 was never false

255 qualname = func_name 

256 if not user_provided_co_name: 256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false

257 co_name = func_name 

258 

259 create_lambda = not _is_valid_func_def_name(co_name) 

260 

261 # if lambda, strip the name, parentheses and colon from the signature 

262 if create_lambda: 

263 name_len = len(func_name_from_str) if func_name_from_str else 0 

264 func_signature_str = func_signature_str[name_len + 1: -2] 

265 # fix the signature if needed 

266 elif func_name_from_str is None: 

267 func_signature_str = co_name + func_signature_str 

268 

269 elif isinstance(func_signature, Signature): 269 ↛ 279line 269 didn't jump to line 279, because the condition on line 269 was never false

270 # create the signature string 

271 create_lambda = not _is_valid_func_def_name(co_name) 

272 

273 if create_lambda: 

274 # create signature string (or argument string in the case of a lambda function 

275 func_signature_str = get_lambda_argument_string(func_signature, evaldict) 

276 else: 

277 func_signature_str = get_signature_string(co_name, func_signature, evaldict) 

278 else: 

279 raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature)) 

280 

281 # extract all information needed from the `Signature` 

282 params_to_kw_assignment_mode = get_signature_params(func_signature) 

283 params_names = list(params_to_kw_assignment_mode.keys()) 

284 

285 # Note: in decorator the annotations were extracted using getattr(func_impl, '__annotations__') instead. 

286 # This seems equivalent but more general (provided by the signature, not the function), but to check 

287 annotations, defaults, kwonlydefaults = get_signature_details(func_signature) 

288 

289 # create the body of the function to compile 

290 # The generated function body should dispatch its received arguments to the inner function. 

291 # For this we will pass as much as possible the arguments as keywords. 

292 # However if there are varpositional arguments we cannot 

293 assignments = [("%s=%s" % (k, k)) if is_kw else k for k, is_kw in params_to_kw_assignment_mode.items()] 

294 params_str = ', '.join(assignments) 

295 if inject_as_first_arg: 

296 params_str = "%s, %s" % (func_name, params_str) 

297 

298 if _is_generator_func(func_impl): 

299 if sys.version_info >= (3, 3): 299 ↛ 302line 299 didn't jump to line 302, because the condition on line 299 was never false

300 body = "def %s\n yield from _func_impl_(%s)\n" % (func_signature_str, params_str) 

301 else: 

302 from makefun._main_legacy_py import get_legacy_py_generator_body_template 

303 body = get_legacy_py_generator_body_template() % (func_signature_str, params_str) 

304 elif isasyncgenfunction(func_impl): 

305 body = "async def %s\n async for y in _func_impl_(%s):\n yield y\n" % (func_signature_str, params_str) 

306 elif create_lambda: 

307 if func_signature_str: 

308 body = "lambda_ = lambda %s: _func_impl_(%s)\n" % (func_signature_str, params_str) 

309 else: 

310 body = "lambda_ = lambda: _func_impl_(%s)\n" % (params_str) 

311 else: 

312 body = "def %s\n return _func_impl_(%s)\n" % (func_signature_str, params_str) 

313 

314 if iscoroutinefunction(func_impl): 

315 body = ("async " + body).replace('return _func_impl_', 'return await _func_impl_') 

316 

317 # create the function by compiling code, mapping the `_func_impl_` symbol to `func_impl` 

318 protect_eval_dict(evaldict, func_name, params_names) 

319 evaldict['_func_impl_'] = func_impl 

320 if create_lambda: 

321 f = _make("lambda_", params_names, body, evaldict) 

322 else: 

323 f = _make(co_name, params_names, body, evaldict) 

324 

325 # add the source annotation if needed 

326 if add_source: 326 ↛ 330line 326 didn't jump to line 330, because the condition on line 326 was never false

327 attrs['__source__'] = body 

328 

329 # add the handler if needed 

330 if add_impl: 330 ↛ 334line 330 didn't jump to line 334, because the condition on line 330 was never false

331 attrs['__func_impl__'] = func_impl 

332 

333 # update the signature 

334 _update_fields(f, name=func_name, qualname=qualname, doc=doc, annotations=annotations, 

335 defaults=tuple(defaults), kwonlydefaults=kwonlydefaults, 

336 module=module_name, kw=attrs) 

337 

338 return f 

339 

340 

341def _is_generator_func(func_impl): 

342 """ 

343 Return True if the func_impl is a generator 

344 :param func_impl: 

345 :return: 

346 """ 

347 if (3, 5) <= sys.version_info < (3, 6): 347 ↛ 351line 347 didn't jump to line 351, because the condition on line 347 was never true

348 # with Python 3.5 isgeneratorfunction returns True for all coroutines 

349 # however we know that it is NOT possible to have a generator 

350 # coroutine in python 3.5: PEP525 was not there yet 

351 return isgeneratorfunction(func_impl) and not iscoroutinefunction(func_impl) 

352 else: 

353 return isgeneratorfunction(func_impl) 

354 

355 

356def _is_lambda_func_name(func_name): 

357 """ 

358 Return True if func_name is the name of a lambda 

359 :param func_name: 

360 :return: 

361 """ 

362 return func_name == (lambda: None).__code__.co_name 362 ↛ exitline 362 didn't run the lambda on line 362

363 

364 

365def _is_valid_func_def_name(func_name): 

366 """ 

367 Return True if func_name is valid in a function definition. 

368 :param func_name: 

369 :return: 

370 """ 

371 return is_identifier(func_name) and not iskeyword(func_name) 

372 

373 

374class _SymbolRef: 

375 """ 

376 A class used to protect signature default values and type hints when the local context would not be able 

377 to evaluate them properly when the new function is created. In this case we store them under a known name, 

378 we add that name to the locals(), and we use this symbol that has a repr() equal to the name. 

379 """ 

380 __slots__ = 'varname' 

381 

382 def __init__(self, varname): 

383 self.varname = varname 

384 

385 def __repr__(self): 

386 return self.varname 

387 

388 

389def get_signature_string(func_name, func_signature, evaldict): 

390 """ 

391 Returns the string to be used as signature. 

392 If there is a non-native symbol in the defaults, it is created as a variable in the evaldict 

393 :param func_name: 

394 :param func_signature: 

395 :return: 

396 """ 

397 no_type_hints_allowed = sys.version_info < (3, 5) 

398 

399 # protect the parameters if needed 

400 new_params = [] 

401 params_changed = False 

402 for p_name, p in func_signature.parameters.items(): 

403 # if default value can not be evaluated, protect it 

404 default_needs_protection = _signature_symbol_needs_protection(p.default, evaldict) 

405 new_default = _protect_signature_symbol(p.default, default_needs_protection, "DEFAULT_%s" % p_name, evaldict) 

406 

407 if no_type_hints_allowed: 407 ↛ 408line 407 didn't jump to line 408, because the condition on line 407 was never true

408 new_annotation = Parameter.empty 

409 annotation_needs_protection = new_annotation is not p.annotation 

410 else: 

411 # if type hint can not be evaluated, protect it 

412 annotation_needs_protection = _signature_symbol_needs_protection(p.annotation, evaldict) 

413 new_annotation = _protect_signature_symbol(p.annotation, annotation_needs_protection, "HINT_%s" % p_name, 

414 evaldict) 

415 

416 # only create if necessary (inspect __init__ methods are slow) 

417 if default_needs_protection or annotation_needs_protection: 

418 # replace the parameter with the possibly new default and hint 

419 p = Parameter(p.name, kind=p.kind, default=new_default, annotation=new_annotation) 

420 params_changed = True 

421 

422 new_params.append(p) 

423 

424 if no_type_hints_allowed: 424 ↛ 425line 424 didn't jump to line 425, because the condition on line 424 was never true

425 new_return_annotation = Parameter.empty 

426 return_needs_protection = new_return_annotation is not func_signature.return_annotation 

427 else: 

428 # if return type hint can not be evaluated, protect it 

429 return_needs_protection = _signature_symbol_needs_protection(func_signature.return_annotation, evaldict) 

430 new_return_annotation = _protect_signature_symbol(func_signature.return_annotation, return_needs_protection, 

431 "RETURNHINT", evaldict) 

432 

433 # only create new signature if necessary (inspect __init__ methods are slow) 

434 if params_changed or return_needs_protection: 

435 s = Signature(parameters=new_params, return_annotation=new_return_annotation) 

436 else: 

437 s = func_signature 

438 

439 # return the final string representation 

440 return "%s%s:" % (func_name, s) 

441 

442 

443def get_lambda_argument_string(func_signature, evaldict): 

444 """ 

445 Returns the string to be used as arguments in a lambda function definition. 

446 If there is a non-native symbol in the defaults, it is created as a variable in the evaldict 

447 :param func_name: 

448 :param func_signature: 

449 :return: 

450 """ 

451 return get_signature_string('', func_signature, evaldict)[1:-2] 

452 

453 

454TYPES_WITH_SAFE_REPR = (int, str, bytes, bool) 

455# IMPORTANT note: float is not in the above list because not all floats have a repr that is valid for the 

456# compiler: float('nan'), float('-inf') and float('inf') or float('+inf') have an invalid repr. 

457 

458 

459def _signature_symbol_needs_protection(symbol, evaldict): 

460 """ 

461 Helper method for signature symbols (defaults, type hints) protection. 

462 

463 Returns True if the given symbol needs to be protected - that is, if its repr() can not be correctly evaluated with 

464 current evaldict. 

465 

466 :param symbol: 

467 :return: 

468 """ 

469 if symbol is not None and symbol is not Parameter.empty and not isinstance(symbol, TYPES_WITH_SAFE_REPR): 

470 try: 

471 # check if the repr() of the default value is equal to itself. 

472 return eval(repr(symbol), evaldict) != symbol # noqa # we cannot use ast.literal_eval, too restrictive 

473 except Exception: 

474 # in case of error this needs protection 

475 return True 

476 else: 

477 return False 

478 

479 

480def _protect_signature_symbol(val, needs_protection, varname, evaldict): 

481 """ 

482 Helper method for signature symbols (defaults, type hints) protection. 

483 

484 Returns either `val`, or a protection symbol. In that case the protection symbol 

485 is created with name `varname` and inserted into `evaldict` 

486 

487 :param val: 

488 :param needs_protection: 

489 :param varname: 

490 :param evaldict: 

491 :return: 

492 """ 

493 if needs_protection: 

494 # store the object in the evaldict and insert name 

495 evaldict[varname] = val 

496 return _SymbolRef(varname) 

497 else: 

498 return val 

499 

500 

501def get_signature_from_string(func_sig_str, evaldict): 

502 """ 

503 Creates a `Signature` object from the given function signature string. 

504 

505 :param func_sig_str: 

506 :return: (func_name, func_sig, func_sig_str). func_sig_str is guaranteed to contain the ':' symbol already 

507 """ 

508 # escape leading newline characters 

509 if func_sig_str.startswith('\n'): 509 ↛ 510line 509 didn't jump to line 510, because the condition on line 509 was never true

510 func_sig_str = func_sig_str[1:] 

511 

512 # match the provided signature. note: fullmatch is not supported in python 2 

513 def_match = FUNC_DEF.match(func_sig_str) 

514 if def_match is None: 514 ↛ 515line 514 didn't jump to line 515, because the condition on line 514 was never true

515 raise SyntaxError('The provided function template is not valid: "%s" does not match ' 

516 '"<func_name>(<func_args>)[ -> <return-hint>]".\n For information the regex used is: "%s"' 

517 '' % (func_sig_str, FUNC_DEF.pattern)) 

518 groups = def_match.groupdict() 

519 

520 # extract function name and parameter names list 

521 func_name = groups['funcname'] 

522 if func_name is None or func_name == '': 

523 func_name_ = 'dummy' 

524 func_name = None 

525 else: 

526 func_name_ = func_name 

527 # params_str = groups['params'] 

528 # params_names = extract_params_names(params_str) 

529 

530 # find the keyword parameters and the others 

531 # posonly_names, kwonly_names, unrestricted_names = separate_positional_and_kw(params_names) 

532 

533 colon_end = groups['colon'] 

534 cmt_return_hint = groups['comment_return_hint'] 

535 if (colon_end is None or len(colon_end) == 0) \ 

536 and (cmt_return_hint is None or len(cmt_return_hint) == 0): 

537 func_sig_str = func_sig_str + ':' 

538 

539 # Create a dummy function 

540 # complete the string if name is empty, so that we can actually use _make 

541 func_sig_str_ = (func_name_ + func_sig_str) if func_name is None else func_sig_str 

542 body = 'def %s\n pass\n' % func_sig_str_ 

543 dummy_f = _make(func_name_, [], body, evaldict) 

544 

545 # return its signature 

546 return func_name, signature(dummy_f), func_sig_str 

547 

548 

549# def extract_params_names(params_str): 

550# return [m.groupdict()['name'] for m in PARAM_DEF.finditer(params_str)] 

551 

552 

553# def separate_positional_and_kw(params_names): 

554# """ 

555# Extracts the names that are positional-only, keyword-only, or non-constrained 

556# :param params_names: 

557# :return: 

558# """ 

559# # by default all parameters can be passed as positional or keyword 

560# posonly_names = [] 

561# kwonly_names = [] 

562# other_names = params_names 

563# 

564# # but if we find explicit separation we have to change our mind 

565# for i in range(len(params_names)): 

566# name = params_names[i] 

567# if name == '*': 

568# del params_names[i] 

569# posonly_names = params_names[0:i] 

570# kwonly_names = params_names[i:] 

571# other_names = [] 

572# break 

573# elif name[0] == '*' and name[1] != '*': # 

574# # that's a *args. Next one will be keyword-only 

575# posonly_names = params_names[0:(i + 1)] 

576# kwonly_names = params_names[(i + 1):] 

577# other_names = [] 

578# break 

579# else: 

580# # continue 

581# pass 

582# 

583# return posonly_names, kwonly_names, other_names 

584 

585 

586def get_signature_params(s): 

587 """ 

588 Utility method to return the parameter names in the provided `Signature` object, by group of kind 

589 

590 :param s: 

591 :return: 

592 """ 

593 # this ordered dictionary will contain parameters and True/False whether we should use keyword assignment or not 

594 params_to_assignment_mode = OrderedDict() 

595 for p_name, p in s.parameters.items(): 

596 if p.kind is Parameter.POSITIONAL_ONLY: 

597 params_to_assignment_mode[p_name] = False 

598 elif p.kind is Parameter.KEYWORD_ONLY: 

599 params_to_assignment_mode[p_name] = True 

600 elif p.kind is Parameter.POSITIONAL_OR_KEYWORD: 

601 params_to_assignment_mode[p_name] = True 

602 elif p.kind is Parameter.VAR_POSITIONAL: 

603 # We have to pass all the arguments that were here in previous positions, as positional too. 

604 for k in params_to_assignment_mode.keys(): 

605 params_to_assignment_mode[k] = False 

606 params_to_assignment_mode["*" + p_name] = False 

607 elif p.kind is Parameter.VAR_KEYWORD: 607 ↛ 610line 607 didn't jump to line 610, because the condition on line 607 was never false

608 params_to_assignment_mode["**" + p_name] = False 

609 else: 

610 raise ValueError("Unknown kind: %s" % p.kind) 

611 

612 return params_to_assignment_mode 

613 

614 

615def get_signature_details(s): 

616 """ 

617 Utility method to extract the annotations, defaults and kwdefaults from a `Signature` object 

618 

619 :param s: 

620 :return: 

621 """ 

622 annotations = dict() 

623 defaults = [] 

624 kwonlydefaults = dict() 

625 if s.return_annotation is not s.empty: 

626 annotations['return'] = s.return_annotation 

627 for p_name, p in s.parameters.items(): 

628 if p.annotation is not s.empty: 

629 annotations[p_name] = p.annotation 

630 if p.default is not s.empty: 

631 # if p_name not in kwonly_names: 

632 if p.kind is not Parameter.KEYWORD_ONLY: 

633 defaults.append(p.default) 

634 else: 

635 kwonlydefaults[p_name] = p.default 

636 return annotations, defaults, kwonlydefaults 

637 

638 

639def extract_module_and_evaldict(frame): 

640 """ 

641 Utility function to extract the module name from the given frame, 

642 and to return a dictionary containing globals and locals merged together 

643 

644 :param frame: 

645 :return: 

646 """ 

647 try: 

648 # get the module name 

649 module_name = frame.f_globals.get('__name__', '?') 

650 

651 # construct a dictionary with all variables 

652 # this is required e.g. if a symbol is used in a type hint 

653 evaldict = copy(frame.f_globals) 

654 evaldict.update(frame.f_locals) 

655 

656 except AttributeError: 

657 # either the frame is None of the f_globals and f_locals are not available 

658 module_name = '?' 

659 evaldict = dict() 

660 

661 return evaldict, module_name 

662 

663 

664def protect_eval_dict(evaldict, func_name, params_names): 

665 """ 

666 remove all symbols that could be harmful in evaldict 

667 

668 :param evaldict: 

669 :param func_name: 

670 :param params_names: 

671 :return: 

672 """ 

673 try: 

674 del evaldict[func_name] 

675 except KeyError: 

676 pass 

677 for n in params_names: 

678 try: 

679 del evaldict[n] 

680 except KeyError: 

681 pass 

682 

683 return evaldict 

684 

685 

686# Atomic get-and-increment provided by the GIL 

687_compile_count = itertools.count() 

688 

689 

690def _make(funcname, params_names, body, evaldict=None): 

691 """ 

692 Make a new function from a given template and update the signature 

693 

694 :param func_name: 

695 :param params_names: 

696 :param body: 

697 :param evaldict: 

698 :param add_source: 

699 :return: 

700 """ 

701 evaldict = evaldict or {} 

702 for n in params_names: 

703 if n in ('_func_', '_func_impl_'): 703 ↛ 704line 703 didn't jump to line 704, because the condition on line 703 was never true

704 raise NameError('%s is overridden in\n%s' % (n, body)) 

705 

706 if not body.endswith('\n'): # newline is needed for old Pythons 706 ↛ 707line 706 didn't jump to line 707, because the condition on line 706 was never true

707 raise ValueError("body should end with a newline") 

708 

709 # Ensure each generated function has a unique filename for profilers 

710 # (such as cProfile) that depend on the tuple of (<filename>, 

711 # <definition line>, <function name>) being unique. 

712 filename = '<makefun-gen-%d>' % (next(_compile_count),) 

713 try: 

714 code = compile(body, filename, 'single') 

715 exec(code, evaldict) # noqa 

716 except BaseException: 

717 print('Error in generated code:', file=sys.stderr) 

718 print(body, file=sys.stderr) 

719 raise 

720 

721 # extract the function from compiled code 

722 func = evaldict[funcname] 

723 

724 return func 

725 

726 

727def _update_fields( 

728 func, name, qualname=None, doc=None, annotations=None, defaults=(), kwonlydefaults=None, module=None, kw=None 

729): 

730 """ 

731 Update the signature of func with the provided information 

732 

733 This method merely exists to remind which field have to be filled. 

734 

735 :param func: 

736 :param name: 

737 :param qualname: 

738 :param kw: 

739 :return: 

740 """ 

741 if kw is None: 741 ↛ 742line 741 didn't jump to line 742, because the condition on line 741 was never true

742 kw = dict() 

743 

744 func.__name__ = name 

745 

746 if qualname is not None: 746 ↛ 749line 746 didn't jump to line 749, because the condition on line 746 was never false

747 func.__qualname__ = qualname 

748 

749 func.__doc__ = doc 

750 func.__dict__ = kw 

751 

752 func.__defaults__ = defaults 

753 if len(kwonlydefaults) == 0: 

754 kwonlydefaults = None 

755 func.__kwdefaults__ = kwonlydefaults 

756 

757 func.__annotations__ = annotations 

758 func.__module__ = module 

759 

760 

761def _get_callerframe(offset=0): 

762 try: 

763 # inspect.stack is extremely slow, the fastest is sys._getframe or inspect.currentframe(). 

764 # See https://gist.github.com/JettJones/c236494013f22723c1822126df944b12 

765 frame = sys._getframe(2 + offset) 

766 # frame = currentframe() 

767 # for _ in range(2 + offset): 

768 # frame = frame.f_back 

769 

770 except AttributeError: # for IronPython and similar implementations 

771 frame = None 

772 

773 return frame 

774 

775 

776def wraps(wrapped_fun, 

777 new_sig=None, # type: Union[str, Signature] 

778 prepend_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

779 append_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

780 remove_args=None, # type: Union[str, Iterable[str]] 

781 func_name=None, # type: str 

782 co_name=None, # type: str 

783 inject_as_first_arg=False, # type: bool 

784 add_source=True, # type: bool 

785 add_impl=True, # type: bool 

786 doc=None, # type: str 

787 qualname=None, # type: str 

788 module_name=None, # type: str 

789 **attrs 

790 ): 

791 """ 

792 A decorator to create a signature-preserving wrapper function. 

793 

794 It is similar to `functools.wraps`, but 

795 

796 - relies on a proper dynamically-generated function. Therefore as opposed to `functools.wraps`, 

797 

798 - the wrapper body will not be executed if the arguments provided are not compliant with the signature - 

799 instead a `TypeError` will be raised before entering the wrapper body. 

800 - the arguments will always be received as keywords by the wrapper, when possible. See 

801 [documentation](./index.md#signature-preserving-function-wrappers) for details. 

802 

803 - you can modify the signature of the resulting function, by providing a new one with `new_sig` or by providing a 

804 list of arguments to remove in `remove_args`, to prepend in `prepend_args`, or to append in `append_args`. 

805 See [documentation](./index.md#editing-a-signature) for details. 

806 

807 Comparison with `@with_signature`:`@wraps(f)` is equivalent to 

808 

809 `@with_signature(signature(f), 

810 func_name=f.__name__, 

811 doc=f.__doc__, 

812 module_name=f.__module__, 

813 qualname=f.__qualname__, 

814 __wrapped__=f, 

815 **f.__dict__, 

816 **attrs)` 

817 

818 In other words, as opposed to `@with_signature`, the metadata (doc, module name, etc.) is provided by the wrapped 

819 `wrapped_fun`, so that the created function seems to be identical (except possibly for the signature). 

820 Note that all options in `with_signature` can still be overridden using parameters of `@wraps`. 

821 

822 The additional `__wrapped__` attribute is set on the created function, to stay consistent 

823 with the `functools.wraps` behaviour. If the signature is modified through `new_sig`, 

824 `remove_args`, `append_args` or `prepend_args`, the additional 

825 `__signature__` attribute will be set so that `inspect.signature` and related functionality 

826 works as expected. See PEP 362 for more detail on `__wrapped__` and `__signature__`. 

827 

828 See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps) 

829 

830 :param wrapped_fun: the function that you intend to wrap with the decorated function. As in `functools.wraps`, 

831 `wrapped_fun` is used as the default reference for the exposed signature, `__name__`, `__qualname__`, `__doc__` 

832 and `__dict__`. 

833 :param new_sig: the new signature of the decorated function. By default it is `None` and means "same signature as 

834 in `wrapped_fun`" (similar behaviour as in `functools.wraps`). If you wish to modify the exposed signature 

835 you can either use `remove/prepend/append_args`, or pass a non-None `new_sig`. It can be either a string 

836 without 'def' such as "foo(a, b: int, *args, **kwargs)" of "(a, b: int)", or a `Signature` object, for example 

837 from the output of `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can 

838 be created manually too. If the signature is provided as a string and contains a non-empty name, this name 

839 will be used instead of the one of `wrapped_fun`. 

840 :param prepend_args: a string or list of strings to prepend to the signature of `wrapped_fun`. These extra arguments 

841 should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a 

842 wrapper with additional arguments, without having to manipulate the signature objects. 

843 :param append_args: a string or list of strings to append to the signature of `wrapped_fun`. These extra arguments 

844 should not be passed to `wrapped_fun`, as it does not know them. This is typically used to easily create a 

845 wrapper with additional arguments, without having to manipulate the signature objects. 

846 :param remove_args: a string or list of strings to remove from the signature of `wrapped_fun`. These arguments 

847 should be injected in the received `kwargs` before calling `wrapped_fun`, as it requires them. This is typically 

848 used to easily create a wrapper with less arguments, without having to manipulate the signature objects. 

849 :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this 

850 is `None` (default), the `__name__` will default to the ones of `wrapped_fun` if `new_sig` is `None` or is a 

851 `Signature`, or to the name defined in `new_sig` if `new_sig` is a `str` and contains a non-empty name. 

852 :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of 

853 the decorated function. This can be handy in case the implementation is shared between several facades and needs 

854 to know from which context it was called. Default=`False` 

855 :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function 

856 (default: True) 

857 :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function 

858 (default: True) 

859 :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated 

860 function. If None (default), the doc of `wrapped_fun` will be used. If `wrapped_fun` is an instance of 

861 `functools.partial`, a special enhanced doc will be generated. 

862 :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will 

863 default to the one of `wrapped_fun`, or the one in `new_sig` if `new_sig` is provided as a string with a 

864 non-empty function name. 

865 :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), 

866 the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the 

867 name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

868 :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the 

869 `__module__` attribute of `wrapped_fun` will be used. 

870 :param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of 

871 `wrapped_fun` is automatically copied. 

872 :return: a decorator 

873 """ 

874 func_name, func_sig, doc, qualname, co_name, module_name, all_attrs = _get_args_for_wrapping(wrapped_fun, new_sig, 

875 remove_args, 

876 prepend_args, 

877 append_args, 

878 func_name, doc, 

879 qualname, co_name, 

880 module_name, attrs) 

881 

882 return with_signature(func_sig, 

883 func_name=func_name, 

884 inject_as_first_arg=inject_as_first_arg, 

885 add_source=add_source, add_impl=add_impl, 

886 doc=doc, 

887 qualname=qualname, 

888 co_name=co_name, 

889 module_name=module_name, 

890 **all_attrs) 

891 

892 

893def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_args, 

894 func_name, doc, qualname, co_name, module_name, attrs): 

895 """ 

896 Internal method used by @wraps and create_wrapper 

897 

898 :param wrapped: 

899 :param new_sig: 

900 :param remove_args: 

901 :param prepend_args: 

902 :param append_args: 

903 :param func_name: 

904 :param doc: 

905 :param qualname: 

906 :param co_name: 

907 :param module_name: 

908 :param attrs: 

909 :return: 

910 """ 

911 # the desired signature 

912 has_new_sig = False 

913 if new_sig is not None: 

914 if remove_args is not None or prepend_args is not None or append_args is not None: 914 ↛ 915line 914 didn't jump to line 915, because the condition on line 914 was never true

915 raise ValueError("Only one of `[remove/prepend/append]_args` or `new_sig` should be provided") 

916 func_sig = new_sig 

917 has_new_sig = True 

918 else: 

919 func_sig = signature(wrapped) 

920 if remove_args: 

921 if isinstance(remove_args, string_types): 

922 remove_args = (remove_args,) 

923 func_sig = remove_signature_parameters(func_sig, *remove_args) 

924 has_new_sig = True 

925 

926 if prepend_args: 

927 if isinstance(prepend_args, string_types): 927 ↛ 932line 927 didn't jump to line 932, because the condition on line 927 was never false

928 prepend_args = (prepend_args,) 

929 else: 

930 prepend_args = () 

931 

932 if append_args: 

933 if isinstance(append_args, string_types): 933 ↛ 938line 933 didn't jump to line 938, because the condition on line 933 was never false

934 append_args = (append_args,) 

935 else: 

936 append_args = () 

937 

938 if prepend_args or append_args: 

939 has_new_sig = True 

940 func_sig = add_signature_parameters(func_sig, first=prepend_args, last=append_args) 

941 

942 # the desired metadata 

943 if func_name is None: 

944 func_name = getattr_partial_aware(wrapped, '__name__', None) 

945 if doc is None: 945 ↛ 954line 945 didn't jump to line 954, because the condition on line 945 was never false

946 doc = getattr(wrapped, '__doc__', None) 

947 if isinstance(wrapped, functools.partial) and not has_new_sig \ 947 ↛ exitline 947 didn't jump to the function exit

948 and doc == functools.partial(lambda: True).__doc__: 

949 # the default generic partial doc. Generate a better doc, since we know that the sig is not messed with 

950 orig_sig = signature(wrapped.func) 

951 doc = gen_partial_doc(getattr_partial_aware(wrapped.func, '__name__', None), 

952 getattr_partial_aware(wrapped.func, '__doc__', None), 

953 orig_sig, func_sig, wrapped.args) 

954 if qualname is None: 954 ↛ 956line 954 didn't jump to line 956, because the condition on line 954 was never false

955 qualname = getattr_partial_aware(wrapped, '__qualname__', None) 

956 if module_name is None: 956 ↛ 958line 956 didn't jump to line 958, because the condition on line 956 was never false

957 module_name = getattr_partial_aware(wrapped, '__module__', None) 

958 if co_name is None: 958 ↛ 964line 958 didn't jump to line 964, because the condition on line 958 was never false

959 code = getattr_partial_aware(wrapped, '__code__', None) 

960 if code is not None: 960 ↛ 964line 960 didn't jump to line 964, because the condition on line 960 was never false

961 co_name = code.co_name 

962 

963 # attributes: start from the wrapped dict, add '__wrapped__' if needed, and override with all attrs. 

964 all_attrs = copy(getattr_partial_aware(wrapped, '__dict__')) 

965 # PEP362: always set `__wrapped__`, and if signature was changed, set `__signature__` too 

966 all_attrs["__wrapped__"] = wrapped 

967 if has_new_sig: 

968 if isinstance(func_sig, Signature): 

969 all_attrs["__signature__"] = func_sig 

970 else: 

971 # __signature__ must be a Signature object, so if it is a string we need to evaluate it. 

972 frame = _get_callerframe(offset=1) 

973 evaldict, _ = extract_module_and_evaldict(frame) 

974 # Here we could wish to directly override `func_name` and `func_sig` so that this does not have to be done 

975 # again by `create_function` later... Would this be risky ? 

976 _func_name, func_sig_as_sig, _ = get_signature_from_string(func_sig, evaldict) 

977 all_attrs["__signature__"] = func_sig_as_sig 

978 

979 all_attrs.update(attrs) 

980 

981 return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs 

982 

983 

984def with_signature(func_signature, # type: Union[str, Signature] 

985 func_name=None, # type: str 

986 inject_as_first_arg=False, # type: bool 

987 add_source=True, # type: bool 

988 add_impl=True, # type: bool 

989 doc=None, # type: str 

990 qualname=None, # type: str 

991 co_name=None, # type: str 

992 module_name=None, # type: str 

993 **attrs 

994 ): 

995 """ 

996 A decorator for functions, to change their signature. The new signature should be compliant with the old one. 

997 

998 ```python 

999 @with_signature(<arguments>) 

1000 def impl(...): 

1001 ... 

1002 ``` 

1003 

1004 is totally equivalent to `impl = create_function(<arguments>, func_impl=impl)` except for one additional behaviour: 

1005 

1006 - If `func_signature` is set to `None`, there is no `TypeError` as in create_function. Instead, this simply 

1007 applies the new metadata (name, doc, module_name, attrs) to the decorated function without creating a wrapper. 

1008 `add_source`, `add_impl` and `inject_as_first_arg` should not be set in this case. 

1009 

1010 :param func_signature: the new signature of the decorated function. Either a string without 'def' such as 

1011 "foo(a, b: int, *args, **kwargs)" of "(a, b: int)", or a `Signature` object, for example from the output of 

1012 `inspect.signature` or from the `funcsigs.signature` backport. Note that these objects can be created manually 

1013 too. If the signature is provided as a string and contains a non-empty name, this name will be used instead 

1014 of the one of the decorated function. Finally `None` can be provided to indicate that user wants to only change 

1015 the medatadata (func_name, doc, module_name, attrs) of the decorated function, without generating a new 

1016 function. 

1017 :param inject_as_first_arg: if `True`, the created function will be injected as the first positional argument of 

1018 the decorated function. This can be handy in case the implementation is shared between several facades and needs 

1019 to know from which context it was called. Default=`False` 

1020 :param func_name: provide a non-`None` value to override the created function `__name__` and `__qualname__`. If this 

1021 is `None` (default), the `__name__` will default to the ones of the decorated function if `func_signature` is a 

1022 `Signature`, or to the name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty 

1023 name. 

1024 :param add_source: a boolean indicating if a '__source__' annotation should be added to the generated function 

1025 (default: True) 

1026 :param add_impl: a boolean indicating if a '__func_impl__' annotation should be added to the generated function 

1027 (default: True) 

1028 :param doc: a string representing the docstring that will be used to set the __doc__ attribute on the generated 

1029 function. If None (default), the doc of the decorated function will be used. 

1030 :param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will 

1031 default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in 

1032 `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

1033 :param co_name: a string representing the name to be used in the compiled code of the function. If None (default), 

1034 the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the 

1035 name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name. 

1036 :param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the 

1037 `__module__` attribute of the decorated function will be used. 

1038 :param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of the 

1039 decorated function is not automatically copied. 

1040 """ 

1041 if func_signature is None and co_name is None: 

1042 # make sure that user does not provide non-default other args 

1043 if inject_as_first_arg or not add_source or not add_impl: 1043 ↛ 1044line 1043 didn't jump to line 1044, because the condition on line 1043 was never true

1044 raise ValueError("If `func_signature=None` no new signature will be generated so only `func_name`, " 

1045 "`module_name`, `doc` and `attrs` should be provided, to modify the metadata.") 

1046 else: 

1047 def replace_f(f): 

1048 # manually apply all the non-None metadata, but do not call create_function - that's useless 

1049 if func_name is not None: 1049 ↛ 1051line 1049 didn't jump to line 1051, because the condition on line 1049 was never false

1050 f.__name__ = func_name 

1051 if doc is not None: 1051 ↛ 1052line 1051 didn't jump to line 1052, because the condition on line 1051 was never true

1052 f.__doc__ = doc 

1053 if qualname is not None: 1053 ↛ 1054line 1053 didn't jump to line 1054, because the condition on line 1053 was never true

1054 f.__qualname__ = qualname 

1055 if module_name is not None: 1055 ↛ 1056line 1055 didn't jump to line 1056, because the condition on line 1055 was never true

1056 f.__module__ = module_name 

1057 for k, v in attrs.items(): 1057 ↛ 1058line 1057 didn't jump to line 1058, because the loop on line 1057 never started

1058 setattr(f, k, v) 

1059 return f 

1060 else: 

1061 def replace_f(f): 

1062 return create_function(func_signature=func_signature, 

1063 func_impl=f, 

1064 func_name=func_name, 

1065 inject_as_first_arg=inject_as_first_arg, 

1066 add_source=add_source, 

1067 add_impl=add_impl, 

1068 doc=doc, 

1069 qualname=qualname, 

1070 co_name=co_name, 

1071 module_name=module_name, 

1072 _with_sig_=True, # special trick to tell create_function that we're @with_signature 

1073 **attrs 

1074 ) 

1075 

1076 return replace_f 

1077 

1078 

1079def remove_signature_parameters(s, 

1080 *param_names): 

1081 """ 

1082 Removes the provided parameters from the signature `s` (returns a new `Signature` instance). 

1083 

1084 :param s: 

1085 :param param_names: a list of parameter names to remove 

1086 :return: 

1087 """ 

1088 params = OrderedDict(s.parameters.items()) 

1089 for param_name in param_names: 

1090 del params[param_name] 

1091 return s.replace(parameters=params.values()) 

1092 

1093 

1094def add_signature_parameters(s, # type: Signature 

1095 first=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

1096 last=(), # type: Union[str, Parameter, Iterable[Union[str, Parameter]]] 

1097 custom=(), # type: Union[Parameter, Iterable[Parameter]] 

1098 custom_idx=-1 # type: int 

1099 ): 

1100 """ 

1101 Adds the provided parameters to the signature `s` (returns a new `Signature` instance). 

1102 

1103 :param s: the original signature to edit 

1104 :param first: a single element or a list of `Parameter` instances to be added at the beginning of the parameter's 

1105 list. Strings can also be provided, in which case the parameter kind will be created based on best guess. 

1106 :param last: a single element or a list of `Parameter` instances to be added at the end of the parameter's list. 

1107 Strings can also be provided, in which case the parameter kind will be created based on best guess. 

1108 :param custom: a single element or a list of `Parameter` instances to be added at a custom position in the list. 

1109 That position is determined with `custom_idx` 

1110 :param custom_idx: the custom position to insert the `custom` parameters to. 

1111 :return: a new signature created from the original one by adding the specified parameters. 

1112 """ 

1113 params = OrderedDict(s.parameters.items()) 

1114 lst = list(params.values()) 

1115 

1116 # insert at custom position (but keep the order, that's why we use 'reversed') 

1117 try: 

1118 for param in reversed(custom): 1118 ↛ 1119line 1118 didn't jump to line 1119, because the loop on line 1118 never started

1119 if param.name in params: 

1120 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) 

1121 else: 

1122 lst.insert(custom_idx, param) 

1123 except TypeError: 

1124 # a single argument 

1125 if custom.name in params: 

1126 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % custom.name) 

1127 else: 

1128 lst.insert(custom_idx, custom) 

1129 

1130 # prepend but keep the order 

1131 first_param_kind = None 

1132 try: 

1133 for param in reversed(first): 

1134 if isinstance(param, string_types): 1134 ↛ 1151line 1134 didn't jump to line 1151, because the condition on line 1134 was never false

1135 # Create a Parameter with auto-guessed 'kind' 

1136 if first_param_kind is None: 1136 ↛ 1146line 1136 didn't jump to line 1146, because the condition on line 1136 was never false

1137 # by default use this 

1138 first_param_kind = Parameter.POSITIONAL_OR_KEYWORD 

1139 try: 

1140 # check the first parameter kind 

1141 first_param_kind = next(iter(params.values())).kind 

1142 except StopIteration: 

1143 # no parameter - ok 

1144 pass 

1145 # if the first parameter is a pos-only or a varpos we have to change to pos only. 

1146 if first_param_kind in (Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL): 1146 ↛ 1147line 1146 didn't jump to line 1147, because the condition on line 1146 was never true

1147 first_param_kind = Parameter.POSITIONAL_ONLY 

1148 param = Parameter(name=param, kind=first_param_kind) 

1149 else: 

1150 # remember the kind 

1151 first_param_kind = param.kind 

1152 

1153 if param.name in params: 1153 ↛ 1154line 1153 didn't jump to line 1154, because the condition on line 1153 was never true

1154 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) 

1155 else: 

1156 lst.insert(0, param) 

1157 except TypeError: 

1158 # a single argument 

1159 if first.name in params: 1159 ↛ 1160line 1159 didn't jump to line 1160, because the condition on line 1159 was never true

1160 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % first.name) 

1161 else: 

1162 lst.insert(0, first) 

1163 

1164 # append 

1165 last_param_kind = None 

1166 try: 

1167 for param in last: 

1168 if isinstance(param, string_types): 1168 ↛ 1185line 1168 didn't jump to line 1185, because the condition on line 1168 was never false

1169 # Create a Parameter with auto-guessed 'kind' 

1170 if last_param_kind is None: 1170 ↛ 1180line 1170 didn't jump to line 1180, because the condition on line 1170 was never false

1171 # by default use this 

1172 last_param_kind = Parameter.POSITIONAL_OR_KEYWORD 

1173 try: 

1174 # check the last parameter kind 

1175 last_param_kind = next(reversed(params.values())).kind 

1176 except StopIteration: 

1177 # no parameter - ok 

1178 pass 

1179 # if the last parameter is a keyword-only or a varkw we have to change to kw only. 

1180 if last_param_kind in (Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD): 1180 ↛ 1181line 1180 didn't jump to line 1181, because the condition on line 1180 was never true

1181 last_param_kind = Parameter.KEYWORD_ONLY 

1182 param = Parameter(name=param, kind=last_param_kind) 

1183 else: 

1184 # remember the kind 

1185 last_param_kind = param.kind 

1186 

1187 if param.name in params: 1187 ↛ 1188line 1187 didn't jump to line 1188, because the condition on line 1187 was never true

1188 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % param.name) 

1189 else: 

1190 lst.append(param) 

1191 except TypeError: 

1192 # a single argument 

1193 if last.name in params: 1193 ↛ 1194line 1193 didn't jump to line 1194, because the condition on line 1193 was never true

1194 raise ValueError("Parameter with name '%s' is present twice in the signature to create" % last.name) 

1195 else: 

1196 lst.append(last) 

1197 

1198 return s.replace(parameters=lst) 

1199 

1200 

1201def with_partial(*preset_pos_args, **preset_kwargs): 

1202 """ 

1203 Decorator to 'partialize' a function using `partial` 

1204 

1205 :param preset_pos_args: 

1206 :param preset_kwargs: 

1207 :return: 

1208 """ 

1209 def apply_decorator(f): 

1210 return partial(f, *preset_pos_args, **preset_kwargs) 

1211 return apply_decorator 

1212 

1213 

1214def partial(f, # type: Callable 

1215 *preset_pos_args, # type: Any 

1216 **preset_kwargs # type: Any 

1217 ): 

1218 """ 

1219 Equivalent of `functools.partial` but relies on a dynamically-created function. As a result the function 

1220 looks nicer to users in terms of apparent documentation, name, etc. 

1221 

1222 See [documentation](./index.md#removing-parameters-easily) for details. 

1223 

1224 :param preset_pos_args: 

1225 :param preset_kwargs: 

1226 :return: 

1227 """ 

1228 # TODO do we need to mimic `partial`'s behaviour concerning positional args? 

1229 

1230 # (1) remove/change all preset arguments from the signature 

1231 orig_sig = signature(f) 

1232 if preset_pos_args or preset_kwargs: 

1233 new_sig = gen_partial_sig(orig_sig, preset_pos_args, preset_kwargs, f) 

1234 else: 

1235 new_sig = None 

1236 

1237 if _is_generator_func(f): 

1238 if sys.version_info >= (3, 3): 1238 ↛ 1242line 1238 didn't jump to line 1242, because the condition on line 1238 was never false

1239 from makefun._main_py35_and_higher import make_partial_using_yield_from 

1240 partial_f = make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs) 

1241 else: 

1242 from makefun._main_legacy_py import make_partial_using_yield 

1243 partial_f = make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs) 

1244 elif isasyncgenfunction(f) and sys.version_info >= (3, 6): 

1245 from makefun._main_py36_and_higher import make_partial_using_async_for_in_yield 

1246 partial_f = make_partial_using_async_for_in_yield(new_sig, f, *preset_pos_args, **preset_kwargs) 

1247 else: 

1248 @wraps(f, new_sig=new_sig) 

1249 def partial_f(*args, **kwargs): 

1250 # since the signature does the checking for us, no need to check for redundancy. 

1251 kwargs.update(preset_kwargs) 

1252 return f(*itertools.chain(preset_pos_args, args), **kwargs) 

1253 

1254 # update the doc. 

1255 # Note that partial_f is generated above with a proper __name__ and __doc__ identical to the wrapped ones 

1256 if new_sig is not None: 

1257 partial_f.__doc__ = gen_partial_doc(partial_f.__name__, partial_f.__doc__, orig_sig, new_sig, preset_pos_args) 

1258 

1259 # Set the func attribute as `functools.partial` does 

1260 partial_f.func = f 

1261 

1262 return partial_f 

1263 

1264 

1265if PY2: 1265 ↛ 1270line 1265 didn't jump to line 1270, because the condition on line 1265 was never true

1266 # In python 2 keyword-only arguments do not exist. 

1267 # so if they do not have a default value, we set them with a default value 

1268 # that is this singleton. This is the only way we can have the same behaviour 

1269 # in python 2 in terms of order of arguments, than what funcools.partial does. 

1270 class KwOnly: 

1271 def __str__(self): 

1272 return repr(self) 

1273 

1274 def __repr__(self): 

1275 return "KW_ONLY_ARG!" 

1276 

1277 KW_ONLY = KwOnly() 

1278else: 

1279 KW_ONLY = None 

1280 

1281 

1282def gen_partial_sig(orig_sig, # type: Signature 

1283 preset_pos_args, # type: Tuple[Any] 

1284 preset_kwargs, # type: Mapping[str, Any] 

1285 f, # type: Callable 

1286 ): 

1287 """ 

1288 Returns the signature of partial(f, *preset_pos_args, **preset_kwargs) 

1289 Raises explicit errors in case of non-matching argument names. 

1290 

1291 By default the behaviour is the same as `functools.partial`: 

1292 

1293 - partialized positional arguments disappear from the signature 

1294 - partialized keyword arguments remain in the signature in the same order, but all keyword arguments after them 

1295 in the parameters order become keyword-only (if python 2, they do not become keyword-only as this is not allowed 

1296 in the compiler, but we pass them a bad default value "KEYWORD_ONLY") 

1297 

1298 :param orig_sig: 

1299 :param preset_pos_args: 

1300 :param preset_kwargs: 

1301 :param f: used in error messages only 

1302 :return: 

1303 """ 

1304 preset_kwargs = copy(preset_kwargs) 

1305 

1306 # remove the first n positional, and assign/change default values for the keyword 

1307 if len(orig_sig.parameters) < len(preset_pos_args): 1307 ↛ 1308line 1307 didn't jump to line 1308, because the condition on line 1307 was never true

1308 raise ValueError("Cannot preset %s positional args, function %s has only %s args." 

1309 "" % (len(preset_pos_args), getattr(f, '__name__', f), len(orig_sig.parameters))) 

1310 

1311 # then the keywords. If they have a new value override it 

1312 new_params = [] 

1313 kwonly_flag = False 

1314 for i, (p_name, p) in enumerate(orig_sig.parameters.items()): 

1315 if i < len(preset_pos_args): 

1316 # preset positional arg: disappears from signature 

1317 continue 

1318 try: 

1319 # is this parameter overridden in `preset_kwargs` ? 

1320 overridden_p_default = preset_kwargs.pop(p_name) 

1321 except KeyError: 

1322 # no: it will appear "as is" in the signature, in the same order 

1323 

1324 # However we need to change the kind if the kind is not already "keyword only" 

1325 # positional only: Parameter.POSITIONAL_ONLY, VAR_POSITIONAL 

1326 # both: POSITIONAL_OR_KEYWORD 

1327 # keyword only: KEYWORD_ONLY, VAR_KEYWORD 

1328 if kwonly_flag and p.kind not in (Parameter.VAR_KEYWORD, Parameter.KEYWORD_ONLY): 

1329 if PY2: 1329 ↛ 1331line 1329 didn't jump to line 1331, because the condition on line 1329 was never true

1330 # Special : we can not make if Keyword-only, but we can not leave it without default value 

1331 new_kind = p.kind 

1332 # set a default value of 

1333 new_default = p.default if p.default is not Parameter.empty else KW_ONLY 

1334 else: 

1335 new_kind = Parameter.KEYWORD_ONLY 

1336 new_default = p.default 

1337 p = Parameter(name=p.name, kind=new_kind, default=new_default, annotation=p.annotation) 

1338 

1339 else: 

1340 # yes: override definition with the default. Note that the parameter will remain in the signature 

1341 # but as "keyword only" (and so will be all following args) 

1342 if p.kind is Parameter.POSITIONAL_ONLY: 1342 ↛ 1343line 1342 didn't jump to line 1343, because the condition on line 1342 was never true

1343 raise NotImplementedError("Predefining a positional-only argument using keyword is not supported as in " 

1344 "python 3.8.8, 'signature()' does not support such functions and raises a" 

1345 "ValueError. Please report this issue if support needs to be added in the " 

1346 "future.") 

1347 

1348 if not PY2 and p.kind not in (Parameter.VAR_KEYWORD, Parameter.KEYWORD_ONLY): 1348 ↛ 1352line 1348 didn't jump to line 1352, because the condition on line 1348 was never false

1349 # change kind to keyword-only 

1350 new_kind = Parameter.KEYWORD_ONLY 

1351 else: 

1352 new_kind = p.kind 

1353 p = Parameter(name=p.name, kind=new_kind, default=overridden_p_default, annotation=p.annotation) 

1354 

1355 # from now on, all other parameters need to be keyword-only 

1356 kwonly_flag = True 

1357 

1358 # preserve order 

1359 new_params.append(p) 

1360 

1361 new_sig = Signature(parameters=tuple(new_params), 

1362 return_annotation=orig_sig.return_annotation) 

1363 

1364 if len(preset_kwargs) > 0: 1364 ↛ 1365line 1364 didn't jump to line 1365, because the condition on line 1364 was never true

1365 raise ValueError("Cannot preset keyword argument(s), not present in the signature of %s: %s" 

1366 "" % (getattr(f, '__name__', f), preset_kwargs)) 

1367 return new_sig 

1368 

1369 

1370def gen_partial_doc(wrapped_name, wrapped_doc, orig_sig, new_sig, preset_pos_args): 

1371 """ 

1372 Generate a documentation indicating which positional arguments and keyword arguments are set in this 

1373 partial implementation, and appending the wrapped function doc. 

1374 

1375 :param wrapped_name: 

1376 :param wrapped_doc: 

1377 :param orig_sig: 

1378 :param new_sig: 

1379 :param preset_pos_args: 

1380 :return: 

1381 """ 

1382 # generate the "equivalent signature": this is the original signature, 

1383 # where all values injected by partial appear 

1384 all_strs = [] 

1385 kw_only = False 

1386 for i, (p_name, _p) in enumerate(orig_sig.parameters.items()): 

1387 if i < len(preset_pos_args): 

1388 # use the preset positional. Use repr() instead of str() so that e.g. "yes" appears with quotes 

1389 all_strs.append(repr(preset_pos_args[i])) 

1390 else: 

1391 # use the one in the new signature 

1392 pnew = new_sig.parameters[p_name] 

1393 if not kw_only: 

1394 if (PY2 and pnew.default is KW_ONLY) or pnew.kind == Parameter.KEYWORD_ONLY: 

1395 kw_only = True 

1396 

1397 if PY2 and kw_only: 1397 ↛ 1398line 1397 didn't jump to line 1398, because the condition on line 1397 was never true

1398 all_strs.append(str(pnew).replace("=%s" % KW_ONLY, "")) 

1399 else: 

1400 all_strs.append(str(pnew)) 

1401 

1402 argstring = ", ".join(all_strs) 

1403 

1404 # Write the final docstring 

1405 if wrapped_doc is None or len(wrapped_doc) == 0: 

1406 partial_doc = "<This function is equivalent to '%s(%s)'.>\n" % (wrapped_name, argstring) 

1407 else: 

1408 new_line = "<This function is equivalent to '%s(%s)', see original '%s' doc below.>\n" \ 

1409 "" % (wrapped_name, argstring, wrapped_name) 

1410 partial_doc = new_line + wrapped_doc 

1411 

1412 return partial_doc 

1413 

1414 

1415class UnsupportedForCompilation(TypeError): 

1416 """ 

1417 Exception raised by @compile_fun when decorated target is not supported 

1418 """ 

1419 pass 

1420 

1421 

1422class UndefinedSymbolError(NameError): 

1423 """ 

1424 Exception raised by @compile_fun when the function requires a name not yet defined 

1425 """ 

1426 pass 

1427 

1428 

1429class SourceUnavailable(OSError): 

1430 """ 

1431 Exception raised by @compile_fun when the function source is not available (inspect.getsource raises an error) 

1432 """ 

1433 pass 

1434 

1435 

1436def compile_fun(recurse=True, # type: Union[bool, Callable] 

1437 except_names=(), # type: Iterable[str] 

1438 ): 

1439 """ 

1440 A draft decorator to `compile` any existing function so that users cant 

1441 debug through it. It can be handy to mask some code from your users for 

1442 convenience (note that this does not provide any obfuscation, people can 

1443 still reverse engineer your code easily. Actually the source code even gets 

1444 copied in the function's `__source__` attribute for convenience): 

1445 

1446 ```python 

1447 from makefun import compile_fun 

1448 

1449 @compile_fun 

1450 def foo(a, b): 

1451 return a + b 

1452 

1453 assert foo(5, -5.0) == 0 

1454 print(foo.__source__) 

1455 ``` 

1456 

1457 yields 

1458 

1459 ``` 

1460 @compile_fun 

1461 def foo(a, b): 

1462 return a + b 

1463 ``` 

1464 

1465 If the function closure includes functions, they are recursively replaced with compiled versions too (only for 

1466 this closure, this does not modify them otherwise). 

1467 

1468 **IMPORTANT** this decorator is a "goodie" in early stage and has not been extensively tested. Feel free to 

1469 contribute ! 

1470 

1471 Note that according to [this post](https://stackoverflow.com/a/471227/7262247) compiling does not make the code 

1472 run any faster. 

1473 

1474 Known issues: `NameError` will appear if your function code depends on symbols that have not yet been defined. 

1475 Make sure all symbols exist first ! See https://github.com/smarie/python-makefun/issues/47 

1476 

1477 :param recurse: a boolean (default `True`) indicating if referenced symbols should be compiled too 

1478 :param except_names: an optional list of symbols to exclude from compilation when `recurse=True` 

1479 :return: 

1480 """ 

1481 if callable(recurse): 

1482 # called with no-args, apply immediately 

1483 target = recurse 

1484 # noinspection PyTypeChecker 

1485 return compile_fun_manually(target, _evaldict=True) 

1486 else: 

1487 # called with parenthesis, return a decorator 

1488 def apply_compile_fun(target): 

1489 return compile_fun_manually(target, recurse=recurse, except_names=except_names, _evaldict=True) 

1490 

1491 return apply_compile_fun 

1492 

1493 

1494def compile_fun_manually(target, 

1495 recurse=True, # type: Union[bool, Callable] 

1496 except_names=(), # type: Iterable[str] 

1497 _evaldict=None # type: Union[bool, Dict] 

1498 ): 

1499 """ 

1500 

1501 :param target: 

1502 :return: 

1503 """ 

1504 if not isinstance(target, FunctionType): 

1505 raise UnsupportedForCompilation("Only functions can be compiled by this decorator") 

1506 

1507 if _evaldict is None or _evaldict is True: 

1508 if _evaldict is True: 1508 ↛ 1511line 1508 didn't jump to line 1511, because the condition on line 1508 was never false

1509 frame = _get_callerframe(offset=1) 

1510 else: 

1511 frame = _get_callerframe() 

1512 _evaldict, _ = extract_module_and_evaldict(frame) 

1513 

1514 # first make sure that source code is available for compilation 

1515 try: 

1516 lines = getsource(target) 

1517 except (OSError, IOError) as e: # noqa # distinct exceptions in old python versions 

1518 if 'could not get source code' in str(e): 1518 ↛ 1521line 1518 didn't jump to line 1521, because the condition on line 1518 was never false

1519 raise SourceUnavailable(target, e) 

1520 else: 

1521 raise 

1522 

1523 # compile all references first 

1524 try: 

1525 # python 3 

1526 func_closure = target.__closure__ 

1527 func_code = target.__code__ 

1528 except AttributeError: 

1529 # python 2 

1530 func_closure = target.func_closure 

1531 func_code = target.func_code 

1532 

1533 # Does not work: if `self.i` is used in the code, `i` will appear here 

1534 # if func_code is not None: 

1535 # for name in func_code.co_names: 

1536 # try: 

1537 # eval(name, _evaldict) 

1538 # except NameError: 

1539 # raise UndefinedSymbolError("Symbol `%s` does not seem to be defined yet. Make sure you apply " 

1540 # "`compile_fun` *after* all required symbols have been defined." % name) 

1541 

1542 if recurse and func_closure is not None: 

1543 # recurse-compile 

1544 for name, cell in zip(func_code.co_freevars, func_closure): 

1545 if name in except_names: 

1546 continue 

1547 if name not in _evaldict: 

1548 raise UndefinedSymbolError("Symbol %s does not seem to be defined yet. Make sure you apply " 

1549 "`compile_fun` *after* all required symbols have been defined." % name) 

1550 try: 

1551 value = cell.cell_contents 

1552 except ValueError: 

1553 # empty cell 

1554 continue 

1555 else: 

1556 # non-empty cell 

1557 try: 

1558 # note : not sure the compilation will be made in the appropriate order of dependencies... 

1559 # if not, users will have to do it manually 

1560 _evaldict[name] = compile_fun_manually(value, 

1561 recurse=recurse, except_names=except_names, 

1562 _evaldict=_evaldict) 

1563 except (UnsupportedForCompilation, SourceUnavailable): 

1564 pass 

1565 

1566 # now compile from sources 

1567 lines = dedent(lines) 

1568 source_lines = lines 

1569 if lines.startswith('@compile_fun'): 

1570 lines = '\n'.join(lines.splitlines()[1:]) 

1571 if '@compile_fun' in lines: 1571 ↛ 1572line 1571 didn't jump to line 1572, because the condition on line 1571 was never true

1572 raise ValueError("@compile_fun seems to appear several times in the function source") 

1573 if lines[-1] != '\n': 

1574 lines += '\n' 

1575 # print("compiling: ") 

1576 # print(lines) 

1577 new_f = _make(target.__name__, (), lines, _evaldict) 

1578 new_f.__source__ = source_lines 

1579 

1580 return new_f