https://github.com/python/cpython/commit/ccaacd87f9202495451be1e4c5d4e33dfe955e5c
commit: ccaacd87f9202495451be1e4c5d4e33dfe955e5c
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T18:02:14+03:00
summary:
[3.14] gh-156106: Add tests for setting and deleting attributes defined in C
(GH-156107) (ПР-156184)
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 785258a27497738..f3cfb468e3f0928 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 eb99c0dafc86563..689bf327176f099 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 7e3896ed34e7bae..9afd726162cb0c9 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -4283,7 +4283,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 e79e52911f66aba..d8f52332005f4ca 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -667,6 +667,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:
@@ -724,6 +762,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 e3d54f6315aade6..74a944d1004d8c7 100644
--- a/Lib/test/test_fileio.py
+++ b/Lib/test/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_frame.py b/Lib/test/test_frame.py
index 18ade18d1a1708c..9fb0b574a81a8c0 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 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 57b42fbb65fe107..262fb86a3cc599b 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -4099,6 +4099,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_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 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 6e8fbd2bf973e6f..9d86c1b050304db 100644
--- a/Lib/test/test_pickle.py
+++ b/Lib/test/test_pickle.py
@@ -383,6 +383,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 0bb3fb80f04c5ca..42b835758e04a53 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
@@ -1089,6 +1096,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 227b6f92cd5df66..22f860e94edd535 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -1773,6 +1773,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]