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.
This commit is contained in:
Serhiy Storchaka 2024-05-06 12:02:37 +03:00 committed by GitHub
parent 716ec4bfcf
commit 153b3f7530
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 61 additions and 89 deletions

View 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: __annotations__ (class attribute)
single: __type_params__ (class attribute) single: __type_params__ (class attribute)
single: __static_attributes__ (class attribute) single: __static_attributes__ (class attribute)
single: __firstlineno__ (class attribute)
Special attributes: Special attributes:
@ -1005,6 +1006,9 @@ Special attributes:
A tuple containing names of attributes of this class which are accessed A tuple containing names of attributes of this class which are accessed
through ``self.X`` from any function in its body. 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 Class instances
--------------- ---------------

View file

@ -328,6 +328,11 @@ Other Language Changes
class scopes are not inlined into their parent scope. (Contributed by class scopes are not inlined into their parent scope. (Contributed by
Jelle Zijlstra in :gh:`109118` and :gh:`118160`.) 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 * ``from __future__ import ...`` statements are now just normal
relative imports if dots are present before the module name. relative imports if dots are present before the module name.
(Contributed by Jeremiah Gabriel Pascual in :gh:`118216`.) (Contributed by Jeremiah Gabriel Pascual in :gh:`118216`.)

View file

@ -624,6 +624,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__eq__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__eq__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__exit__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__exit__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__file__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__file__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__firstlineno__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__float__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__float__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__floordiv__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__floordiv__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__format__)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__format__));

View file

@ -113,6 +113,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__eq__) STRUCT_FOR_ID(__eq__)
STRUCT_FOR_ID(__exit__) STRUCT_FOR_ID(__exit__)
STRUCT_FOR_ID(__file__) STRUCT_FOR_ID(__file__)
STRUCT_FOR_ID(__firstlineno__)
STRUCT_FOR_ID(__float__) STRUCT_FOR_ID(__float__)
STRUCT_FOR_ID(__floordiv__) STRUCT_FOR_ID(__floordiv__)
STRUCT_FOR_ID(__format__) STRUCT_FOR_ID(__format__)

View file

@ -622,6 +622,7 @@ extern "C" {
INIT_ID(__eq__), \ INIT_ID(__eq__), \
INIT_ID(__exit__), \ INIT_ID(__exit__), \
INIT_ID(__file__), \ INIT_ID(__file__), \
INIT_ID(__firstlineno__), \
INIT_ID(__float__), \ INIT_ID(__float__), \
INIT_ID(__floordiv__), \ INIT_ID(__floordiv__), \
INIT_ID(__format__), \ INIT_ID(__format__), \

View file

@ -180,6 +180,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
string = &_Py_ID(__file__); string = &_Py_ID(__file__);
assert(_PyUnicode_CheckConsistency(string, 1)); assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string); _PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(__firstlineno__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(__float__); string = &_Py_ID(__float__);
assert(_PyUnicode_CheckConsistency(string, 1)); assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string); _PyUnicode_InternInPlace(interp, &string);

View file

@ -2035,7 +2035,7 @@ def _test_simple_enum(checked_enum, simple_enum):
) )
for key in set(checked_keys + simple_keys): for key in set(checked_keys + simple_keys):
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__', if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__',
'__static_attributes__'): '__static_attributes__', '__firstlineno__'):
# keys known to be different, or very long # keys known to be different, or very long
continue continue
elif key in member_names: elif key in member_names:

View 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 3567 (Reimplement line number propagation by the compiler)
# Python 3.13a1 3568 (Change semantics of END_FOR) # Python 3.13a1 3568 (Change semantics of END_FOR)
# Python 3.13a5 3569 (Specialize CONTAINS_OP) # Python 3.13a5 3569 (Specialize CONTAINS_OP)
# Python 3.13a6 3570 (Add __firstlineno__ class attribute)
# Python 3.14 will start with 3600 # 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 # Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated. # 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 _RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

View file

@ -1035,79 +1035,6 @@ class ClassFoundException(Exception):
pass 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): def findsource(object):
"""Return the entire source file and starting line number for an object. """Return the entire source file and starting line number for an object.
@ -1140,11 +1067,11 @@ def findsource(object):
return lines, 0 return lines, 0
if isclass(object): if isclass(object):
qualname = object.__qualname__ try:
source = ''.join(lines) firstlineno = object.__firstlineno__
tree = ast.parse(source) except AttributeError:
class_finder = _ClassFinder(object, tree, lines, qualname) raise OSError('source code not available')
return lines, class_finder.get_lineno() return lines, object.__firstlineno__ - 1
if ismethod(object): if ismethod(object):
object = object.__func__ object = object.__func__

View file

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

View file

@ -1958,7 +1958,10 @@ class TestSourcePositions(unittest.TestCase):
def test_load_super_attr(self): def test_load_super_attr(self):
source = "class C:\n def __init__(self):\n super().__init__()" 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( self.assertOpcodeSourcePositionIs(
code, "LOAD_GLOBAL", line=3, end_line=3, column=4, end_column=9 code, "LOAD_GLOBAL", line=3, end_line=3, column=4, end_column=9
) )

View file

@ -5088,7 +5088,8 @@ class DictProxyTests(unittest.TestCase):
self.assertNotIsInstance(it, list) self.assertNotIsInstance(it, list)
keys = list(it) keys = list(it)
keys.sort() keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__', self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
'__module__',
'__static_attributes__', '__weakref__', '__static_attributes__', '__weakref__',
'meth']) 'meth'])
@ -5099,7 +5100,7 @@ class DictProxyTests(unittest.TestCase):
it = self.C.__dict__.values() it = self.C.__dict__.values()
self.assertNotIsInstance(it, list) self.assertNotIsInstance(it, list)
values = list(it) values = list(it)
self.assertEqual(len(values), 6) self.assertEqual(len(values), 7)
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(), @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__') 'trace function introduces __local__')
@ -5109,7 +5110,8 @@ class DictProxyTests(unittest.TestCase):
self.assertNotIsInstance(it, list) self.assertNotIsInstance(it, list)
keys = [item[0] for item in it] keys = [item[0] for item in it]
keys.sort() keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__', self.assertEqual(keys, ['__dict__', '__doc__', '__firstlineno__',
'__module__',
'__static_attributes__', '__weakref__', '__static_attributes__', '__weakref__',
'meth']) 'meth'])

View file

@ -817,6 +817,21 @@ class TestRetrievingSourceCode(GetSourceBase):
def test_getsource_on_code_object(self): def test_getsource_on_code_object(self):
self.assertSourceEqual(mod.eggs.__code__, 12, 18) 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): class TestGetsourceInteractive(unittest.TestCase):
def test_getclasses_interactive(self): def test_getclasses_interactive(self):
# bpo-44648: simulate a REPL session; # bpo-44648: simulate a REPL session;

View file

@ -164,6 +164,7 @@ Use a __prepare__ method that returns an instrumented dict.
... ...
d['__module__'] = 'test.test_metaclass' d['__module__'] = 'test.test_metaclass'
d['__qualname__'] = 'C' d['__qualname__'] = 'C'
d['__firstlineno__'] = 1
d['foo'] = 4 d['foo'] = 4
d['foo'] = 42 d['foo'] = 42
d['bar'] = 123 d['bar'] = 123
@ -183,12 +184,12 @@ Use a metaclass that doesn't derive from type.
... b = 24 ... b = 24
... ...
meta: C () 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: [] kw: []
>>> type(C) is dict >>> type(C) is dict
True True
>>> print(sorted(C.items())) >>> 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. And again, with a __prepare__ attribute.
@ -206,12 +207,13 @@ And again, with a __prepare__ attribute.
prepare: C () [('other', 'booh')] prepare: C () [('other', 'booh')]
d['__module__'] = 'test.test_metaclass' d['__module__'] = 'test.test_metaclass'
d['__qualname__'] = 'C' d['__qualname__'] = 'C'
d['__firstlineno__'] = 1
d['a'] = 1 d['a'] = 1
d['a'] = 2 d['a'] = 2
d['b'] = 3 d['b'] = 3
d['__static_attributes__'] = () d['__static_attributes__'] = ()
meta: C () 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')] kw: [('other', 'booh')]
>>> >>>

View file

@ -1860,7 +1860,7 @@ _SPECIAL_NAMES = frozenset({
'__abstractmethods__', '__annotations__', '__dict__', '__doc__', '__abstractmethods__', '__annotations__', '__dict__', '__doc__',
'__init__', '__module__', '__new__', '__slots__', '__init__', '__module__', '__new__', '__slots__',
'__subclasshook__', '__weakref__', '__class_getitem__', '__subclasshook__', '__weakref__', '__class_getitem__',
'__match_args__', '__static_attributes__', '__match_args__', '__static_attributes__', '__firstlineno__',
}) })
# These special attributes will be not collected as protocol members. # These special attributes will be not collected as protocol members.

View file

@ -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.

View file

@ -2502,6 +2502,11 @@ compiler_class_body(struct compiler *c, stmt_ty s, int firstlineno)
compiler_exit_scope(c); compiler_exit_scope(c);
return ERROR; 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; asdl_type_param_seq *type_params = s->v.ClassDef.type_params;
if (asdl_seq_LEN(type_params) > 0) { if (asdl_seq_LEN(type_params) > 0) {
if (!compiler_set_type_params_in_class(c, loc)) { if (!compiler_set_type_params_in_class(c, loc)) {