Coverage for src/makefun/main.py: 83%
554 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-09-26 12:39 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-09-26 12:39 +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
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
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("_")])
32try: # python 3.3+
33 from inspect import signature, Signature, Parameter
34except ImportError:
35 from funcsigs import signature, Signature, Parameter
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
44try:
45 from inspect import isgeneratorfunction
46except ImportError:
47 # assume no generator function in old Python versions
48 def isgeneratorfunction(f):
49 return False
51try:
52 from inspect import isasyncgenfunction
53except ImportError:
54 # assume no generator function in old Python versions
55 def isasyncgenfunction(f):
56 return False
58try: # python 3.5+
59 from typing import Callable, Any, Union, Iterable, Dict, Tuple, Mapping
60except ImportError:
61 pass
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
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)
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)`.
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)
107def getattr_partial_aware(obj, att_name, *att_default):
108 """ Same as getattr but recurses in obj.func if obj is a partial """
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
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).
135 `func_signature` can be provided in different formats:
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.
144 All the other metadata of the created function are defined as follows:
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>`.
155 Finally two new attributes are optionally created
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`
162 A lambda function will be created in the following cases:
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>`.
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)
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
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)
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
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.
241 # module name default
242 if module_name is None:
243 module_name = getattr_partial_aware(func_impl, '__module__', None)
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)
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
259 create_lambda = not _is_valid_func_def_name(co_name)
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
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)
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))
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())
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)
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)
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)
314 if iscoroutinefunction(func_impl):
315 body = ("async " + body).replace('return _func_impl_', 'return await _func_impl_')
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)
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
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
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)
338 return f
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)
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
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)
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'
382 def __init__(self, varname):
383 self.varname = varname
385 def __repr__(self):
386 return self.varname
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)
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)
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)
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
422 new_params.append(p)
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)
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
439 # return the final string representation
440 return "%s%s:" % (func_name, s)
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]
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.
459def _signature_symbol_needs_protection(symbol, evaldict):
460 """
461 Helper method for signature symbols (defaults, type hints) protection.
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.
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
480def _protect_signature_symbol(val, needs_protection, varname, evaldict):
481 """
482 Helper method for signature symbols (defaults, type hints) protection.
484 Returns either `val`, or a protection symbol. In that case the protection symbol
485 is created with name `varname` and inserted into `evaldict`
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
501def get_signature_from_string(func_sig_str, evaldict):
502 """
503 Creates a `Signature` object from the given function signature string.
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:]
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()
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)
530 # find the keyword parameters and the others
531 # posonly_names, kwonly_names, unrestricted_names = separate_positional_and_kw(params_names)
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 + ':'
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)
545 # return its signature
546 return func_name, signature(dummy_f), func_sig_str
549# def extract_params_names(params_str):
550# return [m.groupdict()['name'] for m in PARAM_DEF.finditer(params_str)]
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
586def get_signature_params(s):
587 """
588 Utility method to return the parameter names in the provided `Signature` object, by group of kind
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)
612 return params_to_assignment_mode
615def get_signature_details(s):
616 """
617 Utility method to extract the annotations, defaults and kwdefaults from a `Signature` object
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
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
644 :param frame:
645 :return:
646 """
647 try:
648 # get the module name
649 module_name = frame.f_globals.get('__name__', '?')
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)
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()
661 return evaldict, module_name
664def protect_eval_dict(evaldict, func_name, params_names):
665 """
666 remove all symbols that could be harmful in evaldict
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
683 return evaldict
686# Atomic get-and-increment provided by the GIL
687_compile_count = itertools.count()
690def _make(funcname, params_names, body, evaldict=None):
691 """
692 Make a new function from a given template and update the signature
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))
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")
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
721 # extract the function from compiled code
722 func = evaldict[funcname]
724 return func
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
733 This method merely exists to remind which field have to be filled.
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()
744 func.__name__ = name
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
749 func.__doc__ = doc
750 func.__dict__ = kw
752 func.__defaults__ = defaults
753 if len(kwonlydefaults) == 0:
754 kwonlydefaults = None
755 func.__kwdefaults__ = kwonlydefaults
757 func.__annotations__ = annotations
758 func.__module__ = module
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
770 except AttributeError: # for IronPython and similar implementations
771 frame = None
773 return frame
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.
794 It is similar to `functools.wraps`, but
796 - relies on a proper dynamically-generated function. Therefore as opposed to `functools.wraps`,
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.
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.
807 Comparison with `@with_signature`:`@wraps(f)` is equivalent to
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)`
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`.
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__`.
828 See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps)
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)
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)
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
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
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 = ()
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 = ()
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)
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
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
979 all_attrs.update(attrs)
981 return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs
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.
998 ```python
999 @with_signature(<arguments>)
1000 def impl(...):
1001 ...
1002 ```
1004 is totally equivalent to `impl = create_function(<arguments>, func_impl=impl)` except for one additional behaviour:
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.
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 )
1076 return replace_f
1079def remove_signature_parameters(s,
1080 *param_names):
1081 """
1082 Removes the provided parameters from the signature `s` (returns a new `Signature` instance).
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())
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).
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())
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)
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
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)
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
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)
1198 return s.replace(parameters=lst)
1201def with_partial(*preset_pos_args, **preset_kwargs):
1202 """
1203 Decorator to 'partialize' a function using `partial`
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
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.
1222 See [documentation](./index.md#removing-parameters-easily) for details.
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?
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
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)
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)
1259 # Set the func attribute as `functools.partial` does
1260 partial_f.func = f
1262 return partial_f
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)
1274 def __repr__(self):
1275 return "KW_ONLY_ARG!"
1277 KW_ONLY = KwOnly()
1278else:
1279 KW_ONLY = None
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.
1291 By default the behaviour is the same as `functools.partial`:
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")
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)
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)))
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
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)
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.")
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)
1355 # from now on, all other parameters need to be keyword-only
1356 kwonly_flag = True
1358 # preserve order
1359 new_params.append(p)
1361 new_sig = Signature(parameters=tuple(new_params),
1362 return_annotation=orig_sig.return_annotation)
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
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.
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
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))
1402 argstring = ", ".join(all_strs)
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
1412 return partial_doc
1415class UnsupportedForCompilation(TypeError):
1416 """
1417 Exception raised by @compile_fun when decorated target is not supported
1418 """
1419 pass
1422class UndefinedSymbolError(NameError):
1423 """
1424 Exception raised by @compile_fun when the function requires a name not yet defined
1425 """
1426 pass
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
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):
1446 ```python
1447 from makefun import compile_fun
1449 @compile_fun
1450 def foo(a, b):
1451 return a + b
1453 assert foo(5, -5.0) == 0
1454 print(foo.__source__)
1455 ```
1457 yields
1459 ```
1460 @compile_fun
1461 def foo(a, b):
1462 return a + b
1463 ```
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).
1468 **IMPORTANT** this decorator is a "goodie" in early stage and has not been extensively tested. Feel free to
1469 contribute !
1471 Note that according to [this post](https://stackoverflow.com/a/471227/7262247) compiling does not make the code
1472 run any faster.
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
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)
1491 return apply_compile_fun
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 """
1501 :param target:
1502 :return:
1503 """
1504 if not isinstance(target, FunctionType):
1505 raise UnsupportedForCompilation("Only functions can be compiled by this decorator")
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)
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
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
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)
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
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
1580 return new_f