]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-123339: Fix cases of inconsistency of __module__ and __firstlineno__ in...
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 30 Sep 2024 04:21:40 +0000 (07:21 +0300)
committerGitHub <noreply@github.com>
Mon, 30 Sep 2024 04:21:40 +0000 (21:21 -0700)
* 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 69a4063ca516360b5eb96f5432ad9f9dfc32a72e)

Doc/reference/datamodel.rst
Lib/collections/abc.py
Lib/decimal.py
Lib/inspect.py
Lib/test/test_builtin.py
Lib/test/test_decimal.py
Lib/test/test_inspect/inspect_fodder2.py
Lib/test/test_inspect/test_inspect.py
Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-20-36-45.gh-issue-123339.QcmpSs.rst [new file with mode: 0644]
Misc/NEWS.d/next/Library/2024-09-02-20-34-04.gh-issue-123339.czgcSu.rst [new file with mode: 0644]
Objects/typeobject.c

index bd90e37ea27cf693b7bd1e353d0c33abbaf9087e..232a2e98789d2bcbbdbd25b70c571f0e50747645 100644 (file)
@@ -1032,7 +1032,10 @@ Special attributes
        .. versionadded:: 3.13
 
    * - .. 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
 
index 86ca8b8a8414b3860aa80599394e806e156f4b5b..034ba377a0dbec362d41f35bf4d001a89c1e2ee6 100644 (file)
@@ -1,3 +1,3 @@
-from _collections_abc import *
-from _collections_abc import __all__
-from _collections_abc import _CallableGenericAlias
+import _collections_abc
+import sys
+sys.modules[__name__] = _collections_abc
index 4d8e15cb68ff0c0107c93d47ba0c95de860528a7..ee3147f5dde270b0dd25d5ecc2a3712a3b1b122a 100644 (file)
@@ -103,6 +103,7 @@ try:
     from _decimal import __version__
     from _decimal import __libmpdec_version__
 except ImportError:
-    from _pydecimal import *
-    from _pydecimal import __version__
-    from _pydecimal import __libmpdec_version__
+    import _pydecimal
+    import sys
+    _pydecimal.__doc__ = __doc__
+    sys.modules[__name__] = _pydecimal
index 9499dc5c6dc81ce98e40779c7fbb0291bd75541b..8df2383f60b0115bc2c64e3409ac34a4d53068fb 100644 (file)
@@ -1082,10 +1082,12 @@ def findsource(object):
 
     if isclass(object):
         try:
-            firstlineno = vars(object)['__firstlineno__']
+            lnum = vars(object)['__firstlineno__'] - 1
         except (TypeError, KeyError):
             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):
         object = object.__func__
index c5394def80e9a97802807c3f025508038409a7eb..3a5531924976f795305a99a8548de7d2d44a8e3c 100644 (file)
@@ -2564,6 +2564,7 @@ class TestType(unittest.TestCase):
         self.assertEqual(A.__module__, __name__)
         self.assertEqual(A.__bases__, (object,))
         self.assertIs(A.__base__, object)
+        self.assertNotIn('__firstlineno__', A.__dict__)
         x = A()
         self.assertIs(type(x), A)
         self.assertIs(x.__class__, A)
@@ -2642,6 +2643,17 @@ class TestType(unittest.TestCase):
             A.__qualname__ = b'B'
         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):
         class A[T]:
             pass
index 12479e32d0f5dbcea784fa67bc405a0f1bcf849f..c591fd54430b1825f12110ae0ba8c3f4249cd9ae 100644 (file)
@@ -4381,7 +4381,8 @@ class CheckAttributes(unittest.TestCase):
 
         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):
 
index 43e9f85202293460882aecbdbf7cd4073e6fc59b..43fda6622537fc71688d7e65ddbe778d83470033 100644 (file)
@@ -357,3 +357,15 @@ class td354(typing.TypedDict):
 
 # line 358
 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__)
index 766b3fe571b5f9e9647804ce500dc5ec721b79f1..e2e4f1230abd50c0ae1d5df7438f2943efe76f45 100644 (file)
@@ -838,6 +838,47 @@ class TestRetrievingSourceCode(GetSourceBase):
             nonlocal __firstlineno__
         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):
     def test_getclasses_interactive(self):
         # bpo-44648: simulate a REPL session;
@@ -950,6 +991,11 @@ class TestOneliners(GetSourceBase):
         self.assertSourceEqual(mod2.td354, 354, 356)
         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):
     fodderModule = mod
 
@@ -1013,7 +1059,7 @@ class TestBuggyCases(GetSourceBase):
             self.assertRaises(IOError, inspect.findsource, 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))
         src = '\n' * 2* mod_len + "def f(): pass"
         co = compile(src, mod.__file__, "exec")
@@ -1021,9 +1067,20 @@ class TestBuggyCases(GetSourceBase):
         eval(co, g, l)
         func = l['f']
         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)
 
+    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):
         self.assertSourceEqual(mod2.ClassWithMethod.method, 118, 119)
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-20-36-45.gh-issue-123339.QcmpSs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-20-36-45.gh-issue-123339.QcmpSs.rst
new file mode 100644 (file)
index 0000000..25b47d5
--- /dev/null
@@ -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.
diff --git a/Misc/NEWS.d/next/Library/2024-09-02-20-34-04.gh-issue-123339.czgcSu.rst b/Misc/NEWS.d/next/Library/2024-09-02-20-34-04.gh-issue-123339.czgcSu.rst
new file mode 100644 (file)
index 0000000..e388541
--- /dev/null
@@ -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.
index 16acb8b682d79689c049e3f850a3ee97d5d27b9d..c911c3020039abb9532b0a0f44eb6e3793b24d65 100644 (file)
@@ -1351,6 +1351,9 @@ type_set_module(PyTypeObject *type, PyObject *value, void *context)
     PyType_Modified(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);
 }