Repository: cython/cython

Issue:
Cython's co_firstlineno doesn't include decorators, like normal python
Using `binding=True` fills in all the attributes to make cython function work with the inspect module, mostly. Some monkey patching is also required for it to recognize cython functions/methods as functions/methods (see at the end).

However, it turns out that python's  `co_firstlineno` starts from the first decorator that decorates the function, while cython's ` co_firstlineno` is set to the actual function start and does not include the decorator, which I think is a bug on cython's part.

You can replicate it by inspecting `co_firstlineno` for e.g. the following function in cython and python using `binding=True`.
```py
def do_nothing(f):
    return f

@do_nothing
@do_nothing
def do_something():
    pass
```

For reference, here's my inspect monkey patching:
```py
import inspect
from inspect import ismethod, isfunction

def ismethod_cython(object):
    if ismethod(object):
        return True
    if object.__class__.__name__ == 'cython_function_or_method' and hasattr(object, '__func__'):
        return True
    return False
inspect.ismethod = ismethod_cython

def isfunction_cython(object):
    if isfunction(object):
        return True
    if object.__class__.__name__ == 'cython_function_or_method' and not hasattr(object, '__func__'):
        return True
    return False
inspect.isfunction = isfunction_cython
```


Discussion:
Right. Probably easy to fix by keeping the first line number around explicitly. Maybe even by using the first decorator line number as the position of the function node right in the parser – if that doesn't break anything else.

Resolving patch:
diff --git a/Cython/Compiler/Parsing.py b/Cython/Compiler/Parsing.py
--- a/Cython/Compiler/Parsing.py
+++ b/Cython/Compiler/Parsing.py
@@ -3367,7 +3367,7 @@ def _reject_cdef_modifier_in_py(s, name):
 
 def p_def_statement(s, decorators=None, is_async_def=False):
     # s.sy == 'def'
-    pos = s.position()
+    pos = decorators[0].pos if decorators else s.position()
     # PEP 492 switches the async/await keywords on in "async def" functions
     if is_async_def:
         s.enter_async()


Test patch:
diff --git a/tests/errors/cfunc_directive_in_pyclass.pyx b/tests/errors/cfunc_directive_in_pyclass.pyx
--- a/tests/errors/cfunc_directive_in_pyclass.pyx
+++ b/tests/errors/cfunc_directive_in_pyclass.pyx
@@ -7,5 +7,5 @@ class Pyclass(object):
         pass
 
 _ERRORS = """
- 6:4: cfunc directive is not allowed here
+ 5:4: cfunc directive is not allowed here
 """
diff --git a/tests/errors/e_autotestdict.pyx b/tests/errors/e_autotestdict.pyx
--- a/tests/errors/e_autotestdict.pyx
+++ b/tests/errors/e_autotestdict.pyx
@@ -7,5 +7,5 @@ def foo():
     pass
 
 _ERRORS = u"""
-6:0: The autotestdict compiler directive is not allowed in function scope
+5:0: The autotestdict compiler directive is not allowed in function scope
 """
diff --git a/tests/errors/pure_errors.py b/tests/errors/pure_errors.py
--- a/tests/errors/pure_errors.py
+++ b/tests/errors/pure_errors.py
@@ -53,5 +53,5 @@ def pyfunc(x):  # invalid
 _ERRORS = """
 44:22: Calling gil-requiring function not allowed without gil
 45:24: Calling gil-requiring function not allowed without gil
-49:0: Python functions cannot be declared 'nogil'
+48:0: Python functions cannot be declared 'nogil'
 """
diff --git a/tests/errors/tree_assert.pyx b/tests/errors/tree_assert.pyx
--- a/tests/errors/tree_assert.pyx
+++ b/tests/errors/tree_assert.pyx
@@ -11,8 +11,8 @@ def test():
 
 
 _ERRORS = u"""
-9:0: Expected path '//ComprehensionNode' not found in result tree
-9:0: Expected path '//ComprehensionNode//FuncDefNode' not found in result tree
-9:0: Unexpected path '//NameNode' found in result tree
-9:0: Unexpected path '//SimpleCallNode' found in result tree
+5:0: Expected path '//ComprehensionNode' not found in result tree
+5:0: Expected path '//ComprehensionNode//FuncDefNode' not found in result tree
+5:0: Unexpected path '//NameNode' found in result tree
+5:0: Unexpected path '//SimpleCallNode' found in result tree
 """
diff --git a/tests/run/annotation_typing.pyx b/tests/run/annotation_typing.pyx
--- a/tests/run/annotation_typing.pyx
+++ b/tests/run/annotation_typing.pyx
@@ -242,7 +242,7 @@ _WARNINGS = """
 218:29: Ambiguous types in annotation, ignoring
 # BUG:
 46:6: 'pytypes_cpdef' redeclared
-121:0: 'struct_io' redeclared
-156:0: 'struct_convert' redeclared
-175:0: 'exception_default' redeclared
+120:0: 'struct_io' redeclared
+149:0: 'struct_convert' redeclared
+168:0: 'exception_default' redeclared
 """
diff --git a/tests/run/cyfunction.pyx b/tests/run/cyfunction.pyx
--- a/tests/run/cyfunction.pyx
+++ b/tests/run/cyfunction.pyx
@@ -391,3 +391,22 @@ cdef class TestOptimisedBuiltinMethod:
 
     def call(self, arg, obj=None):
         (obj or self).append(arg+1)  # optimistically optimised => uses fast fallback method call
+
+
+def do_nothing(f):
+    """Dummy decorator for `test_firstlineno_decorated_function`"""
+    return f
+
+
+@do_nothing
+@do_nothing
+def test_firstlineno_decorated_function():
+    """
+    check that `test_firstlineno_decorated_function` starts 5 lines below `do_nothing`
+
+    >>> test_firstlineno_decorated_function()
+    5
+    """
+    l1 = do_nothing.__code__.co_firstlineno
+    l2 = test_firstlineno_decorated_function.__code__.co_firstlineno
+    return l2 - l1