Repository: OpenMined/PySyft

Issue:
DictPointer.items() can't be iterated
## Description
I got error when trying to iterate on `DictPointer.items()`, (while I can iterate on `ListPointer`).
## How to Reproduce
```python
# on DO side
duet.requests.add_handler(action="accept")

# on DS side
d = sy.lib.python.Dict({"1":1,"2":2})
dptr = d.send(duet)
itemsptr = dptr.items() # you get a ListPointer
itemsptr.__len__() # got error
for e in itemsptr: # got error too
    print(e)
```


## Expected Behavior
A clear and concise description of what you expected to happen.

## Screenshots
If applicable, add screenshots to help explain your problem.

## System Information
 - OS: [e.g. iOS]
 - OS Version: [e.g. 22]
 - Language Version: [e.g. Python 3.7, Node 10.18.1]
 - Package Manager Version: [e.g. Conda 4.6.1, NPM 6.14.1]
 - Browser (if applicable): [e.g. Google Chrome]
 - Browser Version (if applicable): [e.g. 81.0.4044.138]

## Additional Context
Add any other context about the problem here.



Discussion:
@tudorcebere Did we end up supporting Dicts in the `Iterator`? Seems like you can get `.len()` on the `dptr` but not on the `itemsptr` interestingly.

Resolving patch:
diff --git a/src/syft/lib/python/__init__.py b/src/syft/lib/python/__init__.py
--- a/src/syft/lib/python/__init__.py
+++ b/src/syft/lib/python/__init__.py
@@ -277,12 +277,12 @@ def create_python_ast(client: Optional[AbstractNodeClient] = None) -> Globals:
         ("syft.lib.python.Dict.fromkeys", "syft.lib.python.Dict"),
         # Rename get to dict_get because of conflict
         ("syft.lib.python.Dict.dict_get", "syft.lib.python.Any"),
-        ("syft.lib.python.Dict.items", "syft.lib.python.List"),
-        ("syft.lib.python.Dict.keys", "syft.lib.python.List"),
+        ("syft.lib.python.Dict.items", "syft.lib.python.Iterator"),
+        ("syft.lib.python.Dict.keys", "syft.lib.python.Iterator"),
         ("syft.lib.python.Dict.pop", "syft.lib.python.Any"),
         ("syft.lib.python.Dict.popitem", "syft.lib.python.Tuple"),
         ("syft.lib.python.Dict.setdefault", "syft.lib.python.Any"),
-        ("syft.lib.python.Dict.values", "syft.lib.python.List"),
+        ("syft.lib.python.Dict.values", "syft.lib.python.Iterator"),
         # Int methods - subject to further change
         ("syft.lib.python.Int.__add__", "syft.lib.python.Int"),
         ("syft.lib.python.Int.__truediv__", "syft.lib.python.Float"),
@@ -446,6 +446,7 @@ def create_python_ast(client: Optional[AbstractNodeClient] = None) -> Globals:
             "syft.lib.python.collections.OrderedDict.__le__",
             "syft.lib.python.Bool",
         ),
+        ("syft.lib.python.collections.OrderedDict.__iter__", "syft.lib.python.Any"),
         ("syft.lib.python.collections.OrderedDict.__len__", "syft.lib.python.Int"),
         (
             "syft.lib.python.collections.OrderedDict.__lt__",
@@ -471,8 +472,8 @@ def create_python_ast(client: Optional[AbstractNodeClient] = None) -> Globals:
             "syft.lib.python.collections.OrderedDict.fromkeys",
             "syft.lib.python.collections.OrderedDict",
         ),
-        ("syft.lib.python.collections.OrderedDict.items", "syft.lib.python.List"),
-        ("syft.lib.python.collections.OrderedDict.keys", "syft.lib.python.List"),
+        ("syft.lib.python.collections.OrderedDict.items", "syft.lib.python.Iterator"),
+        ("syft.lib.python.collections.OrderedDict.keys", "syft.lib.python.Iterator"),
         (
             "syft.lib.python.collections.OrderedDict.move_to_end",
             "syft.lib.python._SyNone",
@@ -489,7 +490,7 @@ def create_python_ast(client: Optional[AbstractNodeClient] = None) -> Globals:
         ),
         (
             "syft.lib.python.collections.OrderedDict.values",
-            "syft.lib.python.List",
+            "syft.lib.python.Iterator",
         ),
         ("syft.lib.python.collections.OrderedDict.items", "syft.lib.python.List"),
         (
diff --git a/src/syft/lib/python/collections/ordered_dict.py b/src/syft/lib/python/collections/ordered_dict.py
--- a/src/syft/lib/python/collections/ordered_dict.py
+++ b/src/syft/lib/python/collections/ordered_dict.py
@@ -1,5 +1,8 @@
 # stdlib
 from collections import OrderedDict as PyOrderedDict
+from collections.abc import ItemsView
+from collections.abc import KeysView
+from collections.abc import ValuesView
 from typing import Any
 from typing import Optional
 
@@ -11,9 +14,11 @@
 from .... import serialize
 from ....core.common.serde.serializable import bind_protobuf
 from ....core.common.uid import UID
+from ....logger import traceback_and_raise
 from ....proto.lib.python.collections.ordered_dict_pb2 import (
     OrderedDict as OrderedDict_PB,
 )
+from ..iterator import Iterator
 from ..primitive_factory import PrimitiveFactory
 from ..primitive_factory import isprimitive
 from ..primitive_interface import PyPrimitive
@@ -59,6 +64,9 @@ def __getitem__(self, other: Any) -> SyPrimitiveRet:
             # we can have torch.Tensor and other types
             return res
 
+    def __iter__(self, max_len: Optional[int] = None) -> Iterator:
+        return Iterator(super().__iter__(), max_len=max_len)
+
     def __len__(self) -> SyPrimitiveRet:
         res = super().__len__()
         return PrimitiveFactory.generate_primitive(value=res)
@@ -102,13 +110,11 @@ def dict_get(self, other: Any) -> Any:
             # we can have torch.Tensor and other types
             return res
 
-    def items(self) -> SyPrimitiveRet:
-        res = list(super().items())
-        return PrimitiveFactory.generate_primitive(value=res)
+    def items(self, max_len: Optional[int] = None) -> Iterator:  # type: ignore
+        return Iterator(ItemsView(self), max_len=max_len)
 
-    def keys(self) -> SyPrimitiveRet:
-        res = list(super().keys())
-        return PrimitiveFactory.generate_primitive(value=res)
+    def keys(self, max_len: Optional[int] = None) -> Iterator:  # type: ignore
+        return Iterator(KeysView(self), max_len=max_len)
 
     def move_to_end(self, other: Any, last: Any = True) -> Any:
         res = super().move_to_end(other, last)
@@ -130,9 +136,15 @@ def update(self, *args, **kwds: Any) -> SyPrimitiveRet:  # type: ignore
         res = super().update(*args, **kwds)
         return PrimitiveFactory.generate_primitive(value=res)
 
-    def values(self) -> SyPrimitiveRet:
-        res = list(super().values())
-        return PrimitiveFactory.generate_primitive(value=res)
+    def values(self, *args: Any, max_len: Optional[int] = None) -> Iterator:  # type: ignore
+        # this is what the super type does and there is a test in dict_test.py
+        # test_values which checks for this so we could disable the test or
+        # keep this workaround
+        if len(args) > 0:
+            traceback_and_raise(
+                TypeError("values() takes 1 positional argument but 2 were given")
+            )
+        return Iterator(ValuesView(self), max_len=max_len)
 
     def _object2proto(self) -> OrderedDict_PB:
         id_ = serialize(obj=self.id)


Test patch:
diff --git a/tests/syft/lib/python/collections/ordered_dict/ordered_dict_sanity_test.py b/tests/syft/lib/python/collections/ordered_dict/ordered_dict_sanity_test.py
--- a/tests/syft/lib/python/collections/ordered_dict/ordered_dict_sanity_test.py
+++ b/tests/syft/lib/python/collections/ordered_dict/ordered_dict_sanity_test.py
@@ -21,6 +21,7 @@
 
 # syft absolute
 from syft.core.common.uid import UID
+from syft.lib.python import SyNone
 from syft.lib.python.collections import OrderedDict as SyOrderedDict
 
 
@@ -209,9 +210,9 @@ def test_iterators():
     assertEqual(list(od.values()), [t[1] for t in pairs])
     assertEqual(list(od.items()), pairs)
     assertEqual(list(reversed(od)), [t[0] for t in reversed(pairs)])
-    assertEqual(list(reversed(od.keys())), [t[0] for t in reversed(pairs)])
-    assertEqual(list(reversed(od.values())), [t[1] for t in reversed(pairs)])
-    assertEqual(list(reversed(od.items())), list(reversed(pairs)))
+    assertEqual(list(reversed(list(od.keys()))), [t[0] for t in reversed(pairs)])
+    assertEqual(list(reversed(list(od.values()))), [t[1] for t in reversed(pairs)])
+    assertEqual(list(reversed(list(od.items()))), list(reversed(pairs)))
 
 
 def test_detect_deletion_during_iteration():
@@ -248,9 +249,9 @@ def test_iterators_empty():
     assertEqual(list(od.values()), empty)
     assertEqual(list(od.items()), empty)
     assertEqual(list(reversed(od)), empty)
-    assertEqual(list(reversed(od.keys())), empty)
-    assertEqual(list(reversed(od.values())), empty)
-    assertEqual(list(reversed(od.items())), empty)
+    assertEqual(list(reversed(list(od.keys()))), empty)
+    assertEqual(list(reversed(list(od.values()))), empty)
+    assertEqual(list(reversed(list(od.items()))), empty)
 
 
 def test_popitem():
@@ -406,7 +407,8 @@ def test_repr_recursive():
     od = OrderedDict.FromKeys("abc")
     od["x"] = od
     assertEqual(
-        repr(od), "OrderedDict([('a', None), ('b', None), ('c', None), ('x', ...)])"
+        repr(od),
+        f"OrderedDict([('a', {repr(SyNone)}), ('b', {repr(SyNone)}), ('c', {repr(SyNone)}), ('x', ...)])",
     )
 
 
@@ -508,8 +510,8 @@ def test_views():
     # See http://bugs.python.org/issue24286
     s = "the quick brown fox jumped over a lazy dog yesterday before dawn".split()
     od = OrderedDict.FromKeys(s)
-    assertEqual(od.keys(), list(dict(od).keys()))
-    assertEqual(od.items(), list(dict(od).items()))
+    assertEqual(list(od.keys()), list(dict(od).keys()))
+    assertEqual(list(od.items()), list(dict(od).items()))
 
 
 def test_override_update():
@@ -612,11 +614,11 @@ def __hash__(self):
         od[key] = i
 
     # These should not crash.
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         list(od.values())
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         list(od.items())
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         repr(od)
     with pytest.raises(KeyError):
         od.copy()
@@ -675,7 +677,7 @@ def test_dict_delitem():
     od["spam"] = 1
     od["ham"] = 2
     dict.__delitem__(od, "spam")
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         repr(od)
 
 
@@ -694,7 +696,7 @@ def test_dict_pop():
     od["spam"] = 1
     od["ham"] = 2
     dict.pop(od, "spam")
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         repr(od)
 
 
@@ -704,7 +706,7 @@ def test_dict_popitem():
     od["spam"] = 1
     od["ham"] = 2
     dict.popitem(od)
-    with pytest.raises(KeyError):
+    with pytest.raises(RuntimeError):
         repr(od)
 
 
diff --git a/tests/syft/lib/python/collections/ordered_dict/ordered_dict_serde_test.py b/tests/syft/lib/python/collections/ordered_dict/ordered_dict_serde_test.py
--- a/tests/syft/lib/python/collections/ordered_dict/ordered_dict_serde_test.py
+++ b/tests/syft/lib/python/collections/ordered_dict/ordered_dict_serde_test.py
@@ -2,6 +2,7 @@
 from collections import OrderedDict as PyOrderectDict
 
 # third party
+import pytest
 import torch as th
 
 # syft absolute
@@ -69,3 +70,19 @@ def test_list_send() -> None:
     res = ptr.get()
     for res_el, original_el in zip(res, syft_list):
         assert res_el == original_el
+
+
+@pytest.mark.parametrize("method_name", ["items", "keys", "values"])
+def test_iterator_methods(method_name: str) -> None:
+    alice = sy.VirtualMachine(name="alice")
+    alice_client = alice.get_root_client()
+
+    d = OrderedDict({"#1": 1, "#2": 2})
+    dptr = d.send(alice_client)
+
+    itemsptr = getattr(dptr, method_name)()
+    assert type(itemsptr).__name__ == "IteratorPointer"
+
+    for itemptr, local_item in zip(itemsptr, getattr(d, method_name)()):
+        get_item = itemptr.get()
+        assert get_item == local_item
diff --git a/tests/syft/lib/python/collections/ordered_dict/pointer_test.py b/tests/syft/lib/python/collections/ordered_dict/pointer_test.py
--- a/tests/syft/lib/python/collections/ordered_dict/pointer_test.py
+++ b/tests/syft/lib/python/collections/ordered_dict/pointer_test.py
@@ -111,6 +111,10 @@ def test_pointer_objectives(test_objects, func):
 
         if func in ["items", "values", "keys"]:
             py_res = list(py_res)
+            sy_res = list(sy_res)
 
         assert py_res == sy_res
-        assert sy_res == remote_sy_res
+
+        # TODO: support `.get` for IteratorPointer objects
+        if func not in ("items", "keys", "values"):
+            assert sy_res == remote_sy_res
diff --git a/tests/syft/lib/python/dict/dict_serde_test.py b/tests/syft/lib/python/dict/dict_serde_test.py
--- a/tests/syft/lib/python/dict/dict_serde_test.py
+++ b/tests/syft/lib/python/dict/dict_serde_test.py
@@ -2,6 +2,7 @@
 from collections import UserDict
 
 # third party
+import pytest
 import torch as th
 
 # syft absolute
@@ -75,3 +76,19 @@ def test_list_send() -> None:
     res = ptr.get()
     for res_el, original_el in zip(res, syft_list):
         assert res_el == original_el
+
+
+@pytest.mark.parametrize("method_name", ["items", "keys", "values"])
+def test_iterator_methods(method_name: str) -> None:
+    alice = sy.VirtualMachine(name="alice")
+    alice_client = alice.get_root_client()
+
+    d = Dict({"#1": 1, "#2": 2})
+    dptr = d.send(alice_client)
+
+    itemsptr = getattr(dptr, method_name)()
+    assert type(itemsptr).__name__ == "IteratorPointer"
+
+    for itemptr, local_item in zip(itemsptr, getattr(d, method_name)()):
+        get_item = itemptr.get()
+        assert get_item == local_item