boltons
boltons copied to clipboard
`FunctionBuilder.from_func` not working
FunctionBuilder.from_func
doesn't seem to work.
def foo(a, b=2):
return a / b
f = FunctionBuilder.from_func(foo).get_func()
assert f(20) == foo(20) == 10.0 # fails
Actually f(20)
returns None
.
I looked into the code and see that the body
isn't extracted and specified when making the function.
I'd propose something like the following. Should I do a pull request?
If so, might you have your own preferred get_function_body
you'd like me to use?
from boltons.funcutils import FunctionBuilder, _inspect_iscoroutinefunction
import functools
class MyFunctionBuilder(FunctionBuilder):
@classmethod
def from_func(cls, func):
"""Create a new FunctionBuilder instance based on an existing
function. The original function will not be stored or
modified.
"""
# TODO: copy_body? gonna need a good signature regex.
# TODO: might worry about __closure__?
if not callable(func):
raise TypeError('expected callable object, not %r' % (func,))
if isinstance(func, functools.partial):
if _IS_PY2:
raise ValueError('Cannot build FunctionBuilder instances from partials in python 2.')
kwargs = {'name': func.func.__name__,
'doc': func.func.__doc__,
'module': getattr(func.func, '__module__', None), # e.g., method_descriptor
'annotations': getattr(func.func, "__annotations__", {}),
'body': get_function_body(func.func), # <-- NEW: add body
'dict': getattr(func.func, '__dict__', {})}
else:
kwargs = {'name': func.__name__,
'doc': func.__doc__,
'module': getattr(func, '__module__', None), # e.g., method_descriptor
'annotations': getattr(func, "__annotations__", {}),
'body': get_function_body(func), # <-- NEW: add body
'dict': getattr(func, '__dict__', {})}
kwargs.update(cls._argspec_to_dict(func))
if _inspect_iscoroutinefunction(func):
kwargs['is_async'] = True
return cls(**kwargs)
# We'll need a `get_function_body` function for this, something like:
import inspect
from itertools import dropwhile
def get_function_body(func):
source_lines = next(iter(inspect.getsourcelines(func)), None)
if source_lines is None:
raise ValueError(f"No source lines found func: {func}")
source_lines = dropwhile(lambda x: x.startswith('@'), source_lines)
def_line = next(source_lines).strip()
if def_line.startswith('def ') and def_line.endswith(':'):
first_line = next(source_lines)
indentation = len(first_line) - len(first_line.lstrip())
return ''.join([first_line[indentation:]] + [line[indentation:] for line in source_lines])
else:
return def_line.rsplit(':')[-1].strip()
f = MyFunctionBuilder.from_func(foo).get_func()
assert f(20) == foo(20) == 10.0 # works now