Repository: microsoft/Qcodes

Issue:
Encoding bytestrings using NumpyJSONEncoder triggers RecursionError 
The following code

```python
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. 


Discussion:
This is caused by this path in the json encoder

```python
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. 

```python
if hasattr(obj, "__getnewargs__") and not isinstance(obj, (bytes, bytearray)):
```

Resolving patch:
diff --git a/qcodes/utils/json_utils.py b/qcodes/utils/json_utils.py
--- a/qcodes/utils/json_utils.py
+++ b/qcodes/utils/json_utils.py
@@ -74,7 +74,12 @@ def default(self, obj: Any) -> Any:
                     return obj.data
                 # See if the object supports the pickle protocol.
                 # If so, we should be able to use that to serialize.
-                if hasattr(obj, "__getnewargs__"):
+                # __getnewargs__ will return bytes for a bytes object
+                # causing an infinte recursion, so we do not
+                # try to pickle bytes or bytearrays
+                if hasattr(obj, "__getnewargs__") and not isinstance(
+                    obj, (bytes, bytearray)
+                ):
                     return {
                         "__class__": type(obj).__name__,
                         "__args__": getattr(obj, "__getnewargs__")(),


Test patch:
diff --git a/qcodes/tests/helpers/test_json_encoder.py b/qcodes/tests/helpers/test_json_encoder.py
--- a/qcodes/tests/helpers/test_json_encoder.py
+++ b/qcodes/tests/helpers/test_json_encoder.py
@@ -2,8 +2,10 @@
 import warnings
 from collections import OrderedDict, UserDict
 
+import hypothesis.strategies as hst
 import numpy as np
 import pytest
+from hypothesis import given
 
 with warnings.catch_warnings():
     # this context manager can be removed when uncertainties
@@ -168,3 +170,26 @@ def test_numpy_encoder_examplemetadata():
         'myuserdict': {'a': 1}
     }
     assert metadata == data_dict
+
+
+@given(bytes_to_encode=hst.binary(max_size=100))
+def test_bytes(bytes_to_encode):
+    e = NumpyJSONEncoder()
+    v = e.encode(bytes_to_encode)
+
+    default_encoder = json.JSONEncoder()
+    # encoding bytes is the same as using the
+    # default encode on the str value of the bytes
+    assert v == default_encoder.encode(str(bytes_to_encode))
+
+
+@given(bytes_to_encode=hst.binary(max_size=100))
+def test_bytes_array(bytes_to_encode):
+    e = NumpyJSONEncoder()
+    value = bytearray(bytes_to_encode)
+    v = e.encode(value)
+
+    default_encoder = json.JSONEncoder()
+    # encoding bytes is the same as using the
+    # default encode on the str value of the bytes array
+    assert v == default_encoder.encode(str(value))