Coder Social home page Coder Social logo

hanjinliu / magic-class Goto Github PK

View Code? Open in Web Editor NEW
35.0 35.0 5.0 28.97 MB

Generate multifunctional and macro recordable GUIs from Python classes

Home Page: https://hanjinliu.github.io/magic-class/

License: BSD 3-Clause "New" or "Revised" License

Python 99.97% Just 0.03%
gui qt

magic-class's Introduction

Hi there ๐Ÿ‘‹

I'm a PhD student in biophysics. I'm especially interested in image analysis and GUI development.

hanjinliu's GitHub stats Top Langs

magic-class's People

Contributors

hanjinliu avatar jaimergp avatar marcomusy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

magic-class's Issues

MagicClassConstructionError is messy

During conversion from a class into a GUI, MagicClassConstructionError is raised if something went wrong but the error message is not productive. Need handling of traceback.

Preview of function call

We usually need a preview before running a function when it requires such as csv file as an input. However, FunctionGui only support one call button. Currently we have to define a new magicclass to make a preview-implemented GUI.

Ideas

  • Use @mark_preview wrapper in the following way

    def func(self, path: Path, x: float):
        # do something
    
    @mark_preview(func)
    def _func_prev(self, path: Path, x: float):
        # preview

Problem to run any example

Hi,

I tried to run any of the example on my Python 3.8 on a Ubuntu 20.04 but get always errors like this:

/home/sebi06/miniconda3/envs/imageanalysis_czi/bin/python /datadisk1/tuxedo/Github/magic-class/examples/advanced/graph_viewer.py
Traceback (most recent call last):
  File "/datadisk1/tuxedo/Github/magic-class/examples/advanced/graph_viewer.py", line 1, in 
    from magicclass import magicclass, field, click, magicmenu, magiccontext
  File "/datadisk1/tuxedo/Github/magic-class/magicclass/__init__.py", line 3, in 
    from .core import (magicclass, magicmenu, magiccontext, WidgetType, Parameters, Bound, 
  File "/datadisk1/tuxedo/Github/magic-class/magicclass/core.py", line 11, in 
    from macrokit import Expr, register_type, Head
  File "/home/sebi06/miniconda3/envs/imageanalysis_czi/lib/python3.8/site-packages/macrokit/__init__.py", line 4, in 
    from .expression import Expr, Head, symbol
  File "/home/sebi06/miniconda3/envs/imageanalysis_czi/lib/python3.8/site-packages/macrokit/expression.py", line 8, in 
    from ._validator import validator
  File "/home/sebi06/miniconda3/envs/imageanalysis_czi/lib/python3.8/site-packages/macrokit/_validator.py", line 37, in 
    validator = Validator[Head, list[Any]]()
TypeError: 'type' object is not subscriptable

Process finished with exit code 1

Any hint what I am doing wrong here?

Trouble creating magic-class with two functions, each decorated with @magicgui

We are updating some older magic-class code we used to control a microscope with napari. In the process, we discovered that our GUI is no longer being rendered correctly in napari. That led to some testing and questions on my part.

My main question is how do I create two functions in a magic-class, each decorator with magicgui options?

For example, I would expect this code,

from magicclass import magicclass
from magicgui import magicgui

# Test magicgui options      
@magicclass(labels=False,name='Test two magicgui')
class TestTwoMagicgui:

    # initialize
    def __init__(self):
        self.value_1 = 25
        self.value_2 = 400
        self.value_3 = 0

    @magicgui(layout='horizontal',
              value_1={"widget_type": "FloatSpinBox", "min": 5, "max": 225, "label": 'Value 1'},
              call_button = 'Set value 1')
    def set_value_1(self, value_1 = 25.0):
        """
        Set value 1

        :param value_1: float

        :return None:
        """

        self.value_1=value_1

    @magicgui(layout='horizontal',
              value_2 = {"widget_type": "SpinBox",  "min": 0, "max": 100, "label": 'Value 2'},
              value_3 = {"widget_type": "FloatSpinBox", "min": 0.0, "max": 200.0, "label": 'Value 3'},
              call_button = "Set values 2 and 3"
             )
    def set_values_2_3(self, value_2= 50, value_3 = 10.0):
        """
        Set values 2 and 3

        :param value_2: int
        :param value_3: float

        :return None:
        """

        self.value_2 = value_2
        self.value_3 = value_3

if __name__ == "__main__":
    ui = TestTwoMagicgui()
    ui.show()

To show a GUI with two widgets. The first widget should have one entry and the second widget have two entries. Instead, I get a blank GUI that I have to expand to see the title of the window (screenshot attached).

image

Do you have any suggestions about how to properly setup the above magic-class?

Thanks!

`FieldGroup.connect` is not working

Following code is not working.

from magicclass import magicclass, FieldGroup, vfield

class G(FieldGroup):
    a = vfield(int)
    b = vfield(str)

@magicclass
class A:
    g = G()
    @g.connect
    def _f(self, x):
        print(x)

ui = A()
ui.show()

Table widget parameters and methods

Hi @hanjinliu, I have a Table widget in my magicclass GUI created using field. I would like to limit the maximum value (int or float) that can be entered in any of the table's cells. Also, I would like to update a plot embedded in the GUI every time any of the table's values is changed.
I looked at the documentation of magicgui/magiclass but could not find info on this widget.
Could you point at something regarding this?
Thanks!

Return annotation is not supported

Currently magic-class does not support return annotation callback, such as:

from magicclass import magicclass
from napari.types import ImageData

@magicclass
class A:
    def f(self, shape = (128, 128)) -> ImageData:
        return np.random.random(shape)

The biggest problem is macro recording.

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

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

`TypeError: <class 'magicclass._gui._base.MagicTemplate'> is not a generic class` in Python 3.8

There seems to be a bug that only appears in Python 3.8. You can reproduce it as follows:

# Get Python 3.8
conda create -n py38 'python<3.9'
conda activate py38

# Install deps
pip install magicgui[pyqt5] magicclass

# Trigger bug
python3.8 -c 'import magicclass'

Result:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/__init__.py", line 3, in <module>
    from .core import (
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/core.py", line 9, in <module>
    from magicclass._gui.class_gui import (
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/_gui/__init__.py", line 1, in <module>
    from ._base import BaseGui, MagicTemplate
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/_gui/_base.py", line 814, in <module>
    class BaseGui(MagicTemplate[_W]):
  File "/opt/anaconda3/envs/py38/lib/python3.8/typing.py", line 261, in inner
    return func(*args, **kwds)
  File "/opt/anaconda3/envs/py38/lib/python3.8/typing.py", line 897, in __class_getitem__
    _check_generic(cls, params)
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/typing_extensions.py", line 155, in _check_generic
    raise TypeError(f"{cls} is not a generic class")
TypeError: <class 'magicclass._gui._base.MagicTemplate'> is not a generic class

I understand the error. It thinks that class BaseGui(MagicTemplate[_W]) is treating MagicTemplate like a generic class when instead it just defines __getitem__() for another reason. However, I have no idea why the Python version affects this.

"enabled" parameter

Enabled parameter does not seem to work as expected? For instance, in the code below I would expect the PushButton to display as disabled:

@magicclass
class Foo:
    a = field(PushButton,options={"enabled":False})

if __name__=="__main__":
    ui = Foo()
    ui.show()

however it displays as a clickable button:
Screenshot 2022-03-24 at 08 51 05

is this behaviour expected?

Warning when launching GUI

Hi,

I get this warning whenever I use my magic-class built GUI - do you know where it comes from?

qt.qpa.window: <QNSWindow: 0x137414bf0; contentView=<QNSView: 0x1374147e0; QCocoaWindow(0x60000382cd10, window=QWidgetWindow(0x600002b05260, name="magicgui.QPushButtonWindow"))>> has active key-value observers (KVO)! These will stop working now that the window is recreated, and will result in exceptions when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug.
qt.qpa.window: <QNSWindow: 0x13740fac0; contentView=<QNSView: 0x13740f6b0; QCocoaWindow(0x60000382ca50, window=QWidgetWindow(0x600002b3fc00, name="Prepare ExperimentWindow"))>> has active key-value observers (KVO)! These will stop working now that the window is recreated, and will result in exceptions when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug.
qt.qpa.window: <QNSWindow: 0x13744cd50; contentView=<QNSView: 0x13744c860; QCocoaWindow(0x60000382cd10, window=QWidgetWindow(0x600002b07600, name="magicgui.QPushButtonWindow"))>> has active key-value observers (KVO)! These will stop working now that the window is recreated, and will result in exceptions when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug.
qt.qpa.window: <QNSWindow: 0x114f067a0; contentView=<QNSView: 0x114f06390; QCocoaWindow(0x600003830000, window=QWidgetWindow(0x600002b2e100, name="VisualizationWindow"))>> has active key-value observers (KVO)! These will stop working now that the window is recreated, and will result in exceptions when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug.
qt.qpa.window: <QNSWindow: 0x137457e70; contentView=<QNSView: 0x137457a60; QCocoaWindow(0x60000382cfd0, window=QWidgetWindow(0x600002b1b960, name="Further OptionsWindow"))>> has active key-value observers (KVO)! These will stop working now that the window is recreated, and will result in exceptions when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug.

thanks a lot,

Giuseppe

using magicclass and popup boxes

Hi @hanjinliu
Thanks for this amazing tool. I've been enjoying playing around with it.

I was looking at this example using pandas and seaborn
When clicking each button, a popup window appears.
image

Instead of a popup window, how can I have the fields appear in the main window (in this case, above the Swarm Plot button)? Also, my aim is that once the user enters the values within the main window and clicks the Swarm Plot button it just plots it using the values without an additional box?

I can see you've used field or magicgui before, but where do I add that if I was following a similar workflow as your seaborn example?

Cheers
Pradeep

Listen to a field change in a sibling magicclass

If I have classes like this:

@magicclass
class A:
    @magicclass
    class B:
        b = vfield(...)
    @magicclass
    class C:
        def on_b_change(self, value):
            ...

How can I implement on_b_change so that it will trigger when B.b actually changes? I don't think that @wraps works here, but I could be wrong. Also I tried using __magicclass_parent__ in the __post_init__ to search up the tree for the field, but the parent relationship doesn't seem to exist at that point.

Replace `macrokit` with `collections_undo`

collections-undo implements undo/redo to any functions.
Although I have not implemented it yet, the recorded undo stack can also be utilized for macro generation.

A big benefit of this replacement is that implementation of undo/redo in custom GUI will be much easier.

Release 0.8.0

Hi, I noticed you added compatibility for magicgui 0.8.0, but it isn't yet on PyPI. I think this means that installing the latest version of both libraries will fail (?)

Type signature for `@magicclass`

I found my type checker (VSCode's pyright) got very confused by @magicclass, and basically treated anything done to the decorated class as an error. In reality the type signature is not that complex, and I created the following type stub that I think captures it fairly well. The only issue is that I don't think it's possible to tell Python that decorated classes become a ClassGui but also retain their existing fields. Nonetheless, I think this would be an improvement. Would you be interested in this as a PR?

# If the user does pass a class into the decorator
@overload
def magicclass(
    class_: type,
    *,
    layout: str | None = ...,
    labels: bool = ...,
    name: str | None = ...,
    visible: bool | None = ...,
    close_on_run: bool | None = ...,
    popup_mode: PopUpModeStr | PopUpMode = ...,
    error_mode: ErrorModeStr | ErrorMode = ...,
    widget_type: WidgetTypeStr | WidgetType = ...,
    icon: Any = ...,
    stylesheet: str | StyleSheet | None = ...,
    properties: dict[str, Any] | None = ...,
    record: bool = ...,
    symbol: str = ...,
) -> type[ClassGui]:
    ...

# If the user doesn't pass a class into the decorator
@overload
def magicclass(
    *,
    layout: str | None = ...,
    labels: bool = ...,
    name: str | None = ...,
    visible: bool | None = ...,
    close_on_run: bool | None = ...,
    popup_mode: PopUpModeStr | PopUpMode = ...,
    error_mode: ErrorModeStr | ErrorMode = ...,
    widget_type: WidgetTypeStr | WidgetType = ...,
    icon: Any = ...,
    stylesheet: str | StyleSheet | None = ...,
    properties: dict[str, Any] | None = ...,
    record: bool = ...,
    symbol: str = ...,
) -> Callable[[type], type[ClassGui]]:
    ...

Implement undo

Implementing undo is very time consuming.
I might be possible to provide some consistent workflow to do that in magic-class, probably by using macro.

Too deep nesting causes construction error.

I have no idea why it's happening but nesting >3 classes is not allowed.

from magicclass import magicclass

@magicclass
class A:
    @magicclass
    class B:
        @magicclass
        class C:
            @magicclass 
            class D:
                def f(self):...
                
    @B.C.D.wraps
    def f(self):
        print(self)
ui = A()
ui.show()

Explicitly use `show(run=True)` in magicclass applications.

Related to #3. In current implementation, magic classes automatically determine whether to start QApplication, but it seems that it should be specified by the user's side.

The .py files will look like:

if __name__ == "__main__":
    ui = MyGui()
    ui.show(run=True)
  • This change does not affect those apps using magic-class along with napari, as napari starts the event loop.
  • All the examples should be updated to show(run=True).
  • magic-class should implement a bit more than magicgui to allow users calling show() in the IPython runtime.

`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.

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:

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

I believe this is another result of the issue discussed here: napari/napari#4377. If I construct the class inside a factory function as follows, the problem goes away, but this is obviously not desirable:

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)

Help with crating child class containing Matplotlib plots

Hi,

I am trying to create a child class called PlotData (groupbox) containing 2 plots. This is my code:

#Child
	@magicclass(name="Plots",layout="horizontal",widget_type="groupbox")
	class PlotData: 
		#plot widgets in child class 
		plt = field(Figure, options={"nrows": 1, "ncols": 1})
		plt1 = field(Figure, options={"nrows": 1, "ncols": 1})
		def plot_storage_modulus(self):
			self.plt.axes[0].set_yscale('log')
			self.plt.axes[0].set_xscale('log')
			for i in range(self.num_tests-1):
				#set log scale
				self.plt.axes[0].plot(self.data['strain [%]'][i], self.data['storage modulus [Pa]'][i], '-s')
				self.plt.draw()
		
		def plot_loss_modulus(self):
			self.plt1.axes[0].set_yscale('log')
			self.plt1.axes[0].set_xscale('log')
			for i in range(self.num_tests-1):
				#set log scale
				self.plt1.axes[0].plot(self.data['strain [%]'][i], self.data['loss modulus [Pa]'][i], '-o')
				self.plt1.draw()
	

	@PlotData.wraps
	def plot_storage_modulus(self):
	      self.plt.axes[0].set_yscale('log')
	      self.plt.axes[0].set_xscale('log')
	      for i in range(self.num_tests-1):
		      #set log scale
		      self.plt.axes[0].plot(self.data['strain [%]'][i], self.data['storage modulus [Pa]'][i], '-s')
		      self.plt.draw()

	@PlotData.wraps
	def plot_loss_modulus(self):
		self.plt1.axes[0].set_yscale('log')
		self.plt1.axes[0].set_xscale('log')
		for i in range(self.num_tests-1):
			#set log scale
			self.plt1.axes[0].plot(self.data['strain [%]'][i], self.data['loss modulus [Pa]'][i], '-o')
			self.plt1.draw()

however I get an error message saying that the "ParentClass has no attribute plt". This makes sense as the plt field was defined inside the child class PlotData. How can I avert this issue? If I define the plf field in the parent class, then 4 plots appear (2 from the parent and 2 from the child). Thanks a lot :)

`Error: 'NoneType' object has no attribute '__magicclass_parent__'` with a common parent class

Following the docs on inheriting magicclasses, I tried to factor a common field out into a parent class, with its own self-contained logic using @connect. However, I've identified that, if you have two such classes that use the same parent class, you get an unusual fatal error:

from magicclass import magicclass, vfield, MagicTemplate

class Parent(MagicTemplate):
    foo = vfield(False)
    
    @foo.connect
    def _foo_changed(self) -> None:
        print("Foo changed")

@magicclass
class Container:
    @magicclass
    class ChildA(Parent):
        bar = vfield(int)

    @magicclass
    class ChildB(Parent):
        bar = vfield(int)

Container().show()

When you check one of the foo boxes, you get:

Traceback (most recent call last):
  File "src/psygnal/_signal.py", line 980, in _run_emit_loop
  File "src/psygnal/_weak_callback.py", line 288, in cb
  File "/opt/anaconda3/lib/python3.9/site-packages/magicclass/fields/_define.py", line 40, in _callback
    current_self = current_self.__magicclass_parent__
AttributeError: 'NoneType' object has no attribute '__magicclass_parent__'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/opt/anaconda3/lib/python3.9/site-packages/magicgui/widgets/bases/_value_widget.py", line 65, in _on_value_change
    self.changed.emit(value)
  File "src/psygnal/_signal.py", line 935, in emit
  File "src/psygnal/_signal.py", line 982, in _run_emit_loop
psygnal._exceptions.EmitLoopError: calling <psygnal._weak_callback._StrongFunction object at 0x7fa2b9d00640> with args=(True,) caused AttributeError: 'NoneType' object has no attribute '__magicclass_parent__'.

Partialize macro recording if `partial` is used

If partial(self.func, xxx) is appended, it should recorded as self.func(xxx, ...).
This makes implementation of functions such as "open recent" or "repeat with last parameter set" very simple.

Support GUI-based coding

Background

Sometimes, especially in the field of image analysis, an analysis workflow is composed of several time consuming steps (each takes ~10 sec to a few minutes). Of course we can avoid click-and-wait cycle by coding the whole procedure in IPython or Jupyter but it is not as easy as using the GUI and we may often be bothered by some careless errors or looking for the function definition.

Idea

The macro recording system of magic-class is currently used for better tracking of what people did, providing high reproducibility of data analysis. In comparison to this, we can also utilize the recorded macro as a template of some analysis workflow. It would be very nice if we can enter a "coding mode" and during that mode all the functions will not be executed on pressing the "Run" button but macro is still recorded (in a separated place from the macro attribute the main widget has).

Example

from pathlib import Path
from magicclass import magicclass, field

@magicclass
class A:
    params = field(int)
    def load_data(self, path: Path):
        """load and store a data in this widget."""
    def process_data(self, x: float):
        """some data processing things."""
    def show_data(self):
        """"visualize the loaded data.""

ui = A()
ui.show()

After entering the "coding mode" and use the GUI as if you are actually doing something, the "temporary" macro will be like

ui.load_data(".../path/to/something.txt")
ui.params.value = 5
ui.process_data(2.0)
ui.show_data()

and this could be used as a template for batch processing like

from glob import glob
for path in glob(".../**/*/txt"):
    ui.load_data(path)
    ui.params.value = 5
    ui.process_data(2.0)
    ui.show_data()

After having lunch while waiting for this, the recorded macro will be

ui.load_data(".../path/to/data1.txt")
ui.params.value = 5
ui.process_data(2.0)
ui.show_data()
ui.load_data(".../path/to/data2.txt")
ui.params.value = 5
ui.process_data(2.0)
ui.show_data()
ui.load_data(".../path/to/data3.txt")
ui.params.value = 5
ui.process_data(2.0)
ui.show_data()
# ... and so on

How can this be done?

Here's some ideas to do this.

  • Temporarily unbind all the signals that will trigger any methods defined in the class during coding mode.
  • Make a copy of the GUI class and overwrite all the methods.

Input widgets for class attributes with type declarations as in magicgui?

Dear @hanjinliu ,

thanks, this looks great.

One question: is it also possible to keep input widgets for numbers, data, etc. as in magicgui
or is it currently restricted to generating buttons that trigger method calls?

Maybe these could be class attributes with a magicgu-like wrapper?

If we can set properties of the class that could be quite useful.

move_to_center failing in certain cases

just a heads up. the recent move_to_center commit is failing in certain cases (seen in magicgui tests)... will remove the magic-class tests for now:

platform linux -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0 -- /opt/hostedtoolcache/Python/3.10.6/x64/bin/python
[22](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:23)
cachedir: .pytest_cache
[23](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:24)
PyQt5 5.15.7 -- Qt runtime 5.15.2 -- Qt compiled 5.15.2

_______________________________ test_progressbar _______________________________
[163](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:164)
CALL ERROR: Exceptions caught in Qt event loop:
[164](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:165)
________________________________________________________________________________
[165](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:166)
Traceback (most recent call last):
[166](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:167)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qthreading.py", line 793, in init_pbar
[167](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:168)
    pbar.show()
[168](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:169)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qthreading.py", line 330, in show
[169](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:170)
    move_to_screen_center(self._CONTAINER)
[170](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:171)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qt.py", line 77, in move_to_screen_center
[171](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:172)
    qwidget.move(screen_center() - qwidget.rect().center())
[172](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:173)
  File "/opt/hostedtoolcache/Python/3.10.6/x64/lib/python3.10/site-packages/magicgui/widgets/_bases/container_widget.py", line 88, in __getattr__
[173](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:174)
    return object.__getattribute__(self, name)
[174](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:175)
AttributeError: 'ScrollableContainer' object has no attribute 'move'
[175](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:176)
________________________________________________________________________________
[176](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:177)
----------------------------- Captured stderr call -----------------------------
[177](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:178)
Exceptions caught in Qt event loop:
[178](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:179)
________________________________________________________________________________
[179](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:180)
Traceback (most recent call last):
[180](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:181)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qthreading.py", line 793, in init_pbar
[181](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:182)
    pbar.show()
[182](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:183)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qthreading.py", line 330, in show
[183](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:184)
    move_to_screen_center(self._CONTAINER)
[184](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:185)
  File "/home/runner/work/magicgui/magicgui/magic-class/magicclass/utils/qt.py", line 77, in move_to_screen_center
[185](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:186)
    qwidget.move(screen_center() - qwidget.rect().center())
[186](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:187)
  File "/opt/hostedtoolcache/Python/3.10.6/x64/lib/python3.10/site-packages/magicgui/widgets/_bases/container_widget.py", line 88, in __getattr__
[187](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:188)
    return object.__getattribute__(self, name)
[188](https://github.com/napari/magicgui/actions/runs/3077835365/jobs/4972976813#step:7:189)
AttributeError: 'ScrollableContainer' object has no attribute 'move'

NameError: name 'pyqtgraph' is not defined

Hi
I'm using magicclass for writing a UI for napari plugin and it was working fine. But, recently I amgetting this error:

NameError: name 'pyqtgraph' is not defined

Does the error log help for debugging?

Error Log


NameError                                 Traceback (most recent call last)
~\.conda\envs\llsz_napari\lib\site-packages\napari_plugin_engine\manager.py in _load_and_register(self=, mod_name='llsz', plugin_name='llsz_napari')
    317         try:
--> 318             module = load(mod_name)
        module = undefined
        global load = 
        mod_name = 'llsz'
    319             if self.is_registered(module):

~\.conda\envs\llsz_napari\lib\site-packages\napari_plugin_engine\manager.py in load(value='llsz')
   1041         raise ValueError(f"malformed entry point string: {value}")
-> 1042     module = importlib.import_module(match.group('module'))
        module = undefined
        global importlib.import_module = 
        match.group = 
   1043     attrs = filter(None, (match.group('attr') or '').split('.'))

~\.conda\envs\llsz_napari\lib\importlib\__init__.py in import_module(name='llsz', package=None)
    126             level += 1
--> 127     return _bootstrap._gcd_import(name[level:], package, level)
        global _bootstrap._gcd_import = 
        name = 'llsz'
        level = 0
        package = None
    128 

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap.py in _gcd_import(name='llsz', package=None, level=0)

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap.py in _find_and_load(name='llsz', import_=)

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap.py in _find_and_load_unlocked(name='llsz', import_=)

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap.py in _load_unlocked(spec=ModuleSpec(name='llsz', loader=

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap_external.py in exec_module(self=, module=)

~\.conda\envs\llsz_napari\lib\importlib\_bootstrap.py in _call_with_frames_removed(f=, *args=( at 0x00000185D237F240, fil...z_repo\llsz_napari\src\llsz\__init__.py", line 2>, {'__builtins__': {'ArithmeticError': , 'AssertionError': , 'AttributeError': , 'BaseException': , 'BlockingIOError': , 'BrokenPipeError': , 'BufferError': , 'BytesWarning': , 'ChildProcessError': , 'ConnectionAbortedError': , ...}, '__cached__': r'd:\onedrive - wehi.edu.au\wehi_projects\lightshe...pari\src\llsz\__pycache__\__init__.cpython-38.pyc', '__doc__': None, '__file__': r'd:\onedrive - wehi.edu.au\wehi_projects\lightsheet\llsz_repo\llsz_napari\src\llsz\__init__.py', '__loader__': , '__name__': 'llsz', '__package__': 'llsz', '__path__': [r'd:\onedrive - wehi.edu.au\wehi_projects\lightsheet\llsz_repo\llsz_napari\src\llsz'], '__spec__': ModuleSpec(name='llsz', loader=

d:\onedrive - wehi.edu.au\wehi_projects\lightsheet\llsz_repo\llsz_napari\src\llsz\__init__.py in 
      6 #from ._writer import napari_get_writer, napari_write_image
----> 7 from llsz.ui import napari_experimental_provide_dock_widget
        global llsz.ui = undefined
        global napari_experimental_provide_dock_widget = undefined
      8 #from llsz.ui_widget_test import napari_experimental_provide_dock_widget

d:\onedrive - wehi.edu.au\wehi_projects\lightsheet\llsz_repo\llsz_napari\src\llsz\ui.py in 
      6 
----> 7 from magicclass.wrappers import set_design
        global magicclass.wrappers = undefined
        global set_design = undefined
      8 from magicgui import magicgui

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\__init__.py in 
      2 
----> 3 from .core import (
        global core = undefined
        global magicclass = undefined
        global magicmenu = undefined
        global magiccontext = undefined
        global magictoolbar = undefined
        global Parameters = undefined
        global Bound = undefined
        global build_help = undefined
      4     magicclass,

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\core.py in 
     12 
---> 13 from .gui.class_gui import (
        global gui.class_gui = undefined
        global ClassGuiBase = undefined
        global ClassGui = undefined
        global GroupBoxClassGui = undefined
        global MainWindowClassGui = undefined
        global SubWindowsClassGui = undefined
        global ScrollableClassGui = undefined
        global DraggableClassGui = undefined
        global ButtonClassGui = undefined
        global CollapsibleClassGui = undefined
        global HCollapsibleClassGui = undefined
        global SplitClassGui = undefined
        global TabbedClassGui = undefined
        global StackedClassGui = undefined
        global ToolBoxClassGui = undefined
        global ListClassGui = undefined
     14     ClassGuiBase,

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\gui\__init__.py in 
----> 1 from ._base import BaseGui, MagicTemplate
        global _base = undefined
        global BaseGui = undefined
        global MagicTemplate = undefined
      2 from .class_gui import ClassGui
      3 from .menu_gui import MenuGui, MenuGuiBase, ContextMenuGui

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\gui\_base.py in 
     22 from .keybinding import as_shortcut
---> 23 from .mgui_ext import AbstractAction, Action, FunctionGuiPlus, PushButtonPlus, _LabeledWidgetAction, mguiLike
        global mgui_ext = undefined
        global AbstractAction = undefined
        global Action = undefined
        global FunctionGuiPlus = undefined
        global PushButtonPlus = undefined
        global _LabeledWidgetAction = undefined
        global mguiLike = undefined
     24 from .utils import get_parameters, define_callback

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\gui\mgui_ext.py in 
     14 from matplotlib.colors import to_rgb
---> 15 from ..widgets import Separator
        global widgets = undefined
        global Separator = undefined
     16 

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\widgets\__init__.py in 
     42 
---> 43 from .qtgraph import *
        global qtgraph = undefined
     44 from .napari import *

~\.conda\envs\llsz_napari\lib\site-packages\magicclass\widgets\qtgraph\__init__.py in 
     31 
---> 32 del pyqtgraph
        global pyqtgraph = undefined
     33 

NameError: name 'pyqtgraph' is not defined

The above exception was the direct cause of the following exception:

PluginImportError                         Traceback (most recent call last)
~\.conda\envs\llsz_napari\lib\site-packages\napari_plugin_engine\manager.py in discover(self=, path=None, entry_point=None, prefix=None, ignore_errors=True)
    262 
    263             try:
--> 264                 if self._load_and_register(mod_name, name):
        self._load_and_register = >
        mod_name = 'llsz'
        name = 'llsz_napari'
    265                     count += 1
    266                     self._id_counts[name] = 1

~\.conda\envs\llsz_napari\lib\site-packages\napari_plugin_engine\manager.py in _load_and_register(self=, mod_name='llsz', plugin_name='llsz_napari')
    320                 return None
    321         except Exception as exc:
--> 322             raise PluginImportError(
        global PluginImportError = 
        plugin_name = 'llsz_napari'
        global cause = undefined
        exc = undefined
    323                 f'Error while importing module {mod_name}',
    324                 plugin_name=plugin_name,

PluginImportError: Error while importing module llsz
================================================================================

`FunctionGuiTester` and error handling

FunctionGuiTester is super helpful. However what I was hoping would happen is that when I use .call(), it would throw an exception and thereby cause my tests to fail when it encountered an error. Instead, it seems to show the normal GUI error handler (not sure if this is QT or Napari's doing):
image

Is there a way to mock the error handler to make it throw an exception?

`FieldGroup.parent` typing

Widget.parent's type annotation indicates that it returns a magicgui.widgets.Widget, but at least for a FieldGroup, it seems to instead return a QWidget:

> isinstance(self, FieldGroup)
True
> isinstance(self.parent, Widget)
False
> type(self.parent)
<class 'PyQt5.QtWidgets.QStackedWidget'>

callbacks in nested classes

Hi
I am trying to use callbacks within magicclass and running into errors..

When I try this, it works with no problem:

from magicclass import magicclass, vfield

@magicclass
class MyClass:
        a = vfield(int, options={"value": 30},name = "Deskew Angle")

        @a.connect
        def _callback(self):
            print("value changed!",self.a)
ui = MyClass()
ui.show()

However, when I use nested classes and try to use callbacks, I get an error:

from magicclass import magicclass, vfield

@magicclass
class MyClass:
    @magicclass
    class MyClass1:
        a = vfield(int, options={"value": 30},name = "Deskew Angle")

        @a.connect
        def _callback(self):
            print("value changed!",self.a)
ui = MyClass()
ui.show()

ERROR:


ValueError Traceback (most recent call last)
D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\class_gui.py in _convert_attributes_into_widgets(self)
102 # If MagicField is given by field() function.
--> 103 widget = self._create_widget_from_field(name, attr)
104

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\class_gui.py in _create_widget_from_field(self, name, fld)
59 # funcname = callback.name
---> 60 widget.changed.connect(define_callback(self, callback))
61

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\utils.py in define_callback(self, callback)
12 def define_callback(self: BaseGui, callback: Callable):
---> 13 clsname, funcname = callback.qualname.split(".")
14 def _callback():

ValueError: too many values to unpack (expected 2)

The above exception was the direct cause of the following exception:

MagicClassConstructionError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_23928/3118444370.py in
10 def _callback(self):
11 print("value changed!",self.a)
---> 12 ui = MyClass()
13 ui.show()

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\core.py in init(self, *args, **kwargs)
205 )
206 super(oldclass, self).init(*args, **kwargs)
--> 207 self._convert_attributes_into_widgets()
208
209 if hasattr(self, _POST_INIT):

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\class_gui.py in _convert_attributes_into_widgets(self)
213 if isinstance(e, MagicClassConstructionError):
214 e.args = (f"\n{hist_str}\n{e}",)
--> 215 raise e
216 else:
217 raise MagicClassConstructionError(f"\n{hist_str}\n\n{type(e).name}: {e}") from e

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\class_gui.py in _convert_attributes_into_widgets(self)
92 if isinstance(attr, type):
93 # Nested magic-class
---> 94 widget = attr()
95 setattr(self, name, widget)
96

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\core.py in init(self, *args, **kwargs)
205 )
206 super(oldclass, self).init(*args, **kwargs)
--> 207 self._convert_attributes_into_widgets()
208
209 if hasattr(self, _POST_INIT):

D:\Anaconda3\envs\llsz_py39\lib\site-packages\magicclass\gui\class_gui.py in _convert_attributes_into_widgets(self)
215 raise e
216 else:
--> 217 raise MagicClassConstructionError(f"\n{hist_str}\n\n{type(e).name}: {e}") from e
218
219

MagicClassConstructionError:

  MyClass1 (ABCMeta) <--- Error


  a (MagicValueField) <--- Error

ValueError: too many values to unpack (expected 2)

I have the latest version of magicclass installed...
Is there an error with how I'm using this functionality??

Thanks
Pradeep

would like to remove `magicgui.signature`

Hi @hanjinliu

as part of pyapp-kit/magicgui#474, I'd like to generally remove (or at least make private) everything inside of magicgui.signature (since I think functions were the wrong level to generally attack the type->widget mapping). but it looks like you're using MagicParameter quite a bit.

Could you look into what it would take here to remove dependence on magicgui.signature? either by using other public functions from magicgui (such as directly using get_widget_type), or by vendoring whatever you want from magicgui.signature?

thanks :)

Arguments in __init__ should not be disabled

Although magicclass inherits magicgui.Container, all the arguments of its constructor such as visible and name are removed from the constructor of the new class. This does not restrict the interface of widget creation because those arguments can be given in @magicclass decorator, like @magicclass(name=...).

However, this class structure is a little bit problematic when a magicclass is constructed inside another magicclass and its visibility or name should be set dynamically. Especially, when a magicclass is passed to another class using field there is no way to set its options.

cannot import click error

Hi
I installed magicclass using
pip install git+https://github.com/hanjinliu/magic-class.git

When running
from magicclass import click

I get this error:
cannot import name 'click' from 'magicclass'

It was working from the pip release, but I was getting some errors, hence the reason I installed from the updated master branch instead.

Cheers
Pradeep

`Literal` type plus `RadioButtons

When you combine a field with a Literal and the RadioButtons type, magicclass doesn't seem to render anything:

from magicclass import magicclass, field
from typing import Literal

@magicclass
class Gui:
    foo = field(Literal["a", "b", "c"], widget_type="RadioButtons")
    
Gui().show(run=True)
Screen Shot 2023-09-21 at 9 40 11 am

Have a FieldGroup listen to its own changes

I have a FieldGroup, and whenever any field within that group changes, I want to run a function. Here are some approaches I've tried.

Firstly, we can't use the decorator form self.connect() because self isn't defined within the class.

Secondly, I tried doing so in the constructor:

from magicclass import FieldGroup, field

class Fields(FieldGroup):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.connect(self._changed)

    def _changed(self):
        print("Changed!")
    
    x = field(int)
    y = field(str)

Fields().show(run=True)

This doesn't seem to ever call _changed.

Lastly, if I instead use self.changed.connect(self._changed) in the constructor, it blocks the field group from listening to its child widget events.

How should I solve this?

`WidgetOptions` being removed

hey @hanjinliu, I removed magicgui.types.WidgetOptions, which you import in various places outside of a TYPE_CHECKING clause.

I can put it back in, but I would be doing so only to avoid breaking your stuff. If you're just importing for the sake of type hints, I would recommend using:

if TYPE_CHECKING:
    from magicgui.types import WidgetOptions

I see a number of other private imports (such as from widgets._concrete) and just wanted to give you a heads up that I would like to be reorganizing a number of things, most of which were private, and these will probably break some of your stuff. You might be better off vendoring (i.e. copying into this repo) any of the private things that you would like to use here.

Personalise Path widget

Hi, this is a simple help request.
I was trying to customise the Path widget to either select multiple files (to open them) or even an entire folder. Do you have any suggestions on how to tweak the widget?
Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.