mirror of https://github.com/python/cpython
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:
parent
716ec4bfcf
commit
153b3f7530
|
@ -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
|
||||
---------------
|
||||
|
|
|
@ -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`.)
|
||||
|
|
|
@ -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__));
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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__), \
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')]
|
||||
>>>
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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)) {
|
||||
|
|
Loading…
Reference in New Issue