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

Issue:
Macro recording of non-GUI function call
Currently only function calls triggered in GUI is recorded. However it doesn't seem a good idea in terms of reproducibility, like running some function using GUI while running other frequently used functions in Python interactive console. Actually, value changes of ValueWidget *are* recorded. 

All the public methods should be wrapped with a macro recorder. 

To Do
-----

- [x] Add macro recorders on class definition (not instance construction). 
- [x] Upper limit of history of macro object, since calling functions programmatically could cause unlimited increase of macro size.


Discussion / hints:

Fix patch:
diff --git a/magicclass/__init__.py b/magicclass/__init__.py
index f2bdea3a..510be0dd 100644
--- a/magicclass/__init__.py
+++ b/magicclass/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.5.23.dev1"
+__version__ = "0.6.0.dev0"
 
 from .core import (
     magicclass,
diff --git a/magicclass/core.py b/magicclass/core.py
index c86efa5e..d8290a12 100644
--- a/magicclass/core.py
+++ b/magicclass/core.py
@@ -23,7 +23,14 @@
     ToolBoxClassGui,
     ListClassGui,
 )
-from .gui._base import PopUpMode, ErrorMode, defaults, MagicTemplate, check_override
+from .gui._base import (
+    PopUpMode,
+    ErrorMode,
+    defaults,
+    MagicTemplate,
+    check_override,
+    convert_attributes,
+)
 from .gui import ContextMenuGui, MenuGui, ToolBarGui
 from ._app import get_app
 from .types import WidgetType
@@ -146,8 +153,9 @@ def wrapper(cls) -> type[ClassGui]:
         mod = cls.__module__
         qualname = cls.__qualname__
 
+        new_attrs = convert_attributes(cls, hide=class_gui.__mro__)
         oldclass = type(cls.__name__ + _BASE_CLASS_SUFFIX, (cls,), {})
-        newclass = type(cls.__name__, (class_gui, oldclass), {})
+        newclass = type(cls.__name__, (class_gui, oldclass), new_attrs)
 
         newclass.__signature__ = sig
         newclass.__doc__ = doc
@@ -160,9 +168,8 @@ def wrapper(cls) -> type[ClassGui]:
 
         @functools_wraps(oldclass.__init__)
         def __init__(self: MagicTemplate, *args, **kwargs):
-            app = (
-                get_app()
-            )  # Without "app = " Jupyter freezes after closing the window!
+            # Without "app = " Jupyter freezes after closing the window!
+            app = get_app()
 
             class_gui.__init__(
                 self,
@@ -401,8 +408,9 @@ def wrapper(cls) -> type[menugui_class]:
         mod = cls.__module__
         qualname = cls.__qualname__
 
+        new_attrs = convert_attributes(cls, hide=menugui_class.__mro__)
         oldclass = type(cls.__name__ + _BASE_CLASS_SUFFIX, (cls,), {})
-        newclass = type(cls.__name__, (menugui_class, oldclass), {})
+        newclass = type(cls.__name__, (menugui_class, oldclass), new_attrs)
 
         newclass.__signature__ = sig
         newclass.__doc__ = doc
@@ -411,9 +419,8 @@ def wrapper(cls) -> type[menugui_class]:
 
         @functools_wraps(oldclass.__init__)
         def __init__(self: MagicTemplate, *args, **kwargs):
-            app = (
-                get_app()
-            )  # Without "app = " Jupyter freezes after closing the window!
+            # Without "app = " Jupyter freezes after closing the window!
+            app = get_app()
 
             menugui_class.__init__(
                 self,
diff --git a/magicclass/gui/_base.py b/magicclass/gui/_base.py
index ae0cc878..52228130 100644
--- a/magicclass/gui/_base.py
+++ b/magicclass/gui/_base.py
@@ -10,6 +10,7 @@
     overload,
     MutableSequence,
 )
+from types import MethodType
 from typing_extensions import _AnnotatedAlias
 import inspect
 import warnings
@@ -82,6 +83,7 @@ class ErrorMode(Enum):
     "popup_mode": PopUpMode.popup,
     "error_mode": ErrorMode.msgbox,
     "close_on_run": True,
+    "macro-max-history": 1000,
 }
 
 _RESERVED = {
@@ -506,7 +508,7 @@ def _create_widget_from_field(self, name: str, fld: MagicField):
         """
         raise NotImplementedError()
 
-    def _create_widget_from_method(self, obj: Callable):
+    def _create_widget_from_method(self, obj: MethodType):
         """Convert instance methods into GUI objects, such as push buttons or actions."""
         text = obj.__name__.replace("_", " ")
         widget = self._component_class(name=obj.__name__, text=text, gui_only=True)
@@ -522,24 +524,12 @@ def _create_widget_from_method(self, obj: Callable):
             raise ValueError(self._error_mode)
 
         # Wrap function to block macro recording.
-        _inner_func = wrapper(obj, parent=self)
-        if _need_record(obj):
-
-            @functools_wraps(obj)
-            def func(*args, **kwargs):
-                with self.macro.blocked():
-                    out = _inner_func(*args, **kwargs)
-                return out
-
-        else:
-
-            @functools_wraps(obj)
-            def func(*args, **kwargs):
-                return _inner_func(*args, **kwargs)
+        func = functools_wraps(obj)(wrapper(obj, parent=self))
 
         # Signature must be updated like this. Otherwise, already wrapped member function
         # will have signature with "self".
-        func.__signature__ = inspect.signature(obj)
+        obj_sig = inspect.signature(obj)
+        func.__signature__ = obj_sig
 
         # Prepare a button or action
         widget.tooltip = extract_tooltip(func)
@@ -578,15 +568,11 @@ def func(*args, **kwargs):
         n_empty = len([_widget for _widget in fgui if isinstance(_widget, EmptyWidget)])
         nparams = _n_parameters(func) - n_empty
 
-        obj_sig = get_signature(obj)
         if isinstance(func.__signature__, MagicMethodSignature):
-            # NOTE: I don't know the reason why "additional_options" is lost.
             func.__signature__.additional_options = getattr(
                 obj_sig, "additional_options", {}
             )
 
-        # TODO: "update_widget" argument is useful.
-        # see https://github.com/napari/magicgui/pull/309
         if nparams == 0:
             # We don't want a dialog with a single widget "Run" to show up.
             def run_function():
@@ -594,14 +580,6 @@ def run_function():
                 # "compiled" otherwise function wrappings are not ready!
                 mgui = _build_mgui(widget, func, self)
                 mgui.native.setParent(self.native, mgui.native.windowFlags())
-                if (
-                    mgui.call_count == 0
-                    and len(mgui.called._slots) == 0
-                    and _need_record(func)
-                ):
-                    callback = _temporal_function_gui_callback(self, mgui, widget)
-                    mgui.called.connect(callback)
-
                 out = mgui()
 
                 return out
@@ -611,14 +589,6 @@ def run_function():
             def run_function():
                 mgui = _build_mgui(widget, func, self)
                 mgui.native.setParent(self.native, mgui.native.windowFlags())
-                if (
-                    mgui.call_count == 0
-                    and len(mgui.called._slots) == 0
-                    and _need_record(func)
-                ):
-                    callback = _temporal_function_gui_callback(self, mgui, widget)
-                    mgui.called.connect(callback)
-
                 fdialog: FileEdit = mgui[0]
                 result = fdialog._show_file_dialog(
                     fdialog.mode,
@@ -707,10 +677,6 @@ def run_function():
                             # If FunctioGui is docked, we should close QDockWidget.
                             mgui.called.connect(lambda: mgui.parent.hide())
 
-                    if _need_record(func):
-                        callback = _temporal_function_gui_callback(self, mgui, widget)
-                        mgui.called.connect(callback)
-
                 if self._popup_mode != PopUpMode.dock:
                     widget.mgui.show()
                 else:
@@ -756,7 +722,10 @@ def _iter_child_magicclasses(self) -> Iterable[MagicTemplate]:
 
 class BaseGui(MagicTemplate):
     def __init__(self, close_on_run, popup_mode, error_mode):
-        self._macro_instance = GuiMacro(flags={"Get": False, "Return": False})
+        self._macro_instance = GuiMacro(
+            max_lines=defaults["macro-max-history"],
+            flags={"Get": False, "Return": False},
+        )
         self.__magicclass_parent__: BaseGui | None = None
         self.__magicclass_children__: list[MagicTemplate] = []
         self._close_on_run = close_on_run
@@ -919,76 +888,19 @@ def _get_widget_name(widget: Widget):
     return widget.name
 
 
-def _temporal_function_gui_callback(
-    bgui: MagicTemplate, fgui: FunctionGuiPlus, widget: PushButtonPlus
-):
-    if isinstance(fgui, FunctionGui):
-        _function = fgui._function
-    else:
-        raise TypeError("fgui must be FunctionGui object.")
-
-    return_type = fgui.return_annotation
-    result_required = return_type is not inspect.Parameter.empty
-
-    def _after_run(e: Exception = None):
-        if isinstance(e, Exception):
-            # If function call ended with an exception, don't record macro.
-            return
-        bound = fgui._previous_bound
-        result = Symbol("result")
-        # Standard button will be connected with two callbacks.
-        # 1. Build FunctionGui
-        # 2. Emit value changed signal.
-        # But if there are more, they also have to be called.
-        if len(widget.changed._slots) > 2:
-            b = Expr(head=Head.getitem, args=[symbol(bgui), widget.name])
-            ev = Expr(head=Head.getattr, args=[b, Symbol("changed")])
-            expr = Expr(head=Head.call, args=[ev])
-        else:
-            kwargs = {k: v for k, v in bound.arguments.items()}
-            expr = Expr.parse_method(bgui, _function, (), kwargs)
-
-        if fgui._auto_call:
-            # Auto-call will cause many redundant macros. To avoid this, only the last input
-            # will be recorded in magic-class.
-            last_expr = bgui.macro[-1]
-            if (
-                last_expr.head == Head.call
-                and last_expr.args[0].head == Head.getattr
-                and last_expr.at(0, 1) == expr.at(0, 1)
-                and len(bgui.macro) > 0
-            ):
-                bgui.macro.pop()
-                bgui.macro._erase_last()
-
-        if result_required:
-            expr = Expr(Head.assign, [result, expr])
-        bgui.macro.append(expr)
-
-        # Deal with return annotation
-
-        if result_required:
-            from magicgui.type_map import _type2callback
-
-            for callback in _type2callback(return_type):
-                b = Expr(head=Head.getitem, args=[symbol(bgui), widget.name])
-                _gui = Expr(head=Head.getattr, args=[b, Symbol("mgui")])
-                line = Expr.parse_call(callback, (_gui, result, return_type), {})
-                bgui.macro.append(line)
-        bgui.macro._last_setval = None
-        return None
-
-    return _after_run
-
-
 def _build_mgui(widget_: Action | PushButtonPlus, func: Callable, parent: BaseGui):
     if widget_.mgui is not None:
         return widget_.mgui
     try:
-        call_button = get_additional_option(func, "call_button", None)
-        layout = get_additional_option(func, "layout", "vertical")
-        labels = get_additional_option(func, "labels", True)
-        auto_call = get_additional_option(func, "auto_call", False)
+        sig = getattr(func, "__signature__", None)
+        if isinstance(sig, MagicMethodSignature):
+            opt = sig.additional_options
+        else:
+            opt = {}
+        call_button = opt.get("call_button", None)
+        layout = opt.get("layout", "vertical")
+        labels = opt.get("labels", True)
+        auto_call = opt.get("auto_call", False)
         mgui = FunctionGuiPlus(
             func, call_button, layout=layout, labels=labels, auto_call=auto_call
         )
@@ -1001,7 +913,7 @@ def _build_mgui(widget_: Action | PushButtonPlus, func: Callable, parent: BaseGu
 
     widget_.mgui = mgui
     name = widget_.name or ""
-    mgui.native.setWindowTitle(name.replace("_", " "))
+    mgui.native.setWindowTitle(name.replace("_", " ").strip())
     mgui._magicclass_parent_ref = weakref.ref(parent)
     return mgui
 
@@ -1218,10 +1130,6 @@ def _func(w):
     return _func
 
 
-def _need_record(func: Callable):
-    return get_additional_option(func, "record", True)
-
-
 def _is_instance_method(self: MagicTemplate, func: Callable) -> bool:
     # NOTE: following implementation is not compatible with @wraps
     # if not callable(func):
@@ -1269,9 +1177,7 @@ def _set_value():
 
 
 def nested_function_gui_callback(gui: MagicTemplate, fgui: FunctionGui):
-    """
-    Define a FunctionGui callback, including macro recording.
-    """
+    """Define a FunctionGui callback, including macro recording."""
     fgui_name = Symbol(fgui.name)
 
     def _after_run():
@@ -1302,3 +1208,102 @@ def _after_run():
         gui.macro._last_setval = None
 
     return _after_run
+
+
+def _inject_recorder(func: Callable, is_method: bool = True) -> Callable:
+    """Inject macro recording functionality into a function."""
+    sig = get_signature(func)
+    if is_method:
+        sig = sig.replace(
+            parameters=list(sig.parameters.values())[1:],
+            return_annotation=sig.return_annotation,
+        )
+        _func = func
+    else:
+
+        @functools_wraps(func)
+        def _func(self, *args, **kwargs):
+            return func(*args, **kwargs)
+
+        _self_param = inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)
+        _func.__signature__ = sig.replace(
+            parameters=[_self_param] + list(sig.parameters.values()),
+            return_annotation=sig.return_annotation,
+        )
+
+    if isinstance(sig, MagicMethodSignature):
+        opt = sig.additional_options
+        _auto_call = opt.get("auto_call", False)
+    else:
+        _auto_call = False
+
+    @functools_wraps(_func)
+    def _recordable(bgui: MagicTemplate, *args, **kwargs):
+        with bgui.macro.blocked():
+            out = _func(bgui, *args, **kwargs)
+        if not bgui.macro.active:
+            return out
+        bound = sig.bind(*args, **kwargs)
+        kwargs = dict(bound.arguments.items())
+        expr = Expr.parse_method(bgui, _func, (), kwargs)
+
+        if _auto_call:
+            # Auto-call will cause many redundant macros. To avoid this, only the last input
+            # will be recorded in magic-class.
+            last_expr = bgui.macro[-1]
+            if (
+                last_expr.head == Head.call
+                and last_expr.args[0].head == Head.getattr
+                and last_expr.at(0, 1) == expr.at(0, 1)
+                and len(bgui.macro) > 0
+            ):
+                bgui.macro.pop()
+                bgui.macro._erase_last()
+
+        bgui.macro.append(expr)
+        bgui.macro._last_setval = None
+        return out
+
+    if hasattr(_func, "__signature__"):
+        _recordable.__signature__ = _func.__signature__
+    return _recordable
+
+
+_T = TypeVar("_T", bound=BaseGui)
+
+
+def convert_attributes(cls: type[_T], hide: tuple[type, ...]) -> dict[str, Any]:
+    """
+    Convert class attributes into macro recordable ones.
+
+    Returned dictionary can be directly used for the third argument of
+    ``type`` constructor. To avoid converting all the callables in
+    subclasses, subclasses that will be iterated over can be restricted
+    using ``hide`` argument.
+
+    Parameters
+    ----------
+    cls : BaseGui type
+        Class that will be converted.
+    hide : tuple of types
+        MROs that will be ignored during iteration.
+
+    Returns
+    -------
+    dict
+        New namespace.
+    """
+    _dict: dict[str, Callable] = {}
+    _pass_type = (property, classmethod, staticmethod, type, Widget)
+    mro = [c for c in cls.__mro__ if c not in hide]
+    for subcls in reversed(mro):
+        for name, obj in subcls.__dict__.items():
+            if name.startswith("_") or isinstance(obj, _pass_type) or not callable(obj):
+                new_attr = obj
+            elif callable(obj) and get_additional_option(obj, "record", True):
+                new_attr = _inject_recorder(obj)
+            else:
+                new_attr = obj
+
+            _dict[name] = new_attr
+    return _dict
diff --git a/magicclass/gui/_macro.py b/magicclass/gui/_macro.py
index cb01416e..3d08030e 100644
--- a/magicclass/gui/_macro.py
+++ b/magicclass/gui/_macro.py
@@ -297,9 +297,10 @@ def _set_synchronize(check: bool):
 
 
 class GuiMacro(Macro):
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
+    def __init__(self, max_lines: int, flags={}):
+        super().__init__(flags=flags)
         self._widget = None
+        self._max_lines = max_lines
         self.callbacks.append(self._update_widget)
 
     @property
@@ -316,6 +317,8 @@ def widget(self) -> MacroEdit:
     def _update_widget(self, expr=None):
         if self.widget.synchronize:
             self.widget.textedit.append(str(self.args[-1]))
+        if len(self) > self._max_lines:
+            del self[0]
 
     def _erase_last(self):
         if self.widget.synchronize:
diff --git a/magicclass/gui/class_gui.py b/magicclass/gui/class_gui.py
index 4e01994d..c94491ce 100644
--- a/magicclass/gui/class_gui.py
+++ b/magicclass/gui/class_gui.py
@@ -18,6 +18,7 @@
     ErrorMode,
     value_widget_callback,
     nested_function_gui_callback,
+    _inject_recorder,
 )
 from .utils import (
     copy_class,
@@ -352,19 +353,23 @@ def __setattr__(self: cls, name: str, value: Any) -> None:
             else:
                 object.__setattr__(self, name, value)
 
-        def _fast_insert(self: cls, key: int, widget: Widget | Callable) -> None:
-            if isinstance(widget, Callable):
+        def _fast_insert(self: cls, key: int, obj: Widget | Callable) -> None:
+            if isinstance(obj, Callable):
                 # Sometimes uses want to dynamically add new functions to GUI.
-                method_name = getattr(widget, "__name__", None)
-                if method_name and not hasattr(self, method_name):
-                    object.__setattr__(self, method_name, widget)
-
-                if isinstance(widget, FunctionGui):
-                    if widget.parent is None:
-                        f = nested_function_gui_callback(self, widget)
-                        widget.called.connect(f)
+                if isinstance(obj, FunctionGui):
+                    if obj.parent is None:
+                        f = nested_function_gui_callback(self, obj)
+                        obj.called.connect(f)
+                    widget = obj
                 else:
-                    widget = self._create_widget_from_method(widget)
+                    obj = _inject_recorder(obj, is_method=False).__get__(self)
+                    widget = self._create_widget_from_method(obj)
+
+                method_name = getattr(obj, "__name__", None)
+                if method_name and not hasattr(self, method_name):
+                    object.__setattr__(self, method_name, obj)
+            else:
+                widget = obj
 
             # _hide_labels should not contain Container because some ValueWidget like widgets
             # are Containers.
diff --git a/magicclass/gui/menu_gui.py b/magicclass/gui/menu_gui.py
index f1b055e7..4410801c 100644
--- a/magicclass/gui/menu_gui.py
+++ b/magicclass/gui/menu_gui.py
@@ -15,6 +15,7 @@
     ErrorMode,
     ContainerLikeGui,
     nested_function_gui_callback,
+    _inject_recorder,
 )
 from .utils import MagicClassConstructionError, copy_class
 
@@ -171,33 +172,37 @@ def _fast_insert(
         self, key: int, obj: Callable | MenuGuiBase | AbstractAction
     ) -> None:
         if isinstance(obj, Callable):
-            # Sometimes uses want to dynamically add new functions to GUI.
-            method_name = getattr(obj, "__name__", None)
-            if method_name and not hasattr(self, method_name):
-                object.__setattr__(self, method_name, obj)
-
+            # Sometimes users want to dynamically add new functions to GUI.
             if isinstance(obj, FunctionGui):
                 if obj.parent is None:
                     f = nested_function_gui_callback(self, obj)
                     obj.called.connect(f)
+                _obj = obj
             else:
-                obj = self._create_widget_from_method(obj)
+                obj = _inject_recorder(obj, is_method=False).__get__(self)
+                _obj = self._create_widget_from_method(obj)
 
-        if isinstance(obj, (self._component_class, MenuGuiBase)):
-            insert_action_like(self.native, key, obj.native)
-            self._list.insert(key, obj)
+            method_name = getattr(obj, "__name__", None)
+            if method_name and not hasattr(self, method_name):
+                object.__setattr__(self, method_name, obj)
+        else:
+            _obj = obj
 
-        elif isinstance(obj, WidgetAction):
+        if isinstance(_obj, (self._component_class, MenuGuiBase)):
+            insert_action_like(self.native, key, _obj.native)
+            self._list.insert(key, _obj)
+
+        elif isinstance(_obj, WidgetAction):
             from .toolbar import ToolBarGui
 
-            if isinstance(obj.widget, Separator):
+            if isinstance(_obj.widget, Separator):
                 insert_action_like(self.native, key, "sep")
 
-            elif isinstance(obj.widget, ToolBarGui):
-                qmenu = QMenu(obj.widget.name, self.native)
-                qmenu.addAction(obj.native)
-                if obj.widget._icon_path is not None:
-                    qmenu.setIcon(obj.widget.native.windowIcon())
+            elif isinstance(_obj.widget, ToolBarGui):
+                qmenu = QMenu(_obj.widget.name, self.native)
+                qmenu.addAction(_obj.native)
+                if _obj.widget._icon_path is not None:
+                    qmenu.setIcon(_obj.widget.native.windowIcon())
                 insert_action_like(self.native, key, qmenu)
 
             else:
@@ -210,15 +215,15 @@ def _fast_insert(
                     Image,
                     Table,
                 )
-                _obj = obj
-                if (not isinstance(obj.widget, _hide_labels)) and self.labels:
-                    _obj = _LabeledWidgetAction.from_action(obj)
+                _obj = _obj
+                if (not isinstance(_obj.widget, _hide_labels)) and self.labels:
+                    _obj = _LabeledWidgetAction.from_action(_obj)
                 _obj.parent = self
                 insert_action_like(self.native, key, _obj.native)
 
-            self._list.insert(key, obj)
+            self._list.insert(key, _obj)
         else:
-            raise TypeError(f"{type(obj)} is not supported.")
+            raise TypeError(f"{type(_obj)} is not supported.")
 
     def insert(self, key: int, obj: Callable | MenuGuiBase | AbstractAction) -> None:
         """
diff --git a/magicclass/gui/toolbar.py b/magicclass/gui/toolbar.py
index 4cb9efcf..36fe0d64 100644
--- a/magicclass/gui/toolbar.py
+++ b/magicclass/gui/toolbar.py
@@ -15,6 +15,7 @@
     ErrorMode,
     ContainerLikeGui,
     nested_function_gui_callback,
+    _inject_recorder,
 )
 from .utils import MagicClassConstructionError, copy_class, set_context_menu
 from .menu_gui import ContextMenuGui, MenuGui, MenuGuiBase, insert_action_like
@@ -233,26 +234,30 @@ def _fast_insert(self, key: int, obj: AbstractAction | Callable) -> None:
             Object to insert.
         """
         if isinstance(obj, Callable):
-            # Sometimes uses want to dynamically add new functions to GUI.
-            method_name = getattr(obj, "__name__", None)
-            if method_name and not hasattr(self, method_name):
-                object.__setattr__(self, method_name, obj)
-
+            # Sometimes users want to dynamically add new functions to GUI.
             if isinstance(obj, FunctionGui):
                 if obj.parent is None:
                     f = nested_function_gui_callback(self, obj)
                     obj.called.connect(f)
+                _obj = obj
             else:
-                obj = self._create_widget_from_method(obj)
+                obj = _inject_recorder(obj, is_method=False).__get__(self)
+                _obj = self._create_widget_from_method(obj)
+
+            method_name = getattr(obj, "__name__", None)
+            if method_name and not hasattr(self, method_name):
+                object.__setattr__(self, method_name, obj)
+        else:
+            _obj = obj
 
         # _hide_labels should not contain Container because some ValueWidget like widgets
         # are Containers.
-        if isinstance(obj, self._component_class):
-            insert_action_like(self.native, key, obj.native)
-            self._list.insert(key, obj)
+        if isinstance(_obj, self._component_class):
+            insert_action_like(self.native, key, _obj.native)
+            self._list.insert(key, _obj)
 
-        elif isinstance(obj, WidgetAction):
-            if isinstance(obj.widget, Separator):
+        elif isinstance(_obj, WidgetAction):
+            if isinstance(_obj.widget, Separator):
                 insert_action_like(self.native, key, "sep")
 
             else:
@@ -265,14 +270,14 @@ def _fast_insert(self, key: int, obj: AbstractAction | Callable) -> None:
                     Image,
                     Table,
                 )
-                _obj = obj
-                if (not isinstance(obj.widget, _hide_labels)) and self.labels:
-                    _obj = _LabeledWidgetAction.from_action(obj)
+                _obj = _obj
+                if (not isinstance(_obj.widget, _hide_labels)) and self.labels:
+                    _obj = _LabeledWidgetAction.from_action(_obj)
                 _obj.parent = self
                 insert_action_like(self.native, key, _obj.native)
-            self._list.insert(key, obj)
+            self._list.insert(key, _obj)
         else:
-            raise TypeError(f"{type(obj)} is not supported.")
+            raise TypeError(f"{type(_obj)} is not supported.")
 
     def insert(self, key: int, obj: AbstractAction) -> None:
         self._fast_insert(key, obj)
diff --git a/magicclass/widgets/misc.py b/magicclass/widgets/misc.py
index a2ffd9bf..c3af09a4 100644
--- a/magicclass/widgets/misc.py
+++ b/magicclass/widgets/misc.py
@@ -352,6 +352,17 @@ def erase_last(self):
         cursor.deletePreviousChar()
         self.native.setTextCursor(cursor)
 
+    def erase_first(self):
+        """Erase the first line."""
+        cursor = self.native.textCursor()
+        cursor.movePosition(QTextCursor.Start)
+        cursor.select(QTextCursor.LineUnderCursor)
+        cursor.removeSelectedText()
+        cursor.movePosition(QTextCursor.Down)
+        cursor.deletePreviousChar()
+        cursor.movePosition(QTextCursor.End)
+        self.native.setTextCursor(cursor)
+
     @property
     def selected(self) -> str:
         """Return selected string."""


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..1c735413 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,2 @@
+from magicclass import defaults
+defaults["error_mode"] = "stderr"
diff --git a/tests/test_macro.py b/tests/test_macro.py
index e08c6d7c..65c9b8ed 100644
--- a/tests/test_macro.py
+++ b/tests/test_macro.py
@@ -1,4 +1,4 @@
-from magicclass import magicclass, set_options
+from magicclass import magicclass, set_options, defaults
 from enum import Enum
 from pathlib import Path
 from datetime import datetime, date, time
@@ -73,3 +73,39 @@ def f4(self, path: Path, a=1):
     mock.assert_not_called()
     ui.macro.widget.execute_lines(4)
     mock.assert_not_called()
+
+
+def test_blocked():
+    @magicclass
+    class A:
+        def f(self, a: int = 1):
+            self._a = a
+        def g(self):
+            return self.f(0)
+
+    ui = A()
+    ui["f"].changed()
+    ui["f"].mgui[-1].changed()
+    assert ui._a == 1
+    ui["g"].changed()
+    assert ui._a == 0
+    ui.f(2)
+    assert ui._a == 2
+    ui.g()
+    assert ui._a == 0
+    assert str(ui.macro[1]) == "ui.f(a=1)"
+    assert str(ui.macro[2]) == "ui.g()"
+    assert str(ui.macro[3]) == "ui.f(a=2)"
+    assert str(ui.macro[4]) == "ui.g()"
+
+def test_max_history():
+    @magicclass
+    class A:
+        def f(self): pass
+    max_hist = defaults["macro-max-history"]
+    ui = A()
+    for i in range(max_hist + 10):
+        ui.f()
+    assert len(ui.macro) == max_hist
+    ui.f()
+    assert len(ui.macro) == max_hist
diff --git a/tests/test_methods.py b/tests/test_methods.py
index 7601f4c8..f9e2d5f8 100644
--- a/tests/test_methods.py
+++ b/tests/test_methods.py
@@ -33,7 +33,7 @@ class A(MagicTemplate):
 
 
 def test_post_append():
-    from types import FunctionType
+    from types import MethodType
 
     @magicclass
     class A:
@@ -57,12 +57,16 @@ def g(): pass
     @ui.Tool.append
     def g(): pass
 
-    assert isinstance(ui.g, FunctionType)
-    assert isinstance(ui.Menu.g, FunctionType)
-    assert isinstance(ui.Tool.g, FunctionType)
+    assert isinstance(ui.g, MethodType)
+    ui["g"]
+    assert isinstance(ui.Menu.g, MethodType)
+    ui.Menu["g"]
+    assert isinstance(ui.Tool.g, MethodType)
+    ui.Tool["g"]
 
     @ui.append
     def f(): pass
 
+    # the original method should not be updated
     ui.f()
     assert ui.a == 1