Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 75f59bb

Browse filesBrowse files
skirpichevCharlieZhao95picnixz
authored
gh-101410: support custom messages for domain errors in the math module (#124299)
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: CharlieZhao <zhaoyu_hit@qq.com> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 25a614a commit 75f59bb
Copy full SHA for 75f59bb

File tree

Expand file treeCollapse file tree

3 files changed

+109
-29
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+109
-29
lines changed

‎Lib/test/test_math.py

Copy file name to clipboardExpand all lines: Lib/test/test_math.py
+40Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,6 +2503,46 @@ 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+
with self.assertRaisesRegex(ValueError,
2509+
f"expected a nonnegative input, got {x}"):
2510+
math.sqrt(x)
2511+
with self.assertRaisesRegex(ValueError,
2512+
f"expected a positive input, got {x}"):
2513+
math.log(x)
2514+
with self.assertRaisesRegex(ValueError,
2515+
f"expected a positive input, got {x}"):
2516+
math.log(123, x)
2517+
with self.assertRaisesRegex(ValueError,
2518+
f"expected a positive input, got {x}"):
2519+
math.log(x, 123)
2520+
with self.assertRaisesRegex(ValueError,
2521+
f"expected a positive input, got {x}"):
2522+
math.log2(x)
2523+
with self.assertRaisesRegex(ValueError,
2524+
f"expected a positive input, got {x}"):
2525+
math.log10(x)
2526+
x = decimal.Decimal('-1.1')
2527+
with self.assertRaisesRegex(ValueError,
2528+
f"expected a positive input, got {x}"):
2529+
math.log(x)
2530+
x = fractions.Fraction(1, 10**400)
2531+
with self.assertRaisesRegex(ValueError,
2532+
f"expected a positive input, got {float(x)}"):
2533+
math.log(x)
2534+
x = -123
2535+
with self.assertRaisesRegex(ValueError,
2536+
f"expected a positive input, got {x}"):
2537+
math.log(x)
2538+
with self.assertRaisesRegex(ValueError,
2539+
f"expected a float or nonnegative integer, got {x}"):
2540+
math.gamma(x)
2541+
x = 1.0
2542+
with self.assertRaisesRegex(ValueError,
2543+
f"expected a number between -1 and 1, got {x}"):
2544+
math.atanh(x)
2545+
25062546
# Custom assertions.
25072547

25082548
def assertIsNaN(self, value):
+3Lines 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

Copy file name to clipboardExpand all lines: Modules/mathmodule.c
+66-29Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -858,12 +858,15 @@ math_lcm_impl(PyObject *module, PyObject * const *args,
858858
* true (1), but may return false (0) without setting up an exception.
859859
*/
860860
static int
861-
is_error(double x)
861+
is_error(double x, int raise_edom)
862862
{
863863
int result = 1; /* presumption of guilt */
864864
assert(errno); /* non-zero errno is a precondition for calling */
865-
if (errno == EDOM)
866-
PyErr_SetString(PyExc_ValueError, "math domain error");
865+
if (errno == EDOM) {
866+
if (raise_edom) {
867+
PyErr_SetString(PyExc_ValueError, "math domain error");
868+
}
869+
}
867870

868871
else if (errno == ERANGE) {
869872
/* ANSI C generally requires libm functions to set ERANGE
@@ -928,50 +931,69 @@ is_error(double x)
928931
*/
929932

930933
static PyObject *
931-
math_1(PyObject *arg, double (*func) (double), int can_overflow)
934+
math_1(PyObject *arg, double (*func) (double), int can_overflow,
935+
const char *err_msg)
932936
{
933937
double x, r;
934938
x = PyFloat_AsDouble(arg);
935939
if (x == -1.0 && PyErr_Occurred())
936940
return NULL;
937941
errno = 0;
938942
r = (*func)(x);
939-
if (isnan(r) && !isnan(x)) {
940-
PyErr_SetString(PyExc_ValueError,
941-
"math domain error"); /* invalid arg */
942-
return NULL;
943-
}
943+
if (isnan(r) && !isnan(x))
944+
goto domain_err; /* domain error */
944945
if (isinf(r) && isfinite(x)) {
945946
if (can_overflow)
946947
PyErr_SetString(PyExc_OverflowError,
947948
"math range error"); /* overflow */
948949
else
949-
PyErr_SetString(PyExc_ValueError,
950-
"math domain error"); /* singularity */
950+
goto domain_err; /* singularity */
951951
return NULL;
952952
}
953-
if (isfinite(r) && errno && is_error(r))
953+
if (isfinite(r) && errno && is_error(r, 1))
954954
/* this branch unnecessary on most platforms */
955955
return NULL;
956956

957957
return PyFloat_FromDouble(r);
958+
959+
domain_err:
960+
if (err_msg) {
961+
char *buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);
962+
if (buf) {
963+
PyErr_Format(PyExc_ValueError, err_msg, buf);
964+
PyMem_Free(buf);
965+
}
966+
}
967+
else {
968+
PyErr_SetString(PyExc_ValueError, "math domain error");
969+
}
970+
return NULL;
958971
}
959972

960973
/* variant of math_1, to be used when the function being wrapped is known to
961974
set errno properly (that is, errno = EDOM for invalid or divide-by-zero,
962975
errno = ERANGE for overflow). */
963976

964977
static PyObject *
965-
math_1a(PyObject *arg, double (*func) (double))
978+
math_1a(PyObject *arg, double (*func) (double), const char *err_msg)
966979
{
967980
double x, r;
968981
x = PyFloat_AsDouble(arg);
969982
if (x == -1.0 && PyErr_Occurred())
970983
return NULL;
971984
errno = 0;
972985
r = (*func)(x);
973-
if (errno && is_error(r))
986+
if (errno && is_error(r, err_msg ? 0 : 1)) {
987+
if (err_msg && errno == EDOM) {
988+
assert(!PyErr_Occurred()); /* exception is not set by is_error() */
989+
char *buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);
990+
if (buf) {
991+
PyErr_Format(PyExc_ValueError, err_msg, buf);
992+
PyMem_Free(buf);
993+
}
994+
}
974995
return NULL;
996+
}
975997
return PyFloat_FromDouble(r);
976998
}
977999

@@ -1031,21 +1053,33 @@ math_2(PyObject *const *args, Py_ssize_t nargs,
10311053
else
10321054
errno = 0;
10331055
}
1034-
if (errno && is_error(r))
1056+
if (errno && is_error(r, 1))
10351057
return NULL;
10361058
else
10371059
return PyFloat_FromDouble(r);
10381060
}
10391061

10401062
#define FUNC1(funcname, func, can_overflow, docstring) \
10411063
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1042-
return math_1(args, func, can_overflow); \
1064+
return math_1(args, func, can_overflow, NULL); \
1065+
}\
1066+
PyDoc_STRVAR(math_##funcname##_doc, docstring);
1067+
1068+
#define FUNC1D(funcname, func, can_overflow, docstring, err_msg) \
1069+
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1070+
return math_1(args, func, can_overflow, err_msg); \
10431071
}\
10441072
PyDoc_STRVAR(math_##funcname##_doc, docstring);
10451073

10461074
#define FUNC1A(funcname, func, docstring) \
10471075
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1048-
return math_1a(args, func); \
1076+
return math_1a(args, func, NULL); \
1077+
}\
1078+
PyDoc_STRVAR(math_##funcname##_doc, docstring);
1079+
1080+
#define FUNC1AD(funcname, func, docstring, err_msg) \
1081+
static PyObject * math_##funcname(PyObject *self, PyObject *args) { \
1082+
return math_1a(args, func, err_msg); \
10491083
}\
10501084
PyDoc_STRVAR(math_##funcname##_doc, docstring);
10511085

@@ -1077,9 +1111,10 @@ FUNC2(atan2, atan2,
10771111
"atan2($module, y, x, /)\n--\n\n"
10781112
"Return the arc tangent (measured in radians) of y/x.\n\n"
10791113
"Unlike atan(y/x), the signs of both x and y are considered.")
1080-
FUNC1(atanh, atanh, 0,
1114+
FUNC1D(atanh, atanh, 0,
10811115
"atanh($module, x, /)\n--\n\n"
1082-
"Return the inverse hyperbolic tangent of x.")
1116+
"Return the inverse hyperbolic tangent of x.",
1117+
"expected a number between -1 and 1, got %s")
10831118
FUNC1(cbrt, cbrt, 0,
10841119
"cbrt($module, x, /)\n--\n\n"
10851120
"Return the cube root of x.")
@@ -1190,9 +1225,10 @@ math_floor(PyObject *module, PyObject *number)
11901225
return PyLong_FromDouble(floor(x));
11911226
}
11921227

1193-
FUNC1A(gamma, m_tgamma,
1228+
FUNC1AD(gamma, m_tgamma,
11941229
"gamma($module, x, /)\n--\n\n"
1195-
"Gamma function at x.")
1230+
"Gamma function at x.",
1231+
"expected a float or nonnegative integer, got %s")
11961232
FUNC1A(lgamma, m_lgamma,
11971233
"lgamma($module, x, /)\n--\n\n"
11981234
"Natural logarithm of absolute value of Gamma function at x.")
@@ -1212,9 +1248,10 @@ FUNC1(sin, sin, 0,
12121248
FUNC1(sinh, sinh, 1,
12131249
"sinh($module, x, /)\n--\n\n"
12141250
"Return the hyperbolic sine of x.")
1215-
FUNC1(sqrt, sqrt, 0,
1251+
FUNC1D(sqrt, sqrt, 0,
12161252
"sqrt($module, x, /)\n--\n\n"
1217-
"Return the square root of x.")
1253+
"Return the square root of x.",
1254+
"expected a nonnegative input, got %s")
12181255
FUNC1(tan, tan, 0,
12191256
"tan($module, x, /)\n--\n\n"
12201257
"Return the tangent of x (measured in radians).")
@@ -2141,7 +2178,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i)
21412178
errno = ERANGE;
21422179
}
21432180

2144-
if (errno && is_error(r))
2181+
if (errno && is_error(r, 1))
21452182
return NULL;
21462183
return PyFloat_FromDouble(r);
21472184
}
@@ -2195,8 +2232,8 @@ loghelper(PyObject* arg, double (*func)(double))
21952232

21962233
/* Negative or zero inputs give a ValueError. */
21972234
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
2198-
PyErr_SetString(PyExc_ValueError,
2199-
"math domain error");
2235+
PyErr_Format(PyExc_ValueError,
2236+
"expected a positive input, got %S", arg);
22002237
return NULL;
22012238
}
22022239

@@ -2220,7 +2257,7 @@ loghelper(PyObject* arg, double (*func)(double))
22202257
}
22212258

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

22262263

@@ -2369,7 +2406,7 @@ math_fmod_impl(PyObject *module, double x, double y)
23692406
else
23702407
errno = 0;
23712408
}
2372-
if (errno && is_error(r))
2409+
if (errno && is_error(r, 1))
23732410
return NULL;
23742411
else
23752412
return PyFloat_FromDouble(r);
@@ -3010,7 +3047,7 @@ math_pow_impl(PyObject *module, double x, double y)
30103047
}
30113048
}
30123049

3013-
if (errno && is_error(r))
3050+
if (errno && is_error(r, 1))
30143051
return NULL;
30153052
else
30163053
return PyFloat_FromDouble(r);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.