summaryrefslogtreecommitdiff
path: root/simplejson
diff options
context:
space:
mode:
authorGregory P. Smith [Google LLC] <gps@google.com>2021-08-20 15:39:48 -0700
committerGregory P. Smith [Google LLC] <gps@google.com>2021-08-20 15:39:48 -0700
commitf53c2459e477fd81d2659bd8b64e1a10dae5e8cd (patch)
tree9d355d221a8dd3eb89db583b70b5dc1b40e6e61f /simplejson
parent2b75ded9dbaf6768c0d74e887ea84b8e9124aa86 (diff)
downloadsimplejson-f53c2459e477fd81d2659bd8b64e1a10dae5e8cd.tar.gz
Move the PyDict_Check after the _asdict call.
Add a unittest.
Diffstat (limited to 'simplejson')
-rw-r--r--simplejson/_speedups.c12
-rw-r--r--simplejson/tests/test_namedtuple.py27
2 files changed, 33 insertions, 6 deletions
diff --git a/simplejson/_speedups.c b/simplejson/_speedups.c
index 7c3b6b0..ef0dc6b 100644
--- a/simplejson/_speedups.c
+++ b/simplejson/_speedups.c
@@ -386,9 +386,8 @@ static int
_is_namedtuple(PyObject *obj)
{
int rval = 0;
- if (!PyTuple_Check(obj)) {
- return 0;
- }
+ /* We intentionally accept anything with a duck typed _asdict method rather
+ * than requiring it to pass PyTuple_Check(obj). */
PyObject *_asdict = PyObject_GetAttrString(obj, "_asdict");
if (_asdict == NULL) {
PyErr_Clear();
@@ -2856,6 +2855,10 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
return rv;
newobj = PyObject_CallMethod(obj, "_asdict", NULL);
if (newobj != NULL) {
+ if (!PyDict_Check(newobj)) {
+ Py_DECREF(newobj);
+ return -1;
+ }
rv = encoder_listencode_dict(s, rval, newobj, indent_level);
Py_DECREF(newobj);
}
@@ -2956,9 +2959,6 @@ encoder_listencode_dict(PyEncoderObject *s, JSON_Accu *rval, PyObject *dct, Py_s
PyObject *encoded = NULL;
Py_ssize_t idx;
- if (!PyDict_Check(dct)) {
- return -1;
- }
if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) {
open_dict = JSON_InternFromString("{");
close_dict = JSON_InternFromString("}");
diff --git a/simplejson/tests/test_namedtuple.py b/simplejson/tests/test_namedtuple.py
index 4387894..03e6b14 100644
--- a/simplejson/tests/test_namedtuple.py
+++ b/simplejson/tests/test_namedtuple.py
@@ -4,6 +4,11 @@ import simplejson as json
from simplejson.compat import StringIO
try:
+ from unittest import mock
+except ImportError:
+ mock = None
+
+try:
from collections import namedtuple
except ImportError:
class Value(tuple):
@@ -120,3 +125,25 @@ class TestNamedTuple(unittest.TestCase):
self.assertEqual(
json.dumps(f({})),
json.dumps(f(DeadDict()), namedtuple_as_object=True))
+
+ def test_asdict_does_not_return_dict(self):
+ if not mock:
+ if hasattr(unittest, "SkipTest"):
+ raise unittest.SkipTest("unittest.mock required")
+ else:
+ print("unittest.mock not available")
+ return
+ fake = mock.Mock()
+ self.assertTrue(hasattr(fake, '_asdict'))
+ self.assertTrue(callable(fake._asdict))
+ self.assertFalse(isinstance(fake._asdict(), dict))
+ # https://github.com/simplejson/simplejson/pull/284
+ # when running under a debug build of CPython (COPTS=-UNDEBUG)
+ # a C assertion could fire due to an unchecked error of an PyDict
+ # API call on a non-dict internally in _speedups.c. Without a debug
+ # build of CPython this test likely passes either way despite the
+ # potential for internal data corruption. Getting it to crash in
+ # a debug build is not always easy either as it requires an
+ # assert(!PyErr_Occurred()) that could fire later on.
+ self.assertRaises(TypeError):
+ json.dumps({23: fake}, namedtuple_as_object=True, for_json=False)