boltons icon indicating copy to clipboard operation
boltons copied to clipboard

`FunctionBuilder.from_func` not working

Open thorwhalen opened this issue 2 years ago • 1 comments

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.

thorwhalen avatar Mar 10 '22 18:03 thorwhalen

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

thorwhalen avatar Mar 10 '22 18:03 thorwhalen