https://github.com/python/cpython/commit/cdca5021d5b6c2cba8121b639bae1642dfe51ff3
commit: cdca5021d5b6c2cba8121b639bae1642dfe51ff3
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T16:58:22+03:00
summary:

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

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.

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_frame.py
M Lib/test/test_funcattrs.py
M Lib/test/test_io/test_fileio.py
M Lib/test/test_io/test_textio.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 320de2180e7db7..a217177e1deb06 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 eb99c0dafc8656..689bf327176f09 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 POINTER, Structure, c_char, c_int
+from ctypes import CDLL, POINTER, Structure, c_char, c_int
+from test.support import import_helper
 
 
 class X(Structure):
@@ -26,6 +27,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 a0ba5a8351aebd..7524822632bae7 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -4331,7 +4331,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 cc78f01e3e2ebd..84a17eca0aa530 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 0cee756958f3ad..c34cf44d722456 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -682,6 +682,44 @@ 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'),
+            (SyntaxError('msgStr'), '_metadata'),
+            (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:
@@ -739,6 +777,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_frame.py b/Lib/test/test_frame.py
index a3a329cb578a8d..a0a11966ebf3c1 100644
--- a/Lib/test/test_frame.py
+++ b/Lib/test/test_frame.py
@@ -222,6 +222,33 @@ def test_f_lineno_del_segfault(self):
         with self.assertRaises(AttributeError):
             del f.f_lineno
 
+    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
+        with self.assertRaisesRegex(AttributeError, 'cannot be deleted'):
+            del f.f_trace_opcodes
+
     def test_f_generator(self):
         # Test f_generator in different contexts.
 
diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py
index fe14e7cb342c9e..b2fa10d7458012 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/test_fileio.py b/Lib/test/test_io/test_fileio.py
index e53c4749f58cf2..21a160904a7f1e 100644
--- a/Lib/test/test_io/test_fileio.py
+++ b/Lib/test/test_io/test_fileio.py
@@ -83,6 +83,12 @@ def testBlksize(self):
             fst = os.fstat(self.f.fileno())
             blksize = getattr(fst, 'st_blksize', blksize)
         self.assertEqual(self.f._blksize, blksize)
+        # it is read-only
+        with self.assertRaises(AttributeError):
+            self.f._blksize = blksize
+        with self.assertRaises(AttributeError):
+            del self.f._blksize
+
 
     # verify readinto
     def testReadintoByteArray(self):
@@ -503,6 +509,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_io/test_textio.py b/Lib/test/test_io/test_textio.py
index 07f6b1415d0dbd..a210b81d877dc1 100644
--- a/Lib/test/test_io/test_textio.py
+++ b/Lib/test/test_io/test_textio.py
@@ -1448,6 +1448,29 @@ def _to_memoryview(buf):
 class CTextIOWrapperTest(TextIOWrapperTest, CTestCase):
     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_initialization(self):
         r = self.BytesIO(b"\xc3\xa9\n\n")
         b = self.BufferedReader(r, 1000)
diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py
index 2649f3a7aee9b9..e4223262e09933 100644
--- a/Lib/test/test_kqueue.py
+++ b/Lib/test/test_kqueue.py
@@ -126,6 +126,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 21f45339e8a344..5b0fd70105447f 100644
--- a/Lib/test/test_pickle.py
+++ b/Lib/test/test_pickle.py
@@ -389,6 +389,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 375f12e8d4791d..978227a8651e14 100644
--- a/Lib/test/test_sqlite3/test_dbapi.py
+++ b/Lib/test/test_sqlite3/test_dbapi.py
@@ -487,6 +487,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
@@ -1081,6 +1088,8 @@ def test_invalid_array_size(self):
         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)
 
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index 285bcac65ab57b..37323b7ebc6b1a 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -1846,6 +1846,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