diff --git a/Include/pymacro.h b/Include/pymacro.h index 7ecce44a0d2a428..f31c9505a6a812b 100644 --- a/Include/pymacro.h +++ b/Include/pymacro.h @@ -107,15 +107,40 @@ # endif #endif +#if ((defined(__GNUC__) || defined(__clang__)) \ + && defined(_Py_TYPEOF) && !defined(__cplusplus)) + // Implement Py_MIN(), Py_MAX() and Py_ABS() using _Py_TYPEOF() and + // statement expression to only evaluate each argument only once. + // It cannot be used in C++: ISO C++ forbids braced-groups within + // expressions. Statement expression is a GNU extension. Use __extension__ + // to avoid compiler warning in pedantic mode. -/* Minimum value between x and y */ -#define Py_MIN(x, y) (((x) > (y)) ? (y) : (x)) - -/* Maximum value between x and y */ -#define Py_MAX(x, y) (((x) > (y)) ? (x) : (y)) + /* Minimum value between x and y */ +# define Py_MIN(x, y) \ + __extension__ \ + ({ _Py_TYPEOF (x) _x = (x); \ + _Py_TYPEOF (y) _y = (y); \ + _x < _y ? _x : _y; }) + /* Maximum value between x and y */ +# define Py_MAX(x, y) \ + __extension__ \ + ({ _Py_TYPEOF (x) _x = (x); \ + _Py_TYPEOF (y) _y = (y); \ + _x > _y ? _x : _y; }) + /* Absolute value of the number x */ +# define Py_ABS(x) \ + __extension__ \ + ({ _Py_TYPEOF (x) _x = (x); \ + _x < 0 ? -_x : _x; }) +#else + /* Minimum value between x and y */ +# define Py_MIN(x, y) (((x) > (y)) ? (y) : (x)) + /* Maximum value between x and y */ +# define Py_MAX(x, y) (((x) > (y)) ? (x) : (y)) + /* Absolute value of the number x */ +# define Py_ABS(x) ((x) < 0 ? -(x) : (x)) +#endif -/* Absolute value of the number x */ -#define Py_ABS(x) ((x) < 0 ? -(x) : (x)) /* Safer implementation that avoids an undefined behavior for the minimal value of the signed integer type if its absolute value is larger than the maximal value of the signed integer type (in the two's complement diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 625898e6734aaa5..13c7cec64dcf578 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -3540,3 +3540,38 @@ def built_with_c_assertions(): return False return True + + +def inject_memory_error(start=0, stop=0): + """ + Memory allocation fails after 'start' allocation requests, and until 'stop' + allocation requests except when 'stop' is negative or equal to 0 (default) + in which case allocation failures never stop. + + Raise SkipTest if the _testcapi extension module is missing + """ + try: + import _testcapi + except ImportError: + raise unittest.SkipTest("_testcapi required") + + _testcapi.set_nomemory(start, stop) + + +@contextlib.contextmanager +def memory_error_cm(start=0, stop=0): + """ + Similar to inject_memory_error() but can be used as a context manager. + + Raise SkipTest if the _testcapi extension module is missing + """ + try: + import _testcapi + except ImportError: + raise unittest.SkipTest("_testcapi required") + + try: + _testcapi.set_nomemory(start, stop) + yield + finally: + _testcapi.remove_mem_hooks() diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index 33c37648da31fc7..33d3eb93394781e 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -197,14 +197,14 @@ def test_atexit_with_low_memory(self): # callback doesn't cause an infinite loop during finalization. code = textwrap.dedent(""" import atexit - import _testcapi + from test.support import inject_memory_error def callback(): print("hello") atexit.register(callback) # Simulate low memory condition - _testcapi.set_nomemory(0) + inject_memory_error() """) with os_helper.temp_dir() as temp_dir: diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 3297a53dceff005..bab6006df36a44c 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -50,16 +50,10 @@ def __index__(self): @contextlib.contextmanager -def inject_memory_error(testcase, start): - # Raise SkipTest if _testcapi extension module is missing - _testcapi = import_helper.import_module('_testcapi') - +def inject_memory_error(testcase, start=0): with testcase.assertRaises(MemoryError): - try: - _testcapi.set_nomemory(start) + with support.memory_error_cm(start): yield - finally: - _testcapi.remove_mem_hooks() class BaseBytesTest: @@ -1585,7 +1579,7 @@ def test_resize_error(self): del ba[:offset] else: expected = ba.copy() - with inject_memory_error(self, 0): + with inject_memory_error(self): ba.resize(1024) self.assertEqual(ba, expected) @@ -1596,7 +1590,7 @@ def test_resize_error(self): del ba[:offset] else: expected = ba.copy() - with inject_memory_error(self, 0): + with inject_memory_error(self): ba.resize(1) self.assertEqual(ba, expected) @@ -1669,21 +1663,20 @@ def test_take_bytes_error(self): # gh-157242: If bytearray.take_bytes() fails (MemoryError), # the bytearray must be left unchanged. - for logical_offset, to_take, mem_errors in ( + for logical_offset, to_take, start_list in ( (True, 5, (0, 1)), (False, 5, (0, 1)), (True, None, (0,)), ): - for mem_error in mem_errors: - with self.subTest(logical_offset=logical_offset, - to_take=to_take, mem_error=mem_error): + for start in start_list: + with self.subTest(logical_offset=logical_offset, start=start): ba = bytearray(b'0123456789') if logical_offset: expected = ba[3:] del ba[:3] else: expected = ba.copy() - with inject_memory_error(self, mem_error): + with inject_memory_error(self, start): ba.take_bytes(to_take) self.assertEqual(ba, expected) diff --git a/Lib/test/test_cext/extension.c b/Lib/test/test_cext/extension.c index b58c889ba8bcb3b..0a26a0d8753711f 100644 --- a/Lib/test/test_cext/extension.c +++ b/Lib/test/test_cext/extension.c @@ -91,10 +91,15 @@ _testcext_exec(PyObject *module) if (!result) return -1; Py_DECREF(result); - // test Py_BUILD_ASSERT() and Py_BUILD_ASSERT_EXPR() + // Test Py_BUILD_ASSERT() and Py_BUILD_ASSERT_EXPR() Py_BUILD_ASSERT(sizeof(int) == sizeof(unsigned int)); assert(Py_BUILD_ASSERT_EXPR(sizeof(int) == sizeof(unsigned int)) == 0); + // Test Py_MIN(), Py_MAX(), Py_ABS() + assert(Py_MIN(5, 11) == 5); + assert(Py_MAX(5, 11) == 11); + assert(Py_ABS(-5) == 5); + // Test Py_CLEAR(): use typeof()/__typeof__() if available, or memcpy() obj = Py_None; Py_CLEAR(obj); diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 7bd6d966e8536b7..b8b35f68aa7a051 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -1016,8 +1016,6 @@ class C: @support.nomemtest @isolation.runInSubprocess() def test_detach_materialized_dict_no_memory(self): - import _testcapi - class A: def __init__(self): self.a = 1 @@ -1032,11 +1030,8 @@ def __init__(self): d = a.__dict__ try: with support.catch_unraisable_exception() as ex: - _testcapi.set_nomemory(n, n + 1) - try: + with support.memory_error_cm(n, n + 1): del a - finally: - _testcapi.remove_mem_hooks() exc_type = ex.unraisable and ex.unraisable.exc_type except MemoryError: # The failing allocation was not in the deallocation code. diff --git a/Lib/test/test_cppext/extension.cpp b/Lib/test/test_cppext/extension.cpp index 7496581c6756dac..1ff56d0e7fd25a0 100644 --- a/Lib/test/test_cppext/extension.cpp +++ b/Lib/test/test_cppext/extension.cpp @@ -302,6 +302,11 @@ _testcppext_exec(PyObject *module) Py_BUILD_ASSERT(sizeof(int) == sizeof(unsigned int)); assert(Py_BUILD_ASSERT_EXPR(sizeof(int) == sizeof(unsigned int)) == 0); + // Test Py_MIN(), Py_MAX(), Py_ABS() + assert(Py_MIN(5, 11) == 5); + assert(Py_MAX(5, 11) == 11); + assert(Py_ABS(-5) == 5); + // Test Py_CLEAR(): use typeof()/__typeof__() if available, or memcpy() PyObject *obj = Py_None; Py_CLEAR(obj); diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 750e45bb1644686..026c2bf6b31e730 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1670,14 +1670,14 @@ def test_recursion_normalizing_with_no_memory(self): # the size of the list of preallocated MemoryError instances, the # Fatal Python error message mentions MemoryError. code = """if 1: - import _testcapi + from test import support class C(): pass def recurse(cnt): cnt -= 1 if cnt: recurse(cnt) else: - _testcapi.set_nomemory(0) + support.inject_memory_error() C() recurse(16) """ @@ -1853,9 +1853,10 @@ def test_unhandled(self): @support.nomemtest def test_memory_error_in_PyErr_PrintEx(self): code = """if 1: - import _testcapi + from test import support + stop = %d class C(): pass - _testcapi.set_nomemory(0, %d) + support.inject_memory_error(0, stop) C() """ @@ -2020,8 +2021,8 @@ def test_exec_set_nomemory_hang(self): warmup_code = "a = list(range(0, 1))\n" * 60 user_input = warmup_code + dedent(""" try: - import _testcapi - _testcapi.set_nomemory(0) + from test import support + support.inject_memory_error() b = list(range(1000, 2000)) except Exception as e: import traceback diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index 1b0724c2b1c99dc..260f9f4f607f152 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -377,11 +377,12 @@ def test_tier2_invalidates_iterator(self): def test_no_memory(self): # gh-118331: Make sure we don't crash if list allocation fails code = textwrap.dedent(""" - import _testcapi, sys + from test import support + import sys # Prime the freelist l = [None] del l - _testcapi.set_nomemory(0) + support.inject_memory_error() l = [None] """) rc, _, _ = assert_python_failure("-c", code) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index baa4f178427d532..23b82dc1fd2179e 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -1077,18 +1077,9 @@ def test_error_path_no_crash(self): parser.buffer_text = True rc_before = sys.getrefcount(parser) - # We avoid self.assertRaises(MemoryError) here because the - # context manager itself needs memory allocations that fail - # while the nomemory hook is active. - self.testcapi.set_nomemory(1, 10) - raised = False - try: - parser.ExternalEntityParserCreate(None) - except MemoryError: - raised = True - finally: - self.testcapi.remove_mem_hooks() - self.assertTrue(raised, "MemoryError not raised") + with self.assertRaises(MemoryError): + with support.memory_error_cm(1, 10): + parser.ExternalEntityParserCreate(None) rc_after = sys.getrefcount(parser) self.assertEqual(rc_after, rc_before) diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 372c110783bce7a..ed6eb706c40d223 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -104,10 +104,11 @@ def test_no_memory(self): # no memory. Check also that the fix does not break the interactive # loop when an exception is raised. user_input = """ - import sys, _testcapi + import sys + from test import support 1/0 print('After the exception.') - _testcapi.set_nomemory(0) + support.inject_memory_error() sys.exit(0) """ user_input = dedent(user_input) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 979bfe36fff680d..17163182be08c4c 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -613,14 +613,9 @@ def test_replace_oom(self): s1 = "轘" * 4 s2 = "&" s3 = "&" - assertion = self.assertRaises(MemoryError) - _testcapi.set_nomemory(0, 0) - try: - # No allocations made in the test itself: - with assertion: + with self.assertRaises(MemoryError): + with support.memory_error_cm(): s1.replace(s2, s3) # this line used to crash before - finally: - _testcapi.remove_mem_hooks() def test_repeat_id_preserving(self): a = '123abc1@' diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index b9d1745592c09ca..ce93481e2735f78 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -1029,8 +1029,8 @@ def test_no_memory_when_clearing(self): # gh-118331: Make sure we do not raise an exception from the destructor # when clearing weakrefs if allocating the intermediate tuple fails. code = textwrap.dedent(""" - import _testcapi import weakref + from test import support class TestObj: pass @@ -1042,7 +1042,7 @@ def callback(obj): # The choice of 50 is arbitrary, but must be large enough to ensure # the allocation won't be serviced by the free list. wrs = [weakref.ref(obj, callback) for _ in range(50)] - _testcapi.set_nomemory(0) + support.inject_memory_error() del obj """).strip() res, _ = script_helper.run_python_until_end("-c", code) diff --git a/Misc/NEWS.d/next/C_API/2026-09-14-16-10-23.gh-issue-157495.LzMLtE.rst b/Misc/NEWS.d/next/C_API/2026-09-14-16-10-23.gh-issue-157495.LzMLtE.rst new file mode 100644 index 000000000000000..0e89171bba86579 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-14-16-10-23.gh-issue-157495.LzMLtE.rst @@ -0,0 +1,3 @@ +If ``typeof()`` is available, implement :c:macro:`Py_MIN`, :c:macro:`Py_MAX` +and :c:macro:`Py_ABS` using ``typeof()`` and statement expression to only +evaluate each argument once. Patch by Victor Stinner. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 73b32c1d86c72ec..9bddb9ce62d5b99 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -5883,7 +5883,8 @@ _ssl_MemoryBIO_read_impl(PySSLMemoryBIO *self, int len) { int avail, nbytes; - avail = (int)Py_MIN(BIO_ctrl_pending(self->bio), INT_MAX); + size_t pending = BIO_ctrl_pending(self->bio); + avail = (int)Py_MIN(pending, (size_t)INT_MAX); if ((len < 0) || (len > avail)) len = avail; diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index eb769294fd21db8..577bb14df134059 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2045,12 +2045,22 @@ test_macros(PyObject *self, PyObject *Py_UNUSED(args)) static_assert(1 == 1, "bug"); Py_BUILD_ASSERT(1 == 1); - // Py_MIN(), Py_MAX(), Py_ABS() assert(Py_MIN(5, 11) == 5); assert(Py_MAX(5, 11) == 11); assert(Py_ABS(-5) == 5); +#if ((defined(__GNUC__) || defined(__clang__)) \ + && defined(_Py_TYPEOF) && !defined(__cplusplus)) + // When _Py_TYPEOF() is available, arguments are only evaluated once + int x = 5, y = 11; + assert(Py_MIN(++x, ++y) == 6); + x = 5; y = 11; + assert(Py_MAX(++x, ++y) == 12); + x = -5; + assert(Py_ABS(--x) == 6); +#endif + // Py_STRINGIFY() assert(strcmp(Py_STRINGIFY(123), "123") == 0); diff --git a/Modules/cjkcodecs/cjkcodecs.h b/Modules/cjkcodecs/cjkcodecs.h index 41e1287c8650701..b4f316ce0c06e46 100644 --- a/Modules/cjkcodecs/cjkcodecs.h +++ b/Modules/cjkcodecs/cjkcodecs.h @@ -163,7 +163,7 @@ get_module_state(PyObject *mod) do { \ Py_UCS4 _c1 = (c1); \ Py_UCS4 _c2 = (c2); \ - if (_PyUnicodeWriter_Prepare(writer, 2, Py_MAX(_c1, c2)) < 0) \ + if (_PyUnicodeWriter_Prepare(writer, 2, Py_MAX(_c1, _c2)) < 0) \ return MBERR_EXCEPTION; \ PyUnicode_WRITE(writer->kind, writer->data, writer->pos, _c1); \ PyUnicode_WRITE(writer->kind, writer->data, writer->pos + 1, _c2); \ diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 0947d47c8a55582..9a41a4224672233 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -3255,10 +3255,10 @@ _PyMem_DebugRawRealloc(void *ctx, void *p, size_t nbytes) } else { size_t i = original_nbytes - ERASED_SIZE; - memcpy(data, save, Py_MIN(nbytes, ERASED_SIZE)); + memcpy(data, save, Py_MIN(nbytes, (size_t)ERASED_SIZE)); if (nbytes > i) { memcpy(data + i, &save[ERASED_SIZE], - Py_MIN(nbytes - i, ERASED_SIZE)); + Py_MIN(nbytes - i, (size_t)ERASED_SIZE)); } } #endif