Repository: 12rambau/sepal_ui

Issue:
Use Optional instead of Union[None, ...]
Now that we don't support 3.7 any more, we should drop `typing_extentions` and stop using the Union[none, ...] pattern. 

2 option are possible: 
1.   `none|...`
2.   `Optional[...]`

I personnaly prefer 2.

Discussion:

Patch:
diff --git a/sepal_ui/message/en/decorator.json b/sepal_ui/message/en/decorator.json
new file mode 100644
index 00000000..80b1422a
--- /dev/null
+++ b/sepal_ui/message/en/decorator.json
@@ -0,0 +1,6 @@
+{
+  "decorator": {
+    "no_alert": "You should provide the `alert` argument as parent does not have one",
+    "no_button": "You should provide the `button` argument as parent does not have one"
+  }
+}
diff --git a/sepal_ui/scripts/decorator.py b/sepal_ui/scripts/decorator.py
index b28e59e3..eb0e228d 100644
--- a/sepal_ui/scripts/decorator.py
+++ b/sepal_ui/scripts/decorator.py
@@ -13,13 +13,15 @@ import warnings
 from functools import wraps
 from itertools import product
 from pathlib import Path
-from typing import Any, Callable, List, Union
+from typing import Any, Callable, List, Optional
 
 import ee
 import httplib2
 import ipyvuetify as v
 from deprecated.sphinx import versionadded
 
+from sepal_ui.message import ms
+
 # from sepal_ui.scripts.utils import init_ee
 from sepal_ui.scripts.warning import SepalWarning
 
@@ -60,14 +62,14 @@ def init_ee() -> None:
 
 
 @versionadded(version="3.0", reason="moved from utils to a dedicated module")
-def catch_errors(alert: v.Alert, debug: bool = False) -> Any:
+def catch_errors(alert: Optional[v.Alert] = None, debug: bool = False) -> Any:
     """Decorator to execute try/except sentence and catch errors in the alert message.
 
     If debug is True then the error is raised anyway.
 
     Args:
-        alert (sw.Alert): Alert to display errors
-        debug (bool): Whether to raise the error or not, default to false
+        alert: Alert to display errors
+        debug: Whether to raise the error or not, default to false
 
     Returns:
         The return statement of the decorated method
@@ -75,12 +77,20 @@ def catch_errors(alert: v.Alert, debug: bool = False) -> Any:
 
     def decorator_alert_error(func):
         @wraps(func)
-        def wrapper_alert_error(*args, **kwargs):
+        def wrapper_alert_error(self, *args, **kwargs):
+
+            # Change name of variable to assign it again in this scope
+            # check if alert exist in the parent object if alert is not set manually
+            assert hasattr(self, "alert") or alert, ms.decorator.no_alert
+            alert_ = self.alert if not alert else alert
+            alert_.reset()
+
+            # try to execute the method
             value = None
             try:
-                value = func(*args, **kwargs)
+                value = func(self, *args, **kwargs)
             except Exception as e:
-                alert.add_msg(f"{e}", type_="error")
+                alert_.add_msg(f"{e}", type_="error")
                 if debug:
                     raise e
             return value
@@ -119,8 +129,8 @@ def need_ee(func: Callable) -> Any:
 
 @versionadded(version="3.0", reason="moved from utils to a dedicated module")
 def loading_button(
-    alert: Union[v.Alert, None] = None,
-    button: Union[v.Btn, None] = None,
+    alert: Optional[v.Alert] = None,
+    button: Optional[v.Btn] = None,
     debug: bool = False,
 ) -> Any:
     """Decorator to execute try/except sentence and toggle loading button object.
@@ -142,6 +152,9 @@ def loading_button(
 
             # set btn and alert
             # Change name of variable to assign it again in this scope
+            # check if they exist in the parent object if alert is not set manually
+            assert hasattr(self, "alert") or alert, ms.decorator.no_alert
+            assert hasattr(self, "btn") or button, ms.decorator.no_button
             button_ = self.btn if not button else button
             alert_ = self.alert if not alert else alert
 
@@ -186,7 +199,7 @@ def loading_button(
                         [custom_showwarning(w) for w in w_list]
 
             except Exception as e:
-                alert_.add_msg(f"{e}", "error")
+                alert_.add_msg(f"{e}", type_="error")
                 if debug:
                     button_.toggle_loading()  # Stop loading button if there is an error
                     raise e
diff --git a/sepal_ui/sepalwidgets/alert.py b/sepal_ui/sepalwidgets/alert.py
index b3f3056d..9c70a732 100644
--- a/sepal_ui/sepalwidgets/alert.py
+++ b/sepal_ui/sepalwidgets/alert.py
@@ -229,6 +229,7 @@ class Alert(v.Alert, SepalWidget):
         """Empty the messages and hide it."""
         self.children = [""]
         self.hide()
+        self.type = set_type("info")
 
         return self
 
diff --git a/sepal_ui/sepalwidgets/app.py b/sepal_ui/sepalwidgets/app.py
index f9b09496..59f9f397 100644
--- a/sepal_ui/sepalwidgets/app.py
+++ b/sepal_ui/sepalwidgets/app.py
@@ -14,7 +14,7 @@ Example:
 from datetime import datetime
 from itertools import cycle
 from pathlib import Path
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Optional
 
 import ipyvuetify as v
 import pandas as pd
@@ -248,7 +248,7 @@ class AppBar(v.AppBar, SepalWidget):
     def __init__(
         self,
         title: str = "SEPAL module",
-        translator: Union[None, Translator] = None,
+        translator: Optional[Translator] = None,
         **kwargs,
     ) -> None:
         """Custom AppBar widget with the provided title using the sepal color framework.
@@ -316,7 +316,7 @@ class DrawerItem(v.ListItem, SepalWidget):
         icon: str = "",
         card: str = "",
         href: str = "",
-        model: Union[Model, None] = None,
+        model: Optional[Model] = None,
         bind_var: str = "",
         **kwargs,
     ) -> None:
diff --git a/sepal_ui/sepalwidgets/inputs.py b/sepal_ui/sepalwidgets/inputs.py
index 9b7ddc2d..51638b69 100644
--- a/sepal_ui/sepalwidgets/inputs.py
+++ b/sepal_ui/sepalwidgets/inputs.py
@@ -220,7 +220,7 @@ class FileInput(v.Flex, SepalWidget):
         extensions: List[str] = [],
         folder: Union[str, Path] = Path.home(),
         label: str = ms.widgets.fileinput.label,
-        v_model: Union[str, None] = "",
+        v_model: str = "",
         clearable: bool = False,
         root: Union[str, Path] = "",
         **kwargs,
diff --git a/sepal_ui/sepalwidgets/sepalwidget.py b/sepal_ui/sepalwidgets/sepalwidget.py
index 68ac405f..3d7d7273 100644
--- a/sepal_ui/sepalwidgets/sepalwidget.py
+++ b/sepal_ui/sepalwidgets/sepalwidget.py
@@ -12,13 +12,13 @@ Example:
 """
 
 import warnings
-from typing import List, Optional, Union
+from typing import List, Optional, Type, Union
 
 import ipyvuetify as v
 import traitlets as t
 from deprecated.sphinx import versionadded
 from traitlets import observe
-from typing_extensions import Self, Type
+from typing_extensions import Self
 
 __all__ = ["SepalWidget", "Tooltip"]
 
diff --git a/sepal_ui/sepalwidgets/tile.py b/sepal_ui/sepalwidgets/tile.py
index a70a4e52..97a6aeb9 100644
--- a/sepal_ui/sepalwidgets/tile.py
+++ b/sepal_ui/sepalwidgets/tile.py
@@ -41,8 +41,8 @@ class Tile(v.Layout, SepalWidget):
         id_: str,
         title: str,
         inputs: list = [""],
-        btn: Union[v.Btn, None] = None,
-        alert: Union[v.Alert, None] = None,
+        btn: Optional[v.Btn] = None,
+        alert: Optional[v.Alert] = None,
         **kwargs,
     ) -> None:
         """Custom Layout widget for the sepal UI framework.
diff --git a/sepal_ui/sepalwidgets/widget.py b/sepal_ui/sepalwidgets/widget.py
index affbd822..52fa1d2c 100644
--- a/sepal_ui/sepalwidgets/widget.py
+++ b/sepal_ui/sepalwidgets/widget.py
@@ -11,7 +11,7 @@ Example:
 """
 
 from pathlib import Path
-from typing import Optional, Union
+from typing import Optional
 
 import ipyvuetify as v
 import traitlets as t
@@ -124,7 +124,7 @@ class StateIcon(Tooltip):
 
     def __init__(
         self,
-        model: Union[None, Model] = None,
+        model: Optional[Model] = None,
         model_trait: str = "",
         states: dict = {},
         **kwargs,


Test patch:
diff --git a/tests/test_planetapi/test_PlanetModel.py b/tests/test_planetapi/test_PlanetModel.py
index c343f5e3..37da73da 100644
--- a/tests/test_planetapi/test_PlanetModel.py
+++ b/tests/test_planetapi/test_PlanetModel.py
@@ -1,12 +1,11 @@
 """Test the planet PlanetModel model."""
 
 import os
-from typing import Union
+from typing import Any, Union
 
 import planet
 import pytest
 from pytest import FixtureRequest
-from typing_extensions import Any
 
 from sepal_ui.planetapi import PlanetModel
 
diff --git a/tests/test_scripts/test_decorator.py b/tests/test_scripts/test_decorator.py
index 356a7b66..f400c4b8 100644
--- a/tests/test_scripts/test_decorator.py
+++ b/tests/test_scripts/test_decorator.py
@@ -22,22 +22,38 @@ def test_init_ee() -> None:
 
 def test_catch_errors() -> None:
     """Check the catch error decorator."""
+    # create an external alert to test the wiring
+    alert = sw.Alert()
+
     # create a fake object that uses the decorator
     class Obj:
         def __init__(self):
             self.alert = sw.Alert()
             self.btn = sw.Btn()
 
-            self.func1 = sd.catch_errors(alert=self.alert)(self.func)
-            self.func2 = sd.catch_errors(alert=self.alert, debug=True)(self.func)
+        @sd.catch_errors()
+        def func0(self, *args):
+            return 1 / 0
 
-        def func(self, *args):
+        @sd.catch_errors(alert=alert)
+        def func1(self, *args):
+            return 1 / 0
+
+        @sd.catch_errors(debug=True)
+        def func2(self, *args):
             return 1 / 0
 
     obj = Obj()
 
-    obj.func1()
+    # should return an alert error in the the self alert widget
+    obj.func0()
     assert obj.alert.type == "error"
+
+    # should return an alert in the external alert widget
+    obj.func1()
+    assert alert.type == "error"
+
+    # should raise an error
     with pytest.raises(Exception):
         obj.func2()
 
@@ -83,8 +99,8 @@ def test_loading_button() -> None:
     obj.alert.reset()
     with pytest.raises(Exception):
         obj.fun2(obj.btn, None, None)
-    assert obj.btn.disabled is False
-    assert obj.alert.type == "error"
+        assert obj.btn.disabled is False
+        assert obj.alert.type == "error"
 
     # should only display the sepal warning
     obj.alert.reset()
@@ -98,13 +114,13 @@ def test_loading_button() -> None:
     obj.alert.reset()
     with warnings.catch_warnings(record=True) as w_list:
         obj.func4(obj.btn, None, None)
-    assert obj.btn.disabled is False
-    assert obj.alert.type == "warning"
-    assert "sepal" in obj.alert.children[1].children[0]
-    assert "toto" not in obj.alert.children[1].children[0]
-    msg_list = [w.message.args[0] for w in w_list]
-    assert any("sepal" in s for s in msg_list)
-    assert any("toto" in s for s in msg_list)
+        assert obj.btn.disabled is False
+        assert obj.alert.type == "warning"
+        assert "sepal" in obj.alert.children[1].children[0]
+        assert "toto" not in obj.alert.children[1].children[0]
+        msg_list = [w.message.args[0] for w in w_list]
+        assert any("sepal" in s for s in msg_list)
+        assert any("toto" in s for s in msg_list)
 
     return
 
diff --git a/tests/test_sepalwidgets/test_Alert.py b/tests/test_sepalwidgets/test_Alert.py
index cac14ebb..171a152f 100644
--- a/tests/test_sepalwidgets/test_Alert.py
+++ b/tests/test_sepalwidgets/test_Alert.py
@@ -125,6 +125,7 @@ def test_reset() -> None:
     assert alert.viz is False
     assert len(alert.children) == 1
     assert alert.children[0] == ""
+    assert alert.type == "info"
 
     return