-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
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
bpo-28280: Make PyMapping_Keys(), PyMapping_Values() and PyMapping_Items() always return a list #3840
Changes from 11 commits
bd59df1
15c1486
2d4bd68
f01f279
4b5852d
6682e53
47ea6e7
3e81e35
0421b5c
587a9ba
5dc52c1
de2c440
d492b91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -336,6 +336,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 Serhiy Storchaka and Oren Milman in :issue:`28280`.) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to mention my name. I'm just a reviewer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are the author of the issue in bpo, but as you wish. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The one who opened an issue is not the one who fixed it. |
||
|
||
* Added functions :c:func:`PySlice_Unpack` and :c:func:`PySlice_AdjustIndices`. | ||
(Contributed by Serhiy Storchaka in :issue:`27867`.) | ||
|
||
|
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 | ||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a need to test this class? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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): | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Make `PyMapping_Keys()`, `PyMapping_Values()` and `PyMapping_Items()` always | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment that this is mainly a copy of PySequence_Fast. |
||
{ | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just calling list(obj)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't matter. Calling There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add a special case for tuples:
But I think this is a premature optimization. Do you know any class that return There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.%s() returned a non-iterable (type %.200s)", | ||
Py_TYPE(o)->tp_name, | ||
meth_id->string, | ||
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() */ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a "versionchanged" directive.
This is needed for users that write a code that can be used with older Python.