diff --git a/Include/pymacro.h b/Include/pymacro.h index 7ecce44a0d2a42..f31c9505a6a812 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 625898e6734aaa..13c7cec64dcf57 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 33c37648da31fc..33d3eb93394781 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 3297a53dceff00..bab6006df36a44 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 b58c889ba8bcb3..0a26a0d8753711 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 7bd6d966e8536b..b8b35f68aa7a05 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 7496581c6756da..1ff56d0e7fd25a 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 750e45bb164468..026c2bf6b31e73 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 1b0724c2b1c99d..260f9f4f607f15 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 baa4f178427d53..23b82dc1fd2179 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 372c110783bce7..ed6eb706c40d22 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 979bfe36fff680..17163182be08c4 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 b9d1745592c09c..ce93481e2735f7 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 00000000000000..0e89171bba8657 --- /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 73b32c1d86c72e..9bddb9ce62d5b9 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 eb769294fd21db..577bb14df13405 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 41e1287c865070..b4f316ce0c06e4 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 0947d47c8a5558..9a41a422467223 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