Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Inspect getsource does not return the full source code for some decorated functions #102647

Closed as not planned
@CodingYuno

Description

@CodingYuno

Bug report

The inspect getsource method fails to return the full source code of a target function if it has been decorated with a decorator that has been passed an argument with its own argument of a lambda function. This only happens if the decorator argument with the lambda is not the first argument of the decorator and there is an argument before it with a bracket e.g. a class or tuple.

from inspect import getsource


def decor(*args):
    def decorator(f):
        print(getsource(f))
    return decorator


@decor(dict(fun=lambda x: x+1))  # Works
def foo1():
    pass


@decor(dict(fun=lambda x: x+1), list())  # Works
def foo2():
    pass


@decor("1", 1, 0.1, [], {}, dict(fun=lambda x: x+1), list())  # Works
def foo3():
    pass


@decor(list(), dict(fun=lambda x: x+1))  # Fails
def foo4():
    pass


@decor((1, 2), dict(fun=lambda x: x+1))  # Fails
def foo5():
    pass


@decor((), (lambda x: x+1))  # Fails
def foo6():
    pass

The first three examples above will print the correct source code however the final three examples will print only:

@decor(list(), dict(fun=lambda x: x+1))


@decor((1, 2), dict(fun=lambda x: x+1)) 


@decor((), (lambda x: x+1))

The issue is caused by the inspect class BlockFinder wrongly setting self.indecorator to False when the token ")" is passed in the stream to tokeneater. This results in the lambda being able to incorrectly raise an EndOfBlock.

Potential Solution

By tracking if a "(" token has been ran whilst self.indecorator is True and preventing self.indecorator from going False until a ")" token is passed the issue is avoided.

The updated BlockFinder class which prevents this issue:

class BlockFinder:
    """Provide a tokeneater() method to detect the end of a code block."""
    def __init__(self):
        self.indent = 0
        self.islambda = False
        self.started = False
        self.passline = False
        self.indecorator = False
        self.decoratorhasargs = False
        self.last = 1
        self.body_col0 = None
        self.decorator_open_bracket = False
        self.decorator_args_open_bracket = 0

    def tokeneater(self, type, token, srowcol, erowcol, line):
        if not self.started and not self.indecorator:
            if token == "@":
                self.indecorator = True
            elif token in ("def", "class", "lambda"):
                if token == "lambda":
                    self.islambda = True
                self.started = True
            self.passline = True
        elif token == "(":
            if self.indecorator:
                self.decoratorhasargs = True
                if self.decorator_open_bracket:
                    self.decorator_args_open_bracket += 1
                else:
                    self.decorator_open_bracket = True
        elif token == ")":
            if self.indecorator and self.decorator_args_open_bracket:
                self.decorator_args_open_bracket -= 1
            elif self.indecorator:
                self.indecorator = False
                self.decorator_open_bracket = False
                self.decoratorhasargs = False
        elif type == tokenize.NEWLINE:
            self.passline = False
            self.last = srowcol[0]
            if self.islambda:
                raise EndOfBlock
            if self.indecorator and not self.decoratorhasargs:
                self.indecorator = False
        elif self.passline:
            pass
        elif type == tokenize.INDENT:
            if self.body_col0 is None and self.started:
                self.body_col0 = erowcol[1]
            self.indent = self.indent + 1
            self.passline = True
        elif type == tokenize.DEDENT:
            self.indent = self.indent - 1
            if self.indent <= 0:
                raise EndOfBlock
        elif type == tokenize.COMMENT:
            if self.body_col0 is not None and srowcol[1] >= self.body_col0:
                self.last = srowcol[0]
        elif self.indent == 0 and type not in (tokenize.COMMENT, tokenize.NL):
            raise EndOfBlock

Your environment

  • CPython versions tested on: 3.10.8
  • Operating system and architecture: Windows / Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions