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: __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
---------------

View 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`.)

View 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__));

View 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__)

View 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__), \

View 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);

View 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:

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

View 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__

View File

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

View 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
)

View 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'])

View 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;

View 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')]
>>>

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

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);
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)) {