Repository: pyccel/pyccel (python)

Issue:
[DICT] Add Python support for popitem
### Describe the feature
Syntactic and semantic support should be added for `popitem`. This should be tested by implementing Python printing

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def foo():
    a = {1,1.0, 2:2.0}
    b = a.popitem()
    c = a.popitem()
    return a, b, c
```

Pull request description:
Add support for DICT popitem
Add Python support for the dict `popitem` method. Fixes #1887 

**Commit Summary**
- Add Python support for the dict `popitem` method.
- Fix the shape of an IndexedElement holding a 0D element of an inhomogeneous tuple
- Fix the creation of a variable from an assignment to a pyccel function returning an inhomogeneous tuple (related to #337)
- Raise an error instead of trying to return an inhomogeneous element (related to #337)
- Add tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7ea8310..5a2d718a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ All notable changes to this project will be documented in this file.
 -   #1895 : Add Python support for dict initialisation with `{}`.
 -   #1895 : Add Python support for dict initialisation with `dict()`.
 -   #1886 : Add Python support for dict method `pop()`.
+-   #1887 : Add Python support for dict method `popitem()`.
 -   #1944 : Add the appropriate C language equivalent for declaring a Python `dict` container using the STC library.
 -   #1936 : Add missing C output for inline decorator example in documentation
 -   #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 57bb7f2e..6d3d82d5 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -105,7 +105,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `items` | No |
 | `keys` | No |
 | `pop` | Python-only |
-| `popitem` | No |
+| `popitem` | Python-only |
 | `reversed` | No |
 | `setdefault` | No |
 | `update` | No |
diff --git a/pyccel/ast/builtin_methods/dict_methods.py b/pyccel/ast/builtin_methods/dict_methods.py
index e83fc9bd..de542f67 100644
--- a/pyccel/ast/builtin_methods/dict_methods.py
+++ b/pyccel/ast/builtin_methods/dict_methods.py
@@ -10,10 +10,12 @@ always available.
 This module contains objects which describe these methods within Pyccel's AST.
 """
 
+from pyccel.ast.datatypes import InhomogeneousTupleType
 from pyccel.ast.internals import PyccelFunction
 
 __all__ = ('DictMethod',
            'DictPop',
+           'DictPopitem',
            )
 
 #==============================================================================
@@ -99,3 +101,24 @@ class DictPop(DictMethod):
         The value that should be returned if the key is not present in the dictionary.
         """
         return self._args[1]
+
+
+class DictPopitem(DictMethod):
+    """
+    Represents a call to the .popitem() method.
+
+    The popitem() method removes the last inserted key-value pair from the dict.
+
+    Parameters
+    ----------
+    dict_obj : TypedAstNode
+        The object from which the method is called.
+    """
+    __slots__ = ('_class_type',)
+    _shape = (2,)
+    name = 'popitem'
+
+    def __init__(self, dict_obj):
+        dict_type = dict_obj.class_type
+        self._class_type = InhomogeneousTupleType(dict_type.key_type, dict_type.value_type)
+        super().__init__(dict_obj)
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index cc0e78a7..0c4c43fc 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -10,7 +10,7 @@ from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetClear, SetCopy, S
 from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, ListPop,
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
-from pyccel.ast.builtin_methods.dict_methods  import DictPop
+from pyccel.ast.builtin_methods.dict_methods  import DictPop, DictPopitem
 
 from .builtins   import PythonImag, PythonReal, PythonConjugate
 from .core       import ClassDef, PyccelFunctionDef
@@ -171,6 +171,7 @@ SetClass = ClassDef('set',
 DictClass = ClassDef('dict',
         methods=[
             PyccelFunctionDef('pop', func_class = DictPop),
+            PyccelFunctionDef('popitem', func_class = DictPopitem),
         ])
 
 #=======================================================================================
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 128f5aa7..e2f9d649 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -713,7 +713,10 @@ class IndexedElement(TypedAstNode):
             assert len(self._indices) == 1 and isinstance(self._indices[0], LiteralInteger)
             self._class_type = base.class_type[self._indices[0]]
             self._is_slice = False
-            self._shape = (None,)*self.rank
+            if self.rank:
+                self._shape = (None,)*self.rank
+            else:
+                self._shape = None
 
         else:
             # Calculate new shape
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 2c3fad96..14b884f3 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -813,17 +813,28 @@ class SemanticParser(BasicParser):
                 self._additional_exprs[-1].append(self._visit(assign))
                 var = self._visit(tmp_var)
 
+        elif isinstance(var, Variable):
+            # Nothing to do but excludes this case from the subsequent ifs
+            pass
 
-        elif not isinstance(var, Variable):
-            if hasattr(var,'__getitem__'):
-                if len(indices)==1:
-                    return var[indices[0]]
-                else:
-                    return self._visit(var[indices[0]][indices[1:]])
+        elif hasattr(var,'__getitem__'):
+            if len(indices)==1:
+                return var[indices[0]]
             else:
-                var_type = type(var)
-                errors.report(f"Can't index {var_type}", symbol=expr,
-                    severity='fatal')
+                return self._visit(var[indices[0]][indices[1:]])
+
+        elif isinstance(var, PyccelFunction):
+            pyccel_stage.set_stage('syntactic')
+            tmp_var = PyccelSymbol(self.scope.get_new_name())
+            assign = Assign(tmp_var, var)
+            assign.set_current_ast(expr.python_ast)
+            pyccel_stage.set_stage('semantic')
+            self._additional_exprs[-1].append(self._visit(assign))
+            var = self._visit(tmp_var)
+
+        else:
+            errors.report(f"Can't index {type(var)}", symbol=expr,
+                severity='fatal')
 
         indices = tuple(indices)
 
@@ -1301,21 +1312,22 @@ class SemanticParser(BasicParser):
 
         if isinstance(class_type, InhomogeneousTupleType):
             if isinstance(rhs, FunctionCall):
-                iterable = [r.var for r in rhs.funcdef.results]
+                iterable = [self.scope.collect_tuple_element(r.var) for r in rhs.funcdef.results]
+            elif isinstance(rhs, PyccelFunction):
+                iterable = [IndexedElement(rhs, i)  for i in range(rhs.shape[0])]
             else:
-                iterable = rhs
+                iterable = [self.scope.collect_tuple_element(r) for r in rhs]
             elem_vars = []
             for i,tuple_elem in enumerate(iterable):
-                r = self.scope.collect_tuple_element(tuple_elem)
                 elem_name = self.scope.get_new_name( name + '_' + str(i) )
-                elem_d_lhs = self._infer_type( r )
+                elem_d_lhs = self._infer_type( tuple_elem )
 
                 if not arr_in_multirets:
-                    self._ensure_target( r, elem_d_lhs )
+                    self._ensure_target( tuple_elem, elem_d_lhs )
 
                 elem_type = elem_d_lhs.pop('class_type')
 
-                var = self._create_variable(elem_name, elem_type, r, elem_d_lhs,
+                var = self._create_variable(elem_name, elem_type, tuple_elem, elem_d_lhs,
                         insertion_scope = insertion_scope)
                 elem_vars.append(var)
 
@@ -3286,7 +3298,7 @@ class SemanticParser(BasicParser):
             for l,r in zip(lhs, rhs):
                 # Split assign (e.g. for a,b = 1,c)
                 if isinstance(l.class_type, InhomogeneousTupleType) \
-                        and not isinstance(r, FunctionCall):
+                        and not isinstance(r, (FunctionCall, PyccelFunction)):
                     new_lhs.extend(self.scope.collect_tuple_element(v) for v in l)
                     new_rhs.extend(self.scope.collect_tuple_element(v) for v in r)
                     # Repeat step to handle tuples of tuples of etc.
@@ -3360,6 +3372,7 @@ class SemanticParser(BasicParser):
 
             new_expressions.append(new_expr)
 
+
         if (len(new_expressions)==1):
             new_expressions = new_expressions[0]
 
@@ -3767,6 +3780,10 @@ class SemanticParser(BasicParser):
                     a.cls_variable.is_temp = False
 
         results = [self._visit(i.var) for i in return_objs]
+        if any(isinstance(i.class_type, InhomogeneousTupleType) for i in results):
+            # Extraction of underlying variables is not yet implemented here
+            errors.report("Returning tuples is not yet implemented",
+                    severity='error', symbol=expr)
 
         # add the Deallocate node before the Return node and eliminating the Deallocate nodes
         # the arrays that will be returned.


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index b15945b4..c27e6a1c 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -98,3 +98,46 @@ def test_pop_str_keys(language) :
     python_result = pop_str_keys()
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
+
+@pytest.mark.skip("Returning tuples is not yet implemented. See #337")
+def test_pop_item(language):
+    def pop_item():
+        a = {1:1.0, 2:2.0}
+        return a.popitem()
+    epyc_default_element = epyccel(pop_item, language = language)
+    pyccel_result = epyc_default_element()
+    python_result = pop_item()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_item_elements(language):
+    def pop_item():
+        a = {1:1.0, 2:2.0}
+        b = a.popitem()
+        return b[0], b[1]
+    epyc_default_element = epyccel(pop_item, language = language)
+    pyccel_result = epyc_default_element()
+    python_result = pop_item()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_item_str_keys(language):
+    def pop_item_str_keys():
+        a = {'a':1, 'b':2}
+        b = a.popitem()
+        return b[0], b[1]
+    epyc_default_element = epyccel(pop_item_str_keys, language = language)
+    pyccel_result = epyc_default_element()
+    python_result = pop_item_str_keys()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_item_key(language):
+    def pop_item():
+        a = {1:1.0, 2:2.0}
+        return a.popitem()[0]
+    epyc_default_element = epyccel(pop_item, language = language)
+    pyccel_result = epyc_default_element()
+    python_result = pop_item()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result