Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable magicgui decorator on class member functions #53

Closed
haesleinhuepf opened this issue Dec 14, 2020 · 7 comments · Fixed by #56
Closed

Enable magicgui decorator on class member functions #53

haesleinhuepf opened this issue Dec 14, 2020 · 7 comments · Fixed by #56

Comments

@haesleinhuepf
Copy link
Contributor

haesleinhuepf commented Dec 14, 2020

Hi Talley @tlambert03 ,

thanks again for your support recently on the forum. I think I have a related feature request. To simplify my code, it would be nice if one could use magicgui on class member functions. In this way, the function could have access to a broader context, which is not handled in the magicgui.

To illustrate what I mean, take a look at this code:

import napari

from magicgui import magicgui
from napari.layers import Image


class MyObject:
    def __init__(self):
        self.counter = 0

    @magicgui(auto_call=True)
    def process_image(self, image : Image, sigma: float = 5):
        self.counter = self.counter + sigma
        print(self)

# load data
from skimage.io import imread
image = imread('https://samples.fiji.sc/blobs.png')

# start up napari
with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(image, name='blobs')

    # generate a Graphical User Interface from the function above magically
    gui = MyObject().process_image.Gui()
    viewer.window.add_dock_widget(gui)

When executing it and increasing the sigma in the magic user interface, this error is thrown:

ERROR:root:Unhandled exception:
Traceback (most recent call last):
  File "C:\Users\rober\miniconda3\lib\site-packages\magicgui\core.py", line 532, in __call__
    value = self.func(**_kwargs)
  File "C:/structure/code/pyclesperanto_prototype/demo/napari_gui/napari_magicgui.py", line 17, in process_image
    self.counter = self.counter + sigma
AttributeError: 'str' object has no attribute 'counter'

Thus, the self variable is not correctly set in such a context. Do you think this could be implemented? I'm also open to other solutions if you see some.

Thanks!

Cheers,
Robert

@tlambert03
Copy link
Member

tlambert03 commented Dec 14, 2020

Yeah, that's a (good) legitimate feature request. possible, but will take a little doing.

The reason you're getting that error is that when the @magicgui decorator looks at the function signature, it has no idea what "type" self is, and so doesn't know what type of widget it needs to use, and falls back on a basic LineEdit (string) widget. Then when the function is actually called, self is not your myobject instance, but rather a simple string.

(If this seems weird, recall that self is just a convention, not a magical word, in python... It's just a regular positional argument for some "future" instance of the class)

class MyObject:
    def process(self):
        print(self)

m = MyObject()
# this "usual" usage ...
m.process()
# is basically just shorthand for this
MyObject.process(m)

so what you really need to decorate is the bound method... something like this:

class MyObject:
    def __init__(self):
        self.counter = 0.0

    def process_image(self, image: Image, sigma: float = 5):
        self.counter = self.counter + sigma
        print(self)

...
m = MyObject()
gui = magicgui(m.process_image, auto_call=True).Gui()
viewer.window.add_dock_widget(gui)

however, that will fail with AttributeError: 'method' object has no attribute '_widget' on the the current version of magicgui because, unlike with functions, you can't add custom attributes to bound methods (and magicgui currently depends on that)

Anyway, I think it's a great feature, and I have some ideas on how to detect whether we're decorating a (pre-bound) class method... but it will take a little doing.

side note: some reasonably big changes are coming in the next release with the merge of #43 ... so the .Gui() syntax in your example here will change soon.

@tlambert03
Copy link
Member

tlambert03 commented Dec 14, 2020

omg! it already works on the current master!! 🎉
(one of those super happy moments when something you've worked on for a while just magically fixes a different problem)

Try checking out the master branch here and using this code:

import napari
from napari.layers import Image
from skimage.io import imread

from magicgui import magicgui


class MyObject:
    def __init__(self):
        self.counter = 0.0

    def process_image(self, image: Image, sigma: float = 5):
        self.counter += sigma
        print(self.counter, sigma)


image = imread("https://samples.fiji.sc/blobs.png")

# start up napari
with napari.gui_qt():
    viewer = napari.Viewer()
    viewer.add_image(image, name="blobs")

    m = MyObject()
    gui = magicgui(m.process_image, auto_call=True)
    viewer.window.add_dock_widget(gui)

@tlambert03
Copy link
Member

side note: for simple urls like that, you can use viewer.open(url) instead of viewer.add_image(skimage.io.imread(url))

@haesleinhuepf
Copy link
Contributor Author

That sounds great! :-)

Try checking out the master branch here and using this code:

Hm. I just cloned magicgui master and installed it and then tried the code you posted. It shows napari, but doesn't show the magicgui. It throws this error instead:

(te_oki) C:\structure\code\pyclesperanto_prototype\demo\napari_gui>ipython --gui=qt napari_magicgui.py
c:\structure\code\magicgui\magicgui\widgets\_bases.py:675: FutureWarning:

As of magicgui 0.2.0, a `choices` callable may accept only a single positional
argument (an instance of `magicgui.widgets.CategoricalWidget`), and must return
an iterable (the choices to show). Function 'get_layers' accepts 2 arguments.
In the future, this will raise an exception.

  warnings.warn(
ERROR:root:Unhandled exception:
Traceback (most recent call last):
  File "c:\users\rober\miniconda3\lib\site-packages\napari\_qt\event_loop.py", line 79, in gui_qt
    yield app
  File "C:\structure\code\pyclesperanto_prototype\demo\napari_gui\napari_magicgui.py", line 26, in <module>
    viewer.window.add_dock_widget(gui)
  File "c:\users\rober\miniconda3\lib\site-packages\napari\_qt\qt_main_window.py", line 565, in add_dock_widget
    dock_widget = QtViewerDockWidget(
  File "c:\users\rober\miniconda3\lib\site-packages\napari\_qt\widgets\qt_viewer_dock_widget.py", line 82, in __init__
    self.widget = combine_widgets(
  File "c:\users\rober\miniconda3\lib\site-packages\napari\_qt\utils.py", line 187, in combine_widgets
    raise TypeError('"widget" must be a QWidget or a sequence of QWidgets')
TypeError: "widget" must be a QWidget or a sequence of QWidgets

Shall I update any other dependency?

@tlambert03
Copy link
Member

ah! yes you'll probably need the most recent napari master too (with napari/napari#1994)

@haesleinhuepf
Copy link
Contributor Author

Yes, Now, it works! Thanks for implementing this feature request in negative time 🥳

@tlambert03
Copy link
Member

I'm actually going to leave this issue open, since the decorator syntax still won't work, but it could be made to work and I like the idea.

@tlambert03 tlambert03 reopened this Dec 14, 2020
@tlambert03 tlambert03 changed the title Enable magicgui on class member functions Enable magicgui decorator on class member functions Dec 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants