Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
CodingYuno opened this issue Mar 13, 2023 · 1 comment
Labels
type-bug An unexpected behavior, bug, or error

Comments

@CodingYuno
Copy link

CodingYuno commented Mar 13, 2023

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
@CodingYuno CodingYuno added the type-bug An unexpected behavior, bug, or error label Mar 13, 2023
@carljm
Copy link
Member

carljm commented Mar 13, 2023

Thanks for the careful debugging and the report. This was already fixed in #99654 and backported to 3.10 in fe7c309 -- if you upgrade to Python 3.10.10, it should contain the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants