microsoft/Qcodes

Encoding bytestrings using NumpyJSONEncoder triggers RecursionError

jenshnielsen opened this issue · 2 comments

The following code

import json
from qcodes.utils.helpers import NumpyJSONEncoder

a = {        "voltageDC": {
            "__class__": "qcodes_fluke8842aDriver_2.voltageDC",
            "full_name": "fDMMInst1_voltageDC",
            "value": 5.7e-05,
            "ts": "2022-09-12 10:18:34",
            "raw_value": b"+000.057E-3\r\n",
            "name": "voltageDC",
            "unit": "V",
            "inter_delay": 0,
            "post_delay": 0,
            "instrument": "qcodes_fluke8842aDriver_2.Fluke8842aDriver",
            "instrument_name": "fDMMInst1",
            "label": "voltageDC",
        }}
json.dumps(a,  cls=NumpyJSONEncoder)

trigger

---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
Cell In [19], line 18
      2 from qcodes.utils.helpers import NumpyJSONEncoder
      4 a = {        "voltageDC": {
      5             "__class__": "qcodes_fluke8842aDriver_2.voltageDC",
      6             "full_name": "fDMMInst1_voltageDC",
   (...)
     16             "label": "voltageDC",
     17         }}
---> 18 json.dumps(a,  cls=NumpyJSONEncoder)

File ~\Miniconda3\envs\qcodespip38\lib\json\__init__.py:234, in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    232 if cls is None:
    233     cls = JSONEncoder
--> 234 return cls(
    235     skipkeys=skipkeys, ensure_ascii=ensure_ascii,
    236     check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237     separators=separators, default=default, sort_keys=sort_keys,
    238     **kw).encode(obj)

File ~\Miniconda3\envs\qcodespip38\lib\json\encoder.py:199, in JSONEncoder.encode(self, o)
    195         return encode_basestring(o)
    196 # This doesn't pass the iterator directly to ''.join() because the
    197 # exceptions aren't as detailed.  The list call should be roughly
    198 # equivalent to the PySequence_Fast that ''.join() would do.
--> 199 chunks = self.iterencode(o, _one_shot=True)
    200 if not isinstance(chunks, (list, tuple)):
    201     chunks = list(chunks)

File ~\Miniconda3\envs\qcodespip38\lib\json\encoder.py:257, in JSONEncoder.iterencode(self, o, _one_shot)
    252 else:
    253     _iterencode = _make_iterencode(
    254         markers, self.default, _encoder, self.indent, floatstr,
    255         self.key_separator, self.item_separator, self.sort_keys,
    256         self.skipkeys, _one_shot)
--> 257 return _iterencode(o, 0)

File ~\source\repos\Qcodes\qcodes\utils\json_utils.py:54, in NumpyJSONEncoder.default(self, obj)
     51 elif isinstance(obj, np.ndarray):
     52     # for numpy arrays
     53     return obj.tolist()
---> 54 elif isinstance(obj, numbers.Complex) and not isinstance(obj, numbers.Real):
     55     return {
     56         "__dtype__": "complex",
     57         "re": float(obj.real),
     58         "im": float(obj.imag),
     59     }
     60 elif isinstance(obj, uncertainties.UFloat):

File ~\Miniconda3\envs\qcodespip38\lib\abc.py:98, in ABCMeta.__instancecheck__(cls, instance)
     96 def __instancecheck__(cls, instance):
     97     """Override for isinstance(instance, cls)."""
---> 98     return _abc_instancecheck(cls, instance)

RecursionError: maximum recursion depth exceeded in comparison

This should either fail with a more meaningful error, skip the problematic element or correctly handle the bytestring.

This is caused by this path in the json encoder

return {
              "__class__": type(obj).__name__,
              "__args__": getattr(obj, "__getnewargs__")(),
          }

which returns

{'__class__': 'bytes', '__args__': (b'+000.057E-3\r\n',)}

causing the recursion

A simple work around would be not to try to pickle bytes.

if hasattr(obj, "__getnewargs__") and not isinstance(obj, (bytes, bytearray)):