Source code for astropy.utils.codegen
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Utilities for generating new Python code at runtime."""
import inspect
import itertools
import keyword
import os
import re
import textwrap
from .introspection import find_current_module
__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.
"""
[docs]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 isinstance(kwargs, dict):
iter_kwargs = kwargs.items()
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(f"invalid argument name: {argname}")
for item in (varargs, varkwargs):
if item is not None:
if keyword.iskeyword(item) or not _ARGNAME_RE.match(item):
raise SyntaxError(f"invalid argument name: {item}")
def_signature = [", ".join(pos_args)]
if varargs:
def_signature.append(f", *{varargs}")
call_signature = def_signature[:]
if name is None:
name = func.__name__
global_vars = {f"__{name}__func": func}
local_vars = {}
# Make local variables to handle setting the default args
for idx, item in enumerate(key_args):
key, value = item
default_var = f"_kwargs{idx}"
local_vars[default_var] = value
def_signature.append(f", {key}={default_var}")
call_signature.append(", {0}={0}".format(key))
if varkwargs:
def_signature.append(f", **{varkwargs}")
call_signature.append(f", **{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