mirror of
https://github.com/python/cpython.git
synced 2025-07-24 03:35:53 +00:00
[3.13] gh-123339: Fix cases of inconsistency of __module__ and __firstlineno__ in classes (GH-123613) (#124735)
* Setting the __module__ attribute for a class now removes the
__firstlineno__ item from the type's dict.
* The _collections_abc and _pydecimal modules now completely replace the
collections.abc and decimal modules after importing them. This
allows to get the source of classes and functions defined in these
modules.
* inspect.findsource() now checks whether the first line number for a
class is out of bound.
(cherry picked from commit 69a4063ca5
)
This commit is contained in:
parent
ce0eaa6703
commit
5bf32d1300
11 changed files with 110 additions and 12 deletions
|
@ -1032,7 +1032,10 @@ Special attributes
|
||||||
.. versionadded:: 3.13
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
* - .. attribute:: type.__firstlineno__
|
* - .. attribute:: type.__firstlineno__
|
||||||
- The line number of the first line of the class definition, including decorators.
|
- The line number of the first line of the class definition,
|
||||||
|
including decorators.
|
||||||
|
Setting the :attr:`__module__` attribute removes the
|
||||||
|
:attr:`!__firstlineno__` item from the type's dictionary.
|
||||||
|
|
||||||
.. versionadded:: 3.13
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from _collections_abc import *
|
import _collections_abc
|
||||||
from _collections_abc import __all__
|
import sys
|
||||||
from _collections_abc import _CallableGenericAlias
|
sys.modules[__name__] = _collections_abc
|
||||||
|
|
|
@ -103,6 +103,7 @@ try:
|
||||||
from _decimal import __version__
|
from _decimal import __version__
|
||||||
from _decimal import __libmpdec_version__
|
from _decimal import __libmpdec_version__
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from _pydecimal import *
|
import _pydecimal
|
||||||
from _pydecimal import __version__
|
import sys
|
||||||
from _pydecimal import __libmpdec_version__
|
_pydecimal.__doc__ = __doc__
|
||||||
|
sys.modules[__name__] = _pydecimal
|
||||||
|
|
|
@ -1082,10 +1082,12 @@ def findsource(object):
|
||||||
|
|
||||||
if isclass(object):
|
if isclass(object):
|
||||||
try:
|
try:
|
||||||
firstlineno = vars(object)['__firstlineno__']
|
lnum = vars(object)['__firstlineno__'] - 1
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
raise OSError('source code not available')
|
raise OSError('source code not available')
|
||||||
return lines, firstlineno - 1
|
if lnum >= len(lines):
|
||||||
|
raise OSError('lineno is out of bounds')
|
||||||
|
return lines, lnum
|
||||||
|
|
||||||
if ismethod(object):
|
if ismethod(object):
|
||||||
object = object.__func__
|
object = object.__func__
|
||||||
|
|
|
@ -2564,6 +2564,7 @@ class TestType(unittest.TestCase):
|
||||||
self.assertEqual(A.__module__, __name__)
|
self.assertEqual(A.__module__, __name__)
|
||||||
self.assertEqual(A.__bases__, (object,))
|
self.assertEqual(A.__bases__, (object,))
|
||||||
self.assertIs(A.__base__, object)
|
self.assertIs(A.__base__, object)
|
||||||
|
self.assertNotIn('__firstlineno__', A.__dict__)
|
||||||
x = A()
|
x = A()
|
||||||
self.assertIs(type(x), A)
|
self.assertIs(type(x), A)
|
||||||
self.assertIs(x.__class__, A)
|
self.assertIs(x.__class__, A)
|
||||||
|
@ -2642,6 +2643,17 @@ class TestType(unittest.TestCase):
|
||||||
A.__qualname__ = b'B'
|
A.__qualname__ = b'B'
|
||||||
self.assertEqual(A.__qualname__, 'D.E')
|
self.assertEqual(A.__qualname__, 'D.E')
|
||||||
|
|
||||||
|
def test_type_firstlineno(self):
|
||||||
|
A = type('A', (), {'__firstlineno__': 42})
|
||||||
|
self.assertEqual(A.__name__, 'A')
|
||||||
|
self.assertEqual(A.__module__, __name__)
|
||||||
|
self.assertEqual(A.__dict__['__firstlineno__'], 42)
|
||||||
|
A.__module__ = 'testmodule'
|
||||||
|
self.assertEqual(A.__module__, 'testmodule')
|
||||||
|
self.assertNotIn('__firstlineno__', A.__dict__)
|
||||||
|
A.__firstlineno__ = 43
|
||||||
|
self.assertEqual(A.__dict__['__firstlineno__'], 43)
|
||||||
|
|
||||||
def test_type_typeparams(self):
|
def test_type_typeparams(self):
|
||||||
class A[T]:
|
class A[T]:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -4381,7 +4381,8 @@ class CheckAttributes(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(C.__version__, P.__version__)
|
self.assertEqual(C.__version__, P.__version__)
|
||||||
|
|
||||||
self.assertEqual(dir(C), dir(P))
|
self.assertLessEqual(set(dir(C)), set(dir(P)))
|
||||||
|
self.assertEqual([n for n in dir(C) if n[:2] != '__'], sorted(P.__all__))
|
||||||
|
|
||||||
def test_context_attributes(self):
|
def test_context_attributes(self):
|
||||||
|
|
||||||
|
|
|
@ -357,3 +357,15 @@ class td354(typing.TypedDict):
|
||||||
|
|
||||||
# line 358
|
# line 358
|
||||||
td359 = typing.TypedDict('td359', (('x', int), ('y', int)))
|
td359 = typing.TypedDict('td359', (('x', int), ('y', int)))
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
# line 363
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class dc364:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
|
||||||
|
# line 369
|
||||||
|
dc370 = dataclasses.make_dataclass('dc370', (('x', int), ('y', int)))
|
||||||
|
dc371 = dataclasses.make_dataclass('dc370', (('x', int), ('y', int)), module=__name__)
|
||||||
|
|
|
@ -838,6 +838,47 @@ class TestRetrievingSourceCode(GetSourceBase):
|
||||||
nonlocal __firstlineno__
|
nonlocal __firstlineno__
|
||||||
self.assertRaises(OSError, inspect.getsource, C)
|
self.assertRaises(OSError, inspect.getsource, C)
|
||||||
|
|
||||||
|
class TestGetsourceStdlib(unittest.TestCase):
|
||||||
|
# Test Python implementations of the stdlib modules
|
||||||
|
|
||||||
|
def test_getsource_stdlib_collections_abc(self):
|
||||||
|
import collections.abc
|
||||||
|
lines, lineno = inspect.getsourcelines(collections.abc.Sequence)
|
||||||
|
self.assertEqual(lines[0], 'class Sequence(Reversible, Collection):\n')
|
||||||
|
src = inspect.getsource(collections.abc.Sequence)
|
||||||
|
self.assertEqual(src.splitlines(True), lines)
|
||||||
|
|
||||||
|
def test_getsource_stdlib_tomllib(self):
|
||||||
|
import tomllib
|
||||||
|
self.assertRaises(OSError, inspect.getsource, tomllib.TOMLDecodeError)
|
||||||
|
self.assertRaises(OSError, inspect.getsourcelines, tomllib.TOMLDecodeError)
|
||||||
|
|
||||||
|
def test_getsource_stdlib_abc(self):
|
||||||
|
# Pure Python implementation
|
||||||
|
abc = import_helper.import_fresh_module('abc', blocked=['_abc'])
|
||||||
|
with support.swap_item(sys.modules, 'abc', abc):
|
||||||
|
self.assertRaises(OSError, inspect.getsource, abc.ABCMeta)
|
||||||
|
self.assertRaises(OSError, inspect.getsourcelines, abc.ABCMeta)
|
||||||
|
# With C acceleration
|
||||||
|
import abc
|
||||||
|
try:
|
||||||
|
src = inspect.getsource(abc.ABCMeta)
|
||||||
|
lines, lineno = inspect.getsourcelines(abc.ABCMeta)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.assertEqual(lines[0], ' class ABCMeta(type):\n')
|
||||||
|
self.assertEqual(src.splitlines(True), lines)
|
||||||
|
|
||||||
|
def test_getsource_stdlib_decimal(self):
|
||||||
|
# Pure Python implementation
|
||||||
|
decimal = import_helper.import_fresh_module('decimal', blocked=['_decimal'])
|
||||||
|
with support.swap_item(sys.modules, 'decimal', decimal):
|
||||||
|
src = inspect.getsource(decimal.Decimal)
|
||||||
|
lines, lineno = inspect.getsourcelines(decimal.Decimal)
|
||||||
|
self.assertEqual(lines[0], 'class Decimal(object):\n')
|
||||||
|
self.assertEqual(src.splitlines(True), lines)
|
||||||
|
|
||||||
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;
|
||||||
|
@ -950,6 +991,11 @@ class TestOneliners(GetSourceBase):
|
||||||
self.assertSourceEqual(mod2.td354, 354, 356)
|
self.assertSourceEqual(mod2.td354, 354, 356)
|
||||||
self.assertRaises(OSError, inspect.getsource, mod2.td359)
|
self.assertRaises(OSError, inspect.getsource, mod2.td359)
|
||||||
|
|
||||||
|
def test_dataclass(self):
|
||||||
|
self.assertSourceEqual(mod2.dc364, 364, 367)
|
||||||
|
self.assertRaises(OSError, inspect.getsource, mod2.dc370)
|
||||||
|
self.assertRaises(OSError, inspect.getsource, mod2.dc371)
|
||||||
|
|
||||||
class TestBlockComments(GetSourceBase):
|
class TestBlockComments(GetSourceBase):
|
||||||
fodderModule = mod
|
fodderModule = mod
|
||||||
|
|
||||||
|
@ -1013,7 +1059,7 @@ class TestBuggyCases(GetSourceBase):
|
||||||
self.assertRaises(IOError, inspect.findsource, co)
|
self.assertRaises(IOError, inspect.findsource, co)
|
||||||
self.assertRaises(IOError, inspect.getsource, co)
|
self.assertRaises(IOError, inspect.getsource, co)
|
||||||
|
|
||||||
def test_findsource_with_out_of_bounds_lineno(self):
|
def test_findsource_on_func_with_out_of_bounds_lineno(self):
|
||||||
mod_len = len(inspect.getsource(mod))
|
mod_len = len(inspect.getsource(mod))
|
||||||
src = '\n' * 2* mod_len + "def f(): pass"
|
src = '\n' * 2* mod_len + "def f(): pass"
|
||||||
co = compile(src, mod.__file__, "exec")
|
co = compile(src, mod.__file__, "exec")
|
||||||
|
@ -1021,9 +1067,20 @@ class TestBuggyCases(GetSourceBase):
|
||||||
eval(co, g, l)
|
eval(co, g, l)
|
||||||
func = l['f']
|
func = l['f']
|
||||||
self.assertEqual(func.__code__.co_firstlineno, 1+2*mod_len)
|
self.assertEqual(func.__code__.co_firstlineno, 1+2*mod_len)
|
||||||
with self.assertRaisesRegex(IOError, "lineno is out of bounds"):
|
with self.assertRaisesRegex(OSError, "lineno is out of bounds"):
|
||||||
inspect.findsource(func)
|
inspect.findsource(func)
|
||||||
|
|
||||||
|
def test_findsource_on_class_with_out_of_bounds_lineno(self):
|
||||||
|
mod_len = len(inspect.getsource(mod))
|
||||||
|
src = '\n' * 2* mod_len + "class A: pass"
|
||||||
|
co = compile(src, mod.__file__, "exec")
|
||||||
|
g, l = {'__name__': mod.__name__}, {}
|
||||||
|
eval(co, g, l)
|
||||||
|
cls = l['A']
|
||||||
|
self.assertEqual(cls.__firstlineno__, 1+2*mod_len)
|
||||||
|
with self.assertRaisesRegex(OSError, "lineno is out of bounds"):
|
||||||
|
inspect.findsource(cls)
|
||||||
|
|
||||||
def test_getsource_on_method(self):
|
def test_getsource_on_method(self):
|
||||||
self.assertSourceEqual(mod2.ClassWithMethod.method, 118, 119)
|
self.assertSourceEqual(mod2.ClassWithMethod.method, 118, 119)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Setting the :attr:`!__module__` attribute for a class now removes the
|
||||||
|
``__firstlineno__`` item from the type's dict, so they will no longer be
|
||||||
|
inconsistent.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Fix :func:`inspect.getsource` for classes in :mod:`collections.abc` and
|
||||||
|
:mod:`decimal` (for pure Python implementation) modules.
|
||||||
|
:func:`inspect.getcomments` now raises OSError instead of IndexError if the
|
||||||
|
``__firstlineno__`` value for a class is out of bound.
|
|
@ -1351,6 +1351,9 @@ type_set_module(PyTypeObject *type, PyObject *value, void *context)
|
||||||
PyType_Modified(type);
|
PyType_Modified(type);
|
||||||
|
|
||||||
PyObject *dict = lookup_tp_dict(type);
|
PyObject *dict = lookup_tp_dict(type);
|
||||||
|
if (PyDict_Pop(dict, &_Py_ID(__firstlineno__), NULL) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
return PyDict_SetItem(dict, &_Py_ID(__module__), value);
|
return PyDict_SetItem(dict, &_Py_ID(__module__), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue