3

Using the inspect.getsourcelines function, I have been able to get a Python function's source code like this:

import inspect    

def some_decorator(x):
    return x

@some_decorator
def foo():
    print("bar")

print(inspect.getsourcelines(foo)[0])

This code will correctly output the source lines of the function as a list:

['@some_decorator\n', 'def foo():\n', '    print("bar")\n']

However, I only want the code inside the function, not the entire function declaration. So I only want this output (noting also the correct indentation):

['print("bar")\n']

I have attempted to do this using a slice and a strip to remove the first two lines and then remove indentation, but this wouldn't work with many functions and I have to believe there's a better way.

Does the inspect module, or another module which I can pip install, have this functionality?

2
  • It should be simple by: using the amount of whitespace in the first line as base and then finding the first line which has more whitespace than that; that's the start of your function body. Optionally trim the amount of starting whitespace of that first body line from all following lines. – Do you have an example of a function where "that wouldn't work"?
    – deceze
    Commented Jun 27, 2016 at 9:50
  • @deceze That wouldn't work for a multi-line function header, where the subsequent lines can have the same indentation as the function body. Also wouldn't work for a one-liner function definition.
    – blhsing
    Commented May 8, 2024 at 4:06

6 Answers 6

5

The two solutions in the accepted answer break if the definition takes more than one line and annotations are used (since annotations introduce extra ":"). In the following I take care of that case too (but not the async case contained in the accepted answer's second function.

import inspect
from itertools import dropwhile


def get_function_body(func):
    source_lines = inspect.getsourcelines(func)[0]
    source_lines = dropwhile(lambda x: x.startswith('@'), source_lines)
    line = next(source_lines).strip()
    if not line.startswith('def '):
        return line.rsplit(':')[-1].strip()
    elif not line.endswith(':'):
        for line in source_lines:
            line = line.strip()
            if line.endswith(':'):
                break
    # Handle functions that are not one-liners  
    first_line = next(source_lines)
    # Find the indentation of the first line    
    indentation = len(first_line) - len(first_line.lstrip())
    return ''.join([first_line[indentation:]] + [line[indentation:] for line in source_lines])

For example, applied to:

# A pre comment
def f(a, b: str, c='hello', 
      d: float=0.0, *args, **kwargs) -> str:
    """The docs"""
    return f"{c} {b}: {a + d}"

print(get_function_body(f))

I get

"""The docs"""
return f"{c} {b}: {a + d}"
1
  • This fails when the function contains multi-line string literals: e.g. def xx():\n A = """\n TEST"""
    – ideasman42
    Commented May 8, 2024 at 3:40
3

You can find that the code you want all have blank before, so you can try this

print filter(lambda x:x.startswith(' '), inspect.getsourcelines(foo)[0])
2
  • That's a great way of doing it. I'm on Python 3, so I had to adapt it a little bit, but otherwise that's fantastic. Thank you! Here's the Python 3 version for anybody else: print(list(filter(lambda x:x.startswith(' '), inspect.getsourcelines(foo)[0]))) Commented Jun 27, 2016 at 9:57
  • Note that this will not work if the definition itself is indented already, or if code is indented via tabs. Commented Jun 27, 2016 at 10:02
2

You can do something like this:

import inspect
from itertools import dropwhile


def get_function_body(func):
    source_lines = inspect.getsourcelines(func)[0]
    source_lines = dropwhile(lambda x: x.startswith('@'), source_lines)
    def_line = next(source_lines).strip()
    if def_line.startswith('def ') and def_line.endswith(':'):
        # Handle functions that are not one-liners  
        first_line = next(source_lines)
        # Find the indentation of the first line    
        indentation = len(first_line) - len(first_line.lstrip())
        return ''.join([first_line[indentation:]] + [line[indentation:] for line in source_lines])
    else:
        # Handle single line functions
        return def_line.rsplit(':')[-1].strip()

Demo:

def some_decorator(x):
    return x


@some_decorator
def foo():
    print("bar")


def func():
    def inner(a, b='a:b'):
        print (100)
        a = c + d
        print ('woof!')
        def inner_inner():
            print (200)
            print ('spam!')
    return inner

def func_one_liner(): print (200); print (a, b, c)

print (get_function_body(foo))
print (get_function_body(func()))
print (get_function_body(func_one_liner))

func_one_liner = some_decorator(func_one_liner)
print (get_function_body(func_one_liner))

Output:

print("bar")

print (100)
a = c + d
print ('woof!')
def inner_inner():
    print (200)
    print ('spam!')

print (200); print (a, b, c)
print (200); print (a, b, c)

Update:

To handle async and functions with multiline argument signature get_function_body should be updated to:

import inspect
import re
from itertools import dropwhile


def get_function_body(func):
    print()
    print("{func.__name__}'s body:".format(func=func))
    source_lines = inspect.getsourcelines(func)[0]
    source_lines = dropwhile(lambda x: x.startswith('@'), source_lines)
    source = ''.join(source_lines)
    pattern = re.compile(r'(async\s+)?def\s+\w+\s*\(.*?\)\s*:\s*(.*)', flags=re.S)
    lines = pattern.search(source).group(2).splitlines()
    if len(lines) == 1:
        return lines[0]
    else:
        indentation = len(lines[1]) - len(lines[1].lstrip())
        return '\n'.join([lines[0]] + [line[indentation:] for line in lines[1:]])

Demo:

def some_decorator(x):
    return x


@some_decorator
def foo():
    print("bar")


def func():
    def inner(a, b='a:b'):
        print (100)
        a = c + d
        print ('woof!')
        def inner_inner():
            print (200)
            print ('spam!')
    return inner


def func_one_liner(): print (200); print (a, b, c)
async def async_func_one_liner(): print (200); print (a, b, c)


def multi_line_1(
    a=10,
    b=100): print (100); print (200)


def multi_line_2(
    a=10,
    b=100
    ): print (100); print (200)


def multi_line_3(
    a=10,
    b=100
    ):
    print (100 + '\n')
    print (200)

async def multi_line_4(
    a=10,
    b=100
    ):
    print (100 + '\n')
    print (200)

async def multi_line_5(
    a=10,
    b=100
    ): print (100); print (200)

def func_annotate(
    a: 'x', b: 5 + 6, c: list
    ) -> max(2, 9): print (100); print (200)


print (get_function_body(foo))
print (get_function_body(func()))
print (get_function_body(func_one_liner))
print (get_function_body(async_func_one_liner))

func_one_liner = some_decorator(func_one_liner)
print (get_function_body(func_one_liner))


@some_decorator
@some_decorator
def foo():
    print("bar")

print (get_function_body(foo))
print (get_function_body(multi_line_1))
print (get_function_body(multi_line_2))
print (get_function_body(multi_line_3))
print (get_function_body(multi_line_4))
print (get_function_body(multi_line_5))
print (get_function_body(func_annotate))

Output:

foo's body:
print("bar")

inner's body:
print (100)
a = c + d
print ('woof!')
def inner_inner():
    print (200)
    print ('spam!')

func_one_liner's body:
print (200); print (a, b, c)

async_func_one_liner's body:
print (200); print (a, b, c)

func_one_liner's body:
print (200); print (a, b, c)

foo's body:
print("bar")

multi_line_1's body:
print (100); print (200)

multi_line_2's body:
print (100); print (200)

multi_line_3's body:
print (100 + '\n')
print (200)

multi_line_4's body:
print (100 + '\n')
print (200)

multi_line_5's body:
print (100); print (200)

func_annotate's body:
print (100); print (200)
4
  • This works correctly even with one-liner functions! I have to say that I think this is the best, least-"hacky" answer. Thanks also for introducting me to itertools.dropwhile. Commented Jun 27, 2016 at 10:31
  • This fails for async def functions and multi-line function signatures.
    – deceze
    Commented Jun 27, 2016 at 11:49
  • If you want to support yet another variant: single-line definitions using type hints, e.g. def foo(bar:tuple=()) -> dict: print(bar). ;) Commented Jun 27, 2016 at 13:20
  • The two solutions above break if the definition takes more than one line and annotations are used (since annotations introduce extra ":"). I'll post, below, a get_function_body function that works in this case too.
    – thorwhalen
    Commented Jun 5, 2019 at 21:53
2

A more robust solution would be to parse the function defintion into AST, with which you can obtain the starting line number of the first statement of the function body and delete the source lines before it.

To handle one-liner functions, check if the first statement is on the first line, and slice the line from the statement's column offset:

import ast
import inspect

def get_function_body(func):
    lines = inspect.getsourcelines(func)[0]
    first = ast.parse(''.join(lines)).body[0].body[0]
    del lines[:first.lineno - 1]
    if first.lineno == 1:
        lines[0] = lines[0][first.col_offset:]
    return ''.join(lines)

so that:

def some_decorator(x):
    return x

@some_decorator
def foo(
    ):
    print("bar")
    print("baz")

def bar(): print(''
                 '')

print(get_function_body(foo))
print(get_function_body(bar))

outputs:

    print("bar")
    print("baz")

print(''
                 '')

Demo here

1
  • Nice solution. Unfortunately if the function body starts with a comment, it fails to get the comment.
    – Joe
    Commented Jun 5, 2024 at 14:47
1

Using re to handle def and async def:

def_regexp = r"^(\s*)(?:async\s+)?def foobar\s*?\:"
def get_func_code(func):
  lines = inspect.getsourcelines(foo)[0]
  for idx in range(len(lines)):  # in py2.X, use range
      def_match = re.match(line, def_regexp)
      if def_match:
          withespace_len = len(def_match.group(1))  # detect leading whitespace
          return [sline[whitespace_len:] for sline in lines[idx+1:]]

Note that this will not handle single-line definitions. One would need to match opening and closing brackets after the def and contained colons (to avoid tuples and type hints.)


Original Version:

Just look for the first line containing a def statement.

def get_func_code(func):
  lines = inspect.getsourcelines(foo)[0]
  for idx in range(len(lines)):  # in py2.X, use range
      if line.lstrip().startswith('def %s' % func.__name__) or\
         line.lstrip().startswith('async def %s' % func.__name__):  # actually should check for `r"^async\s+def\s+%s" % func.__name__` via re
          withespace_len = len(line.split('def'), 1)[0]  # detect leading whitespace
          return [sline[whitespace_len:] for sline in lines[idx+1:]]

This should safely handle both tab and space indentation, even in mixed cases.

4
  • Fails for async def functions.
    – deceze
    Commented Jun 27, 2016 at 11:50
  • @deceze Thanks, added a check for that as well (and had fun reading the PEP ^^). Commented Jun 27, 2016 at 12:17
  • I am contractually obliged to point out that it's possible for there to be multiple spaces between async and def...
    – deceze
    Commented Jun 27, 2016 at 12:18
  • @deceze Already added a note on that. ;) (If anybody knows a nicer solution than re, speak up.) Commented Jun 27, 2016 at 12:20
0

I like the approach of @Daoctor, with a couple of improvements:

  1. I combined it with the following recipe to get the indentation of each line in a rather agnostic way.
  2. We have to be careful that the functions can be nested and are therefore not always start at column 0 of the line. We can work around that, because we can also determine the indentation of the first line of the function.
  3. Similarly, we can cut that extra-indentation for all lines of the body.

Here is the function (tested):

    def get_function_body(func):
        """
        Get the body of a function
        """
        def indentation(s):
            "Get the indentation (spaces of a line)"
            return len(s) - len(s.lstrip())

        source = inspect.getsourcelines(func)[0]
        # print(source)
        # get the indentation of the first line
        line_0 = source[0]
        ind_0 = indentation(line_0)
        body = []
        for line in source[1:]:
            ind = indentation(line)
            if ind > ind_0:
                # append to the body (minus the extra indentation)
                body.append(line[ind_0:])
        return ''.join(body)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.