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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/enum.py

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 5 additions & 78 deletions
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

Lines changed: 1 addition & 1 deletion
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

0 commit comments

Comments
 (0)