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>
5 from __future__ import print_function
6
7 import functools
8 import re
9 import sys
10 import itertools
11 from collections import OrderedDict
12 from copy import copy
13 from inspect import getsource
14 from keyword import iskeyword
15 from textwrap import dedent
16 from types import FunctionType
17
18
19 if sys.version_info >= (3, 0):
20 is_identifier = str.isidentifier
21 else:
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
32 try: # python 3.3+
33 from inspect import signature, Signature, Parameter
34 except ImportError:
35 from funcsigs import signature, Signature, Parameter
36
37 try:
38 from inspect import iscoroutinefunction
39 except ImportError:
40 # let's assume there are no coroutine functions in old Python
41 def iscoroutinefunction(f):
42 return False
43
44 try:
45 from inspect import isgeneratorfunction
46 except ImportError:
47 # assume no generator function in old Python versions
48 def isgeneratorfunction(f):
49 return False
50
51 try:
52 from inspect import isasyncgenfunction
53 except ImportError:
54 # assume no generator function in old Python versions
55 def isasyncgenfunction(f):
56 return False
57
58 try: # python 3.5+
59 from typing import Callable, Any, Union, Iterable, Dict, Tuple, Mapping
60 except ImportError:
61 pass
62
63
64 PY2 = sys.version_info < (3,)
65 if not PY2:
66 string_types = str,
67 else:
68 string_types = basestring, # noqa
69
70
71 # macroscopic signature strings checker (we do not look inside params, `signature` will do it for us)
72 FUNC_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
79 def 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
107 def 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
118 def 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:
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:
253 func_name = func_name_from_str
254 if not user_provided_qname:
255 qualname = func_name
256 if not user_provided_co_name:
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):
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):
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:
327 attrs['__source__'] = body
328
329 # add the handler if needed
330 if add_impl:
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
341 def _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):
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
356 def _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
363
364
365 def _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
374 class _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
389 def 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:
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:
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
443 def 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
454 TYPES_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
459 def _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 type(symbol) not in 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
480 def _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
501 def 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'):
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:
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
586 def 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:
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
615 def 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
639 def 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
664 def 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
690 def _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_'):
704 raise NameError('%s is overridden in\n%s' % (n, body))
705
706 if not body.endswith('\n'): # newline is needed for old Pythons
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
727 def _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:
742 kw = dict()
743
744 func.__name__ = name
745
746 if qualname is not None:
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
761 def _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
776 def 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
893 def _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:
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):
928 prepend_args = (prepend_args,)
929 else:
930 prepend_args = ()
931
932 if append_args:
933 if isinstance(append_args, string_types):
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:
946 doc = getattr(wrapped, '__doc__', None)
947 if isinstance(wrapped, functools.partial) and not has_new_sig \
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:
955 qualname = getattr_partial_aware(wrapped, '__qualname__', None)
956 if module_name is None:
957 module_name = getattr_partial_aware(wrapped, '__module__', None)
958 if co_name is None:
959 code = getattr_partial_aware(wrapped, '__code__', None)
960 if code is not None:
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
984 def 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:
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:
1050 f.__name__ = func_name
1051 if doc is not None:
1052 f.__doc__ = doc
1053 if qualname is not None:
1054 f.__qualname__ = qualname
1055 if module_name is not None:
1056 f.__module__ = module_name
1057 for k, v in attrs.items():
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
1079 def 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
1094 def 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):
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):
1135 # Create a Parameter with auto-guessed 'kind'
1136 if first_param_kind is None:
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):
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:
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:
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):
1169 # Create a Parameter with auto-guessed 'kind'
1170 if last_param_kind is None:
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):
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:
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:
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
1201 def 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
1214 def 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):
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
1265 if PY2:
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()
1278 else:
1279 KW_ONLY = None
1280
1281
1282 def 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):
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:
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:
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):
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:
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
1370 def 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:
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
1415 class UnsupportedForCompilation(TypeError):
1416 """
1417 Exception raised by @compile_fun when decorated target is not supported
1418 """
1419 pass
1420
1421
1422 class UndefinedSymbolError(NameError):
1423 """
1424 Exception raised by @compile_fun when the function requires a name not yet defined
1425 """
1426 pass
1427
1428
1429 class 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
1436 def 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
1494 def 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:
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):
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:
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