https://github.com/python/cpython/commit/8be8128b8275c3134157883b1634cc007c0012a9
commit: 8be8128b8275c3134157883b1634cc007c0012a9
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T19:09:43Z
summary:

[3.13] gh-156106: Add tests for setting and deleting attributes defined in C 
(GH-156107) (GH-156191)

Test setting a value of an accepted type, of a wrong type and an invalid
value, and deleting the attribute, for the attributes defined with
PyMemberDef and PyGetSetDef which were not covered.

(cherry picked from commit cdca5021d5b6c2cba8121b639bae1642dfe51ff3)

files:
M Lib/test/test_asyncio/test_futures.py
M Lib/test/test_ctypes/test_delattr.py
M Lib/test/test_decimal.py
M Lib/test/test_defaultdict.py
M Lib/test/test_exceptions.py
M Lib/test/test_fileio.py
M Lib/test/test_frame.py
M Lib/test/test_funcattrs.py
M Lib/test/test_io.py
M Lib/test/test_kqueue.py
M Lib/test/test_pickle.py
M Lib/test/test_sqlite3/test_dbapi.py
M Lib/test/test_ssl.py

diff --git a/Lib/test/test_asyncio/test_futures.py 
b/Lib/test/test_asyncio/test_futures.py
index ec3029983b73e81..1f9eccb11f8144e 100644
--- a/Lib/test/test_asyncio/test_futures.py
+++ b/Lib/test/test_asyncio/test_futures.py
@@ -255,6 +255,14 @@ def test_future_cancel_message_setter(self):
         f.cancel('my message')
         f._cancel_message = 'my new message'
         self.assertEqual(f._cancel_message, 'my new message')
+        f._cancel_message = None
+        self.assertIsNone(f._cancel_message)
+        f._cancel_message = 'my new message'
+        if not isinstance(f, futures._PyFuture):
+            # The C implementation does not support deletion.
+            with self.assertRaises(AttributeError):
+                del f._cancel_message
+        self.assertEqual(f._cancel_message, 'my new message')
 
         # Also check that the value is used for cancel().
         with self.assertRaises(asyncio.CancelledError):
diff --git a/Lib/test/test_ctypes/test_delattr.py 
b/Lib/test/test_ctypes/test_delattr.py
index e80b5fa6efb5455..019760eabb3bd46 100644
--- a/Lib/test/test_ctypes/test_delattr.py
+++ b/Lib/test/test_ctypes/test_delattr.py
@@ -1,5 +1,6 @@
 import unittest
-from ctypes import Structure, c_char, c_int
+from ctypes import CDLL, Structure, c_char, c_int
+from test.support import import_helper
 
 
 class X(Structure):
@@ -21,6 +22,25 @@ def test_struct(self):
         with self.assertRaises(TypeError):
             del struct.foo
 
+    def test_raw(self):
+        chararray = (c_char * 5)()
+        with self.assertRaises(AttributeError):
+            del chararray.raw
+
+    def test_func_pointer(self):
+        # Deleting these attributes restores the default.
+        dll = CDLL(import_helper.import_module('_ctypes_test').__file__)
+        func = dll._testfunc_i_bhilfd
+        func.argtypes = [c_int]
+        func.restype = c_int
+        func.errcheck = lambda *args: None
+        del func.argtypes
+        self.assertIsNone(func.argtypes)
+        del func.errcheck
+        self.assertIsNone(func.errcheck)
+        del func.restype
+        self.assertIs(func.restype, c_int)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index fad648b537dc715..ebd366ed4986bea 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -4239,7 +4239,7 @@ def test_invalid_context(self):
 
         # Attributes cannot be deleted
         for attr in ['prec', 'Emax', 'Emin', 'rounding', 'capitals', 'clamp',
-                     'flags', 'traps']:
+                     'flags', 'traps', '_allcr', '_flags', '_traps']:
             self.assertRaises(AttributeError, c.__delattr__, attr)
 
         # Invalid attributes
diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py
index a193eb10f16d178..d9af1a66098d999 100644
--- a/Lib/test/test_defaultdict.py
+++ b/Lib/test/test_defaultdict.py
@@ -37,6 +37,9 @@ def test_basic(self):
         self.assertIn(42, d2.keys())
         self.assertNotIn(12, d2)
         self.assertNotIn(12, d2.keys())
+        d2.default_factory = list
+        del d2.default_factory
+        self.assertEqual(d2.default_factory, None)
         d2.default_factory = None
         self.assertEqual(d2.default_factory, None)
         try:
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index c6a1cac2fe976fe..e34bd7b83a19b03 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -663,6 +663,43 @@ def test_invalid_setattr(self):
         msg = "exception context must be None or derive from BaseException"
         self.assertRaisesRegex(TE, msg, setattr, exc, '__context__', 1)
 
+    def test_object_attributes(self):
+        # These attributes are implemented as plain object members:
+        # they accept any object and are reset to None when deleted.
+        cases = [
+            (SyntaxError('msgStr'), 'msg'),
+            (SyntaxError('msgStr'), 'filename'),
+            (SyntaxError('msgStr'), 'lineno'),
+            (SyntaxError('msgStr'), 'offset'),
+            (SyntaxError('msgStr'), 'end_lineno'),
+            (SyntaxError('msgStr'), 'end_offset'),
+            (SyntaxError('msgStr'), 'text'),
+            (SyntaxError('msgStr'), 'print_file_and_line'),
+            (ImportError('msgStr'), 'msg'),
+            (ImportError('msgStr'), 'name'),
+            (ImportError('msgStr'), 'path'),
+            (ImportError('msgStr'), 'name_from'),
+            (SystemExit(1), 'code'),
+            (StopIteration(), 'value'),
+            (NameError('msgStr'), 'name'),
+            (AttributeError('msgStr'), 'name'),
+            (AttributeError('msgStr'), 'obj'),
+            (OSError(2, 'msgStr'), 'errno'),
+            (OSError(2, 'msgStr'), 'strerror'),
+            (OSError(2, 'msgStr'), 'filename'),
+            (OSError(2, 'msgStr'), 'filename2'),
+            (UnicodeDecodeError('utf-8', b'\xff', 0, 1, 'reasonStr'), 
'reason'),
+        ]
+        if sys.platform == 'win32':
+            cases.append((OSError(2, 'msgStr'), 'winerror'))
+        for exc, name in cases:
+            with self.subTest(exc=type(exc).__name__, name=name):
+                for value in 'strValue', 42, [1, 2], None:
+                    setattr(exc, name, value)
+                    self.assertEqual(getattr(exc, name), value)
+                delattr(exc, name)
+                self.assertIsNone(getattr(exc, name))
+
     def test_invalid_delattr(self):
         TE = TypeError
         try:
@@ -720,6 +757,13 @@ def testChainingDescriptors(self):
         self.assertTrue(e.__suppress_context__)
         e.__suppress_context__ = False
         self.assertFalse(e.__suppress_context__)
+        with self.assertRaisesRegex(TypeError,
+                                    'attribute value type must be bool'):
+            e.__suppress_context__ = 1
+        with self.assertRaisesRegex(TypeError,
+                                    "can't delete numeric/char attribute"):
+            del e.__suppress_context__
+        self.assertFalse(e.__suppress_context__)
 
     def testKeywordArgs(self):
         # test that builtin exception don't take keyword args,
diff --git a/Lib/test/test_fileio.py b/Lib/test/test_fileio.py
index fdb36ed997d046b..4a29c1ab7a5f290 100644
--- a/Lib/test/test_fileio.py
+++ b/Lib/test/test_fileio.py
@@ -81,6 +81,7 @@ def testBlksize(self):
             blksize = getattr(fst, 'st_blksize', blksize)
         self.assertEqual(self.f._blksize, blksize)
 
+
     # verify readinto
     def testReadintoByteArray(self):
         self.f.write(bytes([1, 2, 0, 255]))
@@ -363,6 +364,21 @@ class CAutoFileTests(AutoFileTests, unittest.TestCase):
     FileIO = _io.FileIO
     modulename = '_io'
 
+    def testFinalizing(self):
+        # test the private _finalizing attribute
+        self.assertIs(self.f._finalizing, False)
+        self.f._finalizing = True
+        self.assertIs(self.f._finalizing, True)
+        with self.assertRaisesRegex(TypeError,
+                                    'attribute value type must be bool'):
+            self.f._finalizing = 1
+        with self.assertRaisesRegex(TypeError,
+                                    "can't delete numeric/char attribute"):
+            del self.f._finalizing
+        # closing a file which is being finalized emits a ResourceWarning
+        self.f._finalizing = False
+
+
 class PyAutoFileTests(AutoFileTests, unittest.TestCase):
     FileIO = _pyio.FileIO
     modulename = '_pyio'
diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py
index d11240c451790a5..35801d22341937f 100644
--- a/Lib/test/test_frame.py
+++ b/Lib/test/test_frame.py
@@ -219,6 +219,31 @@ def test_locals_clear_locals(self):
         self.assertEqual(outer.f_locals, {})
         self.assertEqual(inner.f_locals, {})
 
+    def test_f_trace(self):
+        f, _, _ = self.make_frames()
+        def tracer(*args):
+            pass
+        for value in tracer, 42, None:
+            f.f_trace = value
+            self.assertEqual(f.f_trace, value)
+        f.f_trace = tracer
+        del f.f_trace
+        self.assertIsNone(f.f_trace)
+
+    def test_f_trace_lines_and_opcodes(self):
+        f, _, _ = self.make_frames()
+        for name in 'f_trace_lines', 'f_trace_opcodes':
+            with self.subTest(name=name):
+                for value in False, True:
+                    setattr(f, name, value)
+                    self.assertEqual(getattr(f, name), value)
+                with self.assertRaisesRegex(TypeError,
+                                            'attribute value type must be 
bool'):
+                    setattr(f, name, 1)
+        with self.assertRaisesRegex(TypeError,
+                                    "can't delete numeric/char attribute"):
+            del f.f_trace_lines
+
     def test_f_trace_opcodes_del(self):
         f, _, _ = self.make_frames()
         f.f_trace_opcodes = True
diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py
index 375f456dfde8346..526d10d172d9a9c 100644
--- a/Lib/test/test_funcattrs.py
+++ b/Lib/test/test_funcattrs.py
@@ -266,6 +266,41 @@ def e(): return num_one, num_two
             self.fail("__code__ with different numbers of free vars should "
                       "not be possible")
 
+    def test___kwdefaults__(self):
+        def func(a=1, *, b=2, c=3):
+            return a, b, c
+        self.assertEqual(func.__kwdefaults__, {'b': 2, 'c': 3})
+        func.__kwdefaults__ = {'b': 4}
+        self.assertEqual(func.__kwdefaults__, {'b': 4})
+        self.assertEqual(func(c=5), (1, 4, 5))
+        func.__kwdefaults__ = None
+        self.assertIsNone(func.__kwdefaults__)
+        self.assertRaises(TypeError, func)
+        with self.assertRaisesRegex(TypeError,
+                                    '__kwdefaults__ must be set to a dict 
object'):
+            func.__kwdefaults__ = [('b', 4)]
+        del func.__kwdefaults__
+        self.assertIsNone(func.__kwdefaults__)
+
+    def test_invalid___code___deletion(self):
+        def func(): pass
+        with self.assertRaisesRegex(TypeError,
+                                    '__code__ must be set to a code object'):
+            func.__code__ = None
+        with self.assertRaisesRegex(TypeError,
+                                    '__code__ must be set to a code object'):
+            del func.__code__
+
+    def test___doc__(self):
+        def func():
+            "docstring"
+        self.assertEqual(func.__doc__, 'docstring')
+        for value in 'other', 42, None:
+            func.__doc__ = value
+            self.assertEqual(func.__doc__, value)
+        del func.__doc__
+        self.assertIsNone(func.__doc__)
+
     def test_blank_func_defaults(self):
         self.assertEqual(self.b.__defaults__, None)
         del self.b.__defaults__
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index 7f404cbd4e8fd75..810bec1e74df69b 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -4065,6 +4065,29 @@ class CTextIOWrapperTest(TextIOWrapperTest):
     io = io
     shutdown_error = "LookupError: unknown encoding: ascii"
 
+    def test_chunk_size(self):
+        t = self.TextIOWrapper(self.BytesIO(), encoding="utf-8")
+        self.assertGreater(t._CHUNK_SIZE, 0)
+        t._CHUNK_SIZE = 1024
+        self.assertEqual(t._CHUNK_SIZE, 1024)
+        with self.assertRaisesRegex(ValueError,
+                                    'a strictly positive integer is required'):
+            t._CHUNK_SIZE = 0
+        with self.assertRaises(TypeError):
+            t._CHUNK_SIZE = 'x'
+        with self.assertRaises(ValueError):
+            t._CHUNK_SIZE = sys.maxsize + 1
+        with self.assertRaises(ValueError):
+            t._CHUNK_SIZE = -sys.maxsize - 2
+        with self.assertRaises(ValueError):
+            t._CHUNK_SIZE = 2**1000
+        with self.assertRaises(ValueError):
+            t._CHUNK_SIZE = -2**1000
+        with self.assertRaisesRegex(AttributeError, 'cannot be deleted'):
+            del t._CHUNK_SIZE
+        # a failed assignment does not change the value
+        self.assertEqual(t._CHUNK_SIZE, 1024)
+
     def test_reentrant_seek_during_tell(self):
         # gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
         # snapshot, so tell() re-decodes and calls the decoder's getstate(); a
diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py
index e94edcbc107ba92..aa8a4d3d643b7b8 100644
--- a/Lib/test/test_kqueue.py
+++ b/Lib/test/test_kqueue.py
@@ -110,6 +110,31 @@ def test_create_event(self):
         self.assertNotEqual(ev, other)
 
 
+    def test_event_attributes(self):
+        fd = os.open(os.devnull, os.O_WRONLY)
+        self.addCleanup(os.close, fd)
+
+        ev = select.kevent(fd)
+        # All attributes are numeric members: they can be set and cannot be
+        # deleted.
+        for name, value in (('ident', 1), ('filter', select.KQ_FILTER_WRITE),
+                            ('flags', select.KQ_EV_DELETE), ('fflags', 2),
+                            ('data', 3), ('udata', 4)):
+            with self.subTest(name=name):
+                setattr(ev, name, value)
+                self.assertEqual(getattr(ev, name), value)
+                with self.assertRaises(TypeError):
+                    setattr(ev, name, 'not a number')
+                with self.assertRaises(OverflowError):
+                    setattr(ev, name, 2**1000)
+                with self.assertRaises(OverflowError):
+                    setattr(ev, name, -2**1000)
+                with self.assertRaisesRegex(
+                        TypeError, "can't delete numeric/char attribute"):
+                    delattr(ev, name)
+                # a failed assignment does not change the value
+                self.assertEqual(getattr(ev, name), value)
+
     def test_queue_event(self):
         serverSocket = socket.create_server(('127.0.0.1', 0))
         client = socket.socket()
diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py
index 6b0b221644ba7ad..599b33dc2f88950 100644
--- a/Lib/test/test_pickle.py
+++ b/Lib/test/test_pickle.py
@@ -373,6 +373,57 @@ class CPicklerTests(PyPicklerTests):
         pickler = _pickle.Pickler
         unpickler = _pickle.Unpickler
 
+        def test_c_pickler_attributes(self):
+            pickler = _pickle.Pickler(io.BytesIO())
+            for name in 'bin', 'fast':
+                with self.subTest(name=name):
+                    for value in 0, 1, True:
+                        setattr(pickler, name, value)
+                        self.assertEqual(getattr(pickler, name), int(value))
+                    with self.assertRaises(TypeError):
+                        setattr(pickler, name, 'x')
+                    with self.assertRaises(OverflowError):
+                        setattr(pickler, name, sys.maxsize + 1)
+                    with self.assertRaises(OverflowError):
+                        setattr(pickler, name, -sys.maxsize - 2)
+                    with self.assertRaises(OverflowError):
+                        setattr(pickler, name, 2**1000)
+                    with self.assertRaises(OverflowError):
+                        setattr(pickler, name, -2**1000)
+                    with self.assertRaisesRegex(
+                            TypeError, "can't delete numeric/char attribute"):
+                        delattr(pickler, name)
+                    # a failed assignment does not change the value
+                    self.assertEqual(getattr(pickler, name), 1)
+
+            self.assertRaises(AttributeError, getattr, pickler,
+                              'dispatch_table')
+            table = {}
+            pickler.dispatch_table = table
+            self.assertIs(pickler.dispatch_table, table)
+            del pickler.dispatch_table
+            self.assertRaises(AttributeError, getattr, pickler,
+                              'dispatch_table')
+
+            pickler.memo = {}
+            self.assertEqual(pickler.memo.copy(), {})
+            with self.assertRaisesRegex(TypeError, 'must be a 
PicklerMemoProxy'):
+                pickler.memo = None
+            with self.assertRaisesRegex(TypeError,
+                                        'attribute deletion is not supported'):
+                del pickler.memo
+
+        def test_c_unpickler_attributes(self):
+            unpickler = _pickle.Unpickler(io.BytesIO(b'.'))
+            unpickler.memo = {}
+            self.assertEqual(unpickler.memo.copy(), {})
+            with self.assertRaisesRegex(TypeError,
+                                        'must be an UnpicklerMemoProxy'):
+                unpickler.memo = None
+            with self.assertRaisesRegex(TypeError,
+                                        'attribute deletion is not supported'):
+                del unpickler.memo
+
     class CPersPicklerTests(PyPersPicklerTests):
         pickler = _pickle.Pickler
         unpickler = _pickle.Unpickler
diff --git a/Lib/test/test_sqlite3/test_dbapi.py 
b/Lib/test/test_sqlite3/test_dbapi.py
index 841b8dc6abf5ab2..b81cc76c14485ac 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -509,6 +509,13 @@ def test_connection_init_good_isolation_levels(self):
                     cx.isolation_level = level
                     self.assertEqual(cx.isolation_level, level)
 
+    def test_connection_delete_isolation_level(self):
+        with memory_database() as cx:
+            with self.assertRaisesRegex(AttributeError,
+                                        "cannot delete attribute"):
+                del cx.isolation_level
+            self.assertEqual(cx.isolation_level, "")
+
     def test_connection_reinit(self):
         with memory_database() as cx:
             cx.text_factory = bytes
@@ -1106,9 +1113,16 @@ def test_invalid_array_size(self):
         UINT32_MAX = (1 << 32) - 1
         setter = functools.partial(setattr, self.cu, 'arraysize')
 
+        self.cu.arraysize = 2
         self.assertRaises(TypeError, setter, 1.0)
         self.assertRaises(ValueError, setter, -3)
         self.assertRaises(OverflowError, setter, UINT32_MAX + 1)
+        self.assertRaises(OverflowError, setter, 2**1000)
+        self.assertRaises(ValueError, setter, -2**1000)
+        self.assertRaisesRegex(AttributeError, 'cannot be deleted',
+                               delattr, self.cu, 'arraysize')
+        # a failed assignment does not change the value
+        self.assertEqual(self.cu.arraysize, 2)
 
     def test_fetchmany(self):
         # no active SQL statement
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index 1cca904e647e95c..76e156e4aabed4e 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -1744,6 +1744,32 @@ def test__create_stdlib_context_check_hostname(self):
         self.assertEqual(ctx.verify_mode, ssl.CERT_OPTIONAL)
         self.assertTrue(ctx.check_hostname)
 
+    def test_delete_sslobject_attributes(self):
+        # None of the attributes of _ssl._SSLSocket can be deleted.
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        sslobj = ctx.wrap_bio(ssl.MemoryBIO(), ssl.MemoryBIO())._sslobj
+        for name in 'context', 'owner', 'session', 'session_reused':
+            with self.subTest(name=name):
+                value = getattr(sslobj, name)
+                with self.assertRaises(AttributeError):
+                    delattr(sslobj, name)
+                self.assertEqual(getattr(sslobj, name), value)
+
+    def test_delete_attributes(self):
+        # None of the attributes implemented in C can be deleted.
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+        names = ['check_hostname', 'verify_mode', 'verify_flags', 'options',
+                 'minimum_version', 'maximum_version', 'sni_callback',
+                 '_host_flags', 'security_level', 'post_handshake_auth']
+        if hasattr(ctx, 'num_tickets'):
+            names.append('num_tickets')
+        for name in names:
+            with self.subTest(name=name):
+                value = getattr(ctx, name)
+                with self.assertRaises(AttributeError):
+                    delattr(ctx, name)
+                self.assertEqual(getattr(ctx, name), value)
+
     def test_check_hostname(self):
         with warnings_helper.check_warnings():
             ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to