|
| 1 | +import sys |
| 2 | + |
| 3 | +from itertools import chain |
| 4 | + |
| 5 | +from decorator import FunctionMaker |
| 6 | +from inspect import isgeneratorfunction |
| 7 | + |
| 8 | +try: # python 3.3+ |
| 9 | + from inspect import signature |
| 10 | +except ImportError: |
| 11 | + from funcsigs import signature |
| 12 | + |
| 13 | +try: |
| 14 | + from decorator import iscoroutinefunction |
| 15 | +except ImportError: |
| 16 | + try: |
| 17 | + from inspect import iscoroutinefunction |
| 18 | + except ImportError: |
| 19 | + # let's assume there are no coroutine functions in old Python |
| 20 | + def iscoroutinefunction(f): |
| 21 | + return False |
| 22 | + |
| 23 | + |
| 24 | +class MyFunctionMaker(FunctionMaker): |
| 25 | + """ |
| 26 | + Overrides FunctionMaker so that additional arguments can be inserted in the resulting signature. |
| 27 | + """ |
| 28 | + |
| 29 | + def refresh_signature(self): |
| 30 | + """Update self.signature and self.shortsignature based on self.args, |
| 31 | + self.varargs, self.varkw""" |
| 32 | + allargs = list(self.args) |
| 33 | + allshortargs = list(self.args) |
| 34 | + if self.varargs: |
| 35 | + allargs.append('*' + self.varargs) |
| 36 | + allshortargs.append('*' + self.varargs) |
| 37 | + elif self.kwonlyargs: |
| 38 | + allargs.append('*') # single star syntax |
| 39 | + for a in self.kwonlyargs: |
| 40 | + allargs.append('%s=None' % a) |
| 41 | + allshortargs.append('%s=%s' % (a, a)) |
| 42 | + if self.varkw: |
| 43 | + allargs.append('**' + self.varkw) |
| 44 | + allshortargs.append('**' + self.varkw) |
| 45 | + self.signature = ', '.join(allargs) |
| 46 | + self.shortsignature = ', '.join(allshortargs) |
| 47 | + |
| 48 | + @classmethod |
| 49 | + def create(cls, obj, body, evaldict, defaults=None, |
| 50 | + doc=None, module=None, addsource=True, add_args=(), del_args=(), **attrs): |
| 51 | + """ |
| 52 | + Create a function from the strings name, signature and body. |
| 53 | + evaldict is the evaluation dictionary. If addsource is true an |
| 54 | + attribute __source__ is added to the result. The attributes attrs |
| 55 | + are added, if any. |
| 56 | +
|
| 57 | + If add_args is not empty, these arguments will be prepended to the |
| 58 | + positional arguments. |
| 59 | +
|
| 60 | + If del_args is not empty, these arguments will be removed from signature |
| 61 | + """ |
| 62 | + if isinstance(obj, str): # "name(signature)" |
| 63 | + name, rest = obj.strip().split('(', 1) |
| 64 | + signature = rest[:-1] # strip a right parens |
| 65 | + func = None |
| 66 | + else: # a function |
| 67 | + name = None |
| 68 | + signature = None |
| 69 | + func = obj |
| 70 | + self = cls(func, name, signature, defaults, doc, module) |
| 71 | + ibody = '\n'.join(' ' + line for line in body.splitlines()) |
| 72 | + caller = evaldict.get('_call_') # when called from `decorate` |
| 73 | + if caller and iscoroutinefunction(caller): |
| 74 | + body = ('async def %(name)s(%(signature)s):\n' + ibody).replace( |
| 75 | + 'return', 'return await') |
| 76 | + else: |
| 77 | + body = 'def %(name)s(%(signature)s):\n' + ibody |
| 78 | + |
| 79 | + # Handle possible signature changes |
| 80 | + sig_modded = False |
| 81 | + if len(add_args) > 0: |
| 82 | + # prepend them as positional args - hence the reversed() |
| 83 | + for arg in reversed(add_args): |
| 84 | + if arg not in self.args: |
| 85 | + self.args = [arg] + self.args |
| 86 | + sig_modded = True |
| 87 | + else: |
| 88 | + # the argument already exists in the wrapped |
| 89 | + # function, nothing to do. |
| 90 | + pass |
| 91 | + |
| 92 | + if len(del_args) > 0: |
| 93 | + # remove the args |
| 94 | + for to_remove in del_args: |
| 95 | + for where_field in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', 'kwonlydefaults'): |
| 96 | + a = getattr(self, where_field, None) |
| 97 | + if a is not None and to_remove in a: |
| 98 | + try: |
| 99 | + # list |
| 100 | + a.remove(to_remove) |
| 101 | + except AttributeError: |
| 102 | + # dict-like |
| 103 | + del a[to_remove] |
| 104 | + finally: |
| 105 | + sig_modded = True |
| 106 | + |
| 107 | + if sig_modded: |
| 108 | + self.refresh_signature() |
| 109 | + |
| 110 | + # make the function |
| 111 | + func = self.make(body, evaldict, addsource, **attrs) |
| 112 | + |
| 113 | + if sig_modded: |
| 114 | + # delete this annotation otherwise inspect.signature |
| 115 | + # will wrongly return the signature of func.__wrapped__ |
| 116 | + # instead of the signature of func |
| 117 | + func.__wrapped_with_addargs__ = func.__wrapped__ |
| 118 | + del func.__wrapped__ |
| 119 | + |
| 120 | + return func |
| 121 | + |
| 122 | + |
| 123 | +def _extract_additional_args(f_sig, add_args_names, args, kwargs, put_all_in_kwargs=False): |
| 124 | + """ |
| 125 | + Processes the arguments received by our caller so that at the end, args |
| 126 | + and kwargs only contain what is needed by f (according to f_sig). All |
| 127 | + additional arguments are returned separately, in order described by |
| 128 | + `add_args_names`. If some names in `add_args_names` are present in `f_sig`, |
| 129 | + then the arguments will appear both in the additional arguments and in |
| 130 | + *args, **kwargs. |
| 131 | +
|
| 132 | + In the end, only *args can possibly be modified by the procedure (by removing |
| 133 | + from it all additional arguments that were not in f_sig and were prepended). |
| 134 | +
|
| 135 | + So the result is a tuple (add_args, args) |
| 136 | +
|
| 137 | + :return: a tuple (add_args, args) where `add_args` are the values of |
| 138 | + arguments named in `add_args_names` in the same order ; and `args` is |
| 139 | + the positional arguments to send to the wrapped function together with |
| 140 | + kwargs (args now only contains the positional args that are required by |
| 141 | + f, without the extra ones) |
| 142 | + """ |
| 143 | + # -- first extract (and remove) the 'truly' additional ones (the ones not in the signature) |
| 144 | + add_args = [None] * len(add_args_names) |
| 145 | + for i, arg_name in enumerate(add_args_names): |
| 146 | + if arg_name not in f_sig.parameters: |
| 147 | + # remove this argument from the args and put it in the right place |
| 148 | + add_args[i] = args[0] |
| 149 | + args = args[1:] |
| 150 | + |
| 151 | + # -- then copy the ones that already exist in the signature. Thanks,inspect pkg! |
| 152 | + bound = f_sig.bind(*args, **kwargs) |
| 153 | + for i, arg_name in enumerate(add_args_names): |
| 154 | + if arg_name in f_sig.parameters: |
| 155 | + add_args[i] = bound.arguments[arg_name] |
| 156 | + |
| 157 | + # -- finally move args to kwargs of needed |
| 158 | + if put_all_in_kwargs: |
| 159 | + args = tuple() |
| 160 | + kwargs = {arg_name: bound.arguments[arg_name] for arg_name in f_sig.parameters} |
| 161 | + |
| 162 | + return add_args, args, kwargs |
| 163 | + |
| 164 | + |
| 165 | +def _wrap_caller_for_additional_args(func, caller, additional_args, removed_args): |
| 166 | + """ |
| 167 | + This internal function wraps the caller so as to handle all cases |
| 168 | + (if some additional args are already present in the signature or not) |
| 169 | + so as to ensure a consistent caller signature. |
| 170 | +
|
| 171 | + Note: as of today if removed_args is not empty, positional args can not be correctly handled so all arguments |
| 172 | + are passed as kwargs to the wrapper |
| 173 | +
|
| 174 | + :return: a new caller wrapping the caller, to be used in `decorate` |
| 175 | + """ |
| 176 | + f_sig = signature(func) |
| 177 | + |
| 178 | + # We will create a caller above the original caller in order to check |
| 179 | + # if additional_args are already present in the signature or not, and |
| 180 | + # act accordingly |
| 181 | + original_caller = caller |
| 182 | + |
| 183 | + # If we have to remove the parameters, the behaviour and signatures will be a bit different |
| 184 | + # First modify the signature so that we remove the parameters that have to be. |
| 185 | + if len(removed_args) > 0: |
| 186 | + # new_params = OrderedDict(((k, v) for k, v in f_sig.parameters.items() if k not in removed_args)) |
| 187 | + new_params = (v for k, v in f_sig.parameters.items() if k not in removed_args) |
| 188 | + f_sig = f_sig.replace(parameters=new_params) |
| 189 | + |
| 190 | + # -- then create the appropriate function signature according to |
| 191 | + # wrapped function signature assume that original_caller has all |
| 192 | + # additional args as first positional arguments, in order |
| 193 | + if not isgeneratorfunction(original_caller): |
| 194 | + def caller(f, *args, **kwargs): |
| 195 | + # Retrieve the values for additional args. |
| 196 | + add_args, args, kwargs = _extract_additional_args(f_sig, additional_args, |
| 197 | + args, kwargs, |
| 198 | + put_all_in_kwargs=(len(removed_args) > 0)) |
| 199 | + |
| 200 | + # Call the original caller |
| 201 | + # IMPORTANT : args and kwargs are passed without the double-star here! |
| 202 | + return original_caller(f, *add_args, args=args, kwargs=kwargs) |
| 203 | + else: |
| 204 | + def caller(f, *args, **kwargs): |
| 205 | + # Retrieve the value for additional args. |
| 206 | + add_args, args, kwargs = _extract_additional_args(f_sig, additional_args, |
| 207 | + args, kwargs, |
| 208 | + put_all_in_kwargs=(len(removed_args) > 0)) |
| 209 | + |
| 210 | + # Call the original caller |
| 211 | + # IMPORTANT : args and kwargs are passed without the double-star here! |
| 212 | + for res in original_caller(f, *add_args, args=args, kwargs=kwargs): |
| 213 | + yield res |
| 214 | + |
| 215 | + return caller |
| 216 | + |
| 217 | + |
| 218 | +def my_decorate(func, caller, extras=(), additional_args=(), removed_args=(), pytest_place_as=True): |
| 219 | + """ |
| 220 | + decorate(func, caller) decorates a function using a caller. |
| 221 | + If the caller is a generator function, the resulting function |
| 222 | + will be a generator function. |
| 223 | +
|
| 224 | + You can provide additional arguments with `additional_args`. In that case |
| 225 | + the caller's signature should be |
| 226 | +
|
| 227 | + `caller(f, <additional_args_in_order>, *args, **kwargs)`. |
| 228 | +
|
| 229 | + `*args, **kwargs` will always contain the arguments required by the inner |
| 230 | + function `f`. If `additional_args` contains argument names that are already |
| 231 | + present in `func`, they will be present both in <additional_args_in_order> |
| 232 | + AND in `*args, **kwargs` so that it remains easy for the `caller` both to |
| 233 | + get the additional arguments' values directly, and to call `f` with the |
| 234 | + right arguments. |
| 235 | +
|
| 236 | + Note: as of today if removed_args is not empty, positional args can not be correctly handled so all arguments |
| 237 | + are passed as kwargs to the wrapper |
| 238 | +
|
| 239 | + """ |
| 240 | + if len(additional_args) > 0: |
| 241 | + # wrap the caller so as to handle all cases |
| 242 | + # (if some additional args are already present in the signature or not) |
| 243 | + # so as to ensure a consistent caller signature |
| 244 | + caller = _wrap_caller_for_additional_args(func, caller, additional_args, removed_args) |
| 245 | + |
| 246 | + evaldict = dict(_call_=caller, _func_=func) |
| 247 | + es = '' |
| 248 | + for i, extra in enumerate(extras): |
| 249 | + ex = '_e%d_' % i |
| 250 | + evaldict[ex] = extra |
| 251 | + es += ex + ', ' |
| 252 | + |
| 253 | + if '3.5' <= sys.version < '3.6': |
| 254 | + # with Python 3.5 isgeneratorfunction returns True for all coroutines |
| 255 | + # however we know that it is NOT possible to have a generator |
| 256 | + # coroutine in python 3.5: PEP525 was not there yet |
| 257 | + generatorcaller = isgeneratorfunction( |
| 258 | + caller) and not iscoroutinefunction(caller) |
| 259 | + else: |
| 260 | + generatorcaller = isgeneratorfunction(caller) |
| 261 | + if generatorcaller: |
| 262 | + fun = MyFunctionMaker.create( |
| 263 | + func, "for res in _call_(_func_, %s%%(shortsignature)s):\n" |
| 264 | + " yield res" % es, evaldict, |
| 265 | + add_args=additional_args, del_args=removed_args, __wrapped__=func) |
| 266 | + else: |
| 267 | + fun = MyFunctionMaker.create( |
| 268 | + func, "return _call_(_func_, %s%%(shortsignature)s)" % es, |
| 269 | + evaldict, add_args=additional_args, del_args=removed_args, __wrapped__=func) |
| 270 | + if hasattr(func, '__qualname__'): |
| 271 | + fun.__qualname__ = func.__qualname__ |
| 272 | + |
| 273 | + # With this hack our decorator will be ordered correctly by pytest https://github.com/pytest-dev/pytest/issues/4429 |
| 274 | + if pytest_place_as: |
| 275 | + fun.place_as = func |
| 276 | + |
| 277 | + return fun |
0 commit comments