Repository: pyccel/pyccel (python)

Issue:
insert: inserts a given element at a given index in a list.
### Feature Description

Add support for insert() method for lists in the Python translation.

### Test Code

This Code Should Work 

```python
digits = [3, 4, 6]

digits.insert(1, 2)

print(digits)
>> [3, 2, 4, 6]
```

### Proposed Solution
See description in #1529

Pull request:
List support/insert method
Add support for the insert() method of Python lists to the semantic stage.
Add handling of that method to the Python printer. This fixes #1692.

**Commit Summary**

- Add a class representation for the `insert()` method, inheriting from `PyccelInternalFunction`. The constructor ensures compatibility between the datatypes of the `insert()` argument and the list for homogenous lists.
- Update the `_class_type` attribute for methods that return `None` 
- Add the python printer `_print_ListInsert()` to generate the appropriate Python representation of the `insert()` method.
- Add success and error tests for the `insert()` method involving many scenarios where the function could be used.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5634e121..37192de8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,11 @@ All notable changes to this project will be documented in this file.
 
 ### Added
 
--   #1529 : Add Python support for list method `append()`
+-   #1689 : Add Python support for list method `append()`
+-   #1692 : Add Python support for list method `insert()`
 -   #1690 : Add Python support for list method `pop()`
--   #1575 : Add support for homogeneous tuple type annotations on variables.
 -   #1691 : Add Python support for list method `clear()`
+-   #1575 : Add support for homogeneous tuple type annotations on variables.
 -   #1425 : Add support for `numpy.isnan`, `numpy.isinf` and `numpy.isfinite`.
 
 ### Fixed
diff --git a/pyccel/ast/builtin_methods/list_methods.py b/pyccel/ast/builtin_methods/list_methods.py
index 0383b8c6..43f07e84 100644
--- a/pyccel/ast/builtin_methods/list_methods.py
+++ b/pyccel/ast/builtin_methods/list_methods.py
@@ -16,10 +16,11 @@ from pyccel.ast.internals import PyccelInternalFunction
 
 __all__ = ('ListAppend',
            'ListClear',
-           'ListPop'
+           'ListInsert',
+           'ListPop',
            )
 
-
+#==============================================================================
 class ListAppend(PyccelInternalFunction):
     """
     Represents a call to the .append() method.
@@ -38,7 +39,7 @@ class ListAppend(PyccelInternalFunction):
     list_variable : Variable
         The variable representing the list.
     
-    new_elem : Variable
+    new_elem : TypedAstNode
         The argument passed to append() method.
     """
     __slots__ = ("_list_variable", "_append_arg")
@@ -48,7 +49,7 @@ class ListAppend(PyccelInternalFunction):
     _order = None
     _rank = 0
     _precision = -1
-    _class_type = NativeHomogeneousList()
+    _class_type = NativeVoid()
     name = 'append'
 
     def __init__(self, list_variable, new_elem) -> None:
@@ -154,7 +155,7 @@ class ListClear(PyccelInternalFunction) :
     _rank = 0
     _order = None
     _shape = None
-    _class_type = NativeHomogeneousList()
+    _class_type = NativeVoid()
     name = 'clear'
 
     def __init__(self, list_variable):
@@ -169,3 +170,81 @@ class ListClear(PyccelInternalFunction) :
         Provide the name of the list as the return value.
         """
         return self._list_variable
+
+#==============================================================================
+class ListInsert(PyccelInternalFunction):
+    """
+    Represents a call to the .insert() method.
+
+    Represents a call to the .insert() method of an object with a list type,
+    which inserts a given element at a given index in a list.
+    This method returns `None`.
+    The insert method is called as follows:
+
+    >>> a = [2, 3, 4]
+    >>> a.insert(0, 1)
+    >>> print(a)
+    [1, 2, 3, 4]
+
+    Parameters
+    ----------
+    list_variable : Variable
+        The variable representing the list.
+
+    index : TypedAstNode
+        The index value for the element to be added.
+    
+    new_elem : TypedAstNode
+        The argument passed to insert() method.
+    """
+    __slots__ = ("_index", "_list_variable", "_insert_arg")
+    _attribute_nodes = ("_index", "_list_variable", "_insert_arg")
+    _dtype = NativeVoid()
+    _shape = None
+    _order = None
+    _rank = 0
+    _precision = -1
+    _class_type = NativeVoid()
+    name = 'insert'
+
+    def __init__(self, list_variable, index, new_elem) -> None:
+        is_homogeneous = (
+            new_elem.dtype is not NativeGeneric() and
+            list_variable.dtype is not NativeGeneric() and
+            list_variable.dtype == new_elem.dtype and
+            list_variable.precision == new_elem.precision and
+            list_variable.rank - 1 == new_elem.rank
+        )
+        if not is_homogeneous:
+            raise TypeError("Expecting an argument of the same type as the elements of the list")
+        self._index = index
+        self._list_variable = list_variable
+        self._insert_arg = new_elem
+        super().__init__()
+
+    @property
+    def index(self):
+        """
+        Index in which the element will be added.
+
+        Index in which the element will be added.
+        """
+        return self._index
+
+    @property
+    def list_variable(self):
+        """
+        Get the variable representing the list.
+
+        Get the variable representing the list.
+        """
+        return self._list_variable
+
+    @property
+    def insert_argument(self):
+        """
+        Get the argument which is passed to insert().
+
+        Get the argument which is passed to insert().
+        """
+        return self._insert_arg
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 502bceb0..a17c00ef 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -6,7 +6,7 @@
 This module contains all types which define a python class which is automatically recognised by pyccel
 """
 
-from pyccel.ast.builtin_methods.list_methods import ListAppend, ListPop, ListClear
+from pyccel.ast.builtin_methods.list_methods import ListAppend, ListInsert, ListPop, ListClear
 
 
 from .builtins  import PythonImag, PythonReal, PythonConjugate
@@ -139,10 +139,12 @@ ListClass = ClassDef('list', class_type = NativeHomogeneousList(),
         methods=[
             PyccelFunctionDef('append', func_class = ListAppend,
                 decorators = {}),
-            PyccelFunctionDef('pop', func_class = ListPop,
-                decorators = {}),
             PyccelFunctionDef('clear', func_class = ListClear,
                 decorators = {}),
+            PyccelFunctionDef('insert', func_class = ListInsert,
+                decorators = {}),
+            PyccelFunctionDef('pop', func_class = ListPop,
+                decorators = {}),
         ])
 
 #=======================================================================================
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index fb24a9dd..be301203 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -852,6 +852,14 @@ class PythonCodePrinter(CodePrinter):
 
         return f"{list_var}.{method_name}({append_arg})\n"
 
+    def _print_ListInsert(self, expr):
+        method_name = expr.name
+        index = self._print(expr.index)
+        list_var = self._print(expr.list_variable)
+        insert_arg = self._print(expr.insert_argument)
+
+        return f"{list_var}.{method_name}({index}, {insert_arg})\n"
+
     def _print_ListPop(self, expr):
         args = self._print(expr.pop_index) if expr.pop_index else ""
         name = self._print(expr.list_variable)


Test patch:
diff --git a/tests/epyccel/modules/list_user_defined_objs.py b/tests/epyccel/modules/list_user_defined_objs.py
new file mode 100644
index 00000000..59e92023
--- /dev/null
+++ b/tests/epyccel/modules/list_user_defined_objs.py
@@ -0,0 +1,16 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    def __init__(self, x : int):
+        self.x = x
+
+def fn():
+    a = A(1)
+    b = A(2)
+    c = A(3)
+    d = A(4)
+    e = A(5)
+    lst = [a, b, c]
+    lst.insert(0, d)
+    lst.insert(1, e)
+    return lst
diff --git a/tests/epyccel/test_epyccel_lists.py b/tests/epyccel/test_epyccel_lists.py
index 3b8bb145..2ab40691 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -118,6 +118,74 @@ def test_append_range_tuple(language):
     epyc_f = epyccel(f, language=language)
     assert f() == epyc_f()
 
+def test_insert_basic(language):
+    def f():
+        a = [1, 2, 3]
+        a.insert(4, 4)
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_multiple(language):
+    def f():
+        a = [1, 2, 3]
+        a.insert(4, 4)
+        a.insert(2, 5)
+        a.insert(1, 6)
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_list(language):
+    def f():
+        a = [[1, 2, 3]]
+        a.insert(1, [4, 5, 6])
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_range(language):
+    def f():
+        a = [1, 2, 3]
+        for i in range(4, 1000):
+            a.insert(i - 1 ,i)
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_range_list(language):
+    def f():
+        a = [[1, 2, 3]]
+        for i in range(4, 1000):
+            a.insert(i, [i, i + 1])
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_range_tuple(language):
+    def f():
+        a = [[1, 2, 3]]
+        for i in range(4, 1000):
+            a.insert(i, (i, i + 1))
+        return a
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_insert_user_defined_objects(language):
+    import modules.list_user_defined_objs as mod
+
+    modnew = epyccel(mod, language=language)
+    python_list = mod.fn()
+    accelerated_list = modnew.fn()
+    assert len(python_list) == len(accelerated_list)
+    for python_elem, accelerated_elem in zip(python_list, accelerated_list):
+        assert python_elem.x == accelerated_elem.x
 
 def test_clear_1(language):
 
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py
new file mode 100644
index 00000000..fa1a3e07
--- /dev/null
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py
@@ -0,0 +1,8 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+import numpy as np
+
+a = [1,2,3]
+b = np.int32(4)
+a.insert(3, b)
+
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_1.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_1.py
new file mode 100644
index 00000000..07304702
--- /dev/null
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_1.py
@@ -0,0 +1,6 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+
+a = [1,2,3]
+a.insert(3, [4])
+
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_2.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_2.py
new file mode 100644
index 00000000..bb648541
--- /dev/null
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_2.py
@@ -0,0 +1,6 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = [1,2,3]
+b = (4,5)
+a.insert(3, b)
+
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_3.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_3.py
new file mode 100644
index 00000000..fdb8b4a4
--- /dev/null
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_3.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = [[1, 2, 3]]
+a.insert(2, (4.4, 8.4))
+