Skip to content
Open
3 changes: 3 additions & 0 deletions Doc/c-api/number.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ Number Protocol
See the built-in function :func:`divmod`. Returns ``NULL`` on failure. This is
the equivalent of the Python expression ``divmod(o1, o2)``.

.. versionchanged:: 3.10
Always returns a tuple of size 2 on success.


.. c:function:: PyObject* PyNumber_Power(PyObject *o1, PyObject *o2, PyObject *o3)

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@ Porting to Python 3.10
This section lists previously described changes and other bugfixes
that may require changes to your code.

Changes in the Python API
-------------------------

* :func:`divmod` and :c:func:`PyNumber_Divmod` will check the result of special
methods ``__divmod__`` and ``__rdivmod__`` and emit a deprecation warning if
it is not a tuple or raise an error if its size is not 2.
(Contributed by Serhiy Storchaka in :issue:`34676`.)


Build Changes
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,33 @@ def test_divmod(self):
self.assertAlmostEqual(result[1], exp_result[1])

self.assertRaises(TypeError, divmod)
self.assertRaises(TypeError, divmod, 12, 7, 3)
self.assertRaises(TypeError, divmod, 12, [])
self.assertRaises(TypeError, divmod, [], 7)

def fake_divmod(divmod_ret_val):
class FakeDivmod:
def __divmod__(*args):
return divmod_ret_val
return FakeDivmod()
def fake_rdivmod(divmod_ret_val):
class FakeRDivmod:
def __rdivmod__(*args):
return divmod_ret_val
return FakeRDivmod()

self.assertEqual(divmod(fake_divmod((2, 3)), []), (2, 3))
with self.assertWarns(DeprecationWarning):
self.assertEqual(divmod(fake_divmod([2, 3]), []), (2, 3))
self.assertRaises(TypeError, divmod, fake_divmod(42), [])
self.assertRaises(TypeError, divmod, fake_divmod(()), [])
self.assertRaises(TypeError, divmod, fake_divmod((1, 2, 3)), [])
self.assertEqual(divmod([], fake_rdivmod((2, 3))), (2, 3))
with self.assertWarns(DeprecationWarning):
self.assertEqual(divmod([], fake_rdivmod([2, 3])), (2, 3))
self.assertRaises(TypeError, divmod, [], fake_rdivmod(42))
self.assertRaises(TypeError, divmod, [], fake_rdivmod(()))
self.assertRaises(TypeError, divmod, [], fake_rdivmod((1, 2, 3)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to add a namedtuple example here, to state explicitly that tuple subclasses are intended to be supported.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitate. Should we allow tuple subclasses or require exact tuples? Most converting special methods (__int__, __index__, __float__, __complex__) are required to return an instance of exact type. From C side, if use the C API for unpacking the tuple, there is no difference between tuples and tuple subclasses. But there mey be difference at Python level if the tuple subclass overrides some methods like __add__, __iter__, __len__, __getitem__.

For now, I prefer to not promise the namedtuple support.


def test_eval(self):
self.assertEqual(eval('1+1'), 2)
Expand Down
11 changes: 9 additions & 2 deletions Lib/test/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
"rfloordiv",
"mod",
"rmod",
"divmod",
"rdivmod",
"pow",
"rpow",
"rshift",
Expand Down Expand Up @@ -117,6 +115,15 @@ def __gt__(self, *args):
@trackCall
def __ge__(self, *args):
return True

@trackCall
def __divmod__(self, *args):
return 1, 2

@trackCall
def __rdivmod__(self, *args):
return 1, 2

"""

# Synthesize all the other AllTests methods from the names in testmeths.
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,9 +877,9 @@ def test_rop(self):
# Allow other classes to be trained to interact with Decimals
class E:
def __divmod__(self, other):
return 'divmod ' + str(other)
return 'divmod', str(other)
def __rdivmod__(self, other):
return str(other) + ' rdivmod'
return str(other), 'rdivmod'
def __lt__(self, other):
return 'lt ' + str(other)
def __gt__(self, other):
Expand All @@ -893,8 +893,8 @@ def __eq__(self, other):
def __ne__(self, other):
return 'ne ' + str(other)

self.assertEqual(divmod(E(), Decimal(10)), 'divmod 10')
self.assertEqual(divmod(Decimal(10), E()), '10 rdivmod')
self.assertEqual(divmod(E(), Decimal(10)), ('divmod', '10'))
self.assertEqual(divmod(Decimal(10), E()), ('10', 'rdivmod'))
self.assertEqual(eval('Decimal(10) < E()'), 'gt 10')
self.assertEqual(eval('Decimal(10) > E()'), 'lt 10')
self.assertEqual(eval('Decimal(10) <= E()'), 'ge 10')
Expand Down
22 changes: 21 additions & 1 deletion Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2002,11 +2002,31 @@ def __aiter__():
return _AsyncIterator(iter(ret_val))
return __aiter__

def _get_divmod(self):
def __divmod__(other):
ret_val = self.__divmod__._mock_return_value
if ret_val is not DEFAULT:
return ret_val
return (MagicMock(name='mock.__divmod__()[0]'),
MagicMock(name='mock.__divmod__()[1]'))
return __divmod__

def _get_rdivmod(self):
def __rdivmod__(other):
ret_val = self.__rdivmod__._mock_return_value
if ret_val is not DEFAULT:
return ret_val
return (MagicMock(name='mock.__rdivmod__()[0]'),
MagicMock(name='mock.__rdivmod__()[1]'))
return __rdivmod__

_side_effect_methods = {
'__eq__': _get_eq,
'__ne__': _get_ne,
'__iter__': _get_iter,
'__aiter__': _get_async_iter
'__aiter__': _get_async_iter,
'__divmod__': _get_divmod,
'__rdivmod__': _get_rdivmod,
}


Expand Down
24 changes: 14 additions & 10 deletions Lib/unittest/test/testmock/testmagicmethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,18 +477,22 @@ def test_matmul(self):

def test_divmod_and_rdivmod(self):
m = MagicMock()
self.assertIsInstance(divmod(5, m), MagicMock)
n, d = divmod(m, 2)
self.assertIsInstance(n, MagicMock)
self.assertIsInstance(d, MagicMock)
n, d = m.__divmod__(2)
self.assertIsInstance(n, MagicMock)
self.assertIsInstance(d, MagicMock)
n, d = divmod(2, m)
self.assertIsInstance(n, MagicMock)
self.assertIsInstance(d, MagicMock)
n, d = m.__rdivmod__(2)
self.assertIsInstance(n, MagicMock)
self.assertIsInstance(d, MagicMock)
m.__divmod__.return_value = (2, 1)
m.__rdivmod__.return_value = (3, 4)
self.assertEqual(divmod(m, 2), (2, 1))
m = MagicMock()
foo = divmod(2, m)
self.assertIsInstance(foo, MagicMock)
foo_direct = m.__divmod__(2)
self.assertIsInstance(foo_direct, MagicMock)
bar = divmod(m, 2)
self.assertIsInstance(bar, MagicMock)
bar_direct = m.__rdivmod__(2)
self.assertIsInstance(bar_direct, MagicMock)
self.assertEqual(divmod(2, m), (3, 4))

# http://bugs.python.org/issue23310
# Check if you can change behaviour of magic methods in MagicMock init
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
It is now guaranteed that :func:`divmod` and :c:func:`PyNumber_Divmod` always
return a 2-tuple on success.
29 changes: 3 additions & 26 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1794,29 +1794,6 @@ delta_to_microseconds(PyDateTime_Delta *self)
return result;
}

static PyObject *
checked_divmod(PyObject *a, PyObject *b)
{
PyObject *result = PyNumber_Divmod(a, b);
if (result != NULL) {
if (!PyTuple_Check(result)) {
PyErr_Format(PyExc_TypeError,
"divmod() returned non-tuple (type %.200s)",
Py_TYPE(result)->tp_name);
Py_DECREF(result);
return NULL;
}
if (PyTuple_GET_SIZE(result) != 2) {
PyErr_Format(PyExc_TypeError,
"divmod() returned a tuple of size %zd",
PyTuple_GET_SIZE(result));
Py_DECREF(result);
return NULL;
}
}
return result;
}

/* Convert a number of us (as a Python int) to a timedelta.
*/
static PyObject *
Expand All @@ -1830,7 +1807,7 @@ microseconds_to_delta_ex(PyObject *pyus, PyTypeObject *type)
PyObject *num = NULL;
PyObject *result = NULL;

tuple = checked_divmod(pyus, us_per_second);
tuple = PyNumber_Divmod(pyus, us_per_second);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked closely at any of the other stuff, but +1 on the _datetimemodule.c stuff. It is very nice that it is now possible to make this simplification.

if (tuple == NULL) {
goto Done;
}
Expand All @@ -1849,7 +1826,7 @@ microseconds_to_delta_ex(PyObject *pyus, PyTypeObject *type)
Py_INCREF(num);
Py_DECREF(tuple);

tuple = checked_divmod(num, seconds_per_day);
tuple = PyNumber_Divmod(num, seconds_per_day);
if (tuple == NULL)
goto Done;
Py_DECREF(num);
Expand Down Expand Up @@ -2301,7 +2278,7 @@ delta_divmod(PyObject *left, PyObject *right)
return NULL;
}

divmod = checked_divmod(pyus_left, pyus_right);
divmod = PyNumber_Divmod(pyus_left, pyus_right);
Py_DECREF(pyus_left);
Py_DECREF(pyus_right);
if (divmod == NULL)
Expand Down
6 changes: 0 additions & 6 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4903,12 +4903,6 @@ split_py_long_to_s_and_ns(PyObject *module, PyObject *py_long, time_t *s, long *
divmod = PyNumber_Divmod(py_long, get_posix_state(module)->billion);
if (!divmod)
goto exit;
if (!PyTuple_Check(divmod) || PyTuple_GET_SIZE(divmod) != 2) {
PyErr_Format(PyExc_TypeError,
"%.200s.__divmod__() must return a 2-tuple, not %.200s",
_PyType_Name(Py_TYPE(py_long)), _PyType_Name(Py_TYPE(divmod)));
goto exit;
}
*s = _PyLong_AsTime_t(PyTuple_GET_ITEM(divmod, 0));
if ((*s == -1) && PyErr_Occurred())
goto exit;
Expand Down
41 changes: 40 additions & 1 deletion Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,6 @@ BINARY_FUNC(PyNumber_And, nb_and, "&")
BINARY_FUNC(PyNumber_Lshift, nb_lshift, "<<")
BINARY_FUNC(PyNumber_Rshift, nb_rshift, ">>")
BINARY_FUNC(PyNumber_Subtract, nb_subtract, "-")
BINARY_FUNC(PyNumber_Divmod, nb_divmod, "divmod()")

PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
Expand All @@ -954,6 +953,46 @@ PyNumber_Add(PyObject *v, PyObject *w)
return result;
}

PyObject *
PyNumber_Divmod(PyObject *v, PyObject *w)
{
PyObject *orig_result = NULL;
PyObject *result = binary_op(v, w, NB_SLOT(nb_divmod), "divmod()");
if (result == NULL) {
return NULL;
}
if (!PyTuple_Check(result)) {
orig_result = result;
result = PySequence_Tuple(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could Py_DECREF(orig_result) right here, and then you could eliminate the 3 Py_(X)DECREF(orig_result)s below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then there will be an undefined behavior in orig_result != NULL.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed the connection between Py_DECREF and free here, good point.

Setting a single boolean in this branch would eliminate the need for the orig_result != NULL, and would arguably be clearer - but I suppose that's bike-shedding

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now remember why I used this way. Keeping the reference to orig_result allows to use it in the error/warning message. The current version does not use it, but I was going to make a separate PR for more detailed error messages for all operations.

if (result == NULL) {
Py_DECREF(orig_result);
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
goto error;
}
return NULL;
}
}
if (PyTuple_GET_SIZE(result) != 2) {
Py_XDECREF(orig_result);
Py_DECREF(result);
error:
PyErr_SetString(PyExc_TypeError,
"__divmod__() and __rdivmod__() must return a 2-tuple");
return NULL;
}
if (orig_result != NULL) {
Py_DECREF(orig_result);
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"__divmod__() and __rdivmod__() must return a 2-tuple",
1) < 0)
{
Py_DECREF(result);
return NULL;
}
}
return result;
}

static PyObject *
sequence_repeat(ssizeargfunc repeatfunc, PyObject *seq, PyObject *n)
{
Expand Down