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

Issue:
`FieldGroup.connect` is not working
Following code is not working.

```python
from magicclass import magicclass, FieldGroup, vfield

class G(FieldGroup):
    a = vfield(int)
    b = vfield(str)

@magicclass
class A:
    g = G()
    @g.connect
    def _f(self, x):
        print(x)

ui = A()
ui.show()
```


Discussion / hints:

Fix patch:
diff --git a/magicclass/__init__.py b/magicclass/__init__.py
index 24142692..3dc9ae60 100644
--- a/magicclass/__init__.py
+++ b/magicclass/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.6.11"
+__version__ = "0.6.12.dev0"
 
 from .core import (
     magicclass,
diff --git a/magicclass/fields/_define.py b/magicclass/fields/_define.py
new file mode 100644
index 00000000..a0acd212
--- /dev/null
+++ b/magicclass/fields/_define.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+from typing import Any, TYPE_CHECKING, Callable
+
+from ..utils import argcount
+
+if TYPE_CHECKING:
+    from .._gui._base import MagicTemplate
+
+
+def define_callback(self: Any, callback: Callable):
+    """Define a callback function from a method."""
+    return callback.__get__(self)
+
+
+def define_callback_gui(self: MagicTemplate, callback: Callable):
+    """Define a callback function from a method of a magic-class."""
+
+    *_, clsname, funcname = callback.__qualname__.split(".")
+    mro = self.__class__.__mro__
+    for base in mro:
+        if base.__name__ == clsname:
+            _func: Callable = getattr(base, funcname).__get__(self)
+            _func = _normalize_argcount(_func)
+
+            def _callback(v):
+                with self.macro.blocked():
+                    _func(v)
+                return None
+
+            break
+    else:
+
+        def _callback(v):
+            # search for parent instances that have the same name.
+            current_self = self
+            while not (
+                hasattr(current_self, funcname)
+                and current_self.__class__.__qualname__.split(".")[-1] == clsname
+            ):
+                current_self = current_self.__magicclass_parent__
+            _func = _normalize_argcount(getattr(current_self, funcname))
+
+            with self.macro.blocked():
+                _func(v)
+            return None
+
+    return _callback
+
+
+def _normalize_argcount(func: Callable) -> Callable[[Any], Any]:
+    if argcount(func) == 0:
+        return lambda v: func()
+    return func
diff --git a/magicclass/fields/_fields.py b/magicclass/fields/_fields.py
index 99334f37..d065e982 100644
--- a/magicclass/fields/_fields.py
+++ b/magicclass/fields/_fields.py
@@ -17,8 +17,9 @@
 from magicgui.widgets._bases import Widget, ValueWidget, ContainerWidget
 from magicgui.widgets._bases.value_widget import UNSET
 
+from ._define import define_callback, define_callback_gui
+
 from ..utils import (
-    argcount,
     is_instance_method,
     method_as_getter,
     eval_attribute,
@@ -191,9 +192,9 @@ def get_widget(self, obj: Any) -> _W:
 
             if isinstance(widget, (ValueWidget, ContainerWidget)):
                 if isinstance(obj, MagicTemplate):
-                    _def = _define_callback_gui
+                    _def = define_callback_gui
                 else:
-                    _def = _define_callback
+                    _def = define_callback
                 for callback in self._callbacks:
                     # funcname = callback.__name__
                     widget.changed.connect(_def(obj, callback))
@@ -232,9 +233,9 @@ def get_action(self, obj: Any) -> AbstractAction:
 
             if action.support_value:
                 if isinstance(obj, MagicTemplate):
-                    _def = _define_callback_gui
+                    _def = define_callback_gui
                 else:
-                    _def = _define_callback
+                    _def = define_callback
                 for callback in self._callbacks:
                     # funcname = callback.__name__
                     action.changed.connect(_def(obj, callback))
@@ -852,49 +853,3 @@ def _is_subclass(obj: Any, class_or_tuple):
         return issubclass(obj, class_or_tuple)
     except Exception:
         return False
-
-
-def _define_callback(self: Any, callback: Callable):
-    """Define a callback function from a method."""
-    return callback.__get__(self)
-
-
-def _define_callback_gui(self: MagicTemplate, callback: Callable):
-    """Define a callback function from a method of a magic-class."""
-
-    *_, clsname, funcname = callback.__qualname__.split(".")
-    mro = self.__class__.__mro__
-    for base in mro:
-        if base.__name__ == clsname:
-            _func: Callable = getattr(base, funcname).__get__(self)
-            _func = _normalize_argcount(_func)
-
-            def _callback(v):
-                with self.macro.blocked():
-                    _func(v)
-                return None
-
-            break
-    else:
-
-        def _callback(v):
-            # search for parent instances that have the same name.
-            current_self = self
-            while not (
-                hasattr(current_self, funcname)
-                and current_self.__class__.__qualname__.split(".")[-1] == clsname
-            ):
-                current_self = current_self.__magicclass_parent__
-            _func = _normalize_argcount(getattr(current_self, funcname))
-
-            with self.macro.blocked():
-                _func(v)
-            return None
-
-    return _callback
-
-
-def _normalize_argcount(func: Callable) -> Callable[[Any], Any]:
-    if argcount(func) == 0:
-        return lambda v: func()
-    return func
diff --git a/magicclass/fields/_group.py b/magicclass/fields/_group.py
index 8defc1a0..5dd5ba6c 100644
--- a/magicclass/fields/_group.py
+++ b/magicclass/fields/_group.py
@@ -17,6 +17,7 @@
 from psygnal import SignalInstance
 
 from ._fields import MagicField, MagicValueField, _FieldObject
+from ._define import define_callback, define_callback_gui
 from ..utils import Tooltips
 
 if TYPE_CHECKING:
@@ -313,8 +314,16 @@ def get_widget(self, obj) -> Container:
         _id = id(obj)
         wdt = self._containers.get(_id, None)
         if wdt is None:
+            from .._gui import MagicTemplate
+
             wdt = self.copy()
             self._containers[_id] = wdt
+            if isinstance(obj, MagicTemplate):
+                _def = define_callback_gui
+            else:
+                _def = define_callback
+            for callback in self._callbacks:
+                wdt.changed.connect(_def(obj, callback))
         return wdt
 
     def connect(self, callback: Callable):


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 467f7436..d23c7302 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -395,6 +395,36 @@ class Main:
     ui = Main()
     assert ui.params is ui[0]
 
+def test_field_group_signal():
+    mock1 = MagicMock()
+    mock2 = MagicMock()
+
+    class Params(FieldGroup):
+        x = field(int)
+        y = vfield(str)
+
+    class A:
+        params1 = Params(layout="horizontal")
+        params2 = Params(layout="horizontal")
+
+        @params1.connect
+        def _f1(self, w):
+            mock1()
+
+        @params2.connect
+        def _f2(self, w):
+            mock2()
+
+    ui = A()
+    mock1.assert_not_called()
+    mock2.assert_not_called()
+    ui.params1.x.value = 2
+    mock1.assert_called_once()
+    mock2.assert_not_called()
+    ui.params2.y = "x"
+    mock1.assert_called_once()
+    mock2.assert_called_once()
+
 
 def test_nesting_field_group():
     class Params(FieldGroup):