Repository: pyccel/pyccel (python)

Issue:
[DICT] Add Python support for keys
### Describe the feature
Syntactic and semantic support should be added for `keys`. 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 : list[int] = []
    for bi in a.keys():
        b.append(bi)
    return b
```

Pull request:
Add support for dict.keys
Add support for dict.keys. Fixes #1882

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b34157ce..fdc3ac92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,8 +48,9 @@ All notable changes to this project will be documented in this file.
 -   #1745 : Add support for set method `intersection_update()`.
 -   #2059 : Add C support for returning dictionaries from functions.
 -   #2164 : Add support for dict indexing.
--   #1884 : Add support for dict method `items()`.
 -   #1880 : Add support for dict method `clear()`.
+-   #1884 : Add support for dict method `items()`.
+-   #1884 : Add support for dict method `keys()`.
 -   #1886 : Add Python and C support for dict method `pop()`.
 -   #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 c8fcccf6..a6d11971 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -120,11 +120,11 @@ Python contains a limited number of builtin functions defined [here](https://doc
 
 | Method | Supported |
 |----------|-----------|
-| `clear` | **Yes** |
+| **`clear`** | **Yes** |
 | `copy` | Python-only |
 | `get` | Python and C |
-| `items` | **Yes** |
-| `keys` | No |
+| **`items`** | **Yes** |
+| **`keys`** | **Yes** |
 | `pop` | Python and C |
 | `popitem` | Python-only |
 | `reversed` | No |
diff --git a/pyccel/ast/builtin_methods/dict_methods.py b/pyccel/ast/builtin_methods/dict_methods.py
index b2ac5d8f..a28ff1cb 100644
--- a/pyccel/ast/builtin_methods/dict_methods.py
+++ b/pyccel/ast/builtin_methods/dict_methods.py
@@ -21,6 +21,7 @@ __all__ = ('DictClear',
            'DictGet',
            'DictGetItem',
            'DictItems',
+           'DictKeys',
            'DictMethod',
            'DictPop',
            'DictPopitem',
@@ -334,6 +335,53 @@ class DictItems(Iterable):
         item = DictPopitem(self._dict_obj)
         return [IndexedElement(item, 0), IndexedElement(item, 1)]
 
+#==============================================================================
+class DictKeys(Iterable):
+    """
+    Represents a call to the .keys() method.
+
+    Represents a call to the .keys() method which iterates over the keys of a
+    dictionary.
+
+    Parameters
+    ----------
+    dict_obj : TypedAstNode
+        The object from which the method is called.
+    """
+    __slots__ = ('_dict_obj',)
+    _attribute_nodes = Iterable._attribute_nodes + ("_dict_obj",)
+    _shape = None
+    _class_type = SymbolicType()
+    name = 'keys'
+
+    def __init__(self, dict_obj):
+        self._dict_obj = dict_obj
+        super().__init__(1)
+
+    @property
+    def variable(self):
+        """
+        Get the object representing the dict.
+
+        Get the object representing the dict.
+        """
+        return self._dict_obj
+
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
+
+        Returns two objects that could be a key and a value of the dictionary.
+        These elements are used to determine the types of the loop targets.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        item = DictPopitem(self._dict_obj)
+        return [IndexedElement(item, 0)]
+
 #==============================================================================
 class DictGetItem(DictMethod):
     """
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 386e6438..935e586a 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -13,7 +13,7 @@ from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, Lis
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
 from pyccel.ast.builtin_methods.dict_methods import (DictPop, DictPopitem, DictGet, DictClear,DictCopy,
-                                                     DictSetDefault, DictItems, DictGetItem)
+                                                     DictSetDefault, DictItems, DictGetItem, DictKeys)
 
 from .builtins   import PythonImag, PythonReal, PythonConjugate
 from .core       import ClassDef, PyccelFunctionDef
@@ -184,6 +184,7 @@ DictClass = ClassDef('dict',
             PyccelFunctionDef('clear', func_class = DictClear),
             PyccelFunctionDef('get', func_class = DictGet),
             PyccelFunctionDef('items', func_class = DictItems),
+            PyccelFunctionDef('keys', func_class = DictKeys),
             PyccelFunctionDef('pop', func_class = DictPop),
             PyccelFunctionDef('popitem', func_class = DictPopitem),
             PyccelFunctionDef('setdefault', func_class = DictSetDefault),
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index eea36616..48cc9ada 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -19,7 +19,7 @@ from pyccel.ast.builtins  import PythonPrint, PythonType, VariableIterator
 
 from pyccel.ast.builtins  import PythonList, PythonTuple, PythonSet, PythonDict, PythonLen
 
-from pyccel.ast.builtin_methods.dict_methods  import DictItems
+from pyccel.ast.builtin_methods.dict_methods  import DictItems, DictKeys
 
 from pyccel.ast.core      import Declare, For, CodeBlock, ClassDef
 from pyccel.ast.core      import FuncAddressDeclare, FunctionCall, FunctionCallArgument
@@ -2433,7 +2433,7 @@ class CCodePrinter(CodePrinter):
         iterable = expr.iterable
         indices = iterable.loop_counters
 
-        if isinstance(iterable, (VariableIterator, DictItems)) and \
+        if isinstance(iterable, (VariableIterator, DictItems, DictKeys)) and \
                 isinstance(iterable.variable.class_type, (DictType, HomogeneousSetType, HomogeneousListType)):
             var = iterable.variable
             iterable_type = var.class_type
@@ -2445,6 +2445,8 @@ class CCodePrinter(CodePrinter):
             if isinstance(iterable, DictItems):
                 assigns = [Assign(expr.target[0], DottedVariable(VoidType(), 'first', lhs = tmp_ref)),
                            Assign(expr.target[1], DottedVariable(VoidType(), 'second', lhs = tmp_ref))]
+            elif isinstance(iterable, DictKeys):
+                assigns = [Assign(expr.target[0], DottedVariable(VoidType(), 'first', lhs = tmp_ref))]
             else:
                 assigns = [Assign(expr.target[0], tmp_ref)]
             additional_assign = CodeBlock(assigns)
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 90d733d2..ac9ea0a3 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -24,7 +24,7 @@ from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
 from pyccel.ast.builtins import PythonTuple, DtypePrecisionToCastFunction
 from pyccel.ast.builtins import PythonBool, PythonList, PythonSet, VariableIterator
 
-from pyccel.ast.builtin_methods.dict_methods import DictItems
+from pyccel.ast.builtin_methods.dict_methods import DictItems, DictKeys
 
 from pyccel.ast.builtin_methods.list_methods import ListPop
 
@@ -2670,7 +2670,7 @@ class FCodePrinter(CodePrinter):
         iterable = expr.iterable
         indices = iterable.loop_counters
 
-        if isinstance(iterable, (VariableIterator, DictItems)) and \
+        if isinstance(iterable, (VariableIterator, DictItems, DictKeys)) and \
                 isinstance(iterable.variable.class_type, (DictType, HomogeneousSetType)):
             var = iterable.variable
             iterable_type = var.class_type
@@ -2690,6 +2690,9 @@ class FCodePrinter(CodePrinter):
                 val = self._print(expr.target[1])
                 target_assign = (f'{key} = {iterator} % first()\n'
                                  f'{val} = {iterator} % second()\n')
+            elif isinstance(iterable, DictKeys):
+                key = self._print(expr.target[0])
+                target_assign = (f'{key} = {iterator} % first()\n')
             else:
                 target = self._print(expr.target[0])
                 target_assign = f'{target} = {iterator} % of()\n'
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 15c12f0c..0faddee7 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -881,6 +881,11 @@ class PythonCodePrinter(CodePrinter):
 
         return f"{dict_obj}.items()"
 
+    def _print_DictKeys(self, expr):
+        dict_obj = self._print(expr.variable)
+
+        return f"{dict_obj}.keys()"
+
     def _print_DictGetItem(self, expr):
         dict_obj = self._print(expr.dict_obj)
         key = self._print(expr.key)
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 3673676b..eca554d4 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -33,6 +33,7 @@ from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet, Variabl
 from pyccel.ast.builtins import PythonRange, PythonZip, PythonEnumerate, PythonTuple
 from pyccel.ast.builtins import Lambda, PythonMap
 
+from pyccel.ast.builtin_methods.dict_methods import DictKeys
 from pyccel.ast.builtin_methods.list_methods import ListMethod, ListAppend
 from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetUnion, SetCopy, SetIntersectionUpdate
 from pyccel.ast.builtin_methods.dict_methods  import DictGetItem, DictGet
@@ -2157,7 +2158,10 @@ class SemanticParser(BasicParser):
         """
         iterable = self._visit(syntactic_iterable)
         if isinstance(iterable, (Variable, IndexedElement)):
-            iterable = VariableIterator(iterable)
+            if isinstance(iterable.class_type, DictType):
+                iterable = DictKeys(iterable)
+            else:
+                iterable = VariableIterator(iterable)
         elif not isinstance(iterable, Iterable):
             if isinstance(iterable, TypedAstNode):
                 pyccel_stage.set_stage('syntactic')


Test patch:
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index b29990db..32a967b5 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -362,3 +362,30 @@ def test_dict_items(language):
     assert isinstance(python_result[0], type(pyccel_result[0]))
     assert isinstance(python_result[1], type(pyccel_result[1]))
 
+def test_dict_keys(language):
+    def dict_keys():
+        a = {1:1.0, 2:2.0, 3:3.0, 5:4.7}
+        key_sum = 0
+        for key in a.keys(): #pylint:disable=consider-iterating-dictionary
+            key_sum += key
+
+        return key_sum
+
+    epyc_dict_keys = epyccel(dict_keys, language = language)
+    pyccel_result = epyc_dict_keys()
+    python_result = dict_keys()
+    assert python_result == pyccel_result
+
+def test_dict_keys_iter(language):
+    def dict_keys():
+        a = {1:1.0, 2:2.0, 3:3.0, 5:4.7}
+        key_sum = 0
+        for key in a:
+            key_sum += key
+
+        return key_sum
+
+    epyc_dict_keys = epyccel(dict_keys, language = language)
+    pyccel_result = epyc_dict_keys()
+    python_result = dict_keys()
+    assert python_result == pyccel_result