File: decorators.md

package info (click to toggle)
magicgui 0.9.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 21,796 kB
  • sloc: python: 11,202; makefile: 11; sh: 9
file content (279 lines) | stat: -rw-r--r-- 9,099 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# magicgui & magic_factory

## From Object to GUI

The eponymous feature of `magicgui` is the [`magicgui.magicgui`][magicgui.magicgui] function,
which converts an object into a widget.

!!! info
    Currently, the only supported objects are functions, but in the future
    `magicgui.magicgui` may accept other objects, such as
    [dataclass instances](./dataclasses.md)

When used to decorate a function, `@magicgui` will autogenerate a graphical user
interface (GUI) by inspecting the function signature and adding an appropriate
GUI widget for each parameter, as described in [Type Hints to
Widgets](./type_map.md). Parameter `types` are taken from [type
hints](https://docs.python.org/3/library/typing.html), if provided, or inferred
using the type of the default value otherwise.

```python
import math
from enum import Enum
from magicgui import magicgui

# dropdown boxes are best made by creating an enum
class Medium(Enum):
    Glass = 1.520
    Oil = 1.515
    Water = 1.333
    Air = 1.0003

# decorate your function with the @magicgui decorator
@magicgui(call_button="calculate")
def snells_law(aoi=30.0, n1=Medium.Glass, n2=Medium.Water, degrees=True):
    aoi = math.radians(aoi) if degrees else aoi
    try:
        result = math.asin(n1.value * math.sin(aoi) / n2.value)
        return math.degrees(result) if degrees else result
    except ValueError:
        # beyond the critical angle
        return "Total internal reflection!"

snells_law.show() # leave open
```

The object returned by the `magicgui` decorator is an instance of [`magicgui.widgets.FunctionGui`][magicgui.widgets.FunctionGui].  It can still be called like the original function, but it also knows how to present itself as a GUI.

## Two-Way Data Binding

The modified `snells_law` object gains attributes named after each of the
parameters in the function.  Each attribute is an instance of a
[`magicgui.widgets.Widget`][magicgui.widgets.Widget] subclass (suitable for the data type represented by
that parameter). As you make changes in your GUI, the attributes of the
`snells_law` object will be kept in sync.  For instance, change the first
dropdown menu from "Glass" to "Oil", and the corresponding `n1` object on
`snells_law` will change its value to `1.515`:

```python
snells_law.n1.value  # 1.515
```

It goes both ways: set a parameter in the console and it will change in the GUI:

```python
snells_law.aoi.value = 47
snells_law.show()
```

## It's still a function

`magicgui` tries very hard to make it so that the decorated object behaves as
much like the original object as possible.

We can invoke the function in a few ways:

* Because we provided the `call_button` argument to the
  [`magicgui`][magicgui.magicgui] decorator, a new button was created that will
  execute the function with the current gui parameters when clicked.

* We can call the object just like the original function.

    ```python
    snells_law()        # 34.7602
    snells_law(aoi=12)  # 13.7142
    ```

    Now however, the current values from the GUI will be used as the default
    values for any arguments that are not explicitly provided to the function.

    ```python
    snells_law.aoi.value = 12
    snells_law()  # 13.7142
    snells_law(aoi=30)  # 34.7602
    ```

    In essence, your original function now has a "living" signature whose
    defaults change as the user interacts with your GUI.

    ```python
    import inspect

    inspect.signature(snells_law)
    # <MagicSignature(
    #   aoi=12.0, n1=<Medium.Glass: 1.52>, n2=<Medium.Water: 1.333>, degrees=True
    # )>
    # notice how the default `aoi` is now 12 ... because we changed it above
    ```

* You can still override positional or keyword arguments in the original
  function, just as you would with a regular function.

    !!! note
        calling the function with values that differ from the GUI will *not* set
        the values in the GUI... It's just a one-time call.

    ```python
    # in radians, overriding the value for the second medium (n2)
    snells_law(0.8, n2=Medium.Air, degrees=False)  # 'Total internal reflection!'
    ```

## Connecting Events

### Function Calls

With a GUI, you are usually looking for something to happen as a result of
calling the function.  The function will have a new `called` attribute that you
can `connect` to an arbitrary callback function:

```python
@snells_law.called.connect
def my_callback(value: str):
    # The callback receives an `Event` object that has the result
    # of the function call in the `value` attribute
    print(f"Your function was called! The result is: {value}")

result = snells_law()
```

Now when you call `snells_law()`, or click the `calculate` button in the gui,
`my_callback` will be called with the result of the calculation.

### Parameter Changes

You can also listen for changes on individual function parameters by connecting
to the `<parameter_name>.changed` signal:

```python
# whenever the current value for n1 changes, print it to the console:
@snells_law.n1.changed.connect
def _on_n1_changed(x: Medium):
    print(f"n1 was changed to {x}")

snells_law.n1.value = Medium.Air
```

!!! note
    This signal will be emitted regardless of whether the parameter was changed in
    the GUI or via by [directly setting the paramaeter on the gui
    instance](#two-way-data-binding).

## Usage As a Decorator is Optional

Remember: the `@decorator` syntax is just [syntactic
sugar](https://en.wikipedia.org/wiki/Syntactic_sugar).  You don't *have* to use
`@magicgui` to decorate your function declaration. You can also just [call it
with your function as an
argument](https://realpython.com/lessons/syntactic-sugar/):

This decorator usage:

```python
@magicgui(auto_call=True)
def function():
    pass
```

is equivalent to this:

```python
def function():
    pass

function = magicgui(function, auto_call=True)
```

In many cases, it will actually be desirable *not* to use magicgui as a
decorator if you don't need a widget immediately, but want to create one later
(see also the [`magic_factory`](#magic_factory) decorator.)

```python
# some time later...
widget_instance = magicgui(function)
```

## magic_factory

The [`magicgui.magic_factory`][magicgui.magic_factory] function/decorator acts very much like the `magicgui`
decorator, with one important difference:

**Unlike `magicgui`, `magic_factory` does not return a widget instance
immediately.  Instead, it returns a "factory function" that can be *called*
to create a widget instance.**

This is an important distinction to understand.  In most cases, the `@magicgui`
decorator is useful for interactive use or rapid prototyping.  But if you are
writing a library or package where someone *else* will be instantiating your
widget (a napari plugin is a good example), you will likely want to use
`magic_factory` instead, (or create your own [Widget Container](widgets.md#containerwidget)
subclass).

!!! tip "it's just a partial"

    If you're familiar with [`functools.partial`][functools.partial], you can think of
    `magic_factory` as a partial function application of the `magicgui`
    decorator (in fact, `magic_factory` is a subclass of `partial`).
    It is very roughly equivalent to:

    ```python
    def magic_factory(func, *args, **kwargs):
        return partial(magicgui, func, *args, **kwargs)
    ```

### `widget_init`

`magic_factory` gains one additional parameter: `widget_init`.  This accepts
a callable that will be called with the new widget instance each time the
factory is called.  This is a convenient place to add additional initialization
or connect [events](events.md).

```python
from magicgui import magic_factory

def _on_init(widget):
    print("widget created!", widget)
    widget.y.changed.connect(lambda x: print("y changed!", x))

@magic_factory(widget_init=_on_init)
def my_factory(x: int, y: str): ...

new_widget = my_factory()
```



## The (lack of) "magic" in magicgui

Just to demystify the name a bit, there really isn't a whole lot of "magic"
in the `magicgui` decorator.  It's really just a thin wrapper around the
[`magicgui.widgets.create_widget`][magicgui.widgets.create_widget] function, to create a
[`Container`][magicgui.widgets.Container] with a sub-widget for each
parameter in the function signature.

The widget creation is very roughly equivalent to something like this:

```python
from inspect import signature, Parameter
from magicgui.widgets import create_widget, Container
from magicgui.types import Undefined


def pseudo_magicgui(func: 'Callable'):
    return Container(
        widgets=[
            create_widget(p.default, annotation=p.annotation, name=p.name)
            for p in signature(func).parameters.values()
        ]
    )

def some_func(x: int = 2, y: str = 'hello'):
    return x, y

my_widget = pseudo_magicgui(some_func)
my_widget.show()
```

In the case of `magicgui`, a special subclass of `Container`
([`FunctionGui`][magicgui.widgets.FunctionGui]) is used, which additionally adds
a `__call__` method that allows the widget to [behave like the original
function](#its-still-a-function).