Repository: hanjinliu/magic-class (language: python)

Issue:
Explicitly use `show(run=True)` in magicclass applications.
Related to #3. In current implementation, magic classes automatically determine whether to start `QApplication`, but it seems that it should be specified by the user's side.

The `.py` files will look like:

```python
if __name__ == "__main__":
    ui = MyGui()
    ui.show(run=True)
```

- This change does not affect those apps using `magic-class` along with `napari`, as `napari` starts the event loop.
- All the examples should be updated to `show(run=True)`.
- `magic-class` should implement a bit more than `magicgui` to allow users calling `show()` in the IPython runtime.


Discussion / hints:

Fix patch:
diff --git a/examples/use_menubar.py b/examples/use_menubar.py
index 5936ddd2..21c2b9e9 100644
--- a/examples/use_menubar.py
+++ b/examples/use_menubar.py
@@ -1,4 +1,4 @@
-from magicclass import magicclass, magicmenu, set_options, abstractapi
+from magicclass import magicclass, magicmenu, set_options, abstractapi, field
 from magicgui.widgets import Image
 from pathlib import Path
 from skimage.io import imread, imsave
@@ -53,7 +53,7 @@ def Sobel_filter(self):
         out = sobel(self.image.value)
         self.image.value = out
 
-    image = Image()
+    image = field(Image)
 
 if __name__ == "__main__":
     ui = Main()
diff --git a/magicclass/_app.py b/magicclass/_app.py
index 4916bf56..d6b673e1 100644
--- a/magicclass/_app.py
+++ b/magicclass/_app.py
@@ -23,6 +23,13 @@ def gui_qt():
     return None
 
 
+def gui_qt_is_active() -> bool:
+    """True only if "%gui qt" magic is called in ipython kernel."""
+    shell = get_shell()
+
+    return shell and shell.active_eventloop == "qt"
+
+
 def get_app():
     """Get QApplication."""
     gui_qt()
@@ -36,4 +43,5 @@ def get_app():
 
 def run_app():
     """Start the event loop."""
-    return get_app().exec_()
+    if not gui_qt_is_active():
+        return get_app().exec_()
diff --git a/magicclass/_gui/_base.py b/magicclass/_gui/_base.py
index c265aa21..0890f0e2 100644
--- a/magicclass/_gui/_base.py
+++ b/magicclass/_gui/_base.py
@@ -765,7 +765,6 @@ def run_function():
                             title = Separator(
                                 orientation="horizontal", title=text, button=True
                             )
-                            # TODO: should remove mgui from self?
                             title.btn_clicked.connect(mgui.hide)
                             mgui.insert(0, title)
                     mgui._initialized_for_magicclass = True
@@ -1264,13 +1263,6 @@ def convert_attributes(
             _isfunc = callable(obj)
             if isinstance(obj, _MagicTemplateMeta):
                 new_attr = copy_class(obj, cls.__qualname__, name=name)
-            elif isinstance(obj, MagicField) and isinstance(
-                obj.constructor, _MagicTemplateMeta
-            ):
-                obj._constructor = copy_class(
-                    obj.constructor, cls.__qualname__, name=name
-                )
-                new_attr = obj
             elif name.startswith("_") or isinstance(obj, _pass) or not _isfunc:
                 # private method, non-action-like object, not-callable object are passed.
                 new_attr = obj
diff --git a/magicclass/_gui/_macro.py b/magicclass/_gui/_macro.py
index 2df9c7ca..35228051 100644
--- a/magicclass/_gui/_macro.py
+++ b/magicclass/_gui/_macro.py
@@ -277,7 +277,7 @@ def _execute(self, code: Expr):
         """Run macro."""
         parent = self._search_parent_magicclass()
         ns = {Symbol.var("ui"): parent}
-        with parent._error_mode.raise_with_handler(self):
+        with parent._error_mode.raise_with_handler(parent):
             if self._attribute_check:
                 strs = [f"- {exc}" for exc in check_attributes(code, ns)]
                 if strs:
diff --git a/magicclass/_gui/class_gui.py b/magicclass/_gui/class_gui.py
index daedccfc..e1a3f50f 100644
--- a/magicclass/_gui/class_gui.py
+++ b/magicclass/_gui/class_gui.py
@@ -335,14 +335,14 @@ def reset_choices(self, *_: Any):
                 w.reset_choices()
         return None
 
-    def show(self, run: bool = False) -> None:
+    def show(self, run: bool = True) -> None:
         """
         Show GUI. If any of the parent GUI is a dock widget in napari, then this
         will also show up as a dock widget (floating if in popup mode).
 
         Parameters
         ----------
-        run : bool, default False
+        run : bool, default True
             If true, application gets executed.
         """
         mcls_parent = self.__magicclass_parent__
diff --git a/magicclass/_gui/utils.py b/magicclass/_gui/utils.py
index dfbe1215..fc756d73 100644
--- a/magicclass/_gui/utils.py
+++ b/magicclass/_gui/utils.py
@@ -49,7 +49,7 @@ def copy_class(cls: _C, ns: str, name: str) -> _C:
     namespace = {}
     qualname = f"{ns}.{name}"
     for key, attr in cls.__dict__.items():
-        if key == ("__original_class__"):
+        if key == "__original_class__":
             continue
         if isinstance(attr, (FunctionType, abstractapi, thread_worker)):
             if attr.__qualname__.split("<locals>.")[-1].count(".") == 0:
diff --git a/magicclass/fields/_fields.py b/magicclass/fields/_fields.py
index e3f7d8cf..6d5db13a 100644
--- a/magicclass/fields/_fields.py
+++ b/magicclass/fields/_fields.py
@@ -354,8 +354,8 @@ def as_getter(self, obj: Any) -> Callable[[Any], Any]:
         """Make a function that get the value of Widget or Action."""
         return lambda w: self._guis[id(obj)].value
 
-    def as_remote_getter(self, obj: Any):
-        """Called when a MagicField is used in Bound method."""
+    def as_remote_getter(self, obj: MagicTemplate):
+        """Called when a MagicField is used in "bind"."""
         qualname = self._parent_class.__qualname__
         _LOCALS = "<locals>."
         if _LOCALS in qualname:
@@ -368,17 +368,21 @@ def _func(w):
             if objname not in clsnames:
                 ns = ".".join(clsnames)
                 raise ValueError(
-                    f"{obj.__class__.__name__!r} not in {clsnames!r}. "
+                    f"{objname!r} not in {clsnames!r}. "
                     f"Method {self.name!r} is in namespace {ns!r}, so it is invisible "
                     f"from magicclass {obj.__class__.__qualname__!r}."
                 )
             i = clsnames.index(objname) + 1
+            # class A:
+            #   class B:
+            #     f = field(...)
+            #   def func(self, a: Bound[B.f]): ...
             ins = obj
             for clsname in clsnames[i:]:
                 ins = getattr(ins, clsname, ins)
 
-            # Now, ins is an instance of parent class.
-            # Extract correct widget from MagicField
+            # Now, ins is an instance of parent class. Extract correct widget from
+            # MagicField
             _field_widget = self.get_widget(ins)
             if not hasattr(_field_widget, "value"):
                 raise TypeError(


Test patch:
diff --git a/magicclass/testing/_gui_test.py b/magicclass/testing/_gui_test.py
index 3bb1c51b..7ff3a2c0 100644
--- a/magicclass/testing/_gui_test.py
+++ b/magicclass/testing/_gui_test.py
@@ -67,7 +67,12 @@ def _qualname(obj: MethodType | type):
 
 
 def check_function_gui_buildable(ui: MagicTemplate, skips: list = []):
-    """Assert that all methods in ``ui`` can be built into GUI."""
+    """
+    Assert that all methods in ``ui`` can be built into GUI.
+
+    This function can also detect if any of the annotations of a method uses "bind" or
+    "choices" that results in an error.
+    """
 
     failed = []
     for method in _iter_method_with_button(ui):
diff --git a/tests/test_choices.py b/tests/test_choices.py
index ad24ae6b..da8f6b9a 100644
--- a/tests/test_choices.py
+++ b/tests/test_choices.py
@@ -1,3 +1,4 @@
+from typing_extensions import Annotated
 from magicclass import magicclass, set_options, field, vfield, get_function_gui
 from magicgui.widgets import Select
 import pytest
@@ -112,6 +113,21 @@ def _get_choices(self, w=None):
     assert ui[1].choices == (3,)
     assert ui[2].choices == (0, 1)
 
+def test_field_in_other_class():
+    @magicclass
+    class Sub:
+        def _get_choices(self, w=None):
+            return [0, 1]
+
+        def f(self, x: Annotated[int, {"choices": _get_choices}]):
+            pass
+
+    @magicclass
+    class A:
+        sub = field(Sub)
+
+    ui = A()
+
 def test_multi_gui():
     @magicclass
     class A:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index c220eec1..9f0f43c2 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -796,3 +796,29 @@ class A:
     ui = A()
     assert ui.x.max == 10
     assert ui["y"].widget_type == "FloatSlider"
+
+def test_using_same_class():
+    @magicclass(layout="horizontal", labels=False)
+    class Laser:
+        bar = vfield(0, widget_type="ProgressBar").with_options(min=0, max=100)
+        percent = vfield(0, widget_type="SpinBox").with_options(min=0, max=100)
+
+        def apply(self, value: Annotated[int, {"bind": percent}]):
+            """Apply laser power."""
+            self.bar = self.percent = value
+
+    @magicclass(labels=False)
+    class LaserControl:
+        laser_blue = field(Laser)
+        laser_green = field(Laser)
+        laser_red = field(Laser)
+
+    ui = LaserControl()
+
+    ui.laser_blue.percent = 15
+    ui.laser_blue["apply"].changed()
+    assert ui.laser_blue.bar == 15
+    assert ui.laser_green.bar == 0
+    assert ui.laser_red.bar == 0
+    ui.laser_green["apply"].changed()
+    ui.laser_red["apply"].changed()