Skip to content

bpo-33211: Change line number of decorated nodes back to def #6460

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

Closed
wants to merge 19 commits into from

Conversation

emmatyping
Copy link
Member

@emmatyping emmatyping commented Apr 12, 2018

This change moves the patching of decorated defs from the publicly used AST to the private bytecode.
This has the advantage that decorated defs in the ast keep their line numbers correctly, but inspect.getsource should still work.

https://bugs.python.org/issue33211

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments. Also watch out, this PR will get merge conflicts pretty often.

Python/ast.c Outdated
thing->col_offset = n->n_col_offset;
return ast_for_async_funcdef(c, CHILD(n, 1), decorator_seq);
} else {
return NULL; // should never ever happen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe then assert 0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Py_UNREACHABLE()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still not fixed.

Python/compile.c Outdated
// fixup decorators to be the first line number internally.
if (asdl_seq_LEN(decos) > 0) {
expr_ty first_decorator = asdl_seq_GET(decos, 0);
c->u->u_firstlineno = first_decorator->lineno;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is needed? It would be a bit awkward if we externally show everywhere line of def as a first line while internally always use the decorator line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, I think it may be OK if you give a good comment with an explanation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to maintain the first line number of the code object to be the line number of the first decorator so that inspect.getsource will work correctly. The other alternative would be to change this, so that code objects point to both the line number of the def and the first source line number, but I doubt that would be useful, or worth any additional overhead that would cause. I will add a more detailed comment.

@serhiy-storchaka serhiy-storchaka self-requested a review April 22, 2018 21:11
Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about classes? Does they need correcting the first line number?

Please add test for AST line numbers.

Python/ast.c Outdated
thing->col_offset = n->n_col_offset;
return ast_for_async_funcdef(c, CHILD(n, 1), decorator_seq);
} else {
return NULL; // should never ever happen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Py_UNREACHABLE()

@@ -65,7 +65,7 @@ def cm(cls, x):
8 STORE_ATTR 0 (x)
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
""" % (_C.cm.__code__.co_firstlineno + 2,)
""" % (_C.cm.__code__.co_firstlineno + 1,)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is changed? I thought this PR shouldn't change __code__.co_firstlineno

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out. I realized that my changes weren't actually updating the first line number in the compiler! I've fixed that.

@emmatyping
Copy link
Member Author

emmatyping commented Apr 22, 2018

@serhiy-storchaka

What about classes? Does they need correcting the first line number?

Yes, I just added the same change that happens in compiler_function to compiler_class, which should be all that is needed.

Please add test for AST line numbers.

I am planning on doing so. I want things to pass existing tests in CI first however.

Python/ast.c Outdated
thing->col_offset = n->n_col_offset;
return ast_for_async_funcdef(c, CHILD(n, 1), decorator_seq);
} else {
return NULL; // should never ever happen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still not fixed.

@emmatyping
Copy link
Member Author

emmatyping commented Apr 23, 2018

Okay, looks like test_dis and test_inspect passed, so I will get to writing new tests for decorated nodes in test_ast.

Done! Ready for review.

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost ready.

Python/compile.c Outdated
@@ -1949,6 +1949,26 @@ compiler_default_arguments(struct compiler *c, arguments_ty args)
return funcflags;
}

static int corrected_firstlineno(struct compiler *c, stmt_ty s,
asdl_seq * decos)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why weird indentation on a separate line?

Copy link
Member Author

@emmatyping emmatyping Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think patchcheck messed up some of this up, sorry for the noise.

On the Py_UNREACHABLE() line, I originally had double tab indentation (I forgot to change Visual Studio's settings), and its seemed to change that to triple groups of 4 spaces, so I'm guessing when I ran patchcheck it caused other problems. I'll manually verify in future.

Python/compile.c Outdated
@@ -1988,6 +2008,10 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async)
if (!compiler_decorators(c, decos))
return 0;


int first_lineno = corrected_firstlineno(c, s, decos);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is misleading, shift left.

Python/compile.c Outdated
@@ -2050,6 +2074,8 @@ compiler_class(struct compiler *c, stmt_ty s)
if (!compiler_decorators(c, decos))
return 0;

int first_lineno = corrected_firstlineno(c, s, decos);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong indentation.

Python/compile.c Outdated

int first_lineno = corrected_firstlineno(c, s, decos);


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many empty lines

Python/compile.c Outdated
@@ -1988,6 +2008,10 @@ compiler_function(struct compiler *c, stmt_ty s, int is_async)
if (!compiler_decorators(c, decos))
return 0;


Copy link
Member

@ilevkivskyi ilevkivskyi Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many empty lines.

Python/compile.c Outdated
else {
return s->lineno;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty line is unnecessary.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worth to add a note about this change in the "Whats New" document.

@@ -43,12 +44,36 @@ def to_tuple(t):
"def f(**kwargs): pass",
# FunctionDef with all kind of args and docstring
"def f(a, b=1, c=None, d=[], e={}, *args, f=42, **kwargs): 'doc for f()'",
# Decorated FunctionDef
textwrap.dedent("""
@deco
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you use textwrap.dedent() it would clearer if add an indentation of the inner code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please increase the indentation of these fragments.

# Decorated FunctionDef
textwrap.dedent("""
@deco
def f():...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: add a space after :. And maybe use pass?

@@ -207,7 +232,9 @@ def _assertTrueorder(self, ast_node, parent_pos):
parent_pos = (ast_node.lineno, ast_node.col_offset)
for name in ast_node._fields:
value = getattr(ast_node, name)
if isinstance(value, list):
# Since decorators are stored as an attribute of a FunctionDef
# they don't follow the true order of the syntax.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a test that all decorators are before a FunctionDef?

@@ -0,0 +1,3 @@
Change the line number of decorated defs back to the def or class token in the
AST, not the first decorator. Have the change of line number happen in the
compiler.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add "Patch by *yourname"." And add your name in Misc/ACKS.

Python/ast.c Outdated
@@ -1686,19 +1685,14 @@ ast_for_decorated(struct compiling *c, const node *n)
TYPE(CHILD(n, 1)) == classdef);

if (TYPE(CHILD(n, 1)) == funcdef) {
thing = ast_for_funcdef(c, CHILD(n, 1), decorator_seq);
return ast_for_funcdef(c, CHILD(n, 1), decorator_seq);
} else if (TYPE(CHILD(n, 1)) == classdef) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually now you can remove all elses.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd think so, sadly MSVC warns that not all code paths return if there isn't an else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with Py_UNREACHABLE()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, with the Py_UNREACHABLE() it is happy. Do you want something like this?

    if (TYPE(CHILD(n, 1)) == funcdef) {
        return ast_for_funcdef(c, CHILD(n, 1), decorator_seq);
    } else if (TYPE(CHILD(n, 1)) == classdef) {
        return ast_for_classdef(c, CHILD(n, 1), decorator_seq);
    } else if (TYPE(CHILD(n, 1)) == async_funcdef) {
        return ast_for_async_funcdef(c, CHILD(n, 1), decorator_seq);
    }
    Py_UNREACHABLE();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to remove other elses too.

Python/compile.c Outdated
static int corrected_firstlineno(struct compiler *c, stmt_ty s,
asdl_seq * decos)
{
/* To keep the ability to get the relevant source of a decorated item
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Us 4 spaces for indentation.

Python/compile.c Outdated
asdl_seq * decos)
{
/* To keep the ability to get the relevant source of a decorated item
using inspect.getsource, we need to keep the first line number
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align wrapped lines to "To keep", not to "/*".

Python/compile.c Outdated
This used to be done via the AST, but it is better to hide this
internally.
*/
if (asdl_seq_LEN(decos) > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> 0 is not needed. Using just asdl_seq_LEN(decos) in the boolean context is common.

Python/compile.c Outdated
*/
if (asdl_seq_LEN(decos) > 0) {
expr_ty first_decorator = asdl_seq_GET(decos, 0);
c->u->u_firstlineno = first_decorator->lineno;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems c->u still refers to the outer scope.

Add a regression test for these cases:

def f():
    @deco
    def g(): pass
class A:
    @deco
    def m(self): pass

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems c->u still refers to the outer scope.

I'm probably missing something, but the generated AST is:

('Module',
 [('ClassDef',
   (1, 0),
   'A',
   [],
   [],
   [('FunctionDef',
     (3, 4),
     'm',
     ('arguments', [('arg', (3, 10), 'self', None)], None, [], [], None, []),
     [('Pass', (3, 17))],
     [('Name', (2, 5), 'deco', ('Load',))],
     None,
     None)],
   [],
   None)],
 None)

which seems correct. Perhaps I am misunderstanding?

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated AST is correct, the generated code is wrong. Look at f.__code__.co_firstlineno in

def f():
    @deco
    def g(): pass

and

def f():
    @deco
    class A: pass

Python/ast.c Outdated
@@ -1686,19 +1685,14 @@ ast_for_decorated(struct compiling *c, const node *n)
TYPE(CHILD(n, 1)) == classdef);

if (TYPE(CHILD(n, 1)) == funcdef) {
thing = ast_for_funcdef(c, CHILD(n, 1), decorator_seq);
return ast_for_funcdef(c, CHILD(n, 1), decorator_seq);
} else if (TYPE(CHILD(n, 1)) == classdef) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with Py_UNREACHABLE()?

@emmatyping
Copy link
Member Author

@serhiy-storchaka

The generated AST is correct, the generated code is wrong.

Thank you for catching this. I've added tests to both test_ast and test_inspect to make sure the AST and inspect.getsource don't regress.

Also, I believe I have responded to all of your feedback. Thank you!

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there are changes in Python/importlib.h and Python/importlib_external.h? I thought the purpose of this PR is changing line numbers in AST, but keeping the bytecode unchanged.

@@ -43,12 +44,36 @@ def to_tuple(t):
"def f(**kwargs): pass",
# FunctionDef with all kind of args and docstring
"def f(a, b=1, c=None, d=[], e={}, *args, f=42, **kwargs): 'doc for f()'",
# Decorated FunctionDef
textwrap.dedent("""
@deco
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please increase the indentation of these fragments.

Python/compile.c Outdated
// modify first line number for decorated nodes
if (asdl_seq_LEN(decos)) {
expr_ty first_decorator = asdl_seq_GET(decos, 0);
c->u->u_firstlineno = first_decorator->lineno;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not it set in compiler_enter_scope()?

Python/compile.c Outdated
// modify first line number for decorated nodes
if (asdl_seq_LEN(decos)) {
expr_ty first_decorator = asdl_seq_GET(decos, 0);
c->u->u_firstlineno = first_decorator->lineno;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not it set in compiler_enter_scope()?

@emmatyping
Copy link
Member Author

Why there are changes in Python/importlib.h and Python/importlib_external.h? I thought the purpose of this PR is changing line numbers in AST, but keeping the bytecode unchanged.

Yes, that is concerning. I think I will take a closer look at what is going on. W.R.T your comments on compiler_enter_scope, it seems that the modified line number actually doesn't do anything! The change needed is just c->u->u_firstlineno after the enter scope.

So based on these I think I need to figure out what is actually going on before I proceed, I still have much to learn about the compiler's intricacies.

@gvanrossum
Copy link
Member

Um, it seems you still need to act on the comment from 5 days ago?

@emmatyping
Copy link
Member Author

Um, it seems you still need to act on the comment from 5 days ago?

Ah, yes I haven't pushed anything because I haven't found out why the generated bytecode is different yet. I will once I figure things out more.

@emmatyping
Copy link
Member Author

Sorry this is temorarily stalled until I can get my computer working again...

@gvanrossum
Copy link
Member

@serhiy-storchaka What do you think? Is it ready to be merged? I don't suppose this can make it into 3.7, since IIUC the breakage is pretty noticeable.

@serhiy-storchaka
Copy link
Member

If I understand it correctly, the bytecode shouldn't be affected by this PR. But the frozen bytecode is changed, and I don't understand the cause. Either there is a bug in the code generation introduced by this PR, or my understanding is wrong.

I like this PR in general and hope Ethan and me will found the cause.

@emmatyping
Copy link
Member Author

If I understand it correctly, the bytecode shouldn't be affected by this PR. But the frozen bytecode is changed, and I don't understand the cause.

Yes, this is correct. I am going to try to spend more time today trying to understand why it changes the bytecode.

@ilevkivskyi
Copy link
Member

@ethanhs
I just tested, normal functions, coroutines, and classes all work correctly with decorators on master. Is this PR still relevant, or it can be closed?

@emmatyping
Copy link
Member Author

@ilevkivskyi Serhiy asked me to see if there was anything useful in this PR that could be pulled out in the original issue https://bugs.python.org/issue33211#msg330181 (He accidentally redid this work on his own). I don't see anything that would be beneficial that is in this PR and not in #9731, so I will close this.

@emmatyping emmatyping closed this Nov 22, 2018
@emmatyping emmatyping deleted the deflineno branch November 22, 2018 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants