Skip to content

Commit fb01518

Browse files
pythongh-101410: support custom messages for domain errors in the math module
This adds basic support to override default messages for domain errors in the math_1() helper. The sqrt(), atanh(), log2(), log10() and log() functions were modified as examples. New macro supports gradual changing of error messages in other 1-arg functions. Co-authored-by: Sergey B Kirpichev <[email protected]>
1 parent 342e654 commit fb01518

File tree

3 files changed

+76
-21
lines changed

3 files changed

+76
-21
lines changed

Lib/test/test_math.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,6 +2503,44 @@ def test_input_exceptions(self):
25032503
self.assertRaises(TypeError, math.atan2, 1.0)
25042504
self.assertRaises(TypeError, math.atan2, 1.0, 2.0, 3.0)
25052505

2506+
def test_exception_messages(self):
2507+
x = -1.1
2508+
2509+
with self.assertRaises(ValueError,
2510+
msg=f"expected a nonnegative input, got {x}"):
2511+
math.sqrt(x)
2512+
2513+
with self.assertRaises(ValueError,
2514+
msg=f"expected a positive input, got {x}"):
2515+
math.log(x)
2516+
with self.assertRaises(ValueError,
2517+
msg=f"expected a positive input, got {x}"):
2518+
math.log(123, x)
2519+
with self.assertRaises(ValueError,
2520+
msg=f"expected a positive input, got {x}"):
2521+
math.log2(x)
2522+
with self.assertRaises(ValueError,
2523+
msg=f"expected a positive input, got {x}"):
2524+
math.log2(x)
2525+
2526+
x = decimal.Decimal(x)
2527+
2528+
with self.assertRaises(ValueError,
2529+
msg=f"expected a positive input, got {x!r}"):
2530+
math.log(x)
2531+
2532+
x = fractions.Fraction(1, 10**400)
2533+
2534+
with self.assertRaises(ValueError,
2535+
msg=f"expected a positive input, got {float(x)!r}"):
2536+
math.log(x)
2537+
2538+
x = 1.0
2539+
2540+
with self.assertRaises(ValueError,
2541+
msg=f"expected a number between -1 and 1, got {x}"):
2542+
math.atanh(x)
2543+
25062544
# Custom assertions.
25072545

25082546
def assertIsNaN(self, value):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Support custom messages for domain errors in the :mod:`math` module
2+
(:func:`math.sqrt`, :func:`math.log` and :func:`math.atanh` were modified as
3+
examples). Patch by Charlie Zhao and Sergey B Kirpichev.

Modules/mathmodule.c

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -921,33 +921,40 @@ is_error(double x)
921921
*/
922922

923923
static PyObject *
924-
math_1(PyObject *arg, double (*func) (double), int can_overflow)
924+
math_1(PyObject *arg, double (*func) (double), int can_overflow,
925+
const char *err_msg)
925926
{
926927
double x, r;
927928
x = PyFloat_AsDouble(arg);
928929
if (x == -1.0 && PyErr_Occurred())
929930
return NULL;
930931
errno = 0;
931932
r = (*func)(x);
932-
if (isnan(r) && !isnan(x)) {
933-
PyErr_SetString(PyExc_ValueError,
934-
"math domain error"); /* invalid arg */
935-
return NULL;
936-
}
933+
if (isnan(r) && !isnan(x))
934+
goto domain_err; /* domain error */
937935
if (isinf(r) && isfinite(x)) {
938936
if (can_overflow)
939937
PyErr_SetString(PyExc_OverflowError,
940938
"math range error"); /* overflow */
941939
else
942-
PyErr_SetString(PyExc_ValueError,
943-
"math domain error"); /* singularity */
940+
goto domain_err; /* singularity */
944941
return NULL;
945942
}
946943
if (isfinite(r) && errno && is_error(r))
947944
/* this branch unnecessary on most platforms */
948945
return NULL;
949946

950947
return PyFloat_FromDouble(r);
948+
949+
domain_err:
950+
PyObject* a = PyFloat_FromDouble(x);
951+
952+
if (a) {
953+
PyErr_Format(PyExc_ValueError,
954+
err_msg ? err_msg : "math domain error", a);
955+
Py_DECREF(a);
956+
}
957+
return NULL;
951958
}
952959

953960
/* variant of math_1, to be used when the function being wrapped is known to
@@ -1032,7 +1039,13 @@ math_2(PyObject *const *args, Py_ssize_t nargs,
10321039

10331040
#define FUNC1(funcname, func, can_overflow, docstring) \
10341041
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1035-
return math_1(args, func, can_overflow); \
1042+
return math_1(args, func, can_overflow, NULL); \
1043+
}\
1044+
PyDoc_STRVAR(math_##funcname##_doc, docstring);
1045+
1046+
#define FUNC1D(funcname, func, can_overflow, docstring, err_msg) \
1047+
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1048+
return math_1(args, func, can_overflow, err_msg); \
10361049
}\
10371050
PyDoc_STRVAR(math_##funcname##_doc, docstring);
10381051

@@ -1070,9 +1083,10 @@ FUNC2(atan2, atan2,
10701083
"atan2($module, y, x, /)\n--\n\n"
10711084
"Return the arc tangent (measured in radians) of y/x.\n\n"
10721085
"Unlike atan(y/x), the signs of both x and y are considered.")
1073-
FUNC1(atanh, atanh, 0,
1086+
FUNC1D(atanh, atanh, 0,
10741087
"atanh($module, x, /)\n--\n\n"
1075-
"Return the inverse hyperbolic tangent of x.")
1088+
"Return the inverse hyperbolic tangent of x.",
1089+
"expected a number between -1 and 1, got %R")
10761090
FUNC1(cbrt, cbrt, 0,
10771091
"cbrt($module, x, /)\n--\n\n"
10781092
"Return the cube root of x.")
@@ -1205,9 +1219,10 @@ FUNC1(sin, sin, 0,
12051219
FUNC1(sinh, sinh, 1,
12061220
"sinh($module, x, /)\n--\n\n"
12071221
"Return the hyperbolic sine of x.")
1208-
FUNC1(sqrt, sqrt, 0,
1222+
FUNC1D(sqrt, sqrt, 0,
12091223
"sqrt($module, x, /)\n--\n\n"
1210-
"Return the square root of x.")
1224+
"Return the square root of x.",
1225+
"expected a nonnegative input, got %R")
12111226
FUNC1(tan, tan, 0,
12121227
"tan($module, x, /)\n--\n\n"
12131228
"Return the tangent of x (measured in radians).")
@@ -2180,7 +2195,7 @@ math_modf_impl(PyObject *module, double x)
21802195
in that int is larger than PY_SSIZE_T_MAX. */
21812196

21822197
static PyObject*
2183-
loghelper(PyObject* arg, double (*func)(double))
2198+
loghelper(PyObject* arg, double (*func)(double), const char *err_msg)
21842199
{
21852200
/* If it is int, do it ourselves. */
21862201
if (PyLong_Check(arg)) {
@@ -2189,8 +2204,7 @@ loghelper(PyObject* arg, double (*func)(double))
21892204

21902205
/* Negative or zero inputs give a ValueError. */
21912206
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
2192-
PyErr_SetString(PyExc_ValueError,
2193-
"math domain error");
2207+
PyErr_Format(PyExc_ValueError, err_msg, arg);
21942208
return NULL;
21952209
}
21962210

@@ -2214,7 +2228,7 @@ loghelper(PyObject* arg, double (*func)(double))
22142228
}
22152229

22162230
/* Else let libm handle it by itself. */
2217-
return math_1(arg, func, 0);
2231+
return math_1(arg, func, 0, err_msg);
22182232
}
22192233

22202234

@@ -2229,11 +2243,11 @@ math_log(PyObject *module, PyObject * const *args, Py_ssize_t nargs)
22292243
if (!_PyArg_CheckPositional("log", nargs, 1, 2))
22302244
return NULL;
22312245

2232-
num = loghelper(args[0], m_log);
2246+
num = loghelper(args[0], m_log, "expected a positive input, got %R");
22332247
if (num == NULL || nargs == 1)
22342248
return num;
22352249

2236-
den = loghelper(args[1], m_log);
2250+
den = loghelper(args[1], m_log, "expected a positive input, got %R");
22372251
if (den == NULL) {
22382252
Py_DECREF(num);
22392253
return NULL;
@@ -2263,7 +2277,7 @@ static PyObject *
22632277
math_log2(PyObject *module, PyObject *x)
22642278
/*[clinic end generated code: output=5425899a4d5d6acb input=08321262bae4f39b]*/
22652279
{
2266-
return loghelper(x, m_log2);
2280+
return loghelper(x, m_log2, "expected a positive input, got %R");
22672281
}
22682282

22692283

@@ -2280,7 +2294,7 @@ static PyObject *
22802294
math_log10(PyObject *module, PyObject *x)
22812295
/*[clinic end generated code: output=be72a64617df9c6f input=b2469d02c6469e53]*/
22822296
{
2283-
return loghelper(x, m_log10);
2297+
return loghelper(x, m_log10, "expected a positive input, got %R");
22842298
}
22852299

22862300

0 commit comments

Comments
 (0)