Repository: opendilab/treevalue (language: python)

Issue:
Inherit MutableMapping in TreeValue Class
The `TreeValue` class need to inherit `MutableMapping` class for supporting all the features that `dict` object have.

See: https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes


Discussion / hints:

Fix patch:
diff --git a/treevalue/tree/common/storage.pyx b/treevalue/tree/common/storage.pyx
index 576111ae1e..4efe1cb6a2 100644
--- a/treevalue/tree/common/storage.pyx
+++ b/treevalue/tree/common/storage.pyx
@@ -185,27 +185,41 @@ cdef class TreeStorage:
         cdef str k
         cdef object v
         cdef list _items = []
-        for k, v in sorted(self.items(), key=lambda x: x[0]):
+        for k, v in sorted(self.iter_items(), key=lambda x: x[0]):
             _items.append((k, v))
 
         return hash(tuple(_items))
 
-    def keys(self):
+    def iter_keys(self):
         return self.map.keys()
 
-    def values(self):
+    def iter_rev_keys(self):
+        return reversed(self.map.keys())
+
+    def iter_values(self):
         cdef str k
         cdef object v, nv
         for k, v in self.map.items():
             yield _c_undelay_data(self.map, k, v)
 
-    def items(self):
+    def iter_rev_values(self):
+        cdef str k
+        cdef object v, nv
+        for k, v in reversed(self.map.items()):
+            yield _c_undelay_data(self.map, k, v)
+
+    def iter_items(self):
         cdef str k
         cdef object v, nv
         for k, v in self.map.items():
-            v = _c_undelay_data(self.map, k, v)
+            yield k, _c_undelay_data(self.map, k, v)
+
+    def iter_rev_items(self):
+        cdef str k
+        cdef object v, nv
+        for k, v in reversed(self.map.items()):
+            yield k, _c_undelay_data(self.map, k, v)
 
-            yield k, v
 
 cpdef object create_storage(dict value):
     cdef dict _map = {}
diff --git a/treevalue/tree/tree/tree.pxd b/treevalue/tree/tree/tree.pxd
index 8234cc0e28..e45f784273 100644
--- a/treevalue/tree/tree/tree.pxd
+++ b/treevalue/tree/tree/tree.pxd
@@ -6,6 +6,9 @@ from libcpp cimport bool
 from ..common.delay cimport DelayedProxy
 from ..common.storage cimport TreeStorage
 
+cdef class _CObject:
+    pass
+
 cdef class TreeValue:
     cdef readonly TreeStorage _st
     cdef readonly type _type
@@ -20,9 +23,29 @@ cdef class TreeValue:
     cpdef get(self, str key, object default= *)
     cpdef pop(self, str key, object default= *)
 
+    cpdef treevalue_keys keys(self)
+    cpdef treevalue_values values(self)
+    cpdef treevalue_items items(self)
+
 cdef str _prefix_fix(object text, object prefix)
+cdef str _title_repr(TreeStorage st, object type_)
 cdef object _build_tree(TreeStorage st, object type_, str prefix, dict id_pool, tuple path)
 
+# noinspection PyPep8Naming
+cdef class treevalue_keys(_CObject):
+    cdef readonly TreeStorage _st
+    cdef readonly type _type
+
+# noinspection PyPep8Naming
+cdef class treevalue_values(_CObject):
+    cdef readonly TreeStorage _st
+    cdef readonly type _type
+
+# noinspection PyPep8Naming
+cdef class treevalue_items(_CObject):
+    cdef readonly TreeStorage _st
+    cdef readonly type _type
+
 cdef class DetachedDelayedProxy(DelayedProxy):
     cdef DelayedProxy proxy
     cdef readonly bool calculated
diff --git a/treevalue/tree/tree/tree.pyx b/treevalue/tree/tree/tree.pyx
index c7159c8560..066c00ed61 100644
--- a/treevalue/tree/tree/tree.pyx
+++ b/treevalue/tree/tree/tree.pyx
@@ -2,6 +2,7 @@
 # cython:language_level=3
 
 import os
+from collections.abc import Sized, Container, Reversible
 from operator import itemgetter
 
 import cython
@@ -11,6 +12,9 @@ from ..common.delay cimport undelay, _c_delayed_partial, DelayedProxy
 from ..common.storage cimport TreeStorage, create_storage, _c_undelay_data
 from ...utils import format_tree
 
+cdef class _CObject:
+    pass
+
 cdef inline object _item_unwrap(object v):
     if isinstance(v, list) and len(v) == 1:
         return v[0]
@@ -392,7 +396,7 @@ cdef class TreeValue:
         """
         cdef str k
         cdef object v
-        for k, v in self._st.items():
+        for k, v in self._st.iter_items():
             yield k, self._unraw(v)
 
     @cython.binding(True)
@@ -525,48 +529,97 @@ cdef class TreeValue:
         return self._st
 
     @cython.binding(True)
-    def keys(self):
+    cpdef treevalue_keys keys(self):
         """
         Overview:
             Get keys of this treevalue object, like the :class:`dict`.
 
         Returns:
             - keys: A generator of all the keys.
+
+        Examples::
+            >>> from treevalue import TreeValue
+            >>> 
+            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
+            >>> t.keys()
+            treevalue_keys(['a', 'b', 'c'])
+            >>> len(t.keys())
+            3
+            >>> list(t.keys())
+            ['a', 'b', 'c']
+            >>> list(reversed(t.keys()))  # only available in python3.8+
+            ['c', 'b', 'a']
+            >>> 'a' in t.keys()
+            True
+            >>> 'f' in t.keys()
+            False
+
+        .. note::
+            :func:`reversed` is only available in python 3.8 or higher versions.
         """
-        return self._st.keys()
+        return treevalue_keys(self._st, self._type)
 
     @cython.binding(True)
-    def values(self):
+    cpdef treevalue_values values(self):
         """
         Overview:
             Get value of this treevalue object, like the :class:`dict`.
 
         Returns:
             - values: A generator of all the values
+
+        Examples::
+            >>> from treevalue import TreeValue
+            >>> 
+            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
+            >>> t.values()
+            treevalue_values([1, 3, '233'])
+            >>> len(t.values())
+            3
+            >>> list(t.values())
+            [1, 3, '233']
+            >>> list(reversed(t.values()))  # only supported on python3.8+
+            ['233', 3, 1]
+            >>> 1 in t.values()
+            True
+            >>> 'fff' in t.values()
+            False
+
+        .. note::
+            :func:`reversed` is only available in python 3.8 or higher versions.
         """
-        cdef object v
-        for v in self._st.values():
-            if isinstance(v, TreeStorage):
-                yield self._type(v)
-            else:
-                yield v
+        return treevalue_values(self._st, self._type)
 
     @cython.binding(True)
-    def items(self):
+    cpdef treevalue_items items(self):
         """
         Overview:
             Get pairs of keys and values of this treevalue object, like the :class:`items`.
 
         Returns:
             - items: A generator of pairs of keys and values.
+
+        Examples::
+            >>> from treevalue import TreeValue
+            >>> 
+            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
+            >>> t.items()
+            treevalue_items([('a', 1), ('b', 3), ('c', '233')])
+            >>> len(t.items())
+            3
+            >>> list(t.items())
+            [('a', 1), ('b', 3), ('c', '233')]
+            >>> list(reversed(t.items()))  # only supported on python3.8+
+            [('c', '233'), ('b', 3), ('a', 1)]
+            >>> ('a', 1) in t.items()
+            True
+            >>> ('c', '234') in t.values()
+            False
+
+        .. note::
+            :func:`reversed` is only available in python 3.8 or higher versions.
         """
-        cdef str k
-        cdef object v
-        for k, v in self._st.items():
-            if isinstance(v, TreeStorage):
-                yield k, self._type(v)
-            else:
-                yield k, v
+        return treevalue_items(self._st, self._type)
 
 cdef str _prefix_fix(object text, object prefix):
     cdef list lines = []
@@ -578,9 +631,12 @@ cdef str _prefix_fix(object text, object prefix):
 
     return os.linesep.join(lines)
 
+cdef inline str _title_repr(TreeStorage st, object type_):
+    return f'<{type_.__name__} {hex(id(st))}>'
+
 cdef object _build_tree(TreeStorage st, object type_, str prefix, dict id_pool, tuple path):
     cdef object nid = id(st)
-    cdef str self_repr = f'<{type_.__name__} {hex(nid)}>'
+    cdef str self_repr = _title_repr(st, type_)
     cdef list children = []
 
     cdef str k, _t_prefix
@@ -605,6 +661,129 @@ cdef object _build_tree(TreeStorage st, object type_, str prefix, dict id_pool,
     self_repr = _prefix_fix(self_repr, prefix)
     return self_repr, children
 
+try:
+    reversed({'a': 1}.keys())
+except TypeError:
+    _reversible = False
+else:
+    _reversible = True
+
+# noinspection PyPep8Naming
+cdef class treevalue_keys(_CObject, Sized, Container, Reversible):
+    def __cinit__(self, TreeStorage storage, type _type):
+        self._st = storage
+        self._type = _type
+
+    def __len__(self):
+        return self._st.size()
+
+    def __contains__(self, item):
+        return self._st.contains(item)
+
+    def _iter(self):
+        for k in self._st.iter_keys():
+            yield k
+
+    def __iter__(self):
+        return self._iter()
+
+    def _rev_iter(self):
+        for k in self._st.iter_rev_keys():
+            yield k
+
+    def __reversed__(self):
+        if _reversible:
+            return self._rev_iter()
+        else:
+            raise TypeError(f'{type(self).__name__!r} object is not reversible')
+
+    def __repr__(self):
+        return f'{type(self).__name__}({list(self)!r})'
+
+# noinspection PyPep8Naming
+cdef class treevalue_values(_CObject, Sized, Container, Reversible):
+    def __cinit__(self, TreeStorage storage, type _type):
+        self._st = storage
+        self._type = _type
+
+    def __len__(self):
+        return self._st.size()
+
+    def __contains__(self, item):
+        for v in self:
+            if item == v:
+                return True
+
+        return False
+
+    def _iter(self):
+        for v in self._st.iter_values():
+            if isinstance(v, TreeStorage):
+                yield self._type(v)
+            else:
+                yield v
+
+    def __iter__(self):
+        return self._iter()
+
+    def _rev_iter(self):
+        for v in self._st.iter_rev_values():
+            if isinstance(v, TreeStorage):
+                yield self._type(v)
+            else:
+                yield v
+
+    def __reversed__(self):
+        if _reversible:
+            return self._rev_iter()
+        else:
+            raise TypeError(f'{type(self).__name__!r} object is not reversible')
+
+    def __repr__(self):
+        return f'{type(self).__name__}({list(self)!r})'
+
+# noinspection PyPep8Naming
+cdef class treevalue_items(_CObject, Sized, Container, Reversible):
+    def __cinit__(self, TreeStorage storage, type _type):
+        self._st = storage
+        self._type = _type
+
+    def __len__(self):
+        return self._st.size()
+
+    def __contains__(self, item):
+        for k, v in self:
+            if item == (k, v):
+                return True
+
+        return False
+
+    def _iter(self):
+        for k, v in self._st.iter_items():
+            if isinstance(v, TreeStorage):
+                yield k, self._type(v)
+            else:
+                yield k, v
+
+    def __iter__(self):
+        return self._iter()
+
+    def _rev_iter(self):
+        for k, v in self._st.iter_rev_items():
+            if isinstance(v, TreeStorage):
+                yield k, self._type(v)
+            else:
+                yield k, v
+
+    def __reversed__(self):
+        if _reversible:
+            return self._rev_iter()
+        else:
+            raise TypeError(f'{type(self).__name__!r} object is not reversible')
+
+    def __repr__(self):
+        return f'{type(self).__name__}({list(self)!r})'
+
 cdef class DetachedDelayedProxy(DelayedProxy):
     def __init__(self, DelayedProxy proxy):
         self.proxy = proxy


Test patch:
diff --git a/test/tree/common/test_storage.py b/test/tree/common/test_storage.py
index 7f6f7c7249..1b4b041b5d 100644
--- a/test/tree/common/test_storage.py
+++ b/test/tree/common/test_storage.py
@@ -4,6 +4,13 @@
 
 from treevalue.tree.common import create_storage, raw, TreeStorage, delayed_partial
 
+try:
+    _ = reversed({}.keys())
+except TypeError:
+    _reversible = False
+else:
+    _reversible = True
+
 
 # noinspection PyArgumentList,DuplicatedCode,PyTypeChecker
 @pytest.mark.unittest
@@ -501,33 +508,49 @@ def test_keys(self):
         h2 = {'x': 3, 'y': 4}
         t = create_storage({'a': 1, 'b': 2, 'c': raw(h1), 'd': h2, 'f': h2})
 
-        assert set(t.keys()) == {'a', 'b', 'c', 'd', 'f'}
-        assert set(t.get('f').keys()) == {'x', 'y'}
+        assert set(t.iter_keys()) == {'a', 'b', 'c', 'd', 'f'}
+        assert set(t.get('f').iter_keys()) == {'x', 'y'}
+
+        if _reversible:
+            assert list(t.iter_rev_keys()) == list(t.iter_keys())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                t.iter_rev_keys()
 
     def test_values(self):
         h1 = {'x': 3, 'y': 4}
         t = create_storage({'a': 1, 'b': 2, 'd': h1})
 
-        assert set(t.get('d').values()) == {3, 4}
-        assert len(list(t.values())) == 3
-        assert 1 in t.values()
-        assert 2 in t.values()
+        assert set(t.get('d').iter_values()) == {3, 4}
+        assert len(list(t.iter_values())) == 3
+        assert 1 in t.iter_values()
+        assert 2 in t.iter_values()
+        if _reversible:
+            assert list(t.iter_rev_values()) == list(t.iter_values())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                _ = list(t.iter_rev_values())
 
         t1 = create_storage({
             'a': delayed_partial(lambda: t.get('a')),
             'b': delayed_partial(lambda: t.get('b')),
             'd': delayed_partial(lambda: t.get('d')),
         })
-        assert set(t1.get('d').values()) == {3, 4}
-        assert len(list(t1.values())) == 3
-        assert 1 in t1.values()
-        assert 2 in t1.values()
+        assert set(t1.get('d').iter_values()) == {3, 4}
+        assert len(list(t1.iter_values())) == 3
+        assert 1 in t1.iter_values()
+        assert 2 in t1.iter_values()
+        if _reversible:
+            assert list(t1.iter_rev_values()) == list(t1.iter_values())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                _ = list(t1.iter_rev_values())
 
     def test_items(self):
         h1 = {'x': 3, 'y': 4}
         t = create_storage({'a': 1, 'b': 2, 'd': raw(h1)})
 
-        for k, v in t.items():
+        for k, v in t.iter_items():
             if k == 'a':
                 assert v == 1
             elif k == 'b':
@@ -537,12 +560,18 @@ def test_items(self):
             else:
                 pytest.fail('Should not reach here.')
 
+        if _reversible:
+            assert list(t.iter_rev_items()) == list(t.iter_items())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                _ = list(t.iter_rev_items())
+
         t1 = create_storage({
             'a': delayed_partial(lambda: t.get('a')),
             'b': delayed_partial(lambda: t.get('b')),
             'd': delayed_partial(lambda: t.get('d')),
         })
-        for k, v in t1.items():
+        for k, v in t1.iter_items():
             if k == 'a':
                 assert v == 1
             elif k == 'b':
@@ -552,6 +581,12 @@ def test_items(self):
             else:
                 pytest.fail('Should not reach here.')
 
+        if _reversible:
+            assert list(t1.iter_rev_values()) == list(t1.iter_values())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                _ = list(t1.iter_rev_values())
+
     def test_hash(self):
         h = {}
 
diff --git a/test/tree/tree/test_tree.py b/test/tree/tree/test_tree.py
index 4ef09b7aa2..3b736f61d8 100644
--- a/test/tree/tree/test_tree.py
+++ b/test/tree/tree/test_tree.py
@@ -5,6 +5,13 @@
 
 from treevalue import raw, TreeValue, delayed
 
+try:
+    _ = reversed({}.keys())
+except TypeError:
+    _reversible = False
+else:
+    _reversible = True
+
 
 class _Container:
     def __init__(self, value):
@@ -240,12 +247,14 @@ def test_tree_value_bool(self):
         assert tv2
         assert not tv2.c
 
-    def test_tee_value_hash_equal(self):
+    def test_tree_value_hash_equal(self):
         tv1 = TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 3}})
         assert tv1 == tv1
         assert not tv1 == 2
         assert tv1 == TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 3}})
+        assert tv1 != TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 4}})
         assert tv1.c == TreeValue({'x': 2, 'y': 3})
+        assert tv1.c != TreeValue({'x': 2, 'y': 3, 'z': 4})
 
         d = {
             tv1: 1,
@@ -330,23 +339,61 @@ def test_pop(self):
 
     def test_keys(self):
         tv1 = TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 3}, 'd': raw({'x': 2, 'y': 3})})
+        assert len(tv1.keys()) == 4
         assert set(tv1.keys()) == {'a', 'b', 'c', 'd'}
+        assert 'a' in tv1.keys()
+        assert 'b' in tv1.keys()
+        assert 'c' in tv1.keys()
+        assert 'd' in tv1.keys()
+        assert 'e' not in tv1.keys()
+
+        assert repr(tv1.keys()) == "treevalue_keys(['a', 'b', 'c', 'd'])"
+        if _reversible:
+            assert list(reversed(tv1.keys())) == list(tv1.keys())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                reversed(tv1.keys())
 
     def test_values(self):
         tv1 = TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 3}})
+        assert len(tv1.values()) == 3
         assert set(tv1.c.values()) == {2, 3}
-        assert len(list(tv1.values())) == 3
         assert 1 in tv1.values()
         assert 2 in tv1.values()
+        assert 3 not in tv1.values()
+        assert TreeValue({'x': 2, 'y': 3}) in tv1.values()
+        assert TreeValue({'x': 2, 'y': 4}) not in tv1.values()
+
+        assert repr(TreeValue({'a': 1, 'b': 2}).values()) == 'treevalue_values([1, 2])'
+        if _reversible:
+            assert list(reversed(tv1.values())) == list(tv1.values())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                reversed(tv1.values())
 
     def test_items(self):
         tv1 = TreeValue({'a': 1, 'b': 2, 'c': {'x': 2, 'y': 3}, 'd': raw({'x': 2, 'y': 3})})
+        assert len(tv1.items()) == 4
         assert sorted(tv1.items()) == [
             ('a', 1),
             ('b', 2),
             ('c', TreeValue({'x': 2, 'y': 3})),
             ('d', {'x': 2, 'y': 3}),
         ]
+        assert ('a', 1) in tv1.items()
+        assert ('b', 2) in tv1.items()
+        assert ('a', 2) not in tv1.items()
+        assert ('c', TreeValue({'x': 2, 'y': 3})) in tv1.items()
+        assert ('c', TreeValue({'x': 2, 'y': 4})) not in tv1.items()
+        assert ('d', {'x': 2, 'y': 3}) in tv1.items()
+        assert ('d', {'x': 2, 'y': 4}) not in tv1.items()
+
+        assert repr(TreeValue({'a': 1, 'b': 2}).items()) == "treevalue_items([('a', 1), ('b', 2)])"
+        if _reversible:
+            assert list(reversed(tv1.items())) == list(tv1.items())[::-1]
+        else:
+            with pytest.raises(TypeError):
+                reversed(tv1.items())
 
         class MyTreeValue(TreeValue):
             pass