napari / napari

napari: a fast, interactive, multi-dimensional image viewer for python
https://napari.org
BSD 3-Clause "New" or "Revised" License
2.07k stars 410 forks source link

layer.events.current_symbol callback raises exception in when a widget is launched from the plugin menu #6865

Closed pnewstein closed 2 weeks ago

pnewstein commented 3 weeks ago

🐛 Bug Report

points.events.current_symbol callbacks which modify that points layer's symbols raises the following exception when launching a widget plugin from the plugins menu:

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/_qt/menus/plugins_menu.py:105, in PluginsMenu._add_plugin_actions.<locals>._add_toggle_widget(key=('bug-reproduce', 'Example QWidget'), hook_type='dock')
    102     return
    104 if hook_type == 'dock':
--> 105     self._win.add_plugin_dock_widget(*key)
        key = ('bug-reproduce', 'Example QWidget')
        self._win = <napari._qt.qt_main_window.Window object at 0x14f68b4d0>
        self = <napari._qt.menus.plugins_menu.PluginsMenu object at 0x1520d9bd0>
    106 else:
    107     self._win._add_plugin_function_widget(*key)

File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/_qt/qt_main_window.py:897, in Window.add_plugin_dock_widget(self=<napari._qt.qt_main_window.Window object>, plugin_name='bug-reproduce', widget_name='Example QWidget', tabify=False)
    894         wdg = wdg._magic_widget
    895     return dock_widget, wdg
--> 897 wdg = _instantiate_dock_widget(
        Widget = <class 'bug_reproduce._widget.ExampleQWidget'>
        self = <napari._qt.qt_main_window.Window object at 0x14f68b4d0>
    898     Widget, cast('Viewer', self._qt_viewer.viewer)
    899 )
    901 # Add dock widget
    902 dock_kwargs.pop('name', None)

File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/_qt/qt_main_window.py:1551, in _instantiate_dock_widget(wdg_cls=<class 'bug_reproduce._widget.ExampleQWidget'>, viewer=Viewer(camera=Camera(center=(0.0, 255.5, 255.5),...ouse_drag_gen={}, _mouse_wheel_gen={}, keymap={}))
   1546             break
   1547         # cannot look for param.kind == param.VAR_KEYWORD because
   1548         # QWidget allows **kwargs but errs on unknown keyword arguments
   1549 
   1550 # instantiate the widget
-> 1551 return wdg_cls(**kwargs)
        kwargs = {'napari_viewer': Viewer(camera=Camera(center=(0.0, 255.5, 255.5), zoom=1.4732421875, angles=(0.0, 0.0, 90.0), perspective=0.0, mouse_pan=True, mouse_zoom=True), cursor=Cursor(position=(255.0, 490.3561664807345, 609.8205802652776), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=3, ndisplay=2, last_used=0, range=((0.0, 1.0, 1.0), (0.0, 1.0, 1.0), (0.0, 1.0, 1.0)), current_step=(255, 255, 255), order=(0, 1, 2), axis_labels=('0', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Points layer 'Points' at 0x159a27910>], help='use <5> for transform, use <2> for add points, use <3> for select points', status={'layer_base': 'Points', 'source_type': '', 'plugin': '', 'coordinates': ' [255 490 610]'}, tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=True, mouse_move_callbacks=[], mouse_drag_callbacks=[], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x149a4ff60>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={})}
        wdg_cls = <class 'bug_reproduce._widget.ExampleQWidget'>

File ~/bug-reproduce/src/bug_reproduce/_widget.py:126, in ExampleQWidget.__init__(self=<bug_reproduce._widget.ExampleQWidget object>, napari_viewer=Viewer(camera=Camera(center=(0.0, 255.5, 255.5),...ouse_drag_gen={}, _mouse_wheel_gen={}, keymap={}))
    124 pointer_coords = [0, 0, 0]
    125 layer.add(coords=pointer_coords)
--> 126 layer.add(coords=pointer_coords)
        layer = <Points layer 'Points' at 0x159a27910>
        pointer_coords = [0, 0, 0]

File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/utils/_proxies.py:192, in CallablePublicOnlyProxy.__call__(self=<bound method Points.add of <Points layer 'Points'>>, *args=[], **kwargs={'coords': [0, 0, 0]})
    184 args = [
    185     arg.__wrapped__ if isinstance(arg, PublicOnlyProxy) else arg
    186     for arg in args
    187 ]
    188 kwargs = {
    189     k: v.__wrapped__ if isinstance(v, PublicOnlyProxy) else v
    190     for k, v in kwargs.items()
    191 }
--> 192 return self.create(self.__wrapped__(*args, **kwargs))
        args = []
        kwargs = {'coords': [0, 0, 0]}
        self = <bound method Points.add of <Points layer 'Points' at 0x159a27910>>

File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/layers/points/points.py:1990, in Points.add(self=<Points layer 'Points'>, coords=[0, 0, 0])
   1983 cur_points = len(self.data)
   1984 self.events.data(
   1985     value=self.data,
   1986     action=ActionType.ADDING,
   1987     data_indices=(-1,),
   1988     vertex_indices=((),),
   1989 )
-> 1990 self._set_data(np.append(self.data, np.atleast_2d(coords), axis=0))
        self = <Points layer 'Points' at 0x159a27910>
        np.append = <function append at 0x1051ec770>
        np = <module 'numpy' from '/Users/petern/micromamba/envs/napari-test/lib/python3.11/site-packages/numpy/__init__.py'>
        np.atleast_2d = <function atleast_2d at 0x104d4bef0>
        coords = [0, 0, 0]
   1991 self.events.data(
   1992     value=self.data,
   1993     action=ActionType.ADDED,
   1994     data_indices=(-1,),
   1995     vertex_indices=((),),
   1996 )
   1997 self.selected_data = set(np.arange(cur_points, len(self.data)))

File ~/micromamba/envs/napari-test/lib/python3.11/site-packages/napari/layers/points/points.py:606, in Points._set_data(self=<Points layer 'Points'>, data=<class 'numpy.ndarray'> (2, 3) float64)
    603 edge_width = np.repeat([new_edge_width], adding, axis=0)
    605 if len(self._symbol) > 0:
--> 606     new_symbol = copy(self._symbol[-1])
        self = <Points layer 'Points' at 0x159a27910>
        self._symbol = <class 'numpy.ndarray'> (1,) object
    607 else:
    608     new_symbol = self.current_symbol

File ~/micromamba/envs/napari-test/lib/python3.11/copy.py:84, in copy(x=<Symbol.DISC: 'disc'>)
     82 copier = getattr(cls, "__copy__", None)
     83 if copier is not None:
---> 84     return copier(x)
        copier = <method '__copy__' of 'ObjectProxy' objects>
        x = <Symbol.DISC: 'disc'>
     86 reductor = dispatch_table.get(cls)
     87 if reductor is not None:

NotImplementedError: object proxy must define __copy__()

💡 Steps to Reproduce

💡 Expected Behavior

Points should be added with no exception being raised

🌎 Environment

napari: 0.4.19.post1 Platform: macOS-13.5.2-arm64-arm-64bit System: MacOS 13.5.2 Python: 3.11.9 | packaged by conda-forge | (main, Apr 19 2024, 18:34:54) [Clang 16.0.6 ] Qt: 5.15.8 PyQt5: 5.15.9 NumPy: 1.26.4 SciPy: 1.13.0 Dask: 2024.4.2 VisPy: 0.14.2 magicgui: 0.8.2 superqt: 0.6.3 in-n-out: 0.2.0 app-model: 0.2.6 npe2: 0.7.5

OpenGL:

Screens:

Settings path:

💡 Additional Context

Interestingly, at napari/layers/points/points.py:606 (see included traceback), type(self.symbol[-1]) == <class 'napari.utils._proxies.PublicOnlyProxy'> while self.symbol[-1].__class__ = <enum 'Symbol'>

This exception does not occur when the plugin is launched from python; this bug is specific to when the widget is launched through the plugins menu

import bug_reproduce
widget = bug_reproduce._widget.ExampleQWidget(viewer)

When the plugin is launched from python: type(self.symbol[-1]) and self.symbol[-1].__class__ are both <enum 'Symbol'>

This bug is a regression as it does not occur in napari 0.4.18

DragaDoncila commented 3 weeks ago

@pnewstein thank you for the detailed write-up. I've been able to reproduce this issue on napari 0.4.19post1 but it seems to be fixed on main - are you able to install napari from main and confirm that?

pnewstein commented 2 weeks ago

@DragaDoncila You are correct. This issue is fixed on main. Thanks!