Skip to content

gh-101410: support custom messages for domain errors in the math module #124299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,50 @@ def test_input_exceptions(self):
self.assertRaises(TypeError, math.atan2, 1.0)
self.assertRaises(TypeError, math.atan2, 1.0, 2.0, 3.0)

def test_exception_messages(self):
x = -1.1

with self.assertRaises(ValueError,
msg=f"expected a nonnegative input, got {x}"):
math.sqrt(x)

with self.assertRaises(ValueError,
msg=f"expected a positive input, got {x}"):
math.log(x)
with self.assertRaises(ValueError,
msg=f"expected a positive input, got {x}"):
math.log(123, x)
with self.assertRaises(ValueError,
msg=f"expected a positive input, got {x}"):
math.log2(x)
with self.assertRaises(ValueError,
msg=f"expected a positive input, got {x}"):
math.log2(x)

x = decimal.Decimal(x)

with self.assertRaises(ValueError,
msg=f"expected a positive input, got {x!r}"):
math.log(x)

x = fractions.Fraction(1, 10**400)

with self.assertRaises(ValueError,
msg=f"expected a positive input, got {float(x)!r}"):
math.log(x)

x = -2**1000

with self.assertRaises(ValueError,
msg=f"expected a positive input"):
math.log(x)

x = 1.0

with self.assertRaises(ValueError,
msg=f"expected a number between -1 and 1, got {x}"):
math.atanh(x)

# Custom assertions.

def assertIsNaN(self, value):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Support custom messages for domain errors in the :mod:`math` module
(:func:`math.sqrt`, :func:`math.log` and :func:`math.atanh` were modified as
examples). Patch by Charlie Zhao and Sergey B Kirpichev.
46 changes: 31 additions & 15 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -921,33 +921,41 @@ is_error(double x)
*/

static PyObject *
math_1(PyObject *arg, double (*func) (double), int can_overflow)
math_1(PyObject *arg, double (*func) (double), int can_overflow,
const char *err_msg)
{
char *buf;
double x, r;
x = PyFloat_AsDouble(arg);
if (x == -1.0 && PyErr_Occurred())
return NULL;
errno = 0;
r = (*func)(x);
if (isnan(r) && !isnan(x)) {
PyErr_SetString(PyExc_ValueError,
"math domain error"); /* invalid arg */
return NULL;
}
if (isnan(r) && !isnan(x))
goto domain_err; /* domain error */
if (isinf(r) && isfinite(x)) {
if (can_overflow)
PyErr_SetString(PyExc_OverflowError,
"math range error"); /* overflow */
else
PyErr_SetString(PyExc_ValueError,
"math domain error"); /* singularity */
goto domain_err; /* singularity */
return NULL;
}
if (isfinite(r) && errno && is_error(r))
/* this branch unnecessary on most platforms */
return NULL;

return PyFloat_FromDouble(r);

domain_err:
buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);

if (buf) {
PyErr_Format(PyExc_ValueError,
err_msg ? err_msg : "math domain error", buf);
PyMem_Free(buf);
}
return NULL;
}

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

#define FUNC1(funcname, func, can_overflow, docstring) \
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
return math_1(args, func, can_overflow); \
return math_1(args, func, can_overflow, NULL); \
}\
PyDoc_STRVAR(math_##funcname##_doc, docstring);

#define FUNC1D(funcname, func, can_overflow, docstring, err_msg) \
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
return math_1(args, func, can_overflow, err_msg); \
}\
PyDoc_STRVAR(math_##funcname##_doc, docstring);

Expand Down Expand Up @@ -1070,9 +1084,10 @@ FUNC2(atan2, atan2,
"atan2($module, y, x, /)\n--\n\n"
"Return the arc tangent (measured in radians) of y/x.\n\n"
"Unlike atan(y/x), the signs of both x and y are considered.")
FUNC1(atanh, atanh, 0,
FUNC1D(atanh, atanh, 0,
"atanh($module, x, /)\n--\n\n"
"Return the inverse hyperbolic tangent of x.")
"Return the inverse hyperbolic tangent of x.",
"expected a number between -1 and 1, got %s")
FUNC1(cbrt, cbrt, 0,
"cbrt($module, x, /)\n--\n\n"
"Return the cube root of x.")
Expand Down Expand Up @@ -1205,9 +1220,10 @@ FUNC1(sin, sin, 0,
FUNC1(sinh, sinh, 1,
"sinh($module, x, /)\n--\n\n"
"Return the hyperbolic sine of x.")
FUNC1(sqrt, sqrt, 0,
FUNC1D(sqrt, sqrt, 0,
"sqrt($module, x, /)\n--\n\n"
"Return the square root of x.")
"Return the square root of x.",
"expected a nonnegative input, got %s")
FUNC1(tan, tan, 0,
"tan($module, x, /)\n--\n\n"
"Return the tangent of x (measured in radians).")
Expand Down Expand Up @@ -2190,7 +2206,7 @@ loghelper(PyObject* arg, double (*func)(double))
/* Negative or zero inputs give a ValueError. */
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
PyErr_SetString(PyExc_ValueError,
"math domain error");
"expected a positive input");
return NULL;
}

Expand All @@ -2214,7 +2230,7 @@ loghelper(PyObject* arg, double (*func)(double))
}

/* Else let libm handle it by itself. */
return math_1(arg, func, 0);
return math_1(arg, func, 0, "expected a positive input, got %s");
}


Expand Down
Loading