Skip to content

Commit 153b3f7

Browse files
gh-118465: Add __firstlineno__ attribute to class (GH-118475)
It is set by compiler with the line number of the first line of the class definition.
1 parent 716ec4b commit 153b3f7

17 files changed

+61
-89
lines changed

Doc/reference/datamodel.rst

+4
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ A class object can be called (see above) to yield a class instance (see below).
971971
single: __annotations__ (class attribute)
972972
single: __type_params__ (class attribute)
973973
single: __static_attributes__ (class attribute)
974+
single: __firstlineno__ (class attribute)
974975

975976
Special attributes:
976977

@@ -1005,6 +1006,9 @@ Special attributes:
10051006
A tuple containing names of attributes of this class which are accessed
10061007
through ``self.X`` from any function in its body.
10071008

1009+
:attr:`__firstlineno__`
1010+
The line number of the first line of the class definition, including decorators.
1011+
10081012

10091013
Class instances
10101014
---------------

Doc/whatsnew/3.13.rst

+5
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ Other Language Changes
328328
class scopes are not inlined into their parent scope. (Contributed by
329329
Jelle Zijlstra in :gh:`109118` and :gh:`118160`.)
330330

331+
* Classes have a new :attr:`!__firstlineno__` attribute,
332+
populated by the compiler, with the line number of the first line
333+
of the class definition.
334+
(Contributed by Serhiy Storchaka in :gh:`118465`.)
335+
331336
* ``from __future__ import ...`` statements are now just normal
332337
relative imports if dots are present before the module name.
333338
(Contributed by Jeremiah Gabriel Pascual in :gh:`118216`.)

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ struct _Py_global_strings {
113113
STRUCT_FOR_ID(__eq__)
114114
STRUCT_FOR_ID(__exit__)
115115
STRUCT_FOR_ID(__file__)
116+
STRUCT_FOR_ID(__firstlineno__)
116117
STRUCT_FOR_ID(__float__)
117118
STRUCT_FOR_ID(__floordiv__)
118119
STRUCT_FOR_ID(__format__)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/enum.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2035,7 +2035,7 @@ def _test_simple_enum(checked_enum, simple_enum):
20352035
)
20362036
for key in set(checked_keys + simple_keys):
20372037
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__',
2038-
'__static_attributes__'):
2038+
'__static_attributes__', '__firstlineno__'):
20392039
# keys known to be different, or very long
20402040
continue
20412041
elif key in member_names:

Lib/importlib/_bootstrap_external.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ def _write_atomic(path, data, mode=0o666):
471471
# Python 3.13a1 3567 (Reimplement line number propagation by the compiler)
472472
# Python 3.13a1 3568 (Change semantics of END_FOR)
473473
# Python 3.13a5 3569 (Specialize CONTAINS_OP)
474+
# Python 3.13a6 3570 (Add __firstlineno__ class attribute)
474475

475476
# Python 3.14 will start with 3600
476477

@@ -487,7 +488,7 @@ def _write_atomic(path, data, mode=0o666):
487488
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
488489
# in PC/launcher.c must also be updated.
489490

490-
MAGIC_NUMBER = (3569).to_bytes(2, 'little') + b'\r\n'
491+
MAGIC_NUMBER = (3570).to_bytes(2, 'little') + b'\r\n'
491492

492493
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
493494

Lib/inspect.py

+5-78
Original file line numberDiff line numberDiff line change
@@ -1035,79 +1035,6 @@ class ClassFoundException(Exception):
10351035
pass
10361036

10371037

1038-
class _ClassFinder(ast.NodeVisitor):
1039-
1040-
def __init__(self, cls, tree, lines, qualname):
1041-
self.stack = []
1042-
self.cls = cls
1043-
self.tree = tree
1044-
self.lines = lines
1045-
self.qualname = qualname
1046-
self.lineno_found = []
1047-
1048-
def visit_FunctionDef(self, node):
1049-
self.stack.append(node.name)
1050-
self.stack.append('<locals>')
1051-
self.generic_visit(node)
1052-
self.stack.pop()
1053-
self.stack.pop()
1054-
1055-
visit_AsyncFunctionDef = visit_FunctionDef
1056-
1057-
def visit_ClassDef(self, node):
1058-
self.stack.append(node.name)
1059-
if self.qualname == '.'.join(self.stack):
1060-
# Return the decorator for the class if present
1061-
if node.decorator_list:
1062-
line_number = node.decorator_list[0].lineno
1063-
else:
1064-
line_number = node.lineno
1065-
1066-
# decrement by one since lines starts with indexing by zero
1067-
self.lineno_found.append((line_number - 1, node.end_lineno))
1068-
self.generic_visit(node)
1069-
self.stack.pop()
1070-
1071-
def get_lineno(self):
1072-
self.visit(self.tree)
1073-
lineno_found_number = len(self.lineno_found)
1074-
if lineno_found_number == 0:
1075-
raise OSError('could not find class definition')
1076-
elif lineno_found_number == 1:
1077-
return self.lineno_found[0][0]
1078-
else:
1079-
# We have multiple candidates for the class definition.
1080-
# Now we have to guess.
1081-
1082-
# First, let's see if there are any method definitions
1083-
for member in self.cls.__dict__.values():
1084-
if (isinstance(member, types.FunctionType) and
1085-
member.__module__ == self.cls.__module__):
1086-
for lineno, end_lineno in self.lineno_found:
1087-
if lineno <= member.__code__.co_firstlineno <= end_lineno:
1088-
return lineno
1089-
1090-
class_strings = [(''.join(self.lines[lineno: end_lineno]), lineno)
1091-
for lineno, end_lineno in self.lineno_found]
1092-
1093-
# Maybe the class has a docstring and it's unique?
1094-
if self.cls.__doc__:
1095-
ret = None
1096-
for candidate, lineno in class_strings:
1097-
if self.cls.__doc__.strip() in candidate:
1098-
if ret is None:
1099-
ret = lineno
1100-
else:
1101-
break
1102-
else:
1103-
if ret is not None:
1104-
return ret
1105-
1106-
# We are out of ideas, just return the last one found, which is
1107-
# slightly better than previous ones
1108-
return self.lineno_found[-1][0]
1109-
1110-
11111038
def findsource(object):
11121039
"""Return the entire source file and starting line number for an object.
11131040
@@ -1140,11 +1067,11 @@ def findsource(object):
11401067
return lines, 0
11411068

11421069
if isclass(object):
1143-
qualname = object.__qualname__
1144-
source = ''.join(lines)
1145-
tree = ast.parse(source)
1146-
class_finder = _ClassFinder(object, tree, lines, qualname)
1147-
return lines, class_finder.get_lineno()
1070+
try:
1071+
firstlineno = object.__firstlineno__
1072+
except AttributeError:
1073+
raise OSError('source code not available')
1074+
return lines, object.__firstlineno__ - 1
11481075

11491076
if ismethod(object):
11501077
object = object.__func__

Lib/pydoc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def visiblename(name, all=None, obj=None):
326326
'__date__', '__doc__', '__file__', '__spec__',
327327
'__loader__', '__module__', '__name__', '__package__',
328328
'__path__', '__qualname__', '__slots__', '__version__',
329-
'__static_attributes__'}:
329+
'__static_attributes__', '__firstlineno__'}:
330330
return 0
331331
# Private names are hidden, but special names are displayed.
332332
if name.startswith('__') and name.endswith('__'): return 1

Lib/test/test_compile.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1958,7 +1958,10 @@ def test_column_offset_deduplication(self):
19581958

19591959
def test_load_super_attr(self):
19601960
source = "class C:\n def __init__(self):\n super().__init__()"
1961-
code = compile(source, "<test>", "exec").co_consts[0].co_consts[1]
1961+
for const in compile(source, "<test>", "exec").co_consts[0].co_consts:
1962+
if isinstance(const, types.CodeType):
1963+
code = const
1964+
break
19621965
self.assertOpcodeSourcePositionIs(
19631966
code, "LOAD_GLOBAL", line=3, end_line=3, column=4, end_column=9
19641967
)

Lib/test/test_descr.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -5088,7 +5088,8 @@ def test_iter_keys(self):
50885088
self.assertNotIsInstance(it, list)
50895089
keys = list(it)
50905090
keys.sort()
5091-
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
5091+
self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
5092+
'__module__',
50925093
'__static_attributes__', '__weakref__',
50935094
'meth'])
50945095

@@ -5099,7 +5100,7 @@ def test_iter_values(self):
50995100
it = self.C.__dict__.values()
51005101
self.assertNotIsInstance(it, list)
51015102
values = list(it)
5102-
self.assertEqual(len(values), 6)
5103+
self.assertEqual(len(values), 7)
51035104

51045105
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
51055106
'trace function introduces __local__')
@@ -5109,7 +5110,8 @@ def test_iter_items(self):
51095110
self.assertNotIsInstance(it, list)
51105111
keys = [item[0] for item in it]
51115112
keys.sort()
5112-
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
5113+
self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
5114+
'__module__',
51135115
'__static_attributes__', '__weakref__',
51145116
'meth'])
51155117

Lib/test/test_inspect/test_inspect.py

+15
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,21 @@ def monkey(filename, module_globals=None):
817817
def test_getsource_on_code_object(self):
818818
self.assertSourceEqual(mod.eggs.__code__, 12, 18)
819819

820+
def test_getsource_on_generated_class(self):
821+
A = type('A', (), {})
822+
self.assertEqual(inspect.getsourcefile(A), __file__)
823+
self.assertEqual(inspect.getfile(A), __file__)
824+
self.assertIs(inspect.getmodule(A), sys.modules[__name__])
825+
self.assertRaises(OSError, inspect.getsource, A)
826+
self.assertRaises(OSError, inspect.getsourcelines, A)
827+
self.assertIsNone(inspect.getcomments(A))
828+
829+
def test_getsource_on_class_without_firstlineno(self):
830+
__firstlineno__ = 1
831+
class C:
832+
nonlocal __firstlineno__
833+
self.assertRaises(OSError, inspect.getsource, C)
834+
820835
class TestGetsourceInteractive(unittest.TestCase):
821836
def test_getclasses_interactive(self):
822837
# bpo-44648: simulate a REPL session;

Lib/test/test_metaclass.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
...
165165
d['__module__'] = 'test.test_metaclass'
166166
d['__qualname__'] = 'C'
167+
d['__firstlineno__'] = 1
167168
d['foo'] = 4
168169
d['foo'] = 42
169170
d['bar'] = 123
@@ -183,12 +184,12 @@
183184
... b = 24
184185
...
185186
meta: C ()
186-
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
187+
ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
187188
kw: []
188189
>>> type(C) is dict
189190
True
190191
>>> print(sorted(C.items()))
191-
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
192+
[('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
192193
>>>
193194
194195
And again, with a __prepare__ attribute.
@@ -206,12 +207,13 @@
206207
prepare: C () [('other', 'booh')]
207208
d['__module__'] = 'test.test_metaclass'
208209
d['__qualname__'] = 'C'
210+
d['__firstlineno__'] = 1
209211
d['a'] = 1
210212
d['a'] = 2
211213
d['b'] = 3
212214
d['__static_attributes__'] = ()
213215
meta: C ()
214-
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
216+
ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
215217
kw: [('other', 'booh')]
216218
>>>
217219

Lib/typing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1860,7 +1860,7 @@ class _TypingEllipsis:
18601860
'__abstractmethods__', '__annotations__', '__dict__', '__doc__',
18611861
'__init__', '__module__', '__new__', '__slots__',
18621862
'__subclasshook__', '__weakref__', '__class_getitem__',
1863-
'__match_args__', '__static_attributes__',
1863+
'__match_args__', '__static_attributes__', '__firstlineno__',
18641864
})
18651865

18661866
# These special attributes will be not collected as protocol members.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Compiler populates the new ``__firstlineno__`` field on a class with the
2+
line number of the first line of the class definition.

Python/compile.c

+5
Original file line numberDiff line numberDiff line change
@@ -2502,6 +2502,11 @@ compiler_class_body(struct compiler *c, stmt_ty s, int firstlineno)
25022502
compiler_exit_scope(c);
25032503
return ERROR;
25042504
}
2505+
ADDOP_LOAD_CONST_NEW(c, loc, PyLong_FromLong(c->u->u_metadata.u_firstlineno));
2506+
if (compiler_nameop(c, loc, &_Py_ID(__firstlineno__), Store) < 0) {
2507+
compiler_exit_scope(c);
2508+
return ERROR;
2509+
}
25052510
asdl_type_param_seq *type_params = s->v.ClassDef.type_params;
25062511
if (asdl_seq_LEN(type_params) > 0) {
25072512
if (!compiler_set_type_params_in_class(c, loc)) {

0 commit comments

Comments
 (0)