forked from astropy/astropy
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodegen.py
More file actions
144 lines (109 loc) · 4.6 KB
/
codegen.py
File metadata and controls
144 lines (109 loc) · 4.6 KB
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