Skip to content

Commit 947b06c

Browse files
authored
Merge pull request #80 from andrewcleveland/main
Add support for lambda functions
2 parents 504f531 + 48d8165 commit 947b06c

2 files changed

Lines changed: 217 additions & 25 deletions

File tree

src/makefun/main.py

Lines changed: 126 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,24 @@
1111
from collections import OrderedDict
1212
from copy import copy
1313
from inspect import getsource
14+
from keyword import iskeyword
1415
from textwrap import dedent
1516
from types import FunctionType
1617

18+
19+
if sys.version_info >= (3, 0):
20+
is_identifier = str.isidentifier
21+
else:
22+
def is_identifier(string):
23+
"""
24+
Replacement for `str.isidentifier` when it is not available (e.g. on Python 2).
25+
:param string:
26+
:return:
27+
"""
28+
if len(string) == 0 or string[0].isdigit():
29+
return False
30+
return all([s.isalnum() for s in string.split("_")])
31+
1732
try: # python 3.3+
1833
from inspect import signature, Signature, Parameter
1934
except ImportError:
@@ -73,6 +88,7 @@ def create_wrapper(wrapped,
7388
add_impl=True, # type: bool
7489
doc=None, # type: str
7590
qualname=None, # type: str
91+
co_name=None, # type: str
7692
module_name=None, # type: str
7793
**attrs
7894
):
@@ -84,7 +100,8 @@ def create_wrapper(wrapped,
84100
"""
85101
return wraps(wrapped, new_sig=new_sig, prepend_args=prepend_args, append_args=append_args, remove_args=remove_args,
86102
func_name=func_name, inject_as_first_arg=inject_as_first_arg, add_source=add_source,
87-
add_impl=add_impl, doc=doc, qualname=qualname, module_name=module_name, **attrs)(wrapper)
103+
add_impl=add_impl, doc=doc, qualname=qualname, module_name=module_name, co_name=co_name,
104+
**attrs)(wrapper)
88105

89106

90107
def getattr_partial_aware(obj, att_name, *att_default):
@@ -106,6 +123,7 @@ def create_function(func_signature, # type: Union[str, Signature]
106123
add_impl=True, # type: bool
107124
doc=None, # type: str
108125
qualname=None, # type: str
126+
co_name=None, # type: str
109127
module_name=None, # type: str
110128
**attrs):
111129
"""
@@ -130,6 +148,9 @@ def create_function(func_signature, # type: Union[str, Signature]
130148
- `__annotations__` attribute is created to match the annotations in the signature.
131149
- `__doc__` attribute is copied from `func_impl.__doc__` except if overridden using `doc`
132150
- `__module__` attribute is copied from `func_impl.__module__` except if overridden using `module_name`
151+
- `__code__.co_name` (see above) defaults to the same value as the above `__name__` attribute, except when that
152+
value is not a valid Python identifier, in which case it will be `<lambda>`. It can be overridden by providing
153+
a `co_name` that is either a valid Python identifier or `<lambda>`.
133154
134155
Finally two new attributes are optionally created
135156
@@ -138,6 +159,13 @@ def create_function(func_signature, # type: Union[str, Signature]
138159
- `__func_impl__` attribute: set if `add_impl` is `True` (default), this attribute contains a pointer to
139160
`func_impl`
140161
162+
A lambda function will be created in the following cases:
163+
164+
- when `func_signature` is a `Signature` object and `func_impl` is itself a lambda function,
165+
- when the function name, either derived from a `func_signature` string, or given explicitly with `func_name`,
166+
is not a valid Python identifier, or
167+
- when the provided `co_name` is `<lambda>`.
168+
141169
:param func_signature: either a string without 'def' such as "foo(a, b: int, *args, **kwargs)" or "(a, b: int)",
142170
or a `Signature` object, for example from the output of `inspect.signature` or from the `funcsigs.signature`
143171
backport. Note that these objects can be created manually too. If the signature is provided as a string and
@@ -159,6 +187,9 @@ def create_function(func_signature, # type: Union[str, Signature]
159187
:param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will
160188
default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in
161189
`func_signature` if `func_signature` is a `str` and contains a non-empty name.
190+
:param co_name: a string representing the name to be used in the compiled code of the function. If None (default),
191+
the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the
192+
name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name.
162193
:param module_name: the name of the module to be set on the function (under __module__ ). If None (default),
163194
`func_impl.__module__` will be used.
164195
:param attrs: other keyword attributes that should be set on the function. Note that `func_impl.__dict__` is not
@@ -177,10 +208,24 @@ def create_function(func_signature, # type: Union[str, Signature]
177208
# name defaults
178209
user_provided_name = True
179210
if func_name is None:
180-
# allow None for now, we'll raise a ValueError later if needed
211+
# allow None, this will result in a lambda function being created
181212
func_name = getattr_partial_aware(func_impl, '__name__', None)
182213
user_provided_name = False
183214

215+
# co_name default
216+
user_provided_co_name = co_name is not None
217+
if not user_provided_co_name:
218+
if func_name is None:
219+
co_name = '<lambda>'
220+
else:
221+
co_name = func_name
222+
else:
223+
if not (_is_valid_func_def_name(co_name)
224+
or _is_lambda_func_name(co_name)):
225+
raise ValueError("Invalid co_name %r for created function. "
226+
"It is not possible to declare a function "
227+
"with the provided co_name." % co_name)
228+
184229
# qname default
185230
user_provided_qname = True
186231
if qualname is None:
@@ -208,25 +253,28 @@ def create_function(func_signature, # type: Union[str, Signature]
208253
func_name = func_name_from_str
209254
if not user_provided_qname:
210255
qualname = func_name
256+
if not user_provided_co_name:
257+
co_name = func_name
211258

259+
create_lambda = not _is_valid_func_def_name(co_name)
260+
261+
# if lambda, strip the name, parentheses and colon from the signature
262+
if create_lambda:
263+
name_len = len(func_name_from_str) if func_name_from_str else 0
264+
func_signature_str = func_signature_str[name_len + 1: -2]
212265
# fix the signature if needed
213-
if func_name_from_str is None:
214-
if func_name is None:
215-
raise ValueError("Invalid signature for created function: `None` function name. This "
216-
"probably happened because the decorated function %s has no __name__. You may "
217-
"wish to pass an explicit `func_name` or to complete the signature string"
218-
"with the name before the parenthesis." % func_impl)
219-
func_signature_str = func_name + func_signature_str
266+
elif func_name_from_str is None:
267+
func_signature_str = co_name + func_signature_str
220268

221269
elif isinstance(func_signature, Signature):
222270
# create the signature string
223-
if func_name is None:
224-
raise ValueError("Invalid signature for created function: `None` function name. This "
225-
"probably happened because the decorated function %s has no __name__. You may "
226-
"wish to pass an explicit `func_name` or to provide the new signature as a "
227-
"string containing the name" % func_impl)
228-
func_signature_str = get_signature_string(func_name, func_signature, evaldict)
271+
create_lambda = not _is_valid_func_def_name(co_name)
229272

273+
if create_lambda:
274+
# create signature string (or argument string in the case of a lambda function
275+
func_signature_str = get_lambda_argument_string(func_signature, evaldict)
276+
else:
277+
func_signature_str = get_signature_string(co_name, func_signature, evaldict)
230278
else:
231279
raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature))
232280

@@ -255,6 +303,11 @@ def create_function(func_signature, # type: Union[str, Signature]
255303
body = get_legacy_py_generator_body_template() % (func_signature_str, params_str)
256304
elif isasyncgenfunction(func_impl):
257305
body = "async def %s\n async for y in _func_impl_(%s):\n yield y\n" % (func_signature_str, params_str)
306+
elif create_lambda:
307+
if func_signature_str:
308+
body = "lambda_ = lambda %s: _func_impl_(%s)\n" % (func_signature_str, params_str)
309+
else:
310+
body = "lambda_ = lambda: _func_impl_(%s)\n" % (params_str)
258311
else:
259312
body = "def %s\n return _func_impl_(%s)\n" % (func_signature_str, params_str)
260313

@@ -264,7 +317,10 @@ def create_function(func_signature, # type: Union[str, Signature]
264317
# create the function by compiling code, mapping the `_func_impl_` symbol to `func_impl`
265318
protect_eval_dict(evaldict, func_name, params_names)
266319
evaldict['_func_impl_'] = func_impl
267-
f = _make(func_name, params_names, body, evaldict)
320+
if create_lambda:
321+
f = _make("lambda_", params_names, body, evaldict)
322+
else:
323+
f = _make(co_name, params_names, body, evaldict)
268324

269325
# add the source annotation if needed
270326
if add_source:
@@ -297,6 +353,24 @@ def _is_generator_func(func_impl):
297353
return isgeneratorfunction(func_impl)
298354

299355

356+
def _is_lambda_func_name(func_name):
357+
"""
358+
Return True if func_name is the name of a lambda
359+
:param func_name:
360+
:return:
361+
"""
362+
return func_name == (lambda: None).__code__.co_name
363+
364+
365+
def _is_valid_func_def_name(func_name):
366+
"""
367+
Return True if func_name is valid in a function definition.
368+
:param func_name:
369+
:return:
370+
"""
371+
return is_identifier(func_name) and not iskeyword(func_name)
372+
373+
300374
class _SymbolRef:
301375
"""
302376
A class used to protect signature default values and type hints when the local context would not be able
@@ -366,6 +440,17 @@ def get_signature_string(func_name, func_signature, evaldict):
366440
return "%s%s:" % (func_name, s)
367441

368442

443+
def get_lambda_argument_string(func_signature, evaldict):
444+
"""
445+
Returns the string to be used as arguments in a lambda function definition.
446+
If there is a non-native symbol in the defaults, it is created as a variable in the evaldict
447+
:param func_name:
448+
:param func_signature:
449+
:return:
450+
"""
451+
return get_signature_string('', func_signature, evaldict)[1:-2]
452+
453+
369454
TYPES_WITH_SAFE_REPR = (int, str, bytes, bool)
370455
# IMPORTANT note: float is not in the above list because not all floats have a repr that is valid for the
371456
# compiler: float('nan'), float('-inf') and float('inf') or float('+inf') have an invalid repr.
@@ -694,6 +779,7 @@ def wraps(wrapped_fun,
694779
append_args=None, # type: Union[str, Parameter, Iterable[Union[str, Parameter]]]
695780
remove_args=None, # type: Union[str, Iterable[str]]
696781
func_name=None, # type: str
782+
co_name=None, # type: str
697783
inject_as_first_arg=False, # type: bool
698784
add_source=True, # type: bool
699785
add_impl=True, # type: bool
@@ -774,30 +860,36 @@ def wraps(wrapped_fun,
774860
:param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will
775861
default to the one of `wrapped_fun`, or the one in `new_sig` if `new_sig` is provided as a string with a
776862
non-empty function name.
863+
:param co_name: a string representing the name to be used in the compiled code of the function. If None (default),
864+
the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the
865+
name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name.
777866
:param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the
778867
`__module__` attribute of `wrapped_fun` will be used.
779868
:param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of
780869
`wrapped_fun` is automatically copied.
781870
:return: a decorator
782871
"""
783-
func_name, func_sig, doc, qualname, module_name, all_attrs = _get_args_for_wrapping(wrapped_fun, new_sig,
784-
remove_args,
785-
prepend_args, append_args,
786-
func_name, doc,
787-
qualname, module_name, attrs)
872+
func_name, func_sig, doc, qualname, co_name, module_name, all_attrs = _get_args_for_wrapping(wrapped_fun, new_sig,
873+
remove_args,
874+
prepend_args,
875+
append_args,
876+
func_name, doc,
877+
qualname, co_name,
878+
module_name, attrs)
788879

789880
return with_signature(func_sig,
790881
func_name=func_name,
791882
inject_as_first_arg=inject_as_first_arg,
792883
add_source=add_source, add_impl=add_impl,
793884
doc=doc,
794885
qualname=qualname,
886+
co_name=co_name,
795887
module_name=module_name,
796888
**all_attrs)
797889

798890

799891
def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_args,
800-
func_name, doc, qualname, module_name, attrs):
892+
func_name, doc, qualname, co_name, module_name, attrs):
801893
"""
802894
Internal method used by @wraps and create_wrapper
803895
@@ -809,6 +901,7 @@ def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_a
809901
:param func_name:
810902
:param doc:
811903
:param qualname:
904+
:param co_name:
812905
:param module_name:
813906
:param attrs:
814907
:return:
@@ -860,6 +953,10 @@ def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_a
860953
qualname = getattr_partial_aware(wrapped, '__qualname__', None)
861954
if module_name is None:
862955
module_name = getattr_partial_aware(wrapped, '__module__', None)
956+
if co_name is None:
957+
code = getattr_partial_aware(wrapped, '__code__', None)
958+
if code is not None:
959+
co_name = code.co_name
863960

864961
# attributes: start from the wrapped dict, add '__wrapped__' if needed, and override with all attrs.
865962
all_attrs = copy(getattr_partial_aware(wrapped, '__dict__'))
@@ -874,7 +971,7 @@ def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_a
874971
all_attrs['__wrapped__'] = wrapped
875972
all_attrs.update(attrs)
876973

877-
return func_name, func_sig, doc, qualname, module_name, all_attrs
974+
return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs
878975

879976

880977
def with_signature(func_signature, # type: Union[str, Signature]
@@ -884,6 +981,7 @@ def with_signature(func_signature, # type: Union[str, Signature]
884981
add_impl=True, # type: bool
885982
doc=None, # type: str
886983
qualname=None, # type: str
984+
co_name=None, # type: str
887985
module_name=None, # type: str
888986
**attrs
889987
):
@@ -925,12 +1023,15 @@ def impl(...):
9251023
:param qualname: a string representing the qualified name to be used. If None (default), the `__qualname__` will
9261024
default to the one of `func_impl` if `func_signature` is a `Signature`, or to the name defined in
9271025
`func_signature` if `func_signature` is a `str` and contains a non-empty name.
1026+
:param co_name: a string representing the name to be used in the compiled code of the function. If None (default),
1027+
the `__code__.co_name` will default to the one of `func_impl` if `func_signature` is a `Signature`, or to the
1028+
name defined in `func_signature` if `func_signature` is a `str` and contains a non-empty name.
9281029
:param module_name: the name of the module to be set on the function (under __module__ ). If None (default), the
9291030
`__module__` attribute of the decorated function will be used.
9301031
:param attrs: other keyword attributes that should be set on the function. Note that the full `__dict__` of the
9311032
decorated function is not automatically copied.
9321033
"""
933-
if func_signature is None:
1034+
if func_signature is None and co_name is None:
9341035
# make sure that user does not provide non-default other args
9351036
if inject_as_first_arg or not add_source or not add_impl:
9361037
raise ValueError("If `func_signature=None` no new signature will be generated so only `func_name`, "
@@ -959,6 +1060,7 @@ def replace_f(f):
9591060
add_impl=add_impl,
9601061
doc=doc,
9611062
qualname=qualname,
1063+
co_name=co_name,
9621064
module_name=module_name,
9631065
_with_sig_=True, # special trick to tell create_function that we're @with_signature
9641066
**attrs

0 commit comments

Comments
 (0)