]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-118465: Add __firstlineno__ attribute to class (GH-118475)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 6 May 2024 09:02:37 +0000 (12:02 +0300)
committerGitHub <noreply@github.com>
Mon, 6 May 2024 09:02:37 +0000 (12:02 +0300)
It is set by compiler with the line number of the first line of
the class definition.

17 files changed:
Doc/reference/datamodel.rst
Doc/whatsnew/3.13.rst
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/enum.py
Lib/importlib/_bootstrap_external.py
Lib/inspect.py
Lib/pydoc.py
Lib/test/test_compile.py
Lib/test/test_descr.py
Lib/test/test_inspect/test_inspect.py
Lib/test/test_metaclass.py
Lib/typing.py
Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst [new file with mode: 0644]
Python/compile.c

index f9438a141657be1557a7e28ccc60af47a237619b..f5e87160732056862e82230ec94063c7359ab7a0 100644 (file)
@@ -971,6 +971,7 @@ A class object can be called (see above) to yield a class instance (see below).
    single: __annotations__ (class attribute)
    single: __type_params__ (class attribute)
    single: __static_attributes__ (class attribute)
+   single: __firstlineno__ (class attribute)
 
 Special attributes:
 
@@ -1005,6 +1006,9 @@ Special attributes:
       A tuple containing names of attributes of this class which are accessed
       through ``self.X`` from any function in its body.
 
+   :attr:`__firstlineno__`
+      The line number of the first line of the class definition, including decorators.
+
 
 Class instances
 ---------------
index 11c3f931909a3470f27e7b1ba6b6ad8f868ed3b8..558565ccbbeeb51956321d3c53b0fec1f94724fd 100644 (file)
@@ -328,6 +328,11 @@ Other Language Changes
   class scopes are not inlined into their parent scope. (Contributed by
   Jelle Zijlstra in :gh:`109118` and :gh:`118160`.)
 
+* Classes have a new :attr:`!__firstlineno__` attribute,
+  populated by the compiler, with the line number of the first line
+  of the class definition.
+  (Contributed by Serhiy Storchaka in :gh:`118465`.)
+
 * ``from __future__ import ...`` statements are now just normal
   relative imports if dots are present before the module name.
   (Contributed by Jeremiah Gabriel Pascual in :gh:`118216`.)
index 4a6f40c84088e84a0fc2005a8dcbc222d78f3d85..ca7355b2b61aa75d5edbecb79693394ae50fbf65 100644 (file)
@@ -624,6 +624,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__eq__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__exit__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__file__));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__firstlineno__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__float__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__floordiv__));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__format__));
index 8332cdf874c0c988f7ef16e519b0e74cccf29621..fbb25285f0f282a3ac733977377d15c0f8089c3b 100644 (file)
@@ -113,6 +113,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(__eq__)
         STRUCT_FOR_ID(__exit__)
         STRUCT_FOR_ID(__file__)
+        STRUCT_FOR_ID(__firstlineno__)
         STRUCT_FOR_ID(__float__)
         STRUCT_FOR_ID(__floordiv__)
         STRUCT_FOR_ID(__format__)
index 103279a4cf228b1996582b23d214c59805c8e831..508da40c53422d8d9213c5335c990875355ac9dc 100644 (file)
@@ -622,6 +622,7 @@ extern "C" {
     INIT_ID(__eq__), \
     INIT_ID(__exit__), \
     INIT_ID(__file__), \
+    INIT_ID(__firstlineno__), \
     INIT_ID(__float__), \
     INIT_ID(__floordiv__), \
     INIT_ID(__format__), \
index a180054d407b39a6a15483b518c2613d56e7848e..cc2fc15ac5cabfff5ff347d5e11c6daaac73f76a 100644 (file)
@@ -180,6 +180,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     string = &_Py_ID(__file__);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
+    string = &_Py_ID(__firstlineno__);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    _PyUnicode_InternInPlace(interp, &string);
     string = &_Py_ID(__float__);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
index 98a49eafbb98973bf1737a571f685390cffdaea7..5485306c18843b271f3e27d8913261f8b7e03eea 100644 (file)
@@ -2035,7 +2035,7 @@ def _test_simple_enum(checked_enum, simple_enum):
                 )
         for key in set(checked_keys + simple_keys):
             if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__',
-                       '__static_attributes__'):
+                       '__static_attributes__', '__firstlineno__'):
                 # keys known to be different, or very long
                 continue
             elif key in member_names:
index 0a11dc9efc252c60d5f4b71a03acb593962d9e35..30a8cd4475a104e4f9ae96346fbea654940509b9 100644 (file)
@@ -471,6 +471,7 @@ _code_type = type(_write_atomic.__code__)
 #     Python 3.13a1 3567 (Reimplement line number propagation by the compiler)
 #     Python 3.13a1 3568 (Change semantics of END_FOR)
 #     Python 3.13a5 3569 (Specialize CONTAINS_OP)
+#     Python 3.13a6 3570 (Add __firstlineno__ class attribute)
 
 #     Python 3.14 will start with 3600
 
@@ -487,7 +488,7 @@ _code_type = type(_write_atomic.__code__)
 # Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
 # in PC/launcher.c must also be updated.
 
-MAGIC_NUMBER = (3569).to_bytes(2, 'little') + b'\r\n'
+MAGIC_NUMBER = (3570).to_bytes(2, 'little') + b'\r\n'
 
 _RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little')  # For import.c
 
index a0c80bd5c8b6014b5ea03ce36deb5eb09abacfc3..84260b251a4fb893bf72f06204db12fb51565577 100644 (file)
@@ -1035,79 +1035,6 @@ class ClassFoundException(Exception):
     pass
 
 
-class _ClassFinder(ast.NodeVisitor):
-
-    def __init__(self, cls, tree, lines, qualname):
-        self.stack = []
-        self.cls = cls
-        self.tree = tree
-        self.lines = lines
-        self.qualname = qualname
-        self.lineno_found = []
-
-    def visit_FunctionDef(self, node):
-        self.stack.append(node.name)
-        self.stack.append('<locals>')
-        self.generic_visit(node)
-        self.stack.pop()
-        self.stack.pop()
-
-    visit_AsyncFunctionDef = visit_FunctionDef
-
-    def visit_ClassDef(self, node):
-        self.stack.append(node.name)
-        if self.qualname == '.'.join(self.stack):
-            # Return the decorator for the class if present
-            if node.decorator_list:
-                line_number = node.decorator_list[0].lineno
-            else:
-                line_number = node.lineno
-
-            # decrement by one since lines starts with indexing by zero
-            self.lineno_found.append((line_number - 1, node.end_lineno))
-        self.generic_visit(node)
-        self.stack.pop()
-
-    def get_lineno(self):
-        self.visit(self.tree)
-        lineno_found_number = len(self.lineno_found)
-        if lineno_found_number == 0:
-            raise OSError('could not find class definition')
-        elif lineno_found_number == 1:
-            return self.lineno_found[0][0]
-        else:
-            # We have multiple candidates for the class definition.
-            # Now we have to guess.
-
-            # First, let's see if there are any method definitions
-            for member in self.cls.__dict__.values():
-                if (isinstance(member, types.FunctionType) and
-                    member.__module__ == self.cls.__module__):
-                    for lineno, end_lineno in self.lineno_found:
-                        if lineno <= member.__code__.co_firstlineno <= end_lineno:
-                            return lineno
-
-            class_strings = [(''.join(self.lines[lineno: end_lineno]), lineno)
-                             for lineno, end_lineno in self.lineno_found]
-
-            # Maybe the class has a docstring and it's unique?
-            if self.cls.__doc__:
-                ret = None
-                for candidate, lineno in class_strings:
-                    if self.cls.__doc__.strip() in candidate:
-                        if ret is None:
-                            ret = lineno
-                        else:
-                            break
-                else:
-                    if ret is not None:
-                        return ret
-
-            # We are out of ideas, just return the last one found, which is
-            # slightly better than previous ones
-            return self.lineno_found[-1][0]
-
-
 def findsource(object):
     """Return the entire source file and starting line number for an object.
 
@@ -1140,11 +1067,11 @@ def findsource(object):
         return lines, 0
 
     if isclass(object):
-        qualname = object.__qualname__
-        source = ''.join(lines)
-        tree = ast.parse(source)
-        class_finder = _ClassFinder(object, tree, lines, qualname)
-        return lines, class_finder.get_lineno()
+        try:
+            firstlineno = object.__firstlineno__
+        except AttributeError:
+            raise OSError('source code not available')
+        return lines, object.__firstlineno__ - 1
 
     if ismethod(object):
         object = object.__func__
index eaaf8249b205bc5ea4dada1718f6cb672a8352b7..55ccf2152c26cb1aa27f69d8d4fb154ff281a711 100755 (executable)
@@ -326,7 +326,7 @@ def visiblename(name, all=None, obj=None):
                 '__date__', '__doc__', '__file__', '__spec__',
                 '__loader__', '__module__', '__name__', '__package__',
                 '__path__', '__qualname__', '__slots__', '__version__',
-                '__static_attributes__'}:
+                '__static_attributes__', '__firstlineno__'}:
         return 0
     # Private names are hidden, but special names are displayed.
     if name.startswith('__') and name.endswith('__'): return 1
index 484d72e63411a80a971140067f5b1157252118f5..1f4368b15f473c7479e79a07f4d289a8ef3ed957 100644 (file)
@@ -1958,7 +1958,10 @@ class TestSourcePositions(unittest.TestCase):
 
     def test_load_super_attr(self):
         source = "class C:\n  def __init__(self):\n    super().__init__()"
-        code = compile(source, "<test>", "exec").co_consts[0].co_consts[1]
+        for const in compile(source, "<test>", "exec").co_consts[0].co_consts:
+            if isinstance(const, types.CodeType):
+                code = const
+                break
         self.assertOpcodeSourcePositionIs(
             code, "LOAD_GLOBAL", line=3, end_line=3, column=4, end_column=9
         )
index 93f66a721e81087d41d8f75ba3b43e3f7112b236..18144c8cbb2f0af23487aea7d62be8017c76ea51 100644 (file)
@@ -5088,7 +5088,8 @@ class DictProxyTests(unittest.TestCase):
         self.assertNotIsInstance(it, list)
         keys = list(it)
         keys.sort()
-        self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
+        self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
+                                '__module__',
                                 '__static_attributes__', '__weakref__',
                                 'meth'])
 
@@ -5099,7 +5100,7 @@ class DictProxyTests(unittest.TestCase):
         it = self.C.__dict__.values()
         self.assertNotIsInstance(it, list)
         values = list(it)
-        self.assertEqual(len(values), 6)
+        self.assertEqual(len(values), 7)
 
     @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
                         'trace function introduces __local__')
@@ -5109,7 +5110,8 @@ class DictProxyTests(unittest.TestCase):
         self.assertNotIsInstance(it, list)
         keys = [item[0] for item in it]
         keys.sort()
-        self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
+        self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
+                                '__module__',
                                 '__static_attributes__', '__weakref__',
                                 'meth'])
 
index d12240353ff8324bfbb666a8481b70cd3ff3cf49..82e466e978624fb62544dd76755811a58a7e3b48 100644 (file)
@@ -817,6 +817,21 @@ class TestRetrievingSourceCode(GetSourceBase):
     def test_getsource_on_code_object(self):
         self.assertSourceEqual(mod.eggs.__code__, 12, 18)
 
+    def test_getsource_on_generated_class(self):
+        A = type('A', (), {})
+        self.assertEqual(inspect.getsourcefile(A), __file__)
+        self.assertEqual(inspect.getfile(A), __file__)
+        self.assertIs(inspect.getmodule(A), sys.modules[__name__])
+        self.assertRaises(OSError, inspect.getsource, A)
+        self.assertRaises(OSError, inspect.getsourcelines, A)
+        self.assertIsNone(inspect.getcomments(A))
+
+    def test_getsource_on_class_without_firstlineno(self):
+        __firstlineno__ = 1
+        class C:
+            nonlocal __firstlineno__
+        self.assertRaises(OSError, inspect.getsource, C)
+
 class TestGetsourceInteractive(unittest.TestCase):
     def test_getclasses_interactive(self):
         # bpo-44648: simulate a REPL session;
index 70f9c5d9400bf6101dc2b3643fe495a63728fd47..b37b7defe84d1cc09e2f4b0aeafff46c7a0e5804 100644 (file)
@@ -164,6 +164,7 @@ Use a __prepare__ method that returns an instrumented dict.
     ...
     d['__module__'] = 'test.test_metaclass'
     d['__qualname__'] = 'C'
+    d['__firstlineno__'] = 1
     d['foo'] = 4
     d['foo'] = 42
     d['bar'] = 123
@@ -183,12 +184,12 @@ Use a metaclass that doesn't derive from type.
     ...     b = 24
     ...
     meta: C ()
-    ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
+    ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
     kw: []
     >>> type(C) is dict
     True
     >>> print(sorted(C.items()))
-    [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
+    [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
     >>>
 
 And again, with a __prepare__ attribute.
@@ -206,12 +207,13 @@ And again, with a __prepare__ attribute.
     prepare: C () [('other', 'booh')]
     d['__module__'] = 'test.test_metaclass'
     d['__qualname__'] = 'C'
+    d['__firstlineno__'] = 1
     d['a'] = 1
     d['a'] = 2
     d['b'] = 3
     d['__static_attributes__'] = ()
     meta: C ()
-    ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
+    ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
     kw: [('other', 'booh')]
     >>>
 
index 3f6ff491e7b918ce2e9297cd65b412fccd5c8c38..ff0e9b811a58670a888e959d41f957c4fd56e6d3 100644 (file)
@@ -1860,7 +1860,7 @@ _SPECIAL_NAMES = frozenset({
     '__abstractmethods__', '__annotations__', '__dict__', '__doc__',
     '__init__', '__module__', '__new__', '__slots__',
     '__subclasshook__', '__weakref__', '__class_getitem__',
-    '__match_args__', '__static_attributes__',
+    '__match_args__', '__static_attributes__', '__firstlineno__',
 })
 
 # These special attributes will be not collected as protocol members.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst
new file mode 100644 (file)
index 0000000..705a90e
--- /dev/null
@@ -0,0 +1,2 @@
+Compiler populates the new ``__firstlineno__`` field on a class with the
+line number of the first line of the class definition.
index 35a7848f021c3146e2e1841f3534bfbcb4a89d75..79f3baadca6b4adaff6c387f073c39234b99e57e 100644 (file)
@@ -2502,6 +2502,11 @@ compiler_class_body(struct compiler *c, stmt_ty s, int firstlineno)
         compiler_exit_scope(c);
         return ERROR;
     }
+    ADDOP_LOAD_CONST_NEW(c, loc, PyLong_FromLong(c->u->u_metadata.u_firstlineno));
+    if (compiler_nameop(c, loc, &_Py_ID(__firstlineno__), Store) < 0) {
+        compiler_exit_scope(c);
+        return ERROR;
+    }
     asdl_type_param_seq *type_params = s->v.ClassDef.type_params;
     if (asdl_seq_LEN(type_params) > 0) {
         if (!compiler_set_type_params_in_class(c, loc)) {