Repository: pyccel/pyccel (python)

Issue:
[DICT] Add Python support for pop
### Describe the feature
Syntactic and semantic support should be added for `pop`. 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.pop(1)
    c = a.pop(3, 4.0)
    return a, b, c
```

Pull request description:
[DICT] Add support for pop() method
Add support for the `dict` method `pop()` and the initialisation of a dictionary via a call to `{}`. Syntactic, semantic and Python printing support is added. The `DictType` datatype is expanded to match the expected description from the docs. The class `PythonDict` is added to manage the initialisation. The abstract class `DictMethod` and the subclass `DictPop` are defined to handle the `pop()` method. Fixes #1886.

_Blocked by #1897_

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ee292e9..2fd6196e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file.
 -   #1877 : Add C Support for set method `pop()`.
 -   #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()`.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[DEVELOPER\] Added an improved traceback to the developer-mode errors for errors in function calls.
 
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index da29bd09..27d5596c 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -73,3 +73,20 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `vars` | No |
 | **`zip`** | as a loop iterable |
 | \_\_`import`\_\_ | No
+
+## Dictionary methods
+
+| Method | Supported |
+|----------|-----------|
+| `clear` | No |
+| `copy` | No |
+| `get` | No |
+| `items` | No |
+| `keys` | No |
+| `pop` | Python-only |
+| `popitem` | No |
+| `reversed` | No |
+| `setdefault` | No |
+| `update` | No |
+| `values` | No |
+
diff --git a/pyccel/ast/builtin_methods/dict_methods.py b/pyccel/ast/builtin_methods/dict_methods.py
new file mode 100644
index 00000000..e83fc9bd
--- /dev/null
+++ b/pyccel/ast/builtin_methods/dict_methods.py
@@ -0,0 +1,101 @@
+# coding: utf-8
+#------------------------------------------------------------------------------------------#
+# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
+# go to https://github.com/pyccel/pyccel/blob/devel/LICENSE for full license details.      #
+#------------------------------------------------------------------------------------------#
+"""
+The dict container has a number of built-in methods that are 
+always available.
+
+This module contains objects which describe these methods within Pyccel's AST.
+"""
+
+from pyccel.ast.internals import PyccelFunction
+
+__all__ = ('DictMethod',
+           'DictPop',
+           )
+
+#==============================================================================
+class DictMethod(PyccelFunction):
+    """
+    Abstract class for dict method calls.
+
+    A subclass of this base class represents calls to a specific dict
+    method.
+
+    Parameters
+    ----------
+    dict_obj : TypedAstNode
+        The object which the method is called from.
+    
+    *args : TypedAstNode
+        The arguments passed to dict methods.
+    """
+    __slots__ = ("_dict_obj",)
+    _attribute_nodes = PyccelFunction._attribute_nodes + ("_dict_obj",)
+
+    def __init__(self, dict_obj, *args):
+        self._dict_obj = dict_obj
+        super().__init__(*args)
+
+    @property
+    def dict_obj(self):
+        """
+        Get the object representing the dict.
+
+        Get the object representing the dict.
+        """
+        return self._dict_obj
+
+#==============================================================================
+class DictPop(DictMethod):
+    """
+    Represents a call to the .pop() method.
+
+    The pop() method pops an element from the dict. The element is selected
+    via a key. If the key is not present in the dictionary then the default
+    value is returned.
+
+    Parameters
+    ----------
+    dict_obj : TypedAstNode
+        The object from which the method is called.
+
+    k : TypedAstNode
+        The key which is used to select the value from the dictionary.
+
+    d : TypedAstNode, optional
+        The value that should be returned if the key is not present in the
+        dictionary.
+    """
+    __slots__ = ('_class_type',)
+    _shape = None
+    name = 'pop'
+
+    def __init__(self, dict_obj, k, d = None):
+        dict_type = dict_obj.class_type
+        self._class_type = dict_type.value_type
+        if k.class_type != dict_type.key_type:
+            raise TypeError(f"Key passed to pop method has type {k.class_type}. Expected {dict_type.key_type}")
+        if d and d.class_type != dict_type.value_type:
+            raise TypeError(f"Default value passed to pop method has type {d.class_type}. Expected {dict_type.value_type}")
+        super().__init__(dict_obj, k, d)
+
+    @property
+    def key(self):
+        """
+        The key that is used to select the element from the dict.
+
+        The key that is used to select the element from the dict.
+        """
+        return self._args[0]
+
+    @property
+    def default_value(self):
+        """
+        The value that should be returned if the key is not present in the dictionary.
+
+        The value that should be returned if the key is not present in the dictionary.
+        """
+        return self._args[1]
diff --git a/pyccel/ast/builtin_methods/list_methods.py b/pyccel/ast/builtin_methods/list_methods.py
index 4b0917a7..5094649f 100644
--- a/pyccel/ast/builtin_methods/list_methods.py
+++ b/pyccel/ast/builtin_methods/list_methods.py
@@ -41,7 +41,7 @@ class ListMethod(PyccelFunction):
         The arguments passed to list methods.
     """
     __slots__ = ("_list_obj",)
-    _attribute_nodes = ("_list_obj",)
+    _attribute_nodes = PyccelFunction._attribute_nodes + ("_list_obj",)
     name = None
     def __init__(self, list_obj, *args):
         self._list_obj = list_obj
diff --git a/pyccel/ast/builtin_methods/set_methods.py b/pyccel/ast/builtin_methods/set_methods.py
index f37499f5..c4b8af42 100644
--- a/pyccel/ast/builtin_methods/set_methods.py
+++ b/pyccel/ast/builtin_methods/set_methods.py
@@ -40,7 +40,7 @@ class SetMethod(PyccelFunction):
         The arguments passed to the function call.
     """
     __slots__ = ('_set_variable',)
-    _attribute_nodes = ('_set_variable',)
+    _attribute_nodes = PyccelFunction._attribute_nodes + ('_set_variable',)
     def __init__(self,  set_variable, *args):
         self._set_variable = set_variable
         super().__init__(*args)
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 317eed9e..42557348 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -10,6 +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 .builtins   import PythonImag, PythonReal, PythonConjugate
 from .core       import ClassDef, PyccelFunctionDef
@@ -169,6 +170,7 @@ SetClass = ClassDef('set',
 
 DictClass = ClassDef('dict',
         methods=[
+            PyccelFunctionDef('pop', func_class = DictPop),
         ])
 
 #=======================================================================================
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index a457521e..7ab1ebf8 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -847,6 +847,22 @@ class PythonCodePrinter(CodePrinter):
 
         return f"{list_obj}.{method_name}({method_args})\n"
 
+    def _print_DictMethod(self, expr):
+        method_name = expr.name
+        dict_obj = self._print(expr.dict_obj)
+        method_args = ', '.join(self._print(a) for a in expr.args)
+
+        return f"{dict_obj}.{method_name}({method_args})\n"
+
+    def _print_DictPop(self, expr):
+        dict_obj = self._print(expr.dict_obj)
+        key = self._print(expr.key)
+        if expr.default_value:
+            val = self._print(expr.default_value)
+            return f"{dict_obj}.pop({key}, {val})\n"
+        else:
+            return f"{dict_obj}.pop({key})\n"
+
     def _print_Slice(self, expr):
         start = self._print(expr.start) if expr.start else ''
         stop  = self._print(expr.stop)  if expr.stop  else ''


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index a5cca5e1..b15945b4 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -68,3 +68,33 @@ def test_dict_kwarg_init(language):
     python_result = kwarg_init()
     assert isinstance(python_result, type(pyccel_result))
     assert python_result == pyccel_result
+
+def test_pop_element(language) :
+    def pop_element():
+        a = {1:1.0, 2:2.0}
+        return a.pop(1)
+    epyc_element = epyccel(pop_element, language = language)
+    pyccel_result = epyc_element()
+    python_result = pop_element()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_default_element(language) :
+    def pop_default_element():
+        a = {1:True, 2:False}
+        return a.pop(3, True)
+    epyc_default_element = epyccel(pop_default_element, language = language)
+    pyccel_result = epyc_default_element()
+    python_result = pop_default_element()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result
+
+def test_pop_str_keys(language) :
+    def pop_str_keys():
+        a = {'a':1, 'b':2}
+        return a.pop('a')
+    epyc_str_keys = epyccel(pop_str_keys, language = language)
+    pyccel_result = epyc_str_keys()
+    python_result = pop_str_keys()
+    assert isinstance(python_result, type(pyccel_result))
+    assert python_result == pyccel_result