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

Issue:
Can't add both menubar and toolbar on scrollable container with Qt6 backend
This code causes access violation on rendering.

``` python
from magicclass import magicclass, set_design, magictoolbar, magicmenu
@magicclass(widget_type="scrollable")
class A:
    @magicmenu
    class Menu:
        def a(self):...
        def b(self):...
    @magictoolbar
    class Tools:
        def f(self):...
    def f(self):...
ui = A()
ui
```


Discussion / hints:

Fix patch:
diff --git a/magicclass/_gui/_base.py b/magicclass/_gui/_base.py
index 7a4c1dc3..fa99b183 100644
--- a/magicclass/_gui/_base.py
+++ b/magicclass/_gui/_base.py
@@ -63,7 +63,7 @@
     inject_silencer,
     inject_validator_only,
     value_widget_callback,
-    nested_function_gui_callback,
+    MagicGuiPostRunCallback,
 )
 from ._icon import get_icon
 from ._gui_modes import PopUpMode, ErrorMode
@@ -1292,8 +1292,7 @@ def normalize_insertion(parent: BaseGui, obj: Callable | Widget | AbstractAction
         # Sometimes users want to dynamically add new functions to GUI.
         if isinstance(obj, FunctionGui):
             if obj.parent is None:
-                f = nested_function_gui_callback(parent, obj)
-                obj.called.connect(f)
+                MagicGuiPostRunCallback.install(parent, obj)
             _obj = obj
         else:
             obj = convert_function(obj, is_method=False).__get__(parent)
@@ -1451,7 +1450,10 @@ def _method(*args, **kwargs):
 
 def _empty_func(name: str) -> Callable[[Any], None]:
     """Create a named function that does nothing."""
-    f = lambda x: None
+
+    def f(x):
+        pass
+
     f.__name__ = name
     return f
 
diff --git a/magicclass/_gui/_icon.py b/magicclass/_gui/_icon.py
index 18153a37..84ec6bff 100644
--- a/magicclass/_gui/_icon.py
+++ b/magicclass/_gui/_icon.py
@@ -103,7 +103,7 @@ def get_qicon(self, dst) -> QIcon:
         if isinstance(dst.native, QWidget):
             palette = dst.native.palette()
         elif isinstance(dst.native, QAction):
-            if menu := dst.native.parentWidget():
+            if menu := dst.native.parent():
                 palette = menu.palette()
             else:
                 palette = None
diff --git a/magicclass/_gui/_macro_utils.py b/magicclass/_gui/_macro_utils.py
index 7af3ef0e..5c2b44e3 100644
--- a/magicclass/_gui/_macro_utils.py
+++ b/magicclass/_gui/_macro_utils.py
@@ -1,8 +1,10 @@
 from __future__ import annotations
-from typing import TYPE_CHECKING, Callable, TypeVar
+from typing import TYPE_CHECKING, Callable, Generic, TypeVar
+from typing_extensions import ParamSpec
 import inspect
 from functools import partial, partialmethod, wraps as functools_wraps
 import warnings
+import weakref
 from macrokit import Symbol, Expr, Head, symbol
 from magicgui.widgets import FunctionGui
 from magicgui.widgets.bases import ValueWidget
@@ -15,6 +17,7 @@
 if TYPE_CHECKING:
     from ._base import MagicTemplate
 
+_P = ParamSpec("_P")
 _R = TypeVar("_R")
 
 
@@ -64,12 +67,20 @@ def _set_value(value):
     return _set_value
 
 
-def nested_function_gui_callback(gui: MagicTemplate, fgui: FunctionGui[_R]):
-    """Define a FunctionGui callback, including macro recording."""
-    fgui_name = Symbol(fgui.name)
-    _qualname = getattr(fgui._function, "__qualname__", None)
+class MagicGuiPostRunCallback(Generic[_P, _R]):
+    def __init__(self, gui: MagicTemplate, fgui: FunctionGui[_P, _R]):
+        self._gui = weakref.ref(gui)
+        self._fgui = weakref.ref(fgui)
+        self._fgui_name = Symbol(fgui.name)
+        # {x}.func
+        self._sub_expr = Expr(head=Head.getattr, args=[symbol(gui), self._fgui_name])
 
-    def _after_run(out: _R):
+    def __call__(self, out: _R):
+        print("called")
+        fgui = self._fgui()
+        gui = self._gui()
+        if fgui is None or gui is None:
+            return None
         if not fgui.enabled or not gui.macro.active:
             # If widget is read only, it means that value is set in script (not
             # manually). Thus this event should not be recorded as a macro.
@@ -77,8 +88,9 @@ def _after_run(out: _R):
         inputs = get_parameters(fgui)
         args = [Expr(head=Head.kw, args=[Symbol(k), v]) for k, v in inputs.items()]
         # args[0] is self
-        sub = Expr(head=Head.getattr, args=[symbol(gui), fgui_name])  # {x}.func
-        expr = Expr(head=Head.call, args=[sub] + args[1:])  # {x}.func(args...)
+        expr = Expr(
+            head=Head.call, args=[self._sub_expr] + args[1:]
+        )  # {x}.func(args...)
 
         if fgui._auto_call:
             # Auto-call will cause many redundant macros. To avoid this, only the last
@@ -96,6 +108,7 @@ def _after_run(out: _R):
                         "User-defined undo operation of auto-call function is not "
                         "supported yet. Ignore the returned undo function.",
                         UserWarning,
+                        stakclevel=2,
                     )
         else:
             if isinstance(out, UndoCallback):
@@ -105,7 +118,16 @@ def _after_run(out: _R):
         gui.macro.append(expr)
         gui.macro._last_setval = None
 
-    return _after_run
+    @classmethod
+    def install(cls, gui: MagicTemplate, fgui: FunctionGui[_P, _R]):
+        """Install the post-run callback to the function gui."""
+        self = cls(gui, fgui)
+        if hasattr(fgui, "_mgui_post_run_callback"):
+            # avoid duplicated call
+            return self
+        fgui.called.connect(self)
+        fgui._mgui_post_run_callback = self
+        return self
 
 
 _SELF = inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)
diff --git a/magicclass/_gui/class_gui.py b/magicclass/_gui/class_gui.py
index 0d4428c2..8b3a2207 100644
--- a/magicclass/_gui/class_gui.py
+++ b/magicclass/_gui/class_gui.py
@@ -13,6 +13,7 @@
     Image,
     Table,
     Widget,
+    EmptyWidget,
 )
 from magicgui.widgets.bases import (
     ButtonWidget,
@@ -141,7 +142,7 @@ def _convert_attributes_into_widgets(self):
                     # Add menubar to container
                     if self._menubar is None:
                         # if widget has no menubar, a new one should be created.
-                        self._menubar = QMenuBar(parent=self.native)
+                        self._menubar = QMenuBar()
                         if issubclass(self.__class__, MainWindow):
                             self.native: QMainWindow
                             self.native.setMenuBar(self._menubar)
@@ -193,7 +194,8 @@ def _convert_attributes_into_widgets(self):
                                     alignment=Qt.AlignmentFlag.AlignTop,
                                 )
                                 self._toolbar.setContentsMargins(0, 0, 0, 0)
-                                n_insert += 1
+                                # if not isinstance(self, _USE_OUTER_LAYOUT):
+                                #     n_insert += 1
 
                     widget.native.setParent(self._toolbar, widget.native.windowFlags())
                     self._toolbar.addToolBar(
@@ -303,6 +305,8 @@ def _fast_insert(
         if key < 0:
             key += len(self)
         self._list.insert(key, widget)
+        if remove_label:
+            _widget = EmptyWidget(visible=False)
         self._widget._mgui_insert_widget(key, _widget)
 
         # NOTE: Function GUI is invisible by some reason...
@@ -605,35 +609,35 @@ def status(self: MainWindowClassGui, text: str):
 
 # fmt: off
 @make_gui(Container)
-class ClassGui: pass
+class ClassGui: pass  # noqa: E701
 @make_gui(SplitterContainer)
-class SplitClassGui: pass
+class SplitClassGui: pass  # noqa: E701
 @make_gui(ScrollableContainer)
-class ScrollableClassGui: pass
+class ScrollableClassGui: pass  # noqa: E701
 @make_gui(DraggableContainer)
-class DraggableClassGui: pass
+class DraggableClassGui: pass  # noqa: E701
 @make_gui(CollapsibleContainer)
-class CollapsibleClassGui: pass
+class CollapsibleClassGui: pass  # noqa: E701
 @make_gui(HCollapsibleContainer)
-class HCollapsibleClassGui: pass
+class HCollapsibleClassGui: pass  # noqa: E701
 @make_gui(ButtonContainer)
-class ButtonClassGui: pass
+class ButtonClassGui: pass  # noqa: E701
 @make_gui(ToolBoxContainer, no_margin=False)
-class ToolBoxClassGui: pass
+class ToolBoxClassGui: pass  # noqa: E701
 @make_gui(TabbedContainer, no_margin=False)
-class TabbedClassGui: pass
+class TabbedClassGui: pass  # noqa: E701
 @make_gui(StackedContainer, no_margin=False)
-class StackedClassGui: pass
+class StackedClassGui: pass  # noqa: E701
 @make_gui(ListContainer, no_margin=False)
-class ListClassGui: pass
+class ListClassGui: pass  # noqa: E701
 @make_gui(SubWindowsContainer, no_margin=False)
-class SubWindowsClassGui: pass
+class SubWindowsClassGui: pass  # noqa: E701
 @make_gui(GroupBoxContainer, no_margin=False)
-class GroupBoxClassGui: pass
+class GroupBoxClassGui: pass  # noqa: E701
 @make_gui(FrameContainer, no_margin=False)
-class FrameClassGui: pass
+class FrameClassGui: pass  # noqa: E701
 @make_gui(ResizableContainer, no_margin=False)
-class ResizableClassGui: pass
+class ResizableClassGui: pass  # noqa: E701
 @make_gui(MainWindow)
-class MainWindowClassGui: pass
+class MainWindowClassGui: pass  # noqa: E701
 # fmt: on
diff --git a/magicclass/_gui/menu_gui.py b/magicclass/_gui/menu_gui.py
index 49a8d82c..cebc6a1c 100644
--- a/magicclass/_gui/menu_gui.py
+++ b/magicclass/_gui/menu_gui.py
@@ -194,8 +194,9 @@ def _fast_insert(
                 if self.labels:
                     if not isinstance(_obj.widget, _hide_labels) and not remove_label:
                         _obj_labeled = _LabeledWidgetAction.from_action(_obj)
-                _obj_labeled.parent = self
-                insert_action_like(self.native, key, _obj_labeled.native)
+                if not remove_label:
+                    _obj_labeled.parent = self
+                    insert_action_like(self.native, key, _obj_labeled.native)
 
             self._list.insert(key, _obj)
         else:
diff --git a/magicclass/_gui/toolbar.py b/magicclass/_gui/toolbar.py
index 8fb7a945..5ad99b1c 100644
--- a/magicclass/_gui/toolbar.py
+++ b/magicclass/_gui/toolbar.py
@@ -259,8 +259,9 @@ def _fast_insert(
                 if self.labels:
                     if not isinstance(_obj.widget, _hide_labels) and not remove_label:
                         _obj_labeled = _LabeledWidgetAction.from_action(_obj)
-                _obj_labeled.parent = self
-                insert_action_like(self.native, key, _obj_labeled.native)
+                if not remove_label:
+                    _obj_labeled.parent = self
+                    insert_action_like(self.native, key, _obj_labeled.native)
             self._list.insert(key, _obj)
         else:
             raise TypeError(f"{type(_obj)} is not supported.")
diff --git a/magicclass/fields/_fields.py b/magicclass/fields/_fields.py
index 6d5db13a..4660fe58 100644
--- a/magicclass/fields/_fields.py
+++ b/magicclass/fields/_fields.py
@@ -311,7 +311,7 @@ def get_action(self, obj: Any) -> AbstractAction:
         if obj_id in self._guis.keys():
             action = self._guis[obj_id]
         else:
-            if type(self.value) is bool or self.annotation is bool:
+            if type(self.value) is bool or self.annotation is bool:  # noqa: E721
                 # we should not use "isinstance" or "issubclass" because subclass
                 # may be mapped to different widget by users.
                 value = False if self.value is Undefined else self.value
diff --git a/magicclass/utils/qthreading/thread_worker.py b/magicclass/utils/qthreading/thread_worker.py
index 384b753b..8872fd3c 100644
--- a/magicclass/utils/qthreading/thread_worker.py
+++ b/magicclass/utils/qthreading/thread_worker.py
@@ -69,7 +69,8 @@ def arun(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
 if TYPE_CHECKING:
     _async_method: Callable[[Callable[_P, _R]], AsyncMethod[_P, _R]]
 else:
-    _async_method = lambda f: f
+    def _async_method(f):
+        return f
 
 
 def _silent(*args, **kwargs):
@@ -82,7 +83,7 @@ class thread_worker(Callable, Generic[_P, _R]):
     _DEFAULT_PROGRESS_BAR = DefaultProgressBar
     _DEFAULT_TOTAL = 0
     _WINDOW_FLAG = Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.Window  # fmt: skip
-    _BLOCKING_SOURCES = []
+    _BLOCKING_SOURCES: list[None] = []
 
     def __init__(
         self,
diff --git a/magicclass/widgets/containers.py b/magicclass/widgets/containers.py
index 927278da..030ad58d 100644
--- a/magicclass/widgets/containers.py
+++ b/magicclass/widgets/containers.py
@@ -178,7 +178,7 @@ def _mgui_remove_widget(self, widget: Widget):
 class _ScrollableContainer(ContainerBase):
     def __init__(self, layout="vertical", scrollable: bool = False, **kwargs):
         QBaseWidget.__init__(self, QtW.QWidget)
-        self._scroll_area = QtW.QScrollArea(self._qwidget)
+        self._scroll_area = QtW.QScrollArea()
         if layout == "horizontal":
             self._layout: QtW.QLayout = QtW.QHBoxLayout()
             self._layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
@@ -188,13 +188,14 @@ def __init__(self, layout="vertical", scrollable: bool = False, **kwargs):
 
         self._scroll_area.setWidgetResizable(True)
         self._scroll_area.setContentsMargins(0, 0, 0, 0)
-        self._inner_qwidget = QtW.QWidget(self._scroll_area)
+        self._inner_qwidget = QtW.QWidget()
         self._inner_qwidget.setLayout(self._layout)
         self._scroll_area.setWidget(self._inner_qwidget)
 
-        self._qwidget.setLayout(QtW.QVBoxLayout())
-        self._qwidget.layout().addWidget(self._scroll_area)
-        self._qwidget.layout().setContentsMargins(0, 0, 0, 0)
+        _layout = QtW.QVBoxLayout()
+        _layout.addWidget(self._scroll_area)
+        _layout.setContentsMargins(0, 0, 0, 0)
+        self._qwidget.setLayout(_layout)
 
     def _policy(self, value):
         if value:
diff --git a/magicclass/wrappers/_preview.py b/magicclass/wrappers/_preview.py
index d3519f52..cee6a07c 100644
--- a/magicclass/wrappers/_preview.py
+++ b/magicclass/wrappers/_preview.py
@@ -131,7 +131,8 @@ def get_arg_filter(
         if prev_params.keys() != tgt_params.keys():
             raise TypeError(f"Arguments mismatch between {prev_sig!r} and {tgt_sig!r}.")
         # If argument names are identical, input arguments don't have to be filtered.
-        _filter = lambda a: a
+        def _filter(a):
+            return a
 
     elif less > 0:
         idx: list[int] = []
@@ -140,7 +141,8 @@ def get_arg_filter(
                 idx.append(i)
         # If argument names are not identical, input arguments have to be filtered so
         # that arguments match the inputs.
-        _filter = lambda _args: (a for i, a in enumerate(_args) if i in idx)
+        def _filter(_args):
+            return tuple(a for i, a in enumerate(_args) if i in idx)
 
     else:
         raise TypeError(


Test patch:
diff --git a/tests/test_widget_types.py b/tests/test_widget_types.py
index f66c58bc..8f31f5b7 100644
--- a/tests/test_widget_types.py
+++ b/tests/test_widget_types.py
@@ -32,8 +32,6 @@ def b1(self, a: int): ...
 
 @pytest.mark.parametrize("wtype", WidgetType._member_names_)
 def test_all_works(wtype):
-    if QT6 and wtype in ("scrollable", "draggable"):
-        pytest.skip("QScrollArea crashes in QT6. Skip for now.")
     ui = _make_class(wtype)()
     ui.show(run=False)
     ui[0]
diff --git a/tests/test_wraps.py b/tests/test_wraps.py
index 78e33d89..5b336424 100644
--- a/tests/test_wraps.py
+++ b/tests/test_wraps.py
@@ -4,8 +4,6 @@
 )
 from magicclass.types import Bound
 from unittest.mock import MagicMock
-import pytest
-from qtpy import QT6
 
 def test_single_wraps():
     @magicclass
@@ -108,8 +106,6 @@ def f(self, a: Bound[a]):
     assert ui.new_attr == 0
 
 def test_wraps_no_predefinition():
-    if QT6:
-        pytest.skip("insertWidget crashes in QT6. Skip for now.")
     @magicclass
     class A:
         a = vfield(int)
@@ -167,8 +163,6 @@ def any_func(self, a: Bound[a]):
     ui.B.E["any_func"].changed()
 
 def test_wrapped_field():
-    if QT6:
-        pytest.skip("insertWidget crashes in QT6. Skip for now.")
     @magicclass
     class A:
         @magicclass
@@ -195,8 +189,6 @@ class B:
 
 
 def test_wrapped_vfield():
-    if QT6:
-        pytest.skip("insertWidget crashes in QT6. Skip for now.")
     @magicclass
     class A:
         @magicclass