Repository: jsonpickle/jsonpickle (python)

Issue:
scikit learn decision tree gets unpickled as dictionary
I am having a problem unpickling scikit-learn deicsion trees. Example code:

```python
import numpy as np
import jsonpickle
import jsonpickle.ext.numpy as jsonpickle_numpy
from sklearn.tree import DecisionTreeClassifier

#  create data
np.random.seed(13)
x = np.random.randint(low=0, high = 10, size = 12)
X = x.reshape(4, 3)
Y = np.random.randint(low=0, high=2, size=4)
Y.reshape(-1,1)

# train model
clf = DecisionTreeClassifier(max_depth=1)
clf.fit(X, Y)

# freeze and thaw
jsonpickle_numpy.register_handlers()
p = jsonpickle.pickler.Pickler()
u = jsonpickle.unpickler.Unpickler()
j = p.flatten(clf)
clf1 = u.restore(json.loads(json.dumps(p.flatten(clf))))

# predict from thawed
xx = np.array([1, 2, 3])
xx = xx.reshape(1,-1)
clf1.predict(xx)

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-4e5a1309ca7a> in <module>()
     27 xx = np.array([1, 2, 3])
     28 xx = xx.reshape(1,-1)
---> 29 clf1.predict(xx)

/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sklearn/tree/tree.pyc in predict(self, X, check_input)
    403 
    404         X = self._validate_X_predict(X, check_input)
--> 405         proba = self.tree_.predict(X)
    406         n_samples = X.shape[0]
    407 

AttributeError: 'dict' object has no attribute 'predict'
```

On further inspection, the problem seems to be the following:

```python
print type(clf)
print type(clf1)
print type(clf.tree_)
print type(clf1.tree_)

<class 'sklearn.tree.tree.DecisionTreeClassifier'>
<class 'sklearn.tree.tree.DecisionTreeClassifier'>
<type 'sklearn.tree._tree.Tree'>
<type 'dict'>
```

i.e. the classifier object gets unpickled as the correct type, while the decision tree under `clf.tree_` is getting unpickled as a dictionary. I confirned the same behavior with `DecisionTreeRegressor` and ensemble methods which use trees , for example `GradientBoostingClassifier`. 

Pull request description:
tests: add a test suite for sklearn objects
Add the test recipe from #155 to demonstrate that
sklearn DecisionTreeClassifier objects can be properly
serialized.

This behavior was originally fixed in #170.

Closes #155
Signed-off-by: David Aguilar <davvid@gmail.com>

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index f97ac2a..f036d59 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,11 @@ v1.5.1
     * ``decode()`` documentation improvements.  (+341)
     * Serialization of Pandas DataFrame objects that contain
       timedelta64[ns] dtypes are now supported.  (+330) (#331)
+    * Dictionary identity is now preserved.  For example, if the same
+      dictionary appears twice in a list, the reconstituted list
+      will now contain two references to the same dictionary.  (#255) (+332)
+    * Unit tests were added to ensure that sklearn.tree.DecisionTreeClassifier
+      objects are properly serialized.  (#155) (+344)
 
 v1.5.0
 ======
diff --git a/jsonpickle/pickler.py b/jsonpickle/pickler.py
index 12007ee..13226f6 100644
--- a/jsonpickle/pickler.py
+++ b/jsonpickle/pickler.py
@@ -342,7 +342,11 @@ class Pickler(object):
             return lambda obj: {tags.SET: [self._flatten(v) for v in obj]}
 
         if util.is_dictionary(obj):
-            return self._flatten_dict_obj
+            if self._mkref(obj):
+                return self._flatten_dict_obj
+            else:
+                self._push()
+                return self._getref
 
         if util.is_type(obj):
             return _mktyperef
diff --git a/jsonpickle/unpickler.py b/jsonpickle/unpickler.py
index 3c658b3..b3e1326 100644
--- a/jsonpickle/unpickler.py
+++ b/jsonpickle/unpickler.py
@@ -541,6 +541,7 @@ class Unpickler(object):
 
     def _restore_dict(self, obj):
         data = {}
+        self._mkref(data)
 
         # If we are decoding dicts that can have non-string keys then we
         # need to do a two-phase decode where the non-string keys are
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 0effc3a..3999e01 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -13,6 +13,7 @@ pytest-black-multipy
 pytest-cov
 pytest-flake8
 simplejson
+sklearn
 sqlalchemy
 ujson; python_version < '3.8'
 yajl; sys_platform != 'win32' and python_version < '3.8'
diff --git a/setup.cfg b/setup.cfg
index 46707ec..830eca7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -57,6 +57,7 @@ testing =
 	numpy
 	pandas
 	pymongo
+	sklearn
 	sqlalchemy
 
 testing.libs =


Test patch:
diff --git a/tests/jsonpickle_test.py b/tests/jsonpickle_test.py
index d436159..606ceaa 100644
--- a/tests/jsonpickle_test.py
+++ b/tests/jsonpickle_test.py
@@ -1,22 +1,21 @@
 # Copyright (C) 2008 John Paulett (john -at- paulett.org)
-# Copyright (C) 2009-2018 David Aguilar (davvid -at- gmail.com)
+# Copyright (C) 2009-2021 David Aguilar (davvid -at- gmail.com)
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
 # you should have received as part of this distribution.
 from __future__ import absolute_import, division, unicode_literals
-import doctest
 import os
 import unittest
 import collections
 
+import pytest
+
 import jsonpickle
 import jsonpickle.backend
 import jsonpickle.handlers
-
 from jsonpickle import compat, tags, util
 from jsonpickle.compat import PY2, PY3
-
 from helper import SkippableTest
 
 
@@ -1530,6 +1529,15 @@ class PicklingProtocol2TestCase(SkippableTest):
         self.test_cyclical_objects_unpickleable_false(use_tuple=False)
 
 
+def test_dict_references_are_preserved():
+    data = {}
+    actual = jsonpickle.decode(jsonpickle.encode([data, data]))
+    assert isinstance(actual, list)
+    assert isinstance(actual[0], dict)
+    assert isinstance(actual[1], dict)
+    assert actual[0] is actual[1]
+
+
 def test_repeat_objects_are_expanded():
     """Ensure that all objects are present in the json output"""
     # When references are disabled we should create expanded copies
@@ -1555,17 +1563,5 @@ def test_repeat_objects_are_expanded():
     assert flattened['passengers'][0]['child']['name'] == 'bob'
 
 
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(JSONPickleTestCase))
-    suite.addTest(unittest.makeSuite(PicklingTestCase))
-    suite.addTest(unittest.makeSuite(PicklingProtocol2TestCase))
-    suite.addTest(unittest.makeSuite(PicklingProtocol4TestCase))
-    suite.addTest(doctest.DocTestSuite(jsonpickle))
-    suite.addTest(doctest.DocTestSuite(jsonpickle.pickler))
-    suite.addTest(doctest.DocTestSuite(jsonpickle.unpickler))
-    return suite
-
-
 if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
+    pytest.main([__file__])
diff --git a/tests/numpy_test.py b/tests/numpy_test.py
index 908fbef..245dc40 100644
--- a/tests/numpy_test.py
+++ b/tests/numpy_test.py
@@ -1,425 +1,388 @@
 from __future__ import absolute_import, division, unicode_literals
-import unittest
 import datetime
 import warnings
 
-import jsonpickle
-from jsonpickle import handlers
-from jsonpickle.compat import PY2
-
-from helper import SkippableTest
+import pytest
 
 try:
     import numpy as np
     import numpy.testing as npt
     from numpy.compat import asbytes
     from numpy.testing import assert_equal
-
-    from jsonpickle.ext import numpy as numpy_ext
 except ImportError:
-    np = None
-
-
-class NumpyTestCase(SkippableTest):
-    def setUp(self):
-        if np is None:
-            self.should_skip = True
-            return
-        self.should_skip = False
-        import jsonpickle.ext.numpy
-
-        jsonpickle.ext.numpy.register_handlers()
-
-    def tearDown(self):
-        if self.should_skip:
-            return
-        import jsonpickle.ext.numpy
-
-        jsonpickle.ext.numpy.unregister_handlers()
-
-    def roundtrip(self, obj):
-        return jsonpickle.decode(jsonpickle.encode(obj))
-
-    def test_dtype_roundtrip(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        dtypes = [
-            np.int,
-            np.float,
-            np.complex,
-            np.int32,
-            np.str,
-            np.object,
-            np.unicode,
-            np.dtype('f4,i4,f2,i1'),
-            np.dtype(('f4', 'i4'), ('f2', 'i1')),
-            np.dtype('1i4', align=True),
-            np.dtype('M8[7D]'),
-            np.dtype(
-                {
-                    'names': ['f0', 'f1', 'f2'],
-                    'formats': ['<u4', '<u2', '<u2'],
-                    'offsets': [0, 0, 2],
-                },
-                align=True,
-            ),
-        ]
-
-        if not PY2:
-            dtypes.extend(
-                [
-                    np.dtype([('f0', 'i4'), ('f2', 'i1')]),
-                    np.dtype(
-                        [
-                            (
-                                'top',
-                                [
-                                    ('tiles', ('>f4', (64, 64)), (1,)),
-                                    ('rtile', '>f4', (64, 36)),
-                                ],
-                                (3,),
-                            ),
-                            (
-                                'bottom',
-                                [
-                                    ('bleft', ('>f4', (8, 64)), (1,)),
-                                    ('bright', '>f4', (8, 36)),
-                                ],
-                            ),
-                        ]
-                    ),
-                ]
-            )
-
-        for dtype in dtypes:
-            self.assertEqual(self.roundtrip(dtype), dtype)
-
-    def test_generic_roundtrip(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        values = [
-            np.int_(1),
-            np.int32(-2),
-            np.float_(2.5),
-            np.nan,
-            -np.inf,
-            np.inf,
-            np.datetime64('2014-01-01'),
-            np.str_('foo'),
-            np.unicode_('bar'),
-            np.object_({'a': 'b'}),
-            np.complex_(1 - 2j),
-        ]
-        for value in values:
-            decoded = self.roundtrip(value)
-            assert_equal(decoded, value)
-            self.assertTrue(isinstance(decoded, type(value)))
-
-    def test_ndarray_roundtrip(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        arrays = [
-            np.random.random((10, 20)),
-            np.array([[True, False, True]]),
-            np.array(['foo', 'bar']),
-            np.array(['baz'.encode('utf-8')]),
-            np.array(['2010', 'NaT', '2030']).astype('M'),
-            np.rec.array(asbytes('abcdefg') * 100, formats='i2,a3,i4', shape=3),
-            np.rec.array(
-                [
-                    (1, 11, 'a'),
-                    (2, 22, 'b'),
-                    (3, 33, 'c'),
-                    (4, 44, 'd'),
-                    (5, 55, 'ex'),
-                    (6, 66, 'f'),
-                    (7, 77, 'g'),
-                ],
-                formats='u1,f4,a1',
-            ),
-            np.array(['1960-03-12', datetime.date(1960, 3, 12)], dtype='M8[D]'),
-            np.array([0, 1, -1, np.inf, -np.inf, np.nan], dtype='f2'),
-        ]
-
-        if not PY2:
-            arrays.extend(
-                [
-                    np.rec.array(
-                        [('NGC1001', 11), ('NGC1002', 1.0), ('NGC1003', 1.0)],
-                        dtype=[('target', 'S20'), ('V_mag', 'f4')],
-                    )
-                ]
-            )
-        for array in arrays:
-            decoded = self.roundtrip(array)
-            assert_equal(decoded, array)
-            self.assertEqual(decoded.dtype, array.dtype)
-
-    def test_shapes_containing_zeroes(self):
-        """Test shapes which cannot be represented as nested lists"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.eye(3)[3:]
-        _a = self.roundtrip(a)
-        npt.assert_array_equal(a, _a)
+    pytest.skip('numpy is not available', allow_module_level=True)
 
-    def test_accuracy(self):
-        """Test the accuracy of the string representation"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        rand = np.random.randn(3, 3)
-        _rand = self.roundtrip(rand)
-        npt.assert_array_equal(rand, _rand)
-
-    def test_b64(self):
-        """Test the binary encoding"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.random.rand(10, 10)  # array of substantial size is stored as b64
-        _a = self.roundtrip(a)
-        npt.assert_array_equal(a, _a)
-
-    def test_views(self):
-        """Test views under serialization"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        rng = np.arange(20)  # a range of an array
-        view = rng[10:]  # a view referencing a portion of an array
-        data = [rng, view]
-
-        _data = self.roundtrip(data)
-
-        _data[0][15] = -1
-        self.assertEqual(_data[1][5], -1)
-
-    def test_strides(self):
-        """Test non-standard strides and offsets"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        arr = np.eye(3)
-        view = arr[1:, 1:]
-        self.assertTrue(view.base is arr)
-        data = [arr, view]
-
-        _data = self.roundtrip(data)
-
-        # test that the deserialized arrays indeed view the same memory
-        _arr, _view = _data
-        _arr[1, 2] = -1
-        self.assertEqual(_view[0, 1], -1)
-        self.assertTrue(_view.base is _arr)
-
-    def test_weird_arrays(self):
-        """Test references to arrays that do not effectively own their memory"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.arange(9)
-        b = a[5:]
-        a.strides = 1
-
-        # this is kinda fishy; a has overlapping memory, _a does not
-        if PY2:
-            warn_count = 0
-        else:
-            warn_count = 1
-        with warnings.catch_warnings(record=True) as w:
-            _a = self.roundtrip(a)
-            self.assertEqual(len(w), warn_count)
-            npt.assert_array_equal(a, _a)
-
-        # this also requires a deepcopy to work
-        with warnings.catch_warnings(record=True) as w:
-            _a, _b = self.roundtrip([a, b])
-            self.assertEqual(len(w), warn_count)
-            npt.assert_array_equal(a, _a)
-            npt.assert_array_equal(b, _b)
-
-    def test_transpose(self):
-        """test handling of non-c-contiguous memory layout"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        # simple case; view a c-contiguous array
-        a = np.arange(9).reshape(3, 3)
-        b = a[1:, 1:]
-        self.assertTrue(b.base is a.base)
-        _a, _b = self.roundtrip([a, b])
-        self.assertTrue(_b.base is _a.base)
-        npt.assert_array_equal(a, _a)
-        npt.assert_array_equal(b, _b)
+import jsonpickle
+import jsonpickle.ext.numpy
+from jsonpickle import handlers
+from jsonpickle.compat import PY2
 
-        # a and b both view the same contiguous array
-        a = np.arange(9).reshape(3, 3).T
-        b = a[1:, 1:]
-        self.assertTrue(b.base is a.base)
-        _a, _b = self.roundtrip([a, b])
-        self.assertTrue(_b.base is _a.base)
-        npt.assert_array_equal(a, _a)
-        npt.assert_array_equal(b, _b)
 
-        # view an f-contiguous array
-        a = a.copy()
-        a.strides = a.strides[::-1]
-        b = a[1:, 1:]
-        self.assertTrue(b.base is a)
-        _a, _b = self.roundtrip([a, b])
-        self.assertTrue(_b.base is _a)
-        npt.assert_array_equal(a, _a)
-        npt.assert_array_equal(b, _b)
+@pytest.fixture(scope='module', autouse=True)
+def numpy_extension():
+    """Initialize the numpy extension for this test module"""
+    jsonpickle.ext.numpy.register_handlers()
+    yield  # control to the test function.
+    jsonpickle.ext.numpy.unregister_handlers()
+
+
+def roundtrip(obj):
+    return jsonpickle.decode(jsonpickle.encode(obj))
+
+
+def test_dtype_roundtrip():
+    dtypes = [
+        np.int,
+        np.float,
+        np.complex,
+        np.int32,
+        np.str,
+        np.object,
+        np.unicode,
+        np.dtype('f4,i4,f2,i1'),
+        np.dtype(('f4', 'i4'), ('f2', 'i1')),
+        np.dtype('1i4', align=True),
+        np.dtype('M8[7D]'),
+        np.dtype(
+            {
+                'names': ['f0', 'f1', 'f2'],
+                'formats': ['<u4', '<u2', '<u2'],
+                'offsets': [0, 0, 2],
+            },
+            align=True,
+        ),
+    ]
+
+    if not PY2:
+        dtypes.extend(
+            [
+                np.dtype([('f0', 'i4'), ('f2', 'i1')]),
+                np.dtype(
+                    [
+                        (
+                            'top',
+                            [
+                                ('tiles', ('>f4', (64, 64)), (1,)),
+                                ('rtile', '>f4', (64, 36)),
+                            ],
+                            (3,),
+                        ),
+                        (
+                            'bottom',
+                            [
+                                ('bleft', ('>f4', (8, 64)), (1,)),
+                                ('bright', '>f4', (8, 36)),
+                            ],
+                        ),
+                    ]
+                ),
+            ]
+        )
 
-        # now a.data.contiguous is False; we have to make a deepcopy to make
-        # this work note that this is a pretty contrived example though!
-        a = np.arange(8).reshape(2, 2, 2).copy()
-        a.strides = a.strides[0], a.strides[2], a.strides[1]
-        b = a[1:, 1:]
-        self.assertTrue(b.base is a)
-
-        if PY2:
-            warn_count = 0
-        else:
-            warn_count = 1
-
-        with warnings.catch_warnings(record=True) as w:
-            _a, _b = self.roundtrip([a, b])
-            self.assertEqual(len(w), warn_count)
-            npt.assert_array_equal(a, _a)
-            npt.assert_array_equal(b, _b)
-
-    def test_fortran_base(self):
-        """Test a base array in fortran order"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.asfortranarray(np.arange(100).reshape((10, 10)))
-        _a = self.roundtrip(a)
+    for dtype in dtypes:
+        assert roundtrip(dtype) == dtype
+
+
+def test_generic_roundtrip():
+    values = [
+        np.int_(1),
+        np.int32(-2),
+        np.float_(2.5),
+        np.nan,
+        -np.inf,
+        np.inf,
+        np.datetime64('2014-01-01'),
+        np.str_('foo'),
+        np.unicode_('bar'),
+        np.object_({'a': 'b'}),
+        np.complex_(1 - 2j),
+    ]
+    for value in values:
+        decoded = roundtrip(value)
+        assert_equal(decoded, value)
+        assert isinstance(decoded, type(value))
+
+
+def test_ndarray_roundtrip():
+    arrays = [
+        np.random.random((10, 20)),
+        np.array([[True, False, True]]),
+        np.array(['foo', 'bar']),
+        np.array(['baz'.encode('utf-8')]),
+        np.array(['2010', 'NaT', '2030']).astype('M'),
+        np.rec.array(asbytes('abcdefg') * 100, formats='i2,a3,i4', shape=3),
+        np.rec.array(
+            [
+                (1, 11, 'a'),
+                (2, 22, 'b'),
+                (3, 33, 'c'),
+                (4, 44, 'd'),
+                (5, 55, 'ex'),
+                (6, 66, 'f'),
+                (7, 77, 'g'),
+            ],
+            formats='u1,f4,a1',
+        ),
+        np.array(['1960-03-12', datetime.date(1960, 3, 12)], dtype='M8[D]'),
+        np.array([0, 1, -1, np.inf, -np.inf, np.nan], dtype='f2'),
+    ]
+
+    if not PY2:
+        arrays.extend(
+            [
+                np.rec.array(
+                    [('NGC1001', 11), ('NGC1002', 1.0), ('NGC1003', 1.0)],
+                    dtype=[('target', 'S20'), ('V_mag', 'f4')],
+                )
+            ]
+        )
+    for array in arrays:
+        decoded = roundtrip(array)
+        assert_equal(decoded, array)
+        assert decoded.dtype == array.dtype
+
+
+def test_shapes_containing_zeroes():
+    """Test shapes which cannot be represented as nested lists"""
+    expect = np.eye(3)[3:]
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_accuracy():
+    """Test the accuracy of the string representation"""
+    expect = np.random.randn(3, 3)
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_b64():
+    """Test the binary encoding"""
+    # Array of substantial size is stored as b64.
+    expect = np.random.rand(10, 10)
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_views():
+    """Test views under serialization"""
+    rng = np.arange(20)  # a range of an array
+    view = rng[10:]  # a view referencing a portion of an array
+    data = [rng, view]
+
+    actual = roundtrip(data)
+    actual[0][15] = -1
+    assert actual[1][5] == -1
+
+
+def test_strides():
+    """Test non-standard strides and offsets"""
+    arr = np.eye(3)
+    view = arr[1:, 1:]
+    assert view.base is arr
+
+    data = [arr, view]
+    actual = roundtrip(data)
+
+    # test that the deserialized arrays indeed view the same memory
+    new_arr, new_view = actual
+    new_arr[1, 2] = -1
+    assert new_view[0, 1] == -1
+    assert new_view.base is new_arr
+
+
+def test_weird_arrays():
+    """Test references to arrays that do not effectively own their memory"""
+    a = np.arange(9)
+    b = a[5:]
+    a.strides = 1
+
+    # this is kinda fishy; a has overlapping memory, _a does not
+    if PY2:
+        warn_count = 0
+    else:
+        warn_count = 1
+    with warnings.catch_warnings(record=True) as w:
+        _a = roundtrip(a)
+        assert len(w) == warn_count
         npt.assert_array_equal(a, _a)
 
-    def test_buffer(self):
-        """test behavior with memoryviews which are not ndarrays"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        bstring = 'abcdefgh'.encode('utf-8')
-        a = np.frombuffer(bstring, dtype=np.byte)
-        if PY2:
-            warn_count = 0
-        else:
-            warn_count = 1
-        with warnings.catch_warnings(record=True) as w:
-            _a = self.roundtrip(a)
-            npt.assert_array_equal(a, _a)
-            self.assertEqual(len(w), warn_count)
-
-    def test_as_strided(self):
-        """Test the result of as_strided()
-
-        as_strided() returns an object that implements the array interface but
-        is not an ndarray.
-
-        """
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.arange(10)
-        b = np.lib.stride_tricks.as_strided(
-            a, shape=(5,), strides=(a.dtype.itemsize * 2,)
-        )
-        data = [a, b]
-
-        with warnings.catch_warnings(record=True) as w:
-            # as_strided returns a DummyArray object, which we can not
-            # currently serialize correctly FIXME: would be neat to add
-            # support for all objects implementing the __array_interface__
-            _data = self.roundtrip(data)
-            self.assertEqual(len(w), 1)
-
-        # as we were warned, deserialized result is no longer a view
-        _data[0][0] = -1
-        self.assertEqual(_data[1][0], 0)
-
-    def test_immutable(self):
-        """test that immutability flag is copied correctly"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.arange(10)
-        a.flags.writeable = False
-        _a = self.roundtrip(a)
-        try:
-            _a[0] = 0
-            self.assertTrue(False, 'item assignment must raise')
-        except ValueError:
-            self.assertTrue(True)
-
-    def test_byteorder(self):
-        """Test the byteorder for text and binary encodings"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        # small arr is stored as text
-        a = np.arange(10).newbyteorder()
-        b = a[:].newbyteorder()
-        _a, _b = self.roundtrip([a, b])
+    # this also requires a deepcopy to work
+    with warnings.catch_warnings(record=True) as w:
+        _a, _b = roundtrip([a, b])
+        assert len(w) == warn_count
         npt.assert_array_equal(a, _a)
         npt.assert_array_equal(b, _b)
 
-        # bigger arr is stored as binary
-        a = np.arange(100).newbyteorder()
-        b = a[:].newbyteorder()
-        _a, _b = self.roundtrip([a, b])
+
+def test_transpose():
+    """test handling of non-c-contiguous memory layout"""
+    # simple case; view a c-contiguous array
+    a = np.arange(9).reshape(3, 3)
+    b = a[1:, 1:]
+    assert b.base is a.base
+    _a, _b = roundtrip([a, b])
+    assert _b.base is _a.base
+    npt.assert_array_equal(a, _a)
+    npt.assert_array_equal(b, _b)
+
+    # a and b both view the same contiguous array
+    a = np.arange(9).reshape(3, 3).T
+    b = a[1:, 1:]
+    assert b.base is a.base
+    _a, _b = roundtrip([a, b])
+    assert _b.base is _a.base
+    npt.assert_array_equal(a, _a)
+    npt.assert_array_equal(b, _b)
+
+    # view an f-contiguous array
+    a = a.copy()
+    a.strides = a.strides[::-1]
+    b = a[1:, 1:]
+    assert b.base is a
+    _a, _b = roundtrip([a, b])
+    assert _b.base is _a
+    npt.assert_array_equal(a, _a)
+    npt.assert_array_equal(b, _b)
+
+    # now a.data.contiguous is False; we have to make a deepcopy to make
+    # this work note that this is a pretty contrived example though!
+    a = np.arange(8).reshape(2, 2, 2).copy()
+    a.strides = a.strides[0], a.strides[2], a.strides[1]
+    b = a[1:, 1:]
+    assert b.base is a
+
+    if PY2:
+        warn_count = 0
+    else:
+        warn_count = 1
+    with warnings.catch_warnings(record=True) as w:
+        _a, _b = roundtrip([a, b])
+        assert len(w) == warn_count
         npt.assert_array_equal(a, _a)
         npt.assert_array_equal(b, _b)
 
-    def test_zero_dimensional_array(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        self.roundtrip(np.array(0.0))
-
-    def test_nested_data_list_of_dict_with_list_keys(self):
-        """Ensure we can handle numpy arrays within a nested structure"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        obj = [{'key': [np.array(0)]}]
-        self.roundtrip(obj)
-
-        obj = [{'key': [np.array([1.0])]}]
-        self.roundtrip(obj)
-
-    def test_size_threshold_None(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        handler = numpy_ext.NumpyNDArrayHandlerView(size_threshold=None)
-        handlers.registry.unregister(np.ndarray)
-        handlers.registry.register(np.ndarray, handler, base=True)
-        self.roundtrip(np.array([0, 1]))
-
-    def test_ndarray_dtype_object(self):
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        a = np.array(['F' + str(i) for i in range(30)], dtype=np.object)
-        buf = jsonpickle.encode(a)
-        # This is critical for reproducing the numpy segfault issue when
-        # restoring ndarray of dtype object
-        del a
-        _a = jsonpickle.decode(buf)
-        a = np.array(['F' + str(i) for i in range(30)], dtype=np.object)
-        npt.assert_array_equal(a, _a)
 
-    def test_np_random(self):
-        """Ensure random.random() arrays can be serialized"""
-        if self.should_skip:
-            return self.skip('numpy is not importable')
-        obj = np.random.random(100)
-        encoded = jsonpickle.encode(obj)
-        clone = jsonpickle.decode(encoded)
-        self.assertEqual(100, len(clone))
-        for idx, (expect, actual) in enumerate(zip(obj, clone)):
-            self.assertEqual(
-                expect,
-                actual,
-                'Item at index %s differs.  %s != %s' % (idx, expect, actual),
-            )
+def test_fortran_base():
+    """Test a base array in fortran order"""
+    a = np.asfortranarray(np.arange(100).reshape((10, 10)))
+    _a = roundtrip(a)
+    npt.assert_array_equal(a, _a)
 
 
-def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(NumpyTestCase, 'test'))
-    return suite
+def test_buffer():
+    """test behavior with memoryviews which are not ndarrays"""
+    bstring = 'abcdefgh'.encode('utf-8')
+    a = np.frombuffer(bstring, dtype=np.byte)
+    if PY2:
+        warn_count = 0
+    else:
+        warn_count = 1
+    with warnings.catch_warnings(record=True) as w:
+        _a = roundtrip(a)
+        npt.assert_array_equal(a, _a)
+        assert len(w) == warn_count
+
+
+def test_as_strided():
+    """Test the result of as_strided()
+
+    as_strided() returns an object that implements the array interface but
+    is not an ndarray.
+
+    """
+    if PY2:
+        warn_count = 0
+    else:
+        warn_count = 1
+    a = np.arange(10)
+    b = np.lib.stride_tricks.as_strided(a, shape=(5,), strides=(a.dtype.itemsize * 2,))
+    data = [a, b]
+
+    with warnings.catch_warnings(record=True) as w:
+        # as_strided returns a DummyArray object, which we can not
+        # currently serialize correctly FIXME: would be neat to add
+        # support for all objects implementing the __array_interface__
+        _data = roundtrip(data)
+        assert len(w) == warn_count
+
+    # as we were warned, deserialized result is no longer a view
+    _data[0][0] = -1
+    assert _data[1][0] == 0
+
+
+def test_immutable():
+    """test that immutability flag is copied correctly"""
+    a = np.arange(10)
+    a.flags.writeable = False
+    _a = roundtrip(a)
+    with pytest.raises(ValueError):
+        _a[0] = 0
+
+
+def test_byteorder():
+    """Test the byteorder for text and binary encodings"""
+    # small arr is stored as text
+    a = np.arange(10).newbyteorder()
+    b = a[:].newbyteorder()
+    _a, _b = roundtrip([a, b])
+    npt.assert_array_equal(a, _a)
+    npt.assert_array_equal(b, _b)
+
+    # bigger arr is stored as binary
+    a = np.arange(100).newbyteorder()
+    b = a[:].newbyteorder()
+    _a, _b = roundtrip([a, b])
+    npt.assert_array_equal(a, _a)
+    npt.assert_array_equal(b, _b)
+
+
+def test_zero_dimensional_array():
+    expect = np.array(0.0)
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_nested_data_list_of_dict_with_list_keys():
+    """Ensure we can handle numpy arrays within a nested structure"""
+    expect = [{'key': [np.array(0)]}]
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect[0]['key'][0], actual[0]['key'][0])
+
+    expect = [{'key': [np.array([1.0])]}]
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect[0]['key'][0], actual[0]['key'][0])
+
+
+def test_size_threshold_None():
+    handler = jsonpickle.ext.numpy.NumpyNDArrayHandlerView(size_threshold=None)
+    handlers.registry.unregister(np.ndarray)
+    handlers.registry.register(np.ndarray, handler, base=True)
+    expect = np.array([0, 1])
+    actual = roundtrip(expect)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_ndarray_dtype_object():
+    a = np.array(['F' + str(i) for i in range(30)], dtype=np.object)
+    buf = jsonpickle.encode(a)
+    # This is critical for reproducing the numpy segfault issue when
+    # restoring ndarray of dtype object.
+    del a
+    expect = np.array(['F' + str(i) for i in range(30)], dtype=np.object)
+    actual = jsonpickle.decode(buf)
+    npt.assert_array_equal(expect, actual)
+
+
+def test_np_random():
+    """Ensure random.random() arrays can be serialized"""
+    obj = np.random.random(100)
+    encoded = jsonpickle.encode(obj)
+    clone = jsonpickle.decode(encoded)
+    assert 100 == len(clone)
+    for idx, (expect, actual) in enumerate(zip(obj, clone)):
+        assert expect == actual
 
 
 if __name__ == '__main__':
-    unittest.main()
+    pytest.main([__file__])
diff --git a/tests/sklearn_test.py b/tests/sklearn_test.py
new file mode 100644
index 0000000..3eeab99
--- /dev/null
+++ b/tests/sklearn_test.py
@@ -0,0 +1,57 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import pytest
+
+try:
+    import numpy as np
+    from sklearn.tree import DecisionTreeClassifier
+except ImportError:
+    pytest.skip('sklearn is not available', allow_module_level=True)
+
+import jsonpickle
+import jsonpickle.ext.numpy
+
+
+@pytest.fixture(scope='module', autouse=True)
+def numpy_extension():
+    """Initialize the numpy extension for this test module"""
+    jsonpickle.ext.numpy.register_handlers()
+    yield  # control to the test function.
+    jsonpickle.ext.numpy.unregister_handlers()
+
+
+def test_decision_tree():
+    #  Create data.
+    np.random.seed(13)
+    x_values = np.random.randint(low=0, high=10, size=12)
+    x = x_values.reshape(4, 3)
+    y_values = np.random.randint(low=0, high=2, size=4)
+    y = y_values.reshape(-1, 1)
+
+    # train model
+    classifier = DecisionTreeClassifier(max_depth=1)
+    classifier.fit(x, y)
+
+    # freeze and thaw
+    pickler = jsonpickle.pickler.Pickler()
+    unpickler = jsonpickle.unpickler.Unpickler()
+    actual = unpickler.restore(pickler.flatten(classifier))
+
+    assert isinstance(actual, classifier.__class__)
+    if hasattr(classifier, 'tree_'):
+        assert isinstance(actual.tree_, classifier.tree_.__class__)
+
+    # predict from thawed
+    array_values = np.array([1, 2, 3])
+    array = array_values.reshape(1, -1)
+    prediction = actual.predict(array)
+    assert prediction[0] == 1
+
+    assert actual.max_depth == classifier.max_depth
+    assert actual.score(x, y) == classifier.score(x, y)
+    if hasattr(classifier, 'get_depth'):
+        assert actual.get_depth() == classifier.get_depth()
+
+
+if __name__ == '__main__':
+    pytest.main([__file__])