1# -*- coding: utf-8 -*- 2# Licensed under a 3-clause BSD style license - see LICENSE.rst 3"""Utilities for generating new Python code at runtime.""" 4 5 6import inspect 7import itertools 8import keyword 9import os 10import re 11import textwrap 12 13from .introspection import find_current_module 14 15 16__all__ = ['make_function_with_signature'] 17 18 19_ARGNAME_RE = re.compile(r'^[A-Za-z][A-Za-z_]*') 20""" 21Regular expression used my make_func which limits the allowed argument 22names for the created function. Only valid Python variable names in 23the ASCII range and not beginning with '_' are allowed, currently. 24""" 25 26 27def make_function_with_signature(func, args=(), kwargs={}, varargs=None, 28 varkwargs=None, name=None): 29 """ 30 Make a new function from an existing function but with the desired 31 signature. 32 33 The desired signature must of course be compatible with the arguments 34 actually accepted by the input function. 35 36 The ``args`` are strings that should be the names of the positional 37 arguments. ``kwargs`` can map names of keyword arguments to their 38 default values. It may be either a ``dict`` or a list of ``(keyword, 39 default)`` tuples. 40 41 If ``varargs`` is a string it is added to the positional arguments as 42 ``*<varargs>``. Likewise ``varkwargs`` can be the name for a variable 43 keyword argument placeholder like ``**<varkwargs>``. 44 45 If not specified the name of the new function is taken from the original 46 function. Otherwise, the ``name`` argument can be used to specify a new 47 name. 48 49 Note, the names may only be valid Python variable names. 50 """ 51 52 pos_args = [] 53 key_args = [] 54 55 if isinstance(kwargs, dict): 56 iter_kwargs = kwargs.items() 57 else: 58 iter_kwargs = iter(kwargs) 59 60 # Check that all the argument names are valid 61 for item in itertools.chain(args, iter_kwargs): 62 if isinstance(item, tuple): 63 argname = item[0] 64 key_args.append(item) 65 else: 66 argname = item 67 pos_args.append(item) 68 69 if keyword.iskeyword(argname) or not _ARGNAME_RE.match(argname): 70 raise SyntaxError(f'invalid argument name: {argname}') 71 72 for item in (varargs, varkwargs): 73 if item is not None: 74 if keyword.iskeyword(item) or not _ARGNAME_RE.match(item): 75 raise SyntaxError(f'invalid argument name: {item}') 76 77 def_signature = [', '.join(pos_args)] 78 79 if varargs: 80 def_signature.append(f', *{varargs}') 81 82 call_signature = def_signature[:] 83 84 if name is None: 85 name = func.__name__ 86 87 global_vars = {f'__{name}__func': func} 88 local_vars = {} 89 # Make local variables to handle setting the default args 90 for idx, item in enumerate(key_args): 91 key, value = item 92 default_var = f'_kwargs{idx}' 93 local_vars[default_var] = value 94 def_signature.append(f', {key}={default_var}') 95 call_signature.append(', {0}={0}'.format(key)) 96 97 if varkwargs: 98 def_signature.append(f', **{varkwargs}') 99 call_signature.append(f', **{varkwargs}') 100 101 def_signature = ''.join(def_signature).lstrip(', ') 102 call_signature = ''.join(call_signature).lstrip(', ') 103 104 mod = find_current_module(2) 105 frm = inspect.currentframe().f_back 106 107 if mod: 108 filename = mod.__file__ 109 modname = mod.__name__ 110 if filename.endswith('.pyc'): 111 filename = os.path.splitext(filename)[0] + '.py' 112 else: 113 filename = '<string>' 114 modname = '__main__' 115 116 # Subtract 2 from the line number since the length of the template itself 117 # is two lines. Therefore we have to subtract those off in order for the 118 # pointer in tracebacks from __{name}__func to point to the right spot. 119 lineno = frm.f_lineno - 2 120 121 # The lstrip is in case there were *no* positional arguments (a rare case) 122 # in any context this will actually be used... 123 template = textwrap.dedent("""{0}\ 124 def {name}({sig1}): 125 return __{name}__func({sig2}) 126 """.format('\n' * lineno, name=name, sig1=def_signature, 127 sig2=call_signature)) 128 129 code = compile(template, filename, 'single') 130 131 eval(code, global_vars, local_vars) 132 133 new_func = local_vars[name] 134 new_func.__module__ = modname 135 new_func.__doc__ = func.__doc__ 136 137 return new_func 138