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