8000 gh-118465: Add __firstlineno__ attribute to class by serhiy-storchaka · Pull Request #118475 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-118465: Add __firstlineno__ attribute to class #118475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@
single: __annotations__ (class attribute)
single: __type_params__ (class attribute)
single: __static_attributes__ (class attribute)
single: __firstlineno__ (class attribute)

Special attributes:

Expand Down Expand Up @@ -1005,6 +1006,9 @@
A tuple containing names of attributes of this class which are accessed
through ``self.X`` from any function in its body.

:attr:`__firstlineno__`

Check warning on line 1009 in Doc/reference/datamodel.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:attr reference target not found: __firstlineno__
The line number of the first line of the class definition, including decorators.


Class instances
---------------
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.13.rst
8000
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the class has decorators then I think this is the first line of the first decorator. Need a test for this case, and probably to mention in the doc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing tests, and they are passed.

I'll add explicit mentioning of this fact, but AFAIK it was not specified for co_firstlineno etc.

(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`.)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
8000
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ def _write_atomic(path, data, mode=0o666):
# 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

Expand All @@ -487,7 +488,7 @@ def _write_atomic(path, data, mode=0o666):
# 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

Expand Down
83 changes: 5 additions & 78 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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__
Expand Down
2 changes: 1 addition & 1 deletion Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,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
Expand Down
5 changes: 4 additions & 1 deletion Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1958,7 +1958,10 @@ def test_column_offset_deduplication(self):

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
)
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5088,7 +5088,8 @@ def test_iter_keys(self):
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'])

Expand All @@ -5099,7 +5100,7 @@ def test_iter_values(self):
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__')
Expand All @@ -5109,7 +5110,8 @@ def test_iter_items(self):
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'])

Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,21 @@ def monkey(filename, module_globals=None):
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;
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
...
d['__module__'] = 'test.test_metaclass'
d['__qualname__'] = 'C'
d['__firstlineno__'] = 1
d['foo'] = 4
d['foo'] = 42
d['bar'] = 123
Expand All @@ -183,12 +184,12 @@
... 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.
Expand All @@ -206,12 +207,13 @@
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')]
>>>

Expand Down
2 changes: 1 addition & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@ class _TypingEllipsis:
'__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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading
0