Repository: pyccel/pyccel (python)

Issue:
[SETS] update : Updates the set with the union of itself and others.
### Feature Description

**Add support for update() method for sets in the Python translation.**
`update(*others)` : Updates the calling set to include all unique elements present in itself as well as all specified other sets.

### Test Code

This Code Should Work 

```python
set1 = {1, 2}
set2 = {3, 4}
set1.update(set2)
print(set1) 
>>> {1, 2, 3, 4}
```

### Proposed Solution
See description in #1531 

Pull request description:
Set support: `update` method
Add support for the update method of Python setsto the semantic stage.
Add handling of that method to the Python printer. This fixes #1754.

**Commit Summary**

* Add the `SetUpdate` class only to document the behavior of update() and its Pyccel implementation.

* Create `_build_SetUpate`, which, depending on the argument passed to update(), tries to create semantic nodes of `add()` or a syntactic node of a `For` loop and then re-parse it.

Add tests for multiple list methods involving many scenarios where the functions could be used.

This PR builds upon the foundation established in #1731.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6631d23..4cfff96a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
 -   #1740 : Add Python support for set method `copy()`.
 -   #1750 : Add Python support for set method `remove()`.
 -   #1743 : Add Python support for set method `discard()`.
+-   #1754 : Add Python support for set method `update()`.
 -   #1787 : Ensure `STC` is installed with Pyccel.
 -   #1656 : Ensure `gFTL` is installed with Pyccel.
 -   #1830 : Add a `pyccel.lambdify.lambdify` function to accelerate SymPy expressions.
diff --git a/pyccel/ast/builtin_methods/list_methods.py b/pyccel/ast/builtin_methods/list_methods.py
index 5dc9fef9..d1fe3cc4 100644
--- a/pyccel/ast/builtin_methods/list_methods.py
+++ b/pyccel/ast/builtin_methods/list_methods.py
@@ -188,12 +188,12 @@ class ListExtend(ListMethod):
     Represents a call to the .extend() method of an object with a list type,
     which adds items of an iterable (list, tuple, dictionary, etc) at the end
     of a list.
-    This method is handled through the call to `_visit_ListExtend` in
+    This method is handled through the call to `_build_ListExtend` in
     the semantic stage. It then attempts to construct a `For` loop node with
     a body that calls `append()`, or direct `append()` nodes depending on
     the type of the iterable passed to `extend()`.
     This class should never be instantiated; it's only purpose is to help
-    construct the annotation_method `_visit_ListExtend`. 
+    construct the annotation_method `_build_ListExtend`. 
     The extend method is called as follows:
 
     >>> a = [1, 2, 3]
diff --git a/pyccel/ast/builtin_methods/set_methods.py b/pyccel/ast/builtin_methods/set_methods.py
index 74f00106..6d6618cb 100644
--- a/pyccel/ast/builtin_methods/set_methods.py
+++ b/pyccel/ast/builtin_methods/set_methods.py
@@ -20,7 +20,8 @@ __all__ = (
     'SetDiscard',
     'SetMethod',
     'SetPop',
-    'SetRemove'
+    'SetRemove',
+    'SetUpdate'
 )
 
 class SetMethod(PyccelFunction):
@@ -53,7 +54,7 @@ class SetMethod(PyccelFunction):
         """
         return self._set_variable
 
-
+#==============================================================================
 class SetAdd(SetMethod) :
     """
     Represents a call to the .add() method.
@@ -80,7 +81,7 @@ class SetAdd(SetMethod) :
             raise TypeError("Expecting an argument of the same type as the elements of the set")
         super().__init__(set_variable, new_elem)
 
-
+#==============================================================================
 class SetClear(SetMethod):
     """
     Represents a call to the .clear() method.
@@ -101,7 +102,7 @@ class SetClear(SetMethod):
     def __init__(self, set_variable):
         super().__init__(set_variable)
 
-
+#==============================================================================
 class SetCopy(SetMethod):
     """
     Represents a call to the .copy() method.
@@ -122,7 +123,7 @@ class SetCopy(SetMethod):
         self._class_type = set_variable._class_type
         super().__init__(set_variable)
 
-
+#==============================================================================
 class SetPop(SetMethod):
     """
     Represents a call to the .pop() method.
@@ -146,7 +147,7 @@ class SetPop(SetMethod):
         self._class_type = set_variable.class_type.element_type
         super().__init__(set_variable)
 
-
+#==============================================================================
 class SetRemove(SetMethod):
     """
     Represents a call to the .remove() method.
@@ -174,7 +175,7 @@ class SetRemove(SetMethod):
             raise TypeError(f"Can't remove an element of type {item.dtype} from a set of {set_variable.dtype}")
         super().__init__(set_variable, item)
 
-
+#==============================================================================
 class SetDiscard(SetMethod):
     """
     Represents a call to the .discard() method.
@@ -200,3 +201,32 @@ class SetDiscard(SetMethod):
         if set_variable.class_type.element_type != item.class_type:
             raise TypeError("Expecting an argument of the same type as the elements of the set")
         super().__init__(set_variable, item)
+
+#==============================================================================
+class SetUpdate(SetMethod):
+    """
+    Represents a call to the .update() method.
+
+    Represents a call to the .update() method of an object with a set type,
+    Which adds items from another set (or any other iterable).
+    This method is handled through the call to `_build_SetUpdate` in
+    the semantic stage. It then attempts to construct a `For` loop node with
+    a body that calls `add()`, or direct `add()` nodes depending on
+    the type of the iterable passed to `update()`.
+    This class should never be instantiated; it's only purpose is to help
+    construct the annotation_method `_build_SetUpdate`. 
+    The update method is called as follows:
+
+    Parameters
+    ----------
+    set_obj : TypedAstNode
+        The set object which the method is called from.
+        The argument passed to update() method.
+    iterable : TypedAstNode
+        The item to search for, and remove.
+    """
+    __slots__ = ()
+    name = 'update'
+
+    def __init__(self, set_obj, iterable) -> None:
+        super().__init__(set_obj, iterable)
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index a09a754d..ecf086d5 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.set_methods  import SetAdd, SetClear, SetCopy, SetPop, SetRemove, SetDiscard
+from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetClear, SetCopy, SetPop, SetRemove, SetDiscard, SetUpdate
 from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, ListPop,
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
@@ -161,6 +161,7 @@ SetClass = ClassDef('set',
             PyccelFunctionDef('discard', func_class = SetDiscard),
             PyccelFunctionDef('pop', func_class = SetPop),
             PyccelFunctionDef('remove', func_class = SetRemove),
+            PyccelFunctionDef('update', func_class = SetUpdate),
         ])
 
 #=======================================================================================
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 5da2f8c2..7205a2ac 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -29,6 +29,7 @@ from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
                                  PythonTuple, Lambda, PythonMap)
 
 from pyccel.ast.builtin_methods.list_methods import ListMethod, ListAppend
+from pyccel.ast.builtin_methods.set_methods  import SetMethod, SetAdd
 
 from pyccel.ast.core import Comment, CommentBlock, Pass
 from pyccel.ast.core import If, IfSection
@@ -3016,7 +3017,8 @@ class SemanticParser(BasicParser):
                     symbol=expr, severity='error')
 
         # Checking for the result of _visit_ListExtend
-        if isinstance(rhs, For) or (isinstance(rhs, CodeBlock) and isinstance(rhs.body[0], ListMethod)):
+        if isinstance(rhs, For) or (isinstance(rhs, CodeBlock) and
+            isinstance(rhs.body[0], (ListMethod, SetMethod))):
             return rhs
         if isinstance(rhs, ConstructorCall):
             return rhs
@@ -4573,7 +4575,7 @@ class SemanticParser(BasicParser):
         """
         Method to navigate the syntactic DottedName node of an `extend()` call.
 
-        The purpose of this `_visit` method is to construct new nodes from a syntactic 
+        The purpose of this `_build` method is to construct new nodes from a syntactic 
         DottedName node. It checks the type of the iterable passed to `extend()`.
         If the iterable is an instance of `PythonList` or `PythonTuple`, it constructs 
         a CodeBlock node where its body consists of `ListAppend` objects with the 
@@ -4799,3 +4801,50 @@ class SemanticParser(BasicParser):
         else:
             self.insert_import('math', AsName(MathAtan2, 'atan2'))
             return MathAtan2(PythonImag(var), PythonReal(var))
+
+    def _build_SetUpdate(self, expr):
+        """
+        Method to navigate the syntactic DottedName node of an `update()` call.
+
+        The purpose of this `_build` method is to construct new nodes from a syntactic 
+        DottedName node. It checks the type of the iterable passed to `update()`.
+        If the iterable is an instance of `PythonList`, `PythonSet` or `PythonTuple`, it constructs 
+        a CodeBlock node where its body consists of `SetAdd` objects with the 
+        elements of the iterable. If not, it attempts to construct a syntactic `For` 
+        loop to iterate over the iterable object and added its elements to the set 
+        object. Finally, it passes to a `_visit()` call for semantic parsing.
+    
+        Parameters
+        ----------
+        expr : DottedName
+            The syntactic DottedName node that represent the call to `.update()`.
+
+        Returns
+        -------
+        PyccelAstNode
+            CodeBlock or For containing SetAdd objects.
+        """
+        iterable = expr.name[1].args[0].value
+        if isinstance(iterable, (PythonList, PythonSet, PythonTuple)):
+            list_variable = self._visit(expr.name[0])
+            added_list = self._visit(iterable)
+            try:
+                store = [SetAdd(list_variable, a) for a in added_list]
+            except TypeError as e:
+                msg = str(e)
+                errors.report(msg, symbol=expr, severity='fatal')
+            return CodeBlock(store)
+        else:
+            pyccel_stage.set_stage('syntactic')
+            for_target = self.scope.get_new_name()
+            arg = FunctionCallArgument(for_target)
+            func_call = FunctionCall('add', [arg])
+            dotted = DottedName(expr.name[0], func_call)
+            lhs = PyccelSymbol('_', is_temp=True)
+            assign = Assign(lhs, dotted)
+            assign.set_current_ast(expr.python_ast)
+            body = CodeBlock([assign])
+            for_obj = For(for_target, iterable, body)
+            pyccel_stage.set_stage('semantic')
+            return self._visit(for_obj)
+


Test patch:
diff --git a/tests/epyccel/test_epyccel_sets.py b/tests/epyccel/test_epyccel_sets.py
index ecae59a8..afd7d224 100644
--- a/tests/epyccel/test_epyccel_sets.py
+++ b/tests/epyccel/test_epyccel_sets.py
@@ -203,3 +203,83 @@ def test_Discard_wrong_arg(language):
     pyccel_result = epyccel_remove()
     python_result = Discard_wrong_arg()
     assert python_result == pyccel_result
+
+def test_update_basic(language):
+    def update_basic():
+        a = {1, 2, 3}
+        b = {4, 5, 6}
+        a.update(b)
+        return a
+
+    epyccel_update = epyccel(update_basic, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_basic()
+    assert python_result == pyccel_result
+
+def test_update_multiple(language):
+    def update_multiple():
+        a = {1, 2, 3}
+        a.update({4, 5})
+        a.update({6, 7, 8, 9})
+        a.update({10})
+        return a
+
+    epyccel_update = epyccel(update_multiple, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_multiple()
+    assert python_result == pyccel_result
+
+
+def test_update_boolean_tuple(language):
+    def update_boolean_tuple():
+        a = {True}
+        b = (False, True, False)
+        a.update(b)
+        return a
+    epyccel_update = epyccel(update_boolean_tuple, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_boolean_tuple()
+    assert python_result == pyccel_result
+
+
+def test_update_complex_list(language):
+    def update_complex_list():
+        a = {1j, 2 + 3j, 0 + 0j}
+        b = {4j, 5j, 1 + 6j}
+        a.update(b)
+        return a
+    epyccel_update = epyccel(update_complex_list, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_complex_list()
+    assert python_result == pyccel_result
+
+def test_update_range(language):
+    def update_range():
+        a = {1, 2, 3}
+        a.update(range(4, 9))
+        return a
+    epyccel_update = epyccel(update_range, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_range()
+    assert python_result == pyccel_result
+
+def test_update_set_as_arg(language):
+    def update_set_as_arg():
+        a = {1, 2, 3}
+        a.update({4, 5, 6})
+        return a
+
+    epyccel_update = epyccel(update_set_as_arg, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_set_as_arg()
+    assert python_result == pyccel_result
+
+def test_update_tuple_as_arg(language):
+    def update_tuple_as_arg():
+        a = {1, 2, 3}
+        a.update((4, 5, 6))
+        return a
+    epyccel_update = epyccel(update_tuple_as_arg, language=language)
+    pyccel_result = epyccel_update()
+    python_result =  update_tuple_as_arg()
+    assert python_result == pyccel_result
diff --git a/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_1.py b/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_1.py
new file mode 100644
index 00000000..49ff697d
--- /dev/null
+++ b/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_1.py
@@ -0,0 +1,4 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = {1.4, 6.2, 8.1}
+a.update({3j})
diff --git a/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_2.py b/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_2.py
new file mode 100644
index 00000000..9950f019
--- /dev/null
+++ b/tests/errors/semantic/blocking/UPDATE_SET_INCOMPATIBLE_TYPES_2.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = {2, 9, 5}
+b = {8.9, 6.}
+a.update(b)