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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.