File: codegen.py

package info (click to toggle)
python-astropy 1.3-8~bpo8%2B2
  • links: PTS, VCS
  • area: main
  • in suites: jessie-backports
  • size: 44,292 kB
  • sloc: ansic: 160,360; python: 137,322; sh: 11,493; lex: 7,638; yacc: 4,956; xml: 1,796; makefile: 474; cpp: 364
file content (144 lines) | stat: -rw-r--r-- 4,712 bytes parent folder | download | duplicates (2)
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