Skip to content

Commit 5aa7921

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 5aa7921

File tree

3 files changed

+74
-21
lines changed

3 files changed

+74
-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: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -921,33 +921,38 @@ 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+
domain_err:
949+
PyObject* a = PyFloat_FromDouble(x);
950+
if (a) {
951+
PyErr_Format(PyExc_ValueError,
952+
err_msg ? err_msg : "math domain error", a);
953+
Py_DECREF(a);
954+
}
955+
return NULL;
951956
}
952957

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

10331038
#define FUNC1(funcname, func, can_overflow, docstring) \
10341039
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1035-
return math_1(args, func, can_overflow); \
1040+
return math_1(args, func, can_overflow, NULL); \
1041+
}\
1042+
PyDoc_STRVAR(math_##funcname##_doc, docstring);
1043+
1044+
#define FUNC1D(funcname, func, can_overflow, docstring, err_msg) \
1045+
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1046+
return math_1(args, func, can_overflow, err_msg); \
10361047
}\
10371048
PyDoc_STRVAR(math_##funcname##_doc, docstring);
10381049

@@ -1070,9 +1081,10 @@ FUNC2(atan2, atan2,
10701081
"atan2($module, y, x, /)\n--\n\n"
10711082
"Return the arc tangent (measured in radians) of y/x.\n\n"
10721083
"Unlike atan(y/x), the signs of both x and y are considered.")
1073-
FUNC1(atanh, atanh, 0,
1084+
FUNC1D(atanh, atanh, 0,
10741085
"atanh($module, x, /)\n--\n\n"
1075-
"Return the inverse hyperbolic tangent of x.")
1086+
"Return the inverse hyperbolic tangent of x.",
1087+
"expected a number between -1 and 1, got %R")
10761088
FUNC1(cbrt, cbrt, 0,
10771089
"cbrt($module, x, /)\n--\n\n"
10781090
"Return the cube root of x.")
@@ -1205,9 +1217,10 @@ FUNC1(sin, sin, 0,
12051217
FUNC1(sinh, sinh, 1,
12061218
"sinh($module, x, /)\n--\n\n"
12071219
"Return the hyperbolic sine of x.")
1208-
FUNC1(sqrt, sqrt, 0,
1220+
FUNC1D(sqrt, sqrt, 0,
12091221
"sqrt($module, x, /)\n--\n\n"
1210-
"Return the square root of x.")
1222+
"Return the square root of x.",
1223+
"expected a nonnegative input, got %R")
12111224
FUNC1(tan, tan, 0,
12121225
"tan($module, x, /)\n--\n\n"
12131226
"Return the tangent of x (measured in radians).")
@@ -2180,7 +2193,7 @@ math_modf_impl(PyObject *module, double x)
21802193
in that int is larger than PY_SSIZE_T_MAX. */
21812194

21822195
static PyObject*
2183-
loghelper(PyObject* arg, double (*func)(double))
2196+
loghelper(PyObject* arg, double (*func)(double), const char *err_msg)
21842197
{
21852198
/* If it is int, do it ourselves. */
21862199
if (PyLong_Check(arg)) {
@@ -2189,8 +2202,7 @@ loghelper(PyObject* arg, double (*func)(double))
21892202

21902203
/* Negative or zero inputs give a ValueError. */
21912204
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
2192-
PyErr_SetString(PyExc_ValueError,
2193-
"math domain error");
2205+
PyErr_Format(PyExc_ValueError, err_msg, arg);
21942206
return NULL;
21952207
}
21962208

@@ -2214,7 +2226,7 @@ loghelper(PyObject* arg, double (*func)(double))
22142226
}
22152227

22162228
/* Else let libm handle it by itself. */
2217-
return math_1(arg, func, 0);
2229+
return math_1(arg, func, 0, err_msg);
22182230
}
22192231

22202232

@@ -2229,11 +2241,11 @@ math_log(PyObject *module, PyObject * const *args, Py_ssize_t nargs)
22292241
if (!_PyArg_CheckPositional("log", nargs, 1, 2))
22302242
return NULL;
22312243

2232-
num = loghelper(args[0], m_log);
2244+
num = loghelper(args[0], m_log, "expected a positive input, got %R");
22332245
if (num == NULL || nargs == 1)
22342246
return num;
22352247

2236-
den = loghelper(args[1], m_log);
2248+
den = loghelper(args[1], m_log, "expected a positive input, got %R");
22372249
if (den == NULL) {
22382250
Py_DECREF(num);
22392251
return NULL;
@@ -2263,7 +2275,7 @@ static PyObject *
22632275
math_log2(PyObject *module, PyObject *x)
22642276
/*[clinic end generated code: output=5425899a4d5d6acb input=08321262bae4f39b]*/
22652277
{
2266-
return loghelper(x, m_log2);
2278+
return loghelper(x, m_log2, "expected a positive input, got %R");
22672279
}
22682280

22692281

@@ -2280,7 +2292,7 @@ static PyObject *
22802292
math_log10(PyObject *module, PyObject *x)
22812293
/*[clinic end generated code: output=be72a64617df9c6f input=b2469d02c6469e53]*/
22822294
{
2283-
return loghelper(x, m_log10);
2295+
return loghelper(x, m_log10, "expected a positive input, got %R");
22842296
}
22852297

22862298

0 commit comments

Comments
 (0)