From f53c2459e477fd81d2659bd8b64e1a10dae5e8cd Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith [Google LLC]" Date: Fri, 20 Aug 2021 15:39:48 -0700 Subject: Move the PyDict_Check after the _asdict call. Add a unittest. --- simplejson/_speedups.c | 12 ++++++------ simplejson/tests/test_namedtuple.py | 27 +++++++++++++++++++++++++++ 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 @@ -3,6 +3,11 @@ import unittest 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: @@ -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) -- cgit v1.2.1