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

Issue:
`RuntimeError: wrapped C/C++ object of type QLabel has been deleted`
I've been hitting the above error. It seems to happen when we destroy and then create a viewer in quick succession. One such case where this happens is when running pytest. 

Here is a minimal example. You will have to run `pytest test.py` for this to work.

```python
from magicclass import magicclass
from magicgui import magicgui, widgets

@magicclass
class X:
    main_heading = widgets.Label(value="foo")

def test_a(make_napari_viewer):
    viewer = make_napari_viewer()
    widget = X()
    viewer.window.add_dock_widget(widget)

def test_b(make_napari_viewer):
    viewer = make_napari_viewer()
    widget = X()
    viewer.window.add_dock_widget(widget)
```

**Full traceback:**
<details>

```
Traceback (most recent call last):
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/runner.py", line 341, in from_call
    result: Optional[TResult] = func()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/runner.py", line 262, in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_hooks.py", line 433, in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_manager.py", line 112, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_callers.py", line 155, in _multicall
    return outcome.get_result()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_result.py", line 108, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_callers.py", line 80, in _multicall
    res = hook_impl.function(*args)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/runner.py", line 177, in pytest_runtest_call
    raise e
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/runner.py", line 169, in pytest_runtest_call
    item.runtest()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/python.py", line 1788, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_hooks.py", line 433, in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_manager.py", line 112, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_callers.py", line 116, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/pluggy/_callers.py", line 80, in _multicall
    res = hook_impl.function(*args)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/_pytest/python.py", line 194, in pytest_pyfunc_call
    result = testfunction(**testargs)
  File "/Users/milton.m/Programming/napari_lattice/plugin/tests/test_dock_widget.py", line 36, in test_plugin_initialize
    ui = _napari_lattice_widget_wrapper()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/napari_lattice/_dock_widget.py", line 1155, in _napari_lattice_widget_wrapper
    widget = LLSZWidget()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/core.py", line 204, in __init__
    self._convert_attributes_into_widgets()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/class_gui.py", line 289, in _convert_attributes_into_widgets
    format_error(e, _hist, name, attr)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/utils.py", line 84, in format_error
    raise e
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/class_gui.py", line 142, in _convert_attributes_into_widgets
    widget = attr()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/core.py", line 204, in __init__
    self._convert_attributes_into_widgets()
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/class_gui.py", line 289, in _convert_attributes_into_widgets
    format_error(e, _hist, name, attr)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/utils.py", line 90, in format_error
    raise construction_err from None
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/class_gui.py", line 283, in _convert_attributes_into_widgets
    self._fast_insert(n_insert, widget)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/_gui/class_gui.py", line 341, in _fast_insert
    self._widget._mgui_insert_widget(key, _widget)
  File "/opt/anaconda3/envs/napari-env/lib/python3.9/site-packages/magicclass/widgets/containers.py", line 70, in _mgui_insert_widget
    self._splitter.insertWidget(position, widget.native)
magicclass._gui.utils.MagicClassConstructionError: 

                LlszMenu (<class 'magicclass._gui._base._MagicTemplateMeta'>) <--- Error


                main_heading (<class 'magicgui.widgets.Label'>) <--- Error

RuntimeError: wrapped C/C++ object of type QLabel has been deleted
```

</details>

I believe this is another result of the issue discussed here: https://github.com/napari/napari/issues/4377. If I construct the class inside a factory function as follows, the problem goes away, but this is obviously not desirable:

```python
from magicclass import magicclass
from magicgui import magicgui, widgets

def make_x():
    @magicclass
    class X:
        main_heading = widgets.Label(value="foo")
    return X

def test_a(make_napari_viewer):
    viewer = make_napari_viewer()
    widget = make_x()()
    viewer.window.add_dock_widget(widget)

def test_b(make_napari_viewer):
    viewer = make_napari_viewer()
    widget = make_x()()
    viewer.window.add_dock_widget(widget)
```


Discussion / hints:
Could you try using `field`?

```python
from magicclass import magicclass, field

@magicclass
class X:
    main_heading = field("foo", widget_type="Label")
```

It seems like the QLabel widget is destroyed at the end of the first test to me.
 Great, this seems to fix it. Thanks for the quick response!

Fix patch:
diff --git a/magicclass/__init__.py b/magicclass/__init__.py
index 1bf5b0a8..07d4de20 100644
--- a/magicclass/__init__.py
+++ b/magicclass/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.7.6"
+__version__ = "0.7.7.dev0"
 
 from .core import (
     magicclass,
diff --git a/magicclass/_gui/_base.py b/magicclass/_gui/_base.py
index 5df87f2c..a86c3b47 100644
--- a/magicclass/_gui/_base.py
+++ b/magicclass/_gui/_base.py
@@ -80,7 +80,7 @@
     eval_attribute,
 )
 from magicclass.widgets import Separator, FreeWidget
-from magicclass.fields import MagicField, field, vfield
+from magicclass.fields import MagicField, field, vfield, FieldGroup
 from magicclass.signature import (
     ConfirmDict,
     MagicMethodSignature,
@@ -495,9 +495,9 @@ def _unwrap_method(
         copyto: list[str],
     ):
         """
-        This private method converts class methods that are wrapped by its child widget class
-        into widget in child widget. Practically same widget is shared between parent and child,
-        but only visible in the child side.
+        This private method converts class methods that are wrapped by its child widget
+        class into widget in child widget. Practically same widget is shared between
+        parent and child, but only visible in the child side.
 
         Parameters
         ----------
@@ -511,8 +511,8 @@ def _unwrap_method(
         Raises
         ------
         RuntimeError
-            If ``child_clsname`` was not found in child widget list. This error will NEVER be raised
-            in the user's side.
+            If ``child_clsname`` was not found in child widget list. This error will
+            NEVER be raised in the user's side.
         """
         if moveto is not None:
             matcher = copyto + [moveto]
@@ -576,58 +576,6 @@ def _unwrap_method(
                 f"{self.__class__.__name__}"
             )
 
-    @classmethod
-    def field(
-        cls,
-        obj=Undefined,
-        *,
-        name=None,
-        label=None,
-        widget_type=None,
-        options={},
-        record=True,
-    ):
-        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,
-        )
-
-    @classmethod
-    def vfield(
-        cls,
-        obj=Undefined,
-        *,
-        name=None,
-        label=None,
-        widget_type=None,
-        options={},
-        record=True,
-    ):
-        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,
-        )
-
     @contextmanager
     def config_context(
         self,
@@ -709,6 +657,18 @@ def _convert_an_attribute_into_widget(
                 )
             first_widget.bind(self)  # bind self to the first argument
 
+        elif isinstance(attr, Widget) and not isinstance(attr, FieldGroup):
+            warnings.warn(
+                f"Widget {name!r} is given as a class attribute. This is not "
+                "recommended, as it is shared between all the instances. "
+                "Please use a field or vfield instead.",
+                UserWarning,
+                stacklevel=2,
+            )
+            widget = attr
+
+        elif isinstance(attr, str) and attr == "separator":
+            widget = Separator()
         else:
             # convert class method into instance method
             widget = getattr(self, name, None)
diff --git a/rst/main/images/fig_1-5.png b/rst/main/images/fig_1-5.png
deleted file mode 100644
index 00eea12a..00000000
Binary files a/rst/main/images/fig_1-5.png and /dev/null differ
diff --git a/rst/main/quick_start.rst b/rst/main/quick_start.rst
index 6483953d..0e1167c2 100644
--- a/rst/main/quick_start.rst
+++ b/rst/main/quick_start.rst
@@ -90,8 +90,8 @@ Magic classes can also detect other ``magicgui``'s widgets.
 
 .. note::
 
-    I highly recommend using ``field`` function to create widgets in magic classes.
-    See :doc:`./basics/use_field`.
+    Since 0.7.7, using widgets directly in magic classes as class variables emits a warning,
+    since they should always put in ``field`` functions. See :doc:`./basics/use_field`.
 
 If a method is decorated with ``@magicgui``, it will directly added in the container widget,
 in place of a push button. This is natural because decorated methods are no longer functions,
@@ -264,30 +264,6 @@ widgets. You can prevent macro recording with ``do_not_record`` decorator for me
     ui.show()
 
 
-Make Document of Your Widgets Automatically
-===========================================
-
-It is usually time consuming to make a help menu of a widget all the way. You also have to keep
-it updated whenever UI changed.
-
-Magic class has a powerful help widget builder. You can create a help widget using ``build_help``
-function. The widget has a website-like layout and many information such as tooltips of widgets
-and parameter annotations of functions are summerized with rich texts.
-
-.. code-block:: python
-
-    from magicclass import magicclass, build_help
-
-    @magicclass
-    class Main:
-        # many functions and widgets here.
-        ...
-
-    help = build_help(self) # build_help returns a Widget object
-    help.show() # show widget
-
-.. image:: images/fig_1-5.png
-
 Build Command Palette Widget Automatically
 ==========================================
 


Test patch:
diff --git a/tests/test_construction.py b/tests/test_construction.py
index 280c280b..f40214fd 100644
--- a/tests/test_construction.py
+++ b/tests/test_construction.py
@@ -97,7 +97,7 @@ class Menu:
             def m1(self): ...
             sep0 = field(Separator)
             i = field(int)
-            sep1 = Separator()
+            sep1 = "separator"
             def m2(self, path: Path): ...
 
     ui = A()
@@ -111,7 +111,7 @@ class Menu:
             def m1(self): ...
             sep0 = field(Separator)
             i = field(int)
-            sep1 = Separator()
+            sep1 = "separator"
             def m2(self, path: Path): ...
 
     ui = A()