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

Issue:
Deprecate `wraps` and use `location=` argument for this purpose
Current architecture of connecting children to parent is not safe.
Use the similar way as `MagicField.connect`.


Discussion / hints:

Fix patch:
diff --git a/magicclass/__init__.py b/magicclass/__init__.py
index ae850598..db08194d 100644
--- a/magicclass/__init__.py
+++ b/magicclass/__init__.py
@@ -35,7 +35,6 @@
     magicproperty,
     FieldGroup,
     HasFields,
-    dataclass_gui,
 )
 from ._gui._base import defaults, MagicTemplate, PopUpMode
 from ._gui.keybinding import Key
diff --git a/magicclass/_gui/_base.py b/magicclass/_gui/_base.py
index 727b19f6..7bf5d6d9 100644
--- a/magicclass/_gui/_base.py
+++ b/magicclass/_gui/_base.py
@@ -44,13 +44,19 @@
 from .keybinding import as_shortcut
 from .mgui_ext import (
     AbstractAction,
-    Action,
     FunctionGuiPlus,
-    PushButtonPlus,
+    Action,
+    Clickable,
+    is_clickable,
     _LabeledWidgetAction,
     mguiLike,
 )
-from .utils import copy_class, callable_to_classes, show_dialog_from_mgui
+from .utils import (
+    copy_class,
+    callable_to_classes,
+    show_dialog_from_mgui,
+    connect_magicclasses,
+)
 from ._macro import GuiMacro, DummyMacro
 from ._macro_utils import inject_recorder, inject_silencer, value_widget_callback
 from ._icon import get_icon
@@ -66,7 +72,7 @@
     eval_attribute,
 )
 from magicclass.widgets import Separator, FreeWidget
-from magicclass.fields import MagicField, MagicValueField, field, vfield
+from magicclass.fields import MagicField, field, vfield
 from magicclass.signature import (
     ConfirmDict,
     MagicMethodSignature,
@@ -186,18 +192,23 @@ def __get__(self, obj, objtype=None):
         return self
 
 
-_W = TypeVar("_W", bound=Widget)
 _Comp = TypeVar("_Comp", Widget, AbstractAction)
 
 
+def _typeof(current_self) -> type:
+    """Compatible with the copy_class function."""
+    tp = type(current_self)
+    return getattr(tp, "__original_class__", tp)
+
+
 class MagicTemplate(MutableSequence[_Comp], metaclass=_MagicTemplateMeta):
     __doc__ = ""
     __magicclass_parent__: None | MagicTemplate
     __magicclass_children__: list[MagicTemplate]
     _close_on_run: bool
-    _component_class: type[Action | PushButtonPlus]
+    _component_class: type[Clickable]
     _error_mode: ErrorMode
-    _list: list[Action | Widget]
+    _list: list[AbstractAction | Widget]
     _macro_instance: GuiMacro
     _my_symbol: Symbol
     _popup_mode: PopUpMode
@@ -348,7 +359,7 @@ class while it needs access to its parent.
             )
 
         current_self = self
-        while type(current_self) is not ancestor:
+        while _typeof(current_self) is not ancestor:
             current_self = current_self.__magicclass_parent__
             if current_self is None:
                 raise RuntimeError(
@@ -427,6 +438,15 @@ def wrapper(method: _F):
                 wraps(template)(func)
 
             predefined = getattr(cls, func.__name__, None)
+            into_cls = cls
+            while (
+                isinstance(predefined, abstractapi)
+                and (loc := predefined.get_location()) is not None
+            ):
+                predefined.resolve()
+                predefined = getattr(loc, func.__name__, None)
+                into_cls = loc
+
             if predefined is not None:
                 # Update signature to the parent one. This step is necessary when widget design
                 # is defined on the parent side. Parameters should be replaced with a simplest
@@ -453,10 +473,10 @@ def wrapper(method: _F):
 
             if copy:
                 copyto_list = get_additional_option(func, "copyto", [])
-                copyto_list.append(cls.__name__)
+                copyto_list.append(into_cls.__name__)
                 upgrade_signature(func, additional_options={"copyto": copyto_list})
             else:
-                upgrade_signature(func, additional_options={"into": cls.__name__})
+                upgrade_signature(func, additional_options={"into": into_cls.__name__})
             return method
 
         return wrapper if method is None else wrapper(method)
@@ -464,7 +484,7 @@ def wrapper(method: _F):
     def _unwrap_method(
         self,
         method_name: str,
-        widget: FunctionGui | PushButtonPlus | AbstractAction,
+        widget: FunctionGui | Clickable,
         moveto: str,
         copyto: list[str],
     ):
@@ -495,14 +515,14 @@ def _unwrap_method(
 
         _found = 0
         _n_match = len(matcher)
-        for child_instance in self._iter_child_magicclasses():
-            _name = child_instance.__class__.__name__
+        for _, child_ins in self._iter_child_magicclasses():
+            _name = child_ins.__class__.__name__
             if _name in matcher:
-                n_children = len(child_instance)
+                n_children = len(child_ins)
 
                 # get the position of predefined child widget
                 try:
-                    index = _get_index(child_instance, method_name)
+                    index = _get_index(child_ins, method_name)
                     new = False
                 except ValueError:
                     index = n_children
@@ -512,27 +532,27 @@ def _unwrap_method(
                 self._fast_insert(len(self), widget, remove_label=True)
                 copy = _name in copyto
 
-                if not isinstance(widget, (PushButtonPlus, AbstractAction)):
+                if not is_clickable(widget):
                     if copy:
                         widget = widget.copy()
                     if new:
-                        child_instance._fast_insert(n_children, widget)
+                        child_ins._fast_insert(n_children, widget)
                     else:
-                        del child_instance[index]
-                        child_instance._fast_insert(index, widget)
+                        del child_ins[index]
+                        child_ins._fast_insert(index, widget)
 
                 else:
                     # NOTE: wrapping button with action is not supported in the
                     # method above.
                     widget.visible = copy
                     if new:
-                        child_widget = child_instance._create_widget_from_method(
+                        child_widget = child_ins._create_widget_from_method(
                             _empty_func(method_name)
                         )
                         child_widget.text = widget.text
-                        child_instance._fast_insert(n_children, child_widget)
+                        child_ins._fast_insert(n_children, child_widget)
                     else:
-                        child_widget = child_instance[index]
+                        child_widget = child_ins[index]
 
                     child_widget.changed.disconnect()
                     child_widget.changed.connect(widget.changed)
@@ -546,39 +566,10 @@ def _unwrap_method(
 
         else:
             raise RuntimeError(
-                f"{method_name} not found in class {self.__class__.__name__}"
+                f"{method_name} not found in any of the nested classes in class "
+                f"{self.__class__.__name__}"
             )
 
-    # fmt: off
-    @overload
-    @classmethod
-    def field(cls, gui_class: type[_M], *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicField[_M, Any]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, type_of_widget: type[_W], *, name: str | None = None, label: str | None = None, options: dict[str, Any] = {}, record: bool = True) -> MagicField[_W, Any]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, obj: type[_X], *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicField[ValueWidget, _X]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, obj: _X, *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True) -> MagicField[ValueWidget, _X]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, obj: Any | None, *, name: str | None = None, label: str | None = None, widget_type: type[_W] = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicField[_W, Any]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, obj: Any, *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicField[Widget, Any]: ...  # noqa
-
-    @overload
-    @classmethod
-    def field(cls, *, name: str | None = None, label: str | None = None, widget_type: str | type[Widget] | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicField[Widget, Any]: ...  # noqa
-    # fmt: on
-
     @classmethod
     def field(
         cls,
@@ -589,72 +580,21 @@ def field(
         widget_type=None,
         options={},
         record=True,
-    ) -> MagicField:
-        """
-        Make a MagicField object, with the widget in the child class.
-
-        >>> @magicclass
-        ... class A:
-        ...     @magicclass
-        ...     class B:
-        ...         i = ...  # pre-definition
-        ...     i = B.field(1)
-
-        Parameters
-        ----------
-        obj : Any, default is Undefined
-            Reference to determine what type of widget will be created. If Widget
-            subclass is given, it will be used as is. If other type of class is given,
-            it will used as type annotation. If an object (not type) is given, it will
-            be assumed to be the default value.
-        name : str, optional
-            Name of the widget.
-        label : str, optional
-            Label of the widget.
-        widget_type : str, optional
-            Widget type. This argument will be sent to ``create_widget`` function.
-        options : dict, optional
-            Widget options. This parameter will be passed to the ``options`` keyword
-            argument of ``create_widget``.
-        record : bool, default is True
-            A magic-class specific parameter. If true, record value changes as macro.
-
-        Returns
-        -------
-        MagicField
-        """
-        fld = field(
+    ):
+        warnings.warn(
+            "Method 'field' is deprecated. Use field(..., location=...) instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return field(
             obj,
             name=name,
             label=label,
             widget_type=widget_type,
             options=options,
             record=record,
+            location=cls,
         )
-        fld.set_destination(cls)
-        return fld
-
-    # fmt: off
-    @overload
-    @classmethod
-    def vfield(cls, widget_type: type[ValueWidget[_V]], *, name: str | None = None, label: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicValueField[_V]: ...  # noqa
-
-    @overload
-    @classmethod
-    def vfield(cls, annotation: type[_X], *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicValueField[_X]: ...  # noqa
-
-    @overload
-    @classmethod
-    def vfield(cls, obj: _X, *, name: str | None = None, label: str | None = None, widget_type: str | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicValueField[_X]: ...  # noqa
-
-    @overload
-    @classmethod
-    def vfield(cls, obj: Any, *, name: str | None = None, label: str | None = None, widget_type: type[_W] = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicValueField[Any]: ...  # noqa
-
-    @overload
-    @classmethod
-    def vfield(cls, obj: Any, *, name: str | None = None, label: str | None = None, widget_type: str | type[Widget] | None = None, options: dict[str, Any] = {}, record: bool = True, ) -> MagicValueField[Any]: ...  # noqa
-    # fmt: on
 
     @classmethod
     def vfield(
@@ -666,17 +606,21 @@ def vfield(
         widget_type=None,
         options={},
         record=True,
-    ) -> MagicValueField:
-        fld = vfield(
+    ):
+        warnings.warn(
+            "Method 'field' is deprecated. Use vfield(..., location=...) instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return vfield(
             obj,
             name=name,
             label=label,
             widget_type=widget_type,
             options=options,
             record=record,
+            location=cls,
         )
-        fld.set_destination(cls)
-        return fld
 
     @contextmanager
     def config_context(
@@ -728,7 +672,44 @@ def _convert_attributes_into_widgets(self):
         """
         raise NotImplementedError()
 
-    def _create_widget_from_field(self, name: str, fld: MagicField):
+    def _convert_an_attribute_into_widget(
+        self, name: str, attr: Any, tooltips: Tooltips
+    ) -> Widget:
+        if isinstance(attr, type):
+            # Nested magic-class
+            widget = attr()
+            object.__setattr__(self, name, widget)
+            if isinstance(widget, BaseGui):
+                connect_magicclasses(self, widget, name)
+
+        elif isinstance(attr, MagicField):
+            # If MagicField is given by field() function.
+            widget = self._create_widget_from_field(name, attr)
+            if isinstance(widget, BaseGui):
+                connect_magicclasses(self, widget, name)
+            if isinstance(attr, BoxMagicField):
+                for wdt in widget:
+                    if isinstance(wdt, BaseGui):
+                        connect_magicclasses(self, wdt, name)
+            if not widget.tooltip:
+                widget.tooltip = tooltips.attributes.get(name, "")
+
+        elif isinstance(attr, FunctionGui):
+            widget = attr.copy()
+            first_widget = widget[0]
+            if not isinstance(first_widget, ValueWidget):
+                raise TypeError(
+                    f"The first widget of FunctionGui {attr!r} must be ValueWidget."
+                )
+            first_widget.bind(self)  # bind self to the first argument
+
+        else:
+            # convert class method into instance method
+            widget = getattr(self, name, None)
+
+        return widget
+
+    def _create_widget_from_field(self, name: str, fld: MagicField) -> Widget:
         """
         This function is called when magic-class encountered a MagicField in its definition.
 
@@ -765,12 +746,10 @@ def _create_widget_from_method(self, obj: MethodType):
         # Get the number of parameters except for empty widgets.
         # With these lines, "bind" method of magicgui works inside magicclass.
         fgui_info = callable_to_classes(func)
-        n_empty = len(
-            [
-                0
-                for _wdg_cls, _prm in fgui_info
-                if _wdg_cls is EmptyWidget or _prm.options.get("bind", None) is not None
-            ]
+        n_empty = sum(
+            1
+            for _wdg_cls, _prm in fgui_info
+            if _wdg_cls is EmptyWidget or _prm.options.get("bind", None) is not None
         )
         nparams = argcount(func) - n_empty
         if len(fgui_info) == 0:
@@ -851,7 +830,8 @@ def run_function():
         if keybinding is not None:
             shortcut = as_shortcut(keybinding)
             widget.set_shortcut(shortcut)
-
+        if isinstance(obj, abstractapi) and obj.get_location() is not None:
+            widget.visible = False
         return widget
 
     def _search_parent_magicclass(self) -> MagicTemplate:
@@ -863,11 +843,17 @@ def _search_parent_magicclass(self) -> MagicTemplate:
             current_self = parent
         return current_self
 
-    def _iter_child_magicclasses(self) -> Iterable[MagicTemplate]:
+    def _iter_child_magicclasses(
+        self,
+        level: int = 0,
+        max_level: int = 999999,
+    ) -> Iterable[tuple[int, MagicTemplate]]:
         """Iterate over all the child magic classes"""
+        if level >= max_level:
+            return
         for child in self.__magicclass_children__:
-            yield child
-            yield from child._iter_child_magicclasses()
+            yield level + 1, child
+            yield from child._iter_child_magicclasses(level=level + 1)
 
     def _call_with_return_callback(self, fname: str, *args, **kwargs) -> None:
         from magicclass.core import get_function_gui
@@ -1160,7 +1146,7 @@ class MagicGuiBuildError(RuntimeError):
     """Error raised when magicgui cannot build a gui."""
 
 
-def _build_mgui(widget_: Action | PushButtonPlus, func: Callable, parent: BaseGui):
+def _build_mgui(widget_: Clickable, func: Callable, parent: BaseGui):
     """Build a magicgui from a function for the give button/action."""
     if widget_.mgui is not None:
         return widget_.mgui
@@ -1243,11 +1229,11 @@ def _get_index(container: Container, widget_or_name: Widget | str) -> int:
 def _child_that_has_widget(
     self: BaseGui, method: Callable, widget_or_name: Widget | str
 ) -> BaseGui:
-    child_clsname = get_additional_option(method, "into")
-    if child_clsname is None:
+    into: str | None = get_additional_option(method, "into")
+    if into is None:
         return self
-    for child_instance in self._iter_child_magicclasses():
-        if child_instance.__class__.__name__ == child_clsname:
+    for _, child_instance in self._iter_child_magicclasses():
+        if child_instance.__class__.__name__ == into:
             break
     else:
         raise ValueError(f"{widget_or_name} not found.")
@@ -1322,6 +1308,13 @@ 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
@@ -1346,7 +1339,7 @@ def macro(self: BaseGui):
     return _dummy_macro
 
 
-def _define_popup(self: BaseGui, obj, widget: PushButtonPlus | Action):
+def _define_popup(self: BaseGui, obj, widget: Clickable):
     # deal with popup mode.
     popup_mode = self._popup_mode
     if popup_mode == PopUpMode.popup:
diff --git a/magicclass/_gui/class_gui.py b/magicclass/_gui/class_gui.py
index ac6bd3b7..0774e7ae 100644
--- a/magicclass/_gui/class_gui.py
+++ b/magicclass/_gui/class_gui.py
@@ -89,18 +89,6 @@ def _create_widget_from_field(self, name: str, fld: MagicField):
         fld.name = fld.name or name
         widget = fld.get_widget(self)
 
-        if isinstance(widget, BaseGui):
-            widget.__magicclass_parent__ = self
-            self.__magicclass_children__.append(widget)
-            widget._my_symbol = Symbol(name)
-
-        elif isinstance(fld, BoxMagicField):
-            for wdt in widget:
-                if isinstance(wdt, BaseGui):
-                    wdt.__magicclass_parent__ = self
-                    self.__magicclass_children__.append(wdt)
-                    wdt._my_symbol = Symbol(name)
-
         if isinstance(widget, (ValueWidget, ContainerWidget)):
             # If the field has callbacks, connect it to the newly generated widget.
             if fld.record:
@@ -147,27 +135,7 @@ def _convert_attributes_into_widgets(self):
                 continue
 
             try:
-                if isinstance(attr, type):
-                    # Nested magic-class
-                    widget = attr()
-                    object.__setattr__(self, name, widget)
-
-                elif isinstance(attr, MagicField):
-                    # If MagicField is given by field() function.
-                    widget = self._create_widget_from_field(name, attr)
-                    if not widget.tooltip:
-                        widget.tooltip = _tooltips.attributes.get(name, "")
-
-                elif isinstance(attr, FunctionGui):
-                    widget = attr.copy()
-                    widget[0].bind(self)  # bind self to the first argument
-
-                else:
-                    # convert class method into instance method
-                    widget = getattr(self, name, None)
-
-                if isinstance(widget, BaseGui):
-                    connect_magicclasses(self, widget, name)
+                widget = self._convert_an_attribute_into_widget(name, attr, _tooltips)
 
                 if isinstance(widget, MenuGui):
                     # Add menubar to container
diff --git a/magicclass/_gui/menu_gui.py b/magicclass/_gui/menu_gui.py
index 1a45a8b5..26beea4c 100644
--- a/magicclass/_gui/menu_gui.py
+++ b/magicclass/_gui/menu_gui.py
@@ -89,27 +89,9 @@ def _convert_attributes_into_widgets(self):
                 continue
 
             try:
-                if isinstance(attr, type):
-                    # Nested magic-menu
-                    widget = attr()
-                    object.__setattr__(self, name, widget)
-
-                elif isinstance(attr, MagicField):
-                    widget = self._create_widget_from_field(name, attr)
-                    if not widget.tooltip:
-                        widget.tooltip = _tooltips.attributes.get(name, "")
-
-                else:
-                    # convert class method into instance method
-                    widget = getattr(self, name, None)
-
-                if isinstance(widget, FunctionGui):
-                    widget = attr.copy()
-                    widget[0].bind(self)  # bind self to the first argument
-
-                elif isinstance(widget, BaseGui):
-                    connect_magicclasses(self, widget, name)
+                widget = self._convert_an_attribute_into_widget(name, attr, _tooltips)
 
+                if isinstance(widget, BaseGui):
                     if isinstance(widget, MenuGuiBase):
                         widget.native.setParent(
                             self.native, widget.native.windowFlags()
diff --git a/magicclass/_gui/toolbar.py b/magicclass/_gui/toolbar.py
index 1597de0c..daf052a4 100644
--- a/magicclass/_gui/toolbar.py
+++ b/magicclass/_gui/toolbar.py
@@ -127,27 +127,9 @@ def _convert_attributes_into_widgets(self):
                 continue
 
             try:
-                if isinstance(attr, type):
-                    # Nested magic-menu
-                    widget = attr()
-                    object.__setattr__(self, name, widget)
-
-                elif isinstance(attr, MagicField):
-                    widget = self._create_widget_from_field(name, attr)
-                    if not widget.tooltip:
-                        widget.tooltip = _tooltips.attributes.get(name, "")
-
-                else:
-                    # convert class method into instance method
-                    widget = getattr(self, name, None)
-
-                if isinstance(widget, FunctionGui):
-                    widget = attr.copy()
-                    widget[0].bind(self)  # bind self to the first argument
-
-                elif isinstance(widget, BaseGui):
-                    connect_magicclasses(self, widget, name)
+                widget = self._convert_an_attribute_into_widget(name, attr, _tooltips)
 
+                if isinstance(widget, BaseGui):
                     if isinstance(widget, MenuGui):
                         tb = ToolButtonPlus(widget.name)
                         tb.set_menu(widget.native)
diff --git a/magicclass/_gui/utils.py b/magicclass/_gui/utils.py
index 3b57bcfc..f3c0cd06 100644
--- a/magicclass/_gui/utils.py
+++ b/magicclass/_gui/utils.py
@@ -43,20 +43,25 @@ def copy_class(cls: _C, ns: str, name: str) -> _C:
     type
         Copied class object.
     """
+    from magicclass.wrappers import abstractapi
+    from magicclass.utils.qthreading import thread_worker
+
     namespace = {}
     qualname = f"{ns}.{name}"
     for key, attr in cls.__dict__.items():
-        if isinstance(attr, FunctionType):
+        if key == ("__original_class__"):
+            continue
+        if isinstance(attr, (FunctionType, abstractapi, thread_worker)):
             if attr.__qualname__.split("<locals>.")[-1].count(".") == 0:
                 pass
             else:
                 attr.__qualname__ = f"{qualname}.{key}"
         elif isinstance(attr, type):
-            attr = copy_class(attr, qualname, attr.__name__)
+            attr = copy_class(attr, qualname, attr.__qualname__.split(".")[-1])
         namespace[key] = attr
-
     out = type(cls.__name__, cls.__bases__, namespace)
     out.__qualname__ = qualname
+    out.__original_class__ = getattr(cls, "__original_class__", cls)
     return out
 
 
diff --git a/magicclass/core.py b/magicclass/core.py
index a1d1e4e7..bd47716b 100644
--- a/magicclass/core.py
+++ b/magicclass/core.py
@@ -530,7 +530,7 @@ def get_button(ui: MagicTemplate | MethodType, name: str | None = None) -> Click
 
         opt = get_additional_option(getattr(ui, name), "into", None)
         if opt is not None:
-            for child_instance in ui._iter_child_magicclasses():
+            for _, child_instance in ui._iter_child_magicclasses():
                 _name = child_instance.__class__.__name__
                 if _name == opt:
                     widget = child_instance[name]
diff --git a/magicclass/ext/pyqtgraph/graph_items.py b/magicclass/ext/pyqtgraph/graph_items.py
index 016dd97a..abeab83f 100644
--- a/magicclass/ext/pyqtgraph/graph_items.py
+++ b/magicclass/ext/pyqtgraph/graph_items.py
@@ -28,6 +28,7 @@
 
 class LayerItem:
     native: pg.GraphicsItem
+    name: str
 
     def __repr__(self) -> str:
         return f"{self.__class__.__name__} '{self.name}'"
diff --git a/magicclass/fields/_define.py b/magicclass/fields/_define.py
index 963fb14f..4ca3005c 100644
--- a/magicclass/fields/_define.py
+++ b/magicclass/fields/_define.py
@@ -2,7 +2,7 @@
 from typing import Any, TYPE_CHECKING, Callable
 import inspect
 
-from magicclass.utils import argcount, thread_worker
+from magicclass.utils import argcount
 
 if TYPE_CHECKING:
     from magicclass._gui._base import MagicTemplate
diff --git a/magicclass/fields/_fields.py b/magicclass/fields/_fields.py
index 4a6f805b..d0a234b6 100644
--- a/magicclass/fields/_fields.py
+++ b/magicclass/fields/_fields.py
@@ -32,6 +32,7 @@
     eval_attribute,
     is_type_like,
     argcount,
+    get_level,
 )
 from magicclass.signature import (
     MagicMethodSignature,
@@ -214,9 +215,8 @@ def with_choices(self, choices):
 
     def with_widget_type(self, widget_type: type[Widget]):
         """Method to add widget type to the field."""
-        new = self.copy()
-        new._widget_type = widget_type
-        return new
+        self._widget_type = widget_type
+        return self
 
     @property
     def __signature__(self):
@@ -365,13 +365,15 @@ def as_remote_getter(self, obj: Any):
 
         def _func(w):
             # First we have to know where (which instance) MagicField came from.
-            if obj.__class__.__name__ not in clsnames:
+            objname = obj.__class__.__qualname__.split(".")[-1]
+            if objname not in clsnames:
                 ns = ".".join(clsnames)
                 raise ValueError(
+                    f"{obj.__class__.__name__!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(type(obj).__name__) + 1
+            i = clsnames.index(objname) + 1
             ins = obj
             for clsname in clsnames[i:]:
                 ins = getattr(ins, clsname, ins)
@@ -550,6 +552,7 @@ def _wrapper(fn):
             @wraps(fn)
             def _func(self: MagicTemplate, *args, **kwargs):
                 nonlocal _running, _last_run, _last_run_id, _abort_begin
+
                 with threading.Lock():
                     _this_id = default_timer()
                     f = _afunc.__get__(self)
@@ -606,8 +609,7 @@ def _func(self: MagicTemplate, *args, **kwargs):
                 force_async=True,
                 ignore_errors=ignore_errors,
             )
-            self.connect(_afunc)
-            return _afunc.replace(force_async=False)
+            return self.connect(_afunc)
 
         return _wrapper if func is None else _wrapper(func)
 
@@ -811,6 +813,7 @@ def field(
     widget_type: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[_M]:
     ...
 
@@ -823,6 +826,7 @@ def field(
     label: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[_W]:
     ...
 
@@ -836,6 +840,7 @@ def field(
     widget_type: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[ValueWidget[_X]]:
     ...
 
@@ -849,6 +854,7 @@ def field(
     widget_type: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[ValueWidget[_X]]:
     ...
 
@@ -862,6 +868,7 @@ def field(
     widget_type: type[_W] = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[_W]:
     ...
 
@@ -875,6 +882,7 @@ def field(
     widget_type: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[Widget]:
     ...
 
@@ -887,6 +895,7 @@ def field(
     widget_type: str | type[Widget] | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicField[Widget]:
     ...
 
@@ -899,6 +908,7 @@ def field(
     widget_type=None,
     options={},
     record=True,
+    location=None,
 ):
     """
     Make a MagicField object.
@@ -924,12 +934,17 @@ def field(
         argument of ``create_widget``.
     record : bool, default is True
         A magic-class specific parameter. If true, record value changes as macro.
+    location : magic-class, optional
+        Location to put the widget.
 
     Returns
     -------
     MagicField
     """
-    return _get_field(obj, name, label, widget_type, options, record, MagicField)
+    fld = _get_field(obj, name, label, widget_type, options, record, MagicField)
+    if location is not None:
+        fld.set_destination(location)
+    return fld
 
 
 @overload
@@ -940,6 +955,7 @@ def vfield(
     label: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[_V]:
     ...
 
@@ -952,6 +968,7 @@ def vfield(
     label: str | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[Any]:
     ...
 
@@ -965,6 +982,7 @@ def vfield(
     widget_type: str | type[Widget] | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[_X]:
     ...
 
@@ -978,6 +996,7 @@ def vfield(
     widget_type: str | type[Widget] | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[_X]:
     ...
 
@@ -991,6 +1010,7 @@ def vfield(
     widget_type: type[ValueWidget[_V]] = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[_V]:
     ...
 
@@ -1004,6 +1024,7 @@ def vfield(
     widget_type: str | type[Widget] | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[Any]:
     ...
 
@@ -1016,18 +1037,20 @@ def vfield(
     widget_type: str | type[Widget] | None = None,
     options: dict[str, Any] = {},
     record: bool = True,
+    location: type[MagicTemplate] | MagicField | None = None,
 ) -> MagicValueField[Any]:
     ...
 
 
 def vfield(
-    obj: Any = Undefined,
+    obj=Undefined,
     *,
-    name: str | None = None,
-    label: str | None = None,
-    widget_type: str | None = None,
-    options: dict[str, Any] = {},
-    record: bool = True,
+    name=None,
+    label=None,
+    widget_type=None,
+    options={},
+    record=True,
+    location=None,
 ) -> MagicValueField[Widget, Any]:
     """
     Make a MagicValueField object.
@@ -1063,7 +1086,10 @@ def vfield(
     -------
     MagicValueField
     """
-    return _get_field(obj, name, label, widget_type, options, record, MagicValueField)
+    fld = _get_field(obj, name, label, widget_type, options, record, MagicValueField)
+    if location is not None:
+        fld.set_destination(location)
+    return fld
 
 
 def widget_property(func: Callable[..., _W]) -> MagicValueField[_W, Any]:
diff --git a/magicclass/utils/__init__.py b/magicclass/utils/__init__.py
index cc08486c..ab972f46 100644
--- a/magicclass/utils/__init__.py
+++ b/magicclass/utils/__init__.py
@@ -3,6 +3,7 @@
     Tooltips,
     get_signature,
     argcount,
+    get_level,
     is_instance_method,
     method_as_getter,
     eval_attribute,
diff --git a/magicclass/utils/_functions.py b/magicclass/utils/_functions.py
index e7df81fc..062808b4 100644
--- a/magicclass/utils/_functions.py
+++ b/magicclass/utils/_functions.py
@@ -126,6 +126,13 @@ def argcount(func: Callable) -> int:
     return nargs
 
 
+def get_level(f: Callable | type) -> int:
+    level = f.__qualname__.split(_LOCALS)[-1].count(".")
+    if callable(f):
+        return level - 1
+    return level
+
+
 _LOCALS = "<locals>."
 
 
@@ -139,16 +146,16 @@ def method_as_getter(self: BaseGui, getter: Callable):
     if _LOCALS in qualname:
         qualname = qualname.split(_LOCALS)[-1]
     *clsnames, funcname = qualname.split(".")
-    ins = self
-    self_cls = ins.__class__.__qualname__.split(".")[-1]
+    self_cls = self.__class__.__qualname__.split(".")[-1]
     if self_cls not in clsnames:
         ns = ".".join(clsnames)
         raise ValueError(
+            f"{self_cls} not in {clsnames!r}. "
             f"Method {funcname} is in namespace {ns!r}, so it is invisible "
             f"from class {self.__class__.__qualname__!r}."
         )
     i = clsnames.index(self_cls) + 1
-
+    ins = self
     for clsname in clsnames[i:]:
         ins = getattr(ins, clsname)
 
diff --git a/magicclass/wrappers/_abstractapi.py b/magicclass/wrappers/_abstractapi.py
index 7e7d9a2d..12a303b9 100644
--- a/magicclass/wrappers/_abstractapi.py
+++ b/magicclass/wrappers/_abstractapi.py
@@ -1,10 +1,13 @@
 from __future__ import annotations
+import inspect
 from typing import Callable, TYPE_CHECKING
 import functools
 from magicclass._exceptions import AbstractAPIError
 
 if TYPE_CHECKING:
     from typing import NoReturn
+    from magicclass._gui import MagicTemplate
+    from magicclass.fields import MagicField
 
 
 class abstractapi(Callable):
@@ -28,7 +31,12 @@ class abstractapi(Callable):
     ...         print(i)  # do something
     """
 
-    def __init__(self, func: Callable | None = None):
+    def __init__(
+        self,
+        func: Callable | None = None,
+        *,
+        location: type[MagicTemplate] | MagicField | None = None,
+    ):
         if func is not None:
             if not callable(func) or isinstance(func, type):
                 raise TypeError("abstractapi can only be used on functions and methods")
@@ -38,6 +46,9 @@ def __init__(self, func: Callable | None = None):
         else:
             self.__name__ = "unknown"
         self._resolved = False
+        self._location = location
+        if location is not None and not hasattr(self, "__signature__"):
+            self.__signature__ = inspect.Signature([])
 
     def __call__(self, *args, **kwargs) -> NoReturn:
         raise AbstractAPIError(
@@ -47,6 +58,11 @@ def __call__(self, *args, **kwargs) -> NoReturn:
     def __set_name__(self, owner: type, name: str):
         self.__name__ = name
         self.__qualname__ = f"{owner.__qualname__}.{name}"
+        api = getattr(self._location, name, None)
+        if isinstance(api, abstractapi):
+            api.resolve()
+            if not hasattr(self, "__signature__"):
+                api.__signature__ = inspect.Signature([])
 
     def __get__(self, instance, owner=None) -> abstractapi:
         """Always return the same object."""
@@ -68,3 +84,14 @@ def check_resolved(self) -> None:
         if not self._resolved:
             raise AbstractAPIError(f"{self!r} is not redefined in the parent class.")
         return None
+
+    def get_location(self) -> type[MagicTemplate] | None:
+        loc = self._location
+        if loc is None:
+            return None
+
+        from magicclass.fields import MagicField
+
+        if isinstance(loc, MagicField):
+            return loc.constructor
+        return loc
diff --git a/magicclass/wrappers/_misc.py b/magicclass/wrappers/_misc.py
index 1ec54900..f8ae2af7 100644
--- a/magicclass/wrappers/_misc.py
+++ b/magicclass/wrappers/_misc.py
@@ -9,11 +9,10 @@
 from magicclass.signature import get_additional_option, upgrade_signature
 
 if TYPE_CHECKING:
-    from magicclass._gui import BaseGui
+    from magicclass._gui import MagicTemplate
+    from magicclass.fields import MagicField
 
-    _T = TypeVar("T", bound=BaseGui)
-
-_F = TypeVar("F", bound=Callable)
+_F = TypeVar("_F", bound=Callable)
 
 
 def set_options(
@@ -62,13 +61,14 @@ def func(self, a):
     """
 
     def wrapper(func: _F) -> _F:
-        sig = inspect.signature(func)
-        rem = options.keys() - sig.parameters.keys()
-        if rem:
-            warnings.warn(
-                f"Unknown arguments found in set_options of {func.__name__}: {rem}",
-                UserWarning,
-            )
+        if options:
+            sig = inspect.signature(func)
+            rem = options.keys() - sig.parameters.keys()
+            if rem:
+                warnings.warn(
+                    f"Unknown arguments found in set_options of {func.__name__}: {rem}",
+                    UserWarning,
+                )
 
         upgrade_signature(
             func,
@@ -95,6 +95,7 @@ def set_design(
     max_height: int | None = None,
     text: str | None = None,
     icon: str | None = None,
+    location: MagicTemplate | MagicField | None = None,
     font_size: int | None = None,
     font_family: int | None = None,
     font_color: Color | None = None,
@@ -129,6 +130,7 @@ def set_design(
         Button visibility.
     """
     caller_options = locals()
+    caller_options.pop("location")
     caller_options = {k: v for k, v in caller_options.items() if v is not None}
 
     def wrapper(obj):
@@ -143,6 +145,8 @@ def __post_init__(self):
             obj.__post_init__ = __post_init__
         else:
             upgrade_signature(obj, caller_options=caller_options)
+            if location is not None:
+                location.wraps(obj)
         return obj
 
     return wrapper
@@ -242,7 +246,7 @@ def setup_function_gui(target: Callable):
     ...             gui.x.max = xmax_value
     """
 
-    def wrapper(setup: Callable[[BaseGui, FunctionGui], None]):
+    def wrapper(setup: Callable[[MagicTemplate, FunctionGui], None]):
         setup_qualname = setup.__qualname__
         target_qualname = target.__qualname__
         setup_qualname_list = setup_qualname.split(".")
@@ -251,7 +255,7 @@ def wrapper(setup: Callable[[BaseGui, FunctionGui], None]):
             and setup_qualname_list[-2] != target_qualname.split(".")[-2]
         ):
             # have to search for the proper parent instance
-            def _setup(self: BaseGui, mgui: FunctionGui):
+            def _setup(self: MagicTemplate, mgui: FunctionGui):
                 prev_ns = setup_qualname_list[-2]
                 while self.__class__.__name__ != prev_ns:
                     self = self.__magicclass_parent__


Test patch:
diff --git a/tests/test_bind.py b/tests/test_bind.py
index 17463e44..0b5f39ce 100644
--- a/tests/test_bind.py
+++ b/tests/test_bind.py
@@ -1,6 +1,6 @@
 from typing import Tuple
-from magicclass import magicclass, set_options, field, vfield, magictoolbar
-from magicclass.core import get_button
+from typing_extensions import Annotated
+from magicclass import magicclass, set_options, set_design, field, vfield, magictoolbar, get_button, magicmenu, abstractapi
 from magicclass.types import Bound, Optional
 
 def test_bind_value():
@@ -185,7 +185,7 @@ class A:
         def _callback(self):
             self._a = 1
 
-        def func(self, x: Bound[b.x]):
+        def func(self, x: Annotated[int, {"bind": b.x}]):
             self._a = x
 
     ui = A()
@@ -195,6 +195,78 @@ def func(self, x: Bound[b.x]):
     ui["func"].changed()
     assert ui._a == 10
 
+def test_wrapped_with_external_field():
+    @magicclass
+    class B:
+        @magicmenu
+        class C:
+            x = abstractapi()
+        x = abstractapi(location=C)
+        y = abstractapi()
+
+    @magicclass
+    class A:
+        _x = -1
+        _y = -1
+        b = B
+        def _get_value(self, *_):
+            return 10
+
+        @set_design(location=b)
+        def x(self, v: Annotated[int, {"bind": _get_value}]):
+            self._x = v
+
+        @set_design(location=b)
+        def y(self, v: Annotated[int, {"bind": _get_value}]):
+            self._y = v
+
+    ui = A()
+    btn_x = get_button(ui.x)
+    assert ui._x == -1
+    btn_x.changed()
+    assert ui._x == 10
+
+    btn_y = get_button(ui.y)
+    assert ui._y == -1
+    btn_y.changed()
+    assert ui._y == 10
+
+def test_wrapped_with_external_field_via_field():
+    @magicclass
+    class B:
+        @magicmenu
+        class C:
+            x = abstractapi()
+        x = abstractapi(location=C)
+        y = abstractapi()
+
+    @magicclass
+    class A:
+        _x = -1
+        _y = -1
+        b = field(B)
+        def _get_value(self, *_):
+            return 10
+
+        @set_design(location=b)
+        def x(self, v: Annotated[int, {"bind": _get_value}]):
+            self._x = v
+
+        @set_design(location=b)
+        def y(self, v: Annotated[int, {"bind": _get_value}]):
+            self._y = v
+
+    ui = A()
+    btn_x = get_button(ui.x)
+    assert ui._x == -1
+    btn_x.changed()
+    assert ui._x == 10
+
+    btn_y = get_button(ui.y)
+    assert ui._y == -1
+    btn_y.changed()
+    assert ui._y == 10
+
 def test_multi_gui():
     @magicclass
     class A:
diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py
index 3c3cde74..3973da34 100644
--- a/tests/test_callbacks.py
+++ b/tests/test_callbacks.py
@@ -307,7 +307,7 @@ def __init__(self):
         class B(MagicTemplate):
             x = abstractapi()
 
-        x = B.vfield(int)
+        x = vfield(int, location=B)
         @x.connect_async
         def _x(self, v):
             self._val = self, v
diff --git a/tests/test_wraps.py b/tests/test_wraps.py
index ef93f464..78e33d89 100644
--- a/tests/test_wraps.py
+++ b/tests/test_wraps.py
@@ -175,8 +175,8 @@ class A:
         class B:
             a = abstractapi()
             b = abstractapi()
-        a = B.field(int)
-        b = B.field(str)
+        a = field(int, location=B)
+        b = field(str, location=B)
 
     ui = A()
     assert ui.B[0].widget_type == "SpinBox"
@@ -203,8 +203,8 @@ class A:
         class B:
             a = abstractapi()
             b = abstractapi()
-        a = B.vfield(int)
-        b = B.vfield(str)
+        a = vfield(int, location=B)
+        b = vfield(str, location=B)
 
     ui = A()
     assert ui.B[0].widget_type == "SpinBox"