Skip to content

Commit

Permalink
pythongh-101410: support custom messages for domain errors in the mat…
Browse files Browse the repository at this point in the history
…h 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]>
  • Loading branch information
CharlieZhao95 and skirpichev committed Sep 21, 2024
1 parent 342e654 commit 5aa7921
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 21 deletions.
38 changes: 38 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,44 @@ 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 = 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.
54 changes: 33 additions & 21 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -921,33 +921,38 @@ 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)
{
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:
PyObject* a = PyFloat_FromDouble(x);
if (a) {
PyErr_Format(PyExc_ValueError,
err_msg ? err_msg : "math domain error", a);
Py_DECREF(a);
}
return NULL;
}

/* variant of math_1, to be used when the function being wrapped is known to
Expand Down Expand Up @@ -1032,7 +1037,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 +1081,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 %R")
FUNC1(cbrt, cbrt, 0,
"cbrt($module, x, /)\n--\n\n"
"Return the cube root of x.")
Expand Down Expand Up @@ -1205,9 +1217,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 %R")
FUNC1(tan, tan, 0,
"tan($module, x, /)\n--\n\n"
"Return the tangent of x (measured in radians).")
Expand Down Expand Up @@ -2180,7 +2193,7 @@ math_modf_impl(PyObject *module, double x)
in that int is larger than PY_SSIZE_T_MAX. */

static PyObject*
loghelper(PyObject* arg, double (*func)(double))
loghelper(PyObject* arg, double (*func)(double), const char *err_msg)
{
/* If it is int, do it ourselves. */
if (PyLong_Check(arg)) {
Expand All @@ -2189,8 +2202,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");
PyErr_Format(PyExc_ValueError, err_msg, arg);
return NULL;
}

Expand All @@ -2214,7 +2226,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, err_msg);
}


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

num = loghelper(args[0], m_log);
num = loghelper(args[0], m_log, "expected a positive input, got %R");
if (num == NULL || nargs == 1)
return num;

den = loghelper(args[1], m_log);
den = loghelper(args[1], m_log, "expected a positive input, got %R");
if (den == NULL) {
Py_DECREF(num);
return NULL;
Expand Down Expand Up @@ -2263,7 +2275,7 @@ static PyObject *
math_log2(PyObject *module, PyObject *x)
/*[clinic end generated code: output=5425899a4d5d6acb input=08321262bae4f39b]*/
{
return loghelper(x, m_log2);
return loghelper(x, m_log2, "expected a positive input, got %R");
}


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


Expand Down

0 comments on commit 5aa7921

Please sign in to comment.