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