summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-05-06 09:02:37 (GMT)
committerGitHub <noreply@github.com>2024-05-06 09:02:37 (GMT)
commit153b3f75306b5d26e29ea157105d0fdc247ef853 (patch)
treef73ede56af175d27698ef56bec21073f98889bbc
parent716ec4bfcf1a564db9936122c442baa99f9c4a8c (diff)
downloadcpython-153b3f75306b5d26e29ea157105d0fdc247ef853.zip
cpython-153b3f75306b5d26e29ea157105d0fdc247ef853.tar.gz
cpython-153b3f75306b5d26e29ea157105d0fdc247ef853.tar.bz2
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.
-rw-r--r--Doc/reference/datamodel.rst4
-rw-r--r--Doc/whatsnew/3.13.rst5
-rw-r--r--Include/internal/pycore_global_objects_fini_generated.h1
-rw-r--r--Include/internal/pycore_global_strings.h1
-rw-r--r--Include/internal/pycore_runtime_init_generated.h1
-rw-r--r--Include/internal/pycore_unicodeobject_generated.h3
-rw-r--r--Lib/enum.py2
-rw-r--r--Lib/importlib/_bootstrap_external.py3
-rw-r--r--Lib/inspect.py83
-rwxr-xr-xLib/pydoc.py2
-rw-r--r--Lib/test/test_compile.py5
-rw-r--r--Lib/test/test_descr.py8
-rw-r--r--Lib/test/test_inspect/test_inspect.py15
-rw-r--r--Lib/test/test_metaclass.py8
-rw-r--r--Lib/typing.py2
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst2
-rw-r--r--Python/compile.c5
17 files changed, 61 insertions, 89 deletions
diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst
index f9438a1..f5e8716 100644
--- a/Doc/reference/datamodel.rst
+++ b/Doc/reference/datamodel.rst
@@ -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
---------------
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 11c3f93..558565c 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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`.)
diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index 4a6f40c..ca7355b 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -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__));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index 8332cdf..fbb2528 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -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__)
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index 103279a..508da40 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -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__), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index a180054..cc2fc15 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -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);
diff --git a/Lib/enum.py b/Lib/enum.py
index 98a49ea..5485306 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -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:
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 0a11dc9..30a8cd4 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -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
diff --git a/Lib/inspect.py b/Lib/inspect.py
index a0c80bd..84260b2 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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__
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index eaaf824..55ccf21 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -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
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index 484d72e..1f4368b 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -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
)
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 93f66a7..18144c8 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -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'])
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index d122403..82e466e 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -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;
diff --git a/Lib/test/test_metaclass.py b/Lib/test/test_metaclass.py
index 70f9c5d..b37b7de 100644
--- a/Lib/test/test_metaclass.py
+++ b/Lib/test/test_metaclass.py
@@ -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')]
>>>
diff --git a/Lib/typing.py b/Lib/typing.py
index 3f6ff49..ff0e9b8 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -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.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst
new file mode 100644
index 0000000..705a90e
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-01-17-12-36.gh-issue-118465.g3Q8iE.rst
@@ -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.
diff --git a/Python/compile.c b/Python/compile.c
index 35a7848..79f3baa 100644
--- a/Python/compile.c
+++ b/Python/compile.c
@@ -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)) {