Skip to content

Commit 13e05de

Browse files
committed
Fix math.ceil() and math.floor() to fall back to __ceil__ and __floor__
methods (respectively). With Keir Mierle.
1 parent 2fa33db commit 13e05de

File tree

2 files changed

+70
-6
lines changed

2 files changed

+70
-6
lines changed

Lib/test/test_math.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ def testCeil(self):
5858
self.ftest('ceil(-1.0)', math.ceil(-1.0), -1)
5959
self.ftest('ceil(-1.5)', math.ceil(-1.5), -1)
6060

61+
class TestCeil:
62+
def __ceil__(self):
63+
return 42
64+
class TestNoCeil:
65+
pass
66+
self.ftest('ceil(TestCeil())', math.ceil(TestCeil()), 42)
67+
self.assertRaises(TypeError, math.ceil, TestNoCeil())
68+
69+
t = TestNoCeil()
70+
t.__ceil__ = lambda *args: args
71+
self.assertRaises(TypeError, math.ceil, t)
72+
self.assertRaises(TypeError, math.ceil, t, 0)
73+
6174
def testCos(self):
6275
self.assertRaises(TypeError, math.cos)
6376
self.ftest('cos(-pi/2)', math.cos(-math.pi/2), 0)
@@ -101,6 +114,19 @@ def testFloor(self):
101114
self.ftest('floor(1.23e167)', math.floor(1.23e167), 1.23e167)
102115
self.ftest('floor(-1.23e167)', math.floor(-1.23e167), -1.23e167)
103116

117+
class TestFloor:
118+
def __floor__(self):
119+
return 42
120+
class TestNoFloor:
121+
pass
122+
self.ftest('floor(TestFloor())', math.floor(TestFloor()), 42)
123+
self.assertRaises(TypeError, math.floor, TestNoFloor())
124+
125+
t = TestNoFloor()
126+
t.__floor__ = lambda *args: args
127+
self.assertRaises(TypeError, math.floor, t)
128+
self.assertRaises(TypeError, math.floor, t, 0)
129+
104130
def testFmod(self):
105131
self.assertRaises(TypeError, math.fmod)
106132
self.ftest('fmod(10,1)', math.fmod(10,1), 0)

Modules/mathmodule.c

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,28 @@ FUNC1(atan, atan,
107107
FUNC2(atan2, atan2,
108108
"atan2(y, x)\n\nReturn the arc tangent (measured in radians) of y/x.\n"
109109
"Unlike atan(y/x), the signs of both x and y are considered.")
110-
FUNC1(ceil, ceil,
111-
"ceil(x)\n\nReturn the ceiling of x as a float.\n"
112-
"This is the smallest integral value >= x.")
110+
111+
static PyObject * math_ceil(PyObject *self, PyObject *number) {
112+
static PyObject *ceil_str = NULL;
113+
PyObject *method;
114+
115+
if (ceil_str == NULL) {
116+
ceil_str = PyUnicode_FromString("__ceil__");
117+
if (ceil_str == NULL)
118+
return NULL;
119+
}
120+
121+
method = _PyType_Lookup(Py_Type(number), ceil_str);
122+
if (method == NULL)
123+
return math_1(number, ceil);
124+
else
125+
return PyObject_CallFunction(method, "O", number);
126+
}
127+
128+
PyDoc_STRVAR(math_ceil_doc,
129+
"ceil(x)\n\nReturn the ceiling of x as a float.\n"
130+
"This is the smallest integral value >= x.");
131+
113132
FUNC1(cos, cos,
114133
"cos(x)\n\nReturn the cosine of x (measured in radians).")
115134
FUNC1(cosh, cosh,
@@ -118,9 +137,28 @@ FUNC1(exp, exp,
118137
"exp(x)\n\nReturn e raised to the power of x.")
119138
FUNC1(fabs, fabs,
120139
"fabs(x)\n\nReturn the absolute value of the float x.")
121-
FUNC1(floor, floor,
122-
"floor(x)\n\nReturn the floor of x as a float.\n"
123-
"This is the largest integral value <= x.")
140+
141+
static PyObject * math_floor(PyObject *self, PyObject *number) {
142+
static PyObject *floor_str = NULL;
143+
PyObject *method;
144+
145+
if (floor_str == NULL) {
146+
floor_str = PyUnicode_FromString("__floor__");
147+
if (floor_str == NULL)
148+
return NULL;
149+
}
150+
151+
method = _PyType_Lookup(Py_Type(number), floor_str);
152+
if (method == NULL)
153+
return math_1(number, floor);
154+
else
155+
return PyObject_CallFunction(method, "O", number);
156+
}
157+
158+
PyDoc_STRVAR(math_floor_doc,
159+
"floor(x)\n\nReturn the floor of x as a float.\n"
160+
"This is the largest integral value <= x.");
161+
124162
FUNC2(fmod, fmod,
125163
"fmod(x,y)\n\nReturn fmod(x, y), according to platform C."
126164
" x % y may differ.")

0 commit comments

Comments
 (0)