From e8839283bab1f07aa81f7ea5e61252662545da9e Mon Sep 17 00:00:00 2001 From: Abhinaba Banerjee Date: Mon, 12 May 2025 16:28:13 +0530 Subject: [PATCH] gh-133895: Include computed result value in math and cmath error exceptions --- Lib/test/test_math_errors.py | 71 +++++++++++++++++++++++++++++++++++ Modules/cmathmodule.c | 72 ++++++++++++++++++++++++++++++++---- Modules/mathmodule.c | 57 +++++++++++----------------- 3 files changed, 157 insertions(+), 43 deletions(-) create mode 100644 Lib/test/test_math_errors.py diff --git a/Lib/test/test_math_errors.py b/Lib/test/test_math_errors.py new file mode 100644 index 00000000000000..311af35da26129 --- /dev/null +++ b/Lib/test/test_math_errors.py @@ -0,0 +1,71 @@ +import unittest +import math +import cmath + +class TestMathErrors(unittest.TestCase): + def test_math_value_error_with_value(self): + # Test math.sqrt with negative number + try: + math.sqrt(-1) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(math.isnan(e.value)) + + # Test math.log with negative number + try: + math.log(-1) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(math.isnan(e.value)) + + # Test math.acos with value > 1 + try: + math.acos(2) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(math.isnan(e.value)) + + def test_cmath_value_error_with_value(self): + # Test cmath.sqrt with negative number + try: + cmath.sqrt(-1) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(isinstance(e.value, complex)) + self.assertTrue(cmath.isnan(e.value)) + + # Test cmath.log with negative number + try: + cmath.log(-1) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(isinstance(e.value, complex)) + self.assertTrue(cmath.isnan(e.value)) + + # Test cmath.acos with value > 1 + try: + cmath.acos(2) + except ValueError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(isinstance(e.value, complex)) + self.assertTrue(cmath.isnan(e.value)) + + def test_math_overflow_error_with_value(self): + # Test math.exp with very large number + try: + math.exp(1000) + except OverflowError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(math.isinf(e.value)) + + def test_cmath_overflow_error_with_value(self): + # Test cmath.exp with very large number + try: + cmath.exp(1000) + except OverflowError as e: + self.assertTrue(hasattr(e, 'value')) + self.assertTrue(isinstance(e.value, complex)) + self.assertTrue(cmath.isinf(e.value.real) or cmath.isinf(e.value.imag)) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c index 81cbf0d554de3c..4d3f92ad079f53 100644 --- a/Modules/cmathmodule.c +++ b/Modules/cmathmodule.c @@ -35,11 +35,23 @@ class Py_complex_protected_return_converter(CReturnConverter): self.declare(data) data.return_conversion.append(""" if (errno == EDOM) { - PyErr_SetString(PyExc_ValueError, "math domain error"); + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyComplex_FromCComplex(_return_value); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); goto exit; } else if (errno == ERANGE) { - PyErr_SetString(PyExc_OverflowError, "math range error"); + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyComplex_FromCComplex(_return_value); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); goto exit; } else { @@ -408,8 +420,8 @@ cmath_cosh_impl(PyObject *module, Py_complex z) /* special treatment for cosh(+/-inf + iy) if y is not a NaN */ if (!isfinite(z.real) || !isfinite(z.imag)) { - if (isinf(z.real) && isfinite(z.imag) && - (z.imag != 0.)) { + if (isinf(z.real) && isfinite(z.imag) + && (z.imag != 0.)) { if (z.real > 0) { r.real = copysign(INF, cos(z.imag)); r.imag = copysign(INF, sin(z.imag)); @@ -899,10 +911,24 @@ cmath_log_impl(PyObject *module, Py_complex x, PyObject *y_obj) static PyObject * math_error(void) { - if (errno == EDOM) - PyErr_SetString(PyExc_ValueError, "math domain error"); - else if (errno == ERANGE) - PyErr_SetString(PyExc_OverflowError, "math range error"); + if (errno == EDOM) { + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyComplex_FromCComplex(_return_value); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); + } + else if (errno == ERANGE) { + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyComplex_FromCComplex(_return_value); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); + } else /* Unexpected math error */ PyErr_SetFromErrno(PyExc_ValueError); return NULL; @@ -1338,3 +1364,33 @@ PyInit_cmath(void) { return PyModuleDef_Init(&cmathmodule); } + +static int +Py_complex_protected_return_converter(PyObject *obj, void *ptr) +{ + Py_complex *p = (Py_complex *)ptr; + if (PyComplex_Check(obj)) { + *p = PyComplex_AsCComplex(obj); + return 1; + } + if (PyFloat_Check(obj)) { + p->real = PyFloat_AsDouble(obj); + p->imag = 0.0; + return 1; + } + if (PyLong_Check(obj)) { + p->real = PyLong_AsDouble(obj); + p->imag = 0.0; + return 1; + } + if (PyErr_Occurred()) { + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyComplex_FromCComplex(*p); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); + } + return 0; +} diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 11d9b7418a25a2..92501073391b74 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -83,7 +83,7 @@ module math Double and triple length extended precision algorithms from: Accurate Sum and Dot Product - by Takeshi Ogita, Siegfried M. Rump, and Shin’Ichi Oishi + by Takeshi Ogita, Siegfried M. Rump, and Shin'Ichi Oishi https://doi.org/10.1137/030601818 https://www.tuhh.de/ti3/paper/rump/OgRuOi05.pdf @@ -847,44 +847,31 @@ math_lcm_impl(PyObject *module, PyObject * const *args, static int is_error(double x, int raise_edom) { - int result = 1; /* presumption of guilt */ - assert(errno); /* non-zero errno is a precondition for calling */ - if (errno == EDOM) { + if (Py_IS_NAN(x)) { if (raise_edom) { - PyErr_SetString(PyExc_ValueError, "math domain error"); + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyFloat_FromDouble(x); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); } + return 1; } - - else if (errno == ERANGE) { - /* ANSI C generally requires libm functions to set ERANGE - * on overflow, but also generally *allows* them to set - * ERANGE on underflow too. There's no consistency about - * the latter across platforms. - * Alas, C99 never requires that errno be set. - * Here we suppress the underflow errors (libm functions - * should return a zero on underflow, and +- HUGE_VAL on - * overflow, so testing the result for zero suffices to - * distinguish the cases). - * - * On some platforms (Ubuntu/ia64) it seems that errno can be - * set to ERANGE for subnormal results that do *not* underflow - * to zero. So to be safe, we'll ignore ERANGE whenever the - * function result is less than 1.5 in absolute value. - * - * bpo-46018: Changed to 1.5 to ensure underflows in expm1() - * are correctly detected, since the function may underflow - * toward -1.0 rather than 0.0. - */ - if (fabs(x) < 1.5) - result = 0; - else - PyErr_SetString(PyExc_OverflowError, - "math range error"); + if (Py_IS_INFINITY(x)) { + if (raise_edom) { + PyObject *exc = PyErr_GetRaisedException(); + PyObject *value = PyFloat_FromDouble(x); + if (value) { + PyObject_SetAttrString(exc, "value", value); + } + Py_DECREF(value); + PyErr_SetRaisedException(exc); + } + return 1; } - else - /* Unexpected math error */ - PyErr_SetFromErrno(PyExc_ValueError); - return result; + return 0; } /*