Repository: jsonpickle/jsonpickle (python)

Issue:
Numpy handlers does not handle views correctly
When using the numpy handler, some classes cannot be serialized. One example is the LogisticRegression class in scikit-learn>=0.24:

[the following example uses scikit-learn==0.24 and the latest jsonpickle==3.0.1, although previous versions of jsonpickle have the same issue as well]

```python
import sklearn.datasets
from sklearn.linear_model import LogisticRegression
import jsonpickle

from jsonpickle.ext.numpy import register_handlers

register_handlers()


iris = sklearn.datasets.load_iris()
X = iris.data[:, :2]
y = (iris.target != 0) * 1

pipe = LogisticRegression(solver="lbfgs")

pipe.fit(X, y)

res = pipe.predict(X)

# Try serializing/deserializing
js = jsonpickle.dumps(pipe)

pipe2 = jsonpickle.loads(js)
```
This fails with the following error:
```
...
File ~/miniconda3/envs/dstools/lib/python3.8/site-packages/jsonpickle/ext/numpy.py:328, in NumpyNDArrayHandlerView.restore(self, data)
    324 else:
    325     # decode array view, which references the data of another array
    326     base = self.context.restore(base, reset=False)
    327     assert (
--> 328         base.flags.forc
    329     ), "Current implementation assumes base is C or F contiguous"
    331     arr = np.ndarray(
    332         buffer=base.data,
    333         dtype=self.restore_dtype(data).newbyteorder(data.get('byteorder', '|')),
   (...)
    336         strides=data.get('strides', None),
    337     )
    339     self.restore_flags(data, arr)

AttributeError: 'list' object has no attribute 'flags'
```

NOTE: if I do NOT use the numpy extension, i.e., if I comment the following lines:

```
# from jsonpickle.ext.numpy import register_handlers

# register_handlers()
```
then everything works. This seems to point to a specific issue with the numpy handler.

Pull request description:
Fix #431
This issue was caused by the numpy handler assuming that a certain object was always a numpy array, even though in some cases it wasn't. Add a check and coercion to validate the assumption.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c179967..5f7cf20 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,9 @@
+v3.0.2
+======
+    * Properly raise warning if a custom pickling handler returns None. (#433)
+    * Fix issue with serialization of certain sklearn objects breaking when
+      the numpy handler was enabled. (#431) (+434)
+
 v3.0.1
 ======
     * Remove accidental pin of setuptools to versions below 59. This allows
diff --git a/jsonpickle/ext/numpy.py b/jsonpickle/ext/numpy.py
index 2214607..af2bf71 100644
--- a/jsonpickle/ext/numpy.py
+++ b/jsonpickle/ext/numpy.py
@@ -324,6 +324,9 @@ class NumpyNDArrayHandlerView(NumpyNDArrayHandlerBinary):
         else:
             # decode array view, which references the data of another array
             base = self.context.restore(base, reset=False)
+            if not isinstance(base, np.ndarray):
+                # the object is probably a nested list
+                base = np.array(base)
             assert (
                 base.flags.forc
             ), "Current implementation assumes base is C or F contiguous"


Test patch:
diff --git a/tests/sklearn_test.py b/tests/sklearn_test.py
index 3eeab99..864cc29 100644
--- a/tests/sklearn_test.py
+++ b/tests/sklearn_test.py
@@ -4,6 +4,8 @@ import pytest
 
 try:
     import numpy as np
+    import sklearn.datasets
+    from sklearn.linear_model import LogisticRegression
     from sklearn.tree import DecisionTreeClassifier
 except ImportError:
     pytest.skip('sklearn is not available', allow_module_level=True)
@@ -53,5 +55,22 @@ def test_decision_tree():
         assert actual.get_depth() == classifier.get_depth()
 
 
+def test_nested_array_serialization():
+    iris = sklearn.datasets.load_iris()
+    X = iris.data[:, :2]
+    y = (iris.target != 0) * 1
+
+    pipe = LogisticRegression(solver="lbfgs")
+    pipe.fit(X, y)
+    res = pipe.predict(X)
+
+    # Try serializing/deserializing.
+    # Pipe is a LogisticRegression object that contains a nested array
+    js = jsonpickle.dumps(pipe)
+    pipe2 = jsonpickle.loads(js)
+    # coef_ only appears on an instance that has already been fit to something
+    assert np.all(pipe.coef_ == pipe2.coef_)
+
+
 if __name__ == '__main__':
     pytest.main([__file__])