Skip to content

bpo-28280: Make PyMapping_Keys(), PyMapping_Values() and PyMapping_Items() always return a list #3840

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
Oct 8, 2017
Merged
21 changes: 15 additions & 6 deletions Doc/c-api/mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,29 @@ Mapping Protocol

.. c:function:: PyObject* PyMapping_Keys(PyObject *o)

On success, return a list or tuple of the keys in object *o*. On failure,
return *NULL*.
On success, return a list of the keys in object *o*. On failure, return
*NULL*.

.. versionchanged:: 3.7
Previously, the function returned a list or a tuple.


.. c:function:: PyObject* PyMapping_Values(PyObject *o)

On success, return a list or tuple of the values in object *o*. On failure,
return *NULL*.
On success, return a list of the values in object *o*. On failure, return
*NULL*.

.. versionchanged:: 3.7
Previously, the function returned a list or a tuple.


.. c:function:: PyObject* PyMapping_Items(PyObject *o)

On success, return a list or tuple of the items in object *o*, where each item
is a tuple containing a key-value pair. On failure, return *NULL*.
On success, return a list of the items in object *o*, where each item is a
tuple containing a key-value pair. On failure, return *NULL*.

.. versionchanged:: 3.7
Previously, the function returned a list or a tuple.


.. c:function:: PyObject* PyMapping_GetItemString(PyObject *o, const char *key)
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ Build and C API Changes
is now of type ``const char *`` rather of ``char *``. (Contributed by Serhiy
Storchaka in :issue:`28769`.)

* The result of :c:func:`PyMapping_Keys`, :c:func:`PyMapping_Values` and
:c:func:`PyMapping_Items` is now always a list, rather than a list or a
tuple. (Contributed by Oren Milman in :issue:`28280`.)

* Added functions :c:func:`PySlice_Unpack` and :c:func:`PySlice_AdjustIndices`.
(Contributed by Serhiy Storchaka in :issue:`27867`.)

Expand Down
46 changes: 45 additions & 1 deletion Lib/test/test_capi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Run the _testcapi module tests (tests for the Python/C API): by defn,
# these are all functions _testcapi exports whose name begins with 'test_'.

from collections import namedtuple
from collections import namedtuple, OrderedDict
import os
import pickle
import platform
Expand Down Expand Up @@ -272,6 +272,50 @@ class C(): pass
self.assertIn(b'MemoryError 2 20', out)
self.assertIn(b'MemoryError 3 30', out)

def test_mapping_keys_values_items(self):
class Mapping1(dict):
Copy link
Member

Choose a reason for hiding this comment

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

Is there a need to test this class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its purpose is to test cases in which the methods already return a list, i.e. when PyList_CheckExact(meth_output) is true.

def keys(self):
return list(super().keys())
def values(self):
return list(super().values())
def items(self):
return list(super().items())
class Mapping2(dict):
def keys(self):
return tuple(super().keys())
def values(self):
return tuple(super().values())
def items(self):
return tuple(super().items())
dict_obj = {'foo': 1, 'bar': 2, 'spam': 3}

for mapping in [{}, OrderedDict(), Mapping1(), Mapping2(),
dict_obj, OrderedDict(dict_obj),
Mapping1(dict_obj), Mapping2(dict_obj)]:
self.assertListEqual(_testcapi.get_mapping_keys(mapping),
list(mapping.keys()))
self.assertListEqual(_testcapi.get_mapping_values(mapping),
list(mapping.values()))
self.assertListEqual(_testcapi.get_mapping_items(mapping),
list(mapping.items()))

def test_mapping_keys_values_items_bad_arg(self):
self.assertRaises(AttributeError, _testcapi.get_mapping_keys, None)
self.assertRaises(AttributeError, _testcapi.get_mapping_values, None)
self.assertRaises(AttributeError, _testcapi.get_mapping_items, None)

class BadMapping:
def keys(self):
return None
def values(self):
return None
def items(self):
return None
bad_mapping = BadMapping()
self.assertRaises(TypeError, _testcapi.get_mapping_keys, bad_mapping)
self.assertRaises(TypeError, _testcapi.get_mapping_values, bad_mapping)
self.assertRaises(TypeError, _testcapi.get_mapping_items, bad_mapping)


class TestPendingCalls(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make `PyMapping_Keys()`, `PyMapping_Values()` and `PyMapping_Items()` always
Copy link
Member

Choose a reason for hiding this comment

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

Mention that it was possible to return a tuple.

return a `list` (rather than a `list` or a `tuple`). Patch by Oren Milman.
22 changes: 22 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4306,6 +4306,25 @@ py_w_stopcode(PyObject *self, PyObject *args)
#endif


static PyObject *
get_mapping_keys(PyObject* self, PyObject *obj)
{
return PyMapping_Keys(obj);
}

static PyObject *
get_mapping_values(PyObject* self, PyObject *obj)
{
return PyMapping_Values(obj);
}

static PyObject *
get_mapping_items(PyObject* self, PyObject *obj)
{
return PyMapping_Items(obj);
}


static PyObject *
test_pythread_tss_key_state(PyObject *self, PyObject *args)
{
Expand Down Expand Up @@ -4573,6 +4592,9 @@ static PyMethodDef TestMethods[] = {
#ifdef W_STOPCODE
{"W_STOPCODE", py_w_stopcode, METH_VARARGS},
#endif
{"get_mapping_keys", get_mapping_keys, METH_O},
{"get_mapping_values", get_mapping_values, METH_O},
{"get_mapping_items", get_mapping_items, METH_O},
{"test_pythread_tss_key_state", test_pythread_tss_key_state, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
Expand Down
76 changes: 49 additions & 27 deletions Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -2147,55 +2147,77 @@ PyMapping_HasKey(PyObject *o, PyObject *key)
return 0;
}

/* This function is quite similar to PySequence_Fast(), but specialized to be
a helper for PyMapping_Keys(), PyMapping_Items() and PyMapping_Values().
*/
static PyObject *
method_output_as_list(PyObject *o, _Py_Identifier *meth_id)
{
PyObject *it, *result, *meth_output;

assert(o != NULL);
meth_output = _PyObject_CallMethodId(o, meth_id, NULL);
if (meth_output == NULL || PyList_CheckExact(meth_output)) {
return meth_output;
}
it = PyObject_GetIter(meth_output);
Copy link
Member

Choose a reason for hiding this comment

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

Why not just calling list(obj)?

Copy link
Member

Choose a reason for hiding this comment

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

PySequence_List() few lines below is just a C API equivalent of calling list(). But first we want to produce better error message if keys() returns non-iterable. It is in the current code, and removing it will simplify the code, but introduce a regression.

Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to pass meth_output to PySequence_List() rather than iter(meth_output). list_extend() has a fast-path for tuple.

Copy link
Member

Choose a reason for hiding this comment

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

This doesn't matter. keys() returning tuple should be very rare. I don't know any example.

Calling PyObject_GetIter() on an iterator is cheap, it should return the iterator itself. But calling PyObject_GetIter() on a general iterable creates a new iterator object. This is not so cheap. This will add an overhead (and possible side effects) in most common cases.

Copy link
Member

Choose a reason for hiding this comment

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

list(iter(obj)) is not exactly the same than list(obj). My point on performance was the cost of PySequence_List(iterator) vs PySequence_List(tuple).

Copy link
Member

Choose a reason for hiding this comment

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

We could add a special case for tuples:

if (PyTuple_CheckExact(meth_output)) {
    result = PySequence_List(meth_output);
    Py_DECREF(meth_output);
    return result;
}

But I think this is a premature optimization. Do you know any class that return keys() as a tuple?

Copy link
Member

Choose a reason for hiding this comment

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

Nah, it's fine. I don't care much. The current code is ok.

if (it == NULL) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
PyErr_Format(PyExc_TypeError,
"%.200s.%U() returned a non-iterable (type %.200s)",
Py_TYPE(o)->tp_name,
meth_id->object,
Py_TYPE(meth_output)->tp_name);
}
Py_DECREF(meth_output);
return NULL;
}
Py_DECREF(meth_output);
result = PySequence_List(it);
Py_DECREF(it);
return result;
}

PyObject *
PyMapping_Keys(PyObject *o)
{
PyObject *keys;
PyObject *fast;
_Py_IDENTIFIER(keys);

if (PyDict_CheckExact(o))
if (o == NULL) {
return null_error();
}
if (PyDict_CheckExact(o)) {
return PyDict_Keys(o);
keys = _PyObject_CallMethodId(o, &PyId_keys, NULL);
if (keys == NULL)
return NULL;
fast = PySequence_Fast(keys, "o.keys() are not iterable");
Py_DECREF(keys);
return fast;
}
return method_output_as_list(o, &PyId_keys);
}

PyObject *
PyMapping_Items(PyObject *o)
{
PyObject *items;
PyObject *fast;
_Py_IDENTIFIER(items);

if (PyDict_CheckExact(o))
if (o == NULL) {
return null_error();
}
if (PyDict_CheckExact(o)) {
return PyDict_Items(o);
items = _PyObject_CallMethodId(o, &PyId_items, NULL);
if (items == NULL)
return NULL;
fast = PySequence_Fast(items, "o.items() are not iterable");
Py_DECREF(items);
return fast;
}
return method_output_as_list(o, &PyId_items);
}

PyObject *
PyMapping_Values(PyObject *o)
{
PyObject *values;
PyObject *fast;
_Py_IDENTIFIER(values);

if (PyDict_CheckExact(o))
if (o == NULL) {
return null_error();
}
if (PyDict_CheckExact(o)) {
return PyDict_Values(o);
values = _PyObject_CallMethodId(o, &PyId_values, NULL);
if (values == NULL)
return NULL;
fast = PySequence_Fast(values, "o.values() are not iterable");
Py_DECREF(values);
return fast;
}
return method_output_as_list(o, &PyId_values);
}

/* isinstance(), issubclass() */
Expand Down