diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6ebf4316..b24b08281 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,13 +16,13 @@ repos: files: "^\\.github/workflows/.*\\.ya?ml$" - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.49.0 + rev: v1.50.1 hooks: - id: typos args: [--force-exclude] # omitting --write-changes - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.4 + rev: v0.16.6 hooks: - id: ruff-check args: ["--fix", "--unsafe-fixes"] diff --git a/README.md b/README.md index 818b2de2e..d5c229f1f 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,14 @@ conda install -c conda-forge magicgui pyqt # or pyside6 instead of pyqt from magicgui import magicgui from enum import 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", result_widget=True) def snells_law(aoi=30.0, n1=Medium.Glass, n2=Medium.Water, degrees=True): @@ -83,6 +85,7 @@ def snells_law(aoi=30.0, n1=Medium.Glass, n2=Medium.Water, degrees=True): except ValueError: return "Total internal reflection!" + # your function is now capable of showing a GUI snells_law.show(run=True) ``` diff --git a/docs/api/migration.md b/docs/api/migration.md index 13e2aaf09..fd11d3a70 100644 --- a/docs/api/migration.md +++ b/docs/api/migration.md @@ -34,8 +34,8 @@ value directly, you can do one of two things: ```python title="👍 New Method (>= v0.3.0)" @widget.changed.connect -def my_callback(new_value: int): - ... # use new_value directly +def my_callback(new_value: int): ... # use new_value directly + # or, if you don't need to use new_value @widget.changed.connect @@ -50,13 +50,13 @@ For the few packages who were manually emitting change events, you should no longer provide the `value=` keyword when emitting. ```python title="👎 Old Method (< v0.3.0)" -widget.changed(value='whatever') +widget.changed(value="whatever") ``` ```python title="👍 New Method (>= v0.3.0)" -widget.changed.emit('whatever') +widget.changed.emit("whatever") # OR (if you prefer the direct __call__ syntax) -widget.changed('whatever') +widget.changed("whatever") ``` ## v0.2.0 migration guide @@ -76,9 +76,10 @@ instantiated [`magicgui.widgets.Widget`][magicgui.widgets.Widget]. ```python title="👎 Old Method (< v0.2.0)" from magicgui import magicgui, event_loop + @magicgui -def function(x, y): - ... +def function(x, y): ... + with event_loop(): gui = function.Gui(show=True) @@ -87,9 +88,10 @@ with event_loop(): ```python title="👍 New Method (>= v0.2.0)" from magicgui import magicgui + @magicgui -def function(x, y): - ... +def function(x, y): ... + function.show(run=True) ``` @@ -109,11 +111,11 @@ to use `widget.native` instead of `widget` ```python from magicgui import magicgui, use_app -use_app('qt') +use_app("qt") + @magicgui -def function(x, y): - ... +def function(x, y): ... ``` ```python @@ -134,13 +136,14 @@ show *multiple* widgets next to each other, then you would still want to use the ```python from magicgui import magicgui, event_loop + @magicgui -def function_a(x=1, y=3): - ... +def function_a(x=1, y=3): ... + @magicgui -def function_b(z='asdf'): - ... +def function_b(z="asdf"): ... + with event_loop(): function_a.show() diff --git a/docs/dataclasses.md b/docs/dataclasses.md index 77092bb14..17b5ad031 100644 --- a/docs/dataclasses.md +++ b/docs/dataclasses.md @@ -20,13 +20,15 @@ boilerplate. ``` python title="Example dataclass" from dataclasses import dataclass + @dataclass # (1)! class Person: - name: str # (2)! + name: str # (2)! age: int = 0 # (3)! - p = Person(name='John', age=30) # (4)! - print(p) # (5)! + + p = Person(name="John", age=30) # (4)! + print(p) # (5)! ``` 1. The `@dataclass` decorator is used to mark a class as a dataclass. This @@ -88,12 +90,14 @@ that has two additional features: ``` python from magicgui.experimental import guiclass + @guiclass class MyDataclass: a: int = 0 - b: str = 'hello' + b: str = "hello" c: bool = True + obj = MyDataclass() obj.gui.show() ``` @@ -110,7 +114,7 @@ As you interact programmatically with the `obj` instance, the widgets in the ``` python obj = MyDataclass(a=10) -obj.b = 'world' +obj.b = "world" obj.c = False obj.gui.show() @@ -141,15 +145,17 @@ Any additional keyword arguments to the `button` decorator will be passed to the ``` python from magicgui.experimental import guiclass, button + @guiclass class Greeter: first_name: str @button def say_hello(self): - print(f'Hello {self.first_name}') + print(f"Hello {self.first_name}") + -greeter = Greeter('Talley') +greeter = Greeter("Talley") greeter.gui.show() ``` diff --git a/docs/decorators.md b/docs/decorators.md index b653e622a..2dbc9f3b8 100644 --- a/docs/decorators.md +++ b/docs/decorators.md @@ -22,6 +22,7 @@ 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 @@ -29,6 +30,7 @@ class Medium(Enum): 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): @@ -40,7 +42,8 @@ def snells_law(aoi=30.0, n1=Medium.Glass, n2=Medium.Water, degrees=True): # beyond the critical angle return "Total internal reflection!" -snells_law.show() # leave open + +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. @@ -80,7 +83,7 @@ We can invoke the function in a few ways: * We can call the object just like the original function. ```python - snells_law() # 34.7602 + snells_law() # 34.7602 snells_law(aoi=12) # 13.7142 ``` @@ -133,6 +136,7 @@ def my_callback(value: str): # of the function call in the `value` attribute print(f"Your function was called! The result is: {value}") + result = snells_law() ``` @@ -150,6 +154,7 @@ to the `.changed` signal: def _on_n1_changed(x: Medium): print(f"n1 was changed to {x}") + snells_law.n1.value = Medium.Air ``` @@ -180,6 +185,7 @@ is equivalent to this: def function(): pass + function = magicgui(function, auto_call=True) ``` @@ -230,13 +236,16 @@ 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() ``` @@ -258,7 +267,7 @@ from magicgui.widgets import create_widget, Container from magicgui.types import Undefined -def pseudo_magicgui(func: 'Callable'): +def pseudo_magicgui(func: "Callable"): return Container( widgets=[ create_widget(p.default, annotation=p.annotation, name=p.name) @@ -266,9 +275,11 @@ def pseudo_magicgui(func: 'Callable'): ] ) -def some_func(x: int = 2, y: str = 'hello'): + +def some_func(x: int = 2, y: str = "hello"): return x, y + my_widget = pseudo_magicgui(some_func) my_widget.show() ``` diff --git a/docs/events.md b/docs/events.md index afa3e5579..6efae4322 100644 --- a/docs/events.md +++ b/docs/events.md @@ -23,7 +23,7 @@ widget's `changed` event: ```python from magicgui import widgets - text = widgets.LineEdit(value='type something') + text = widgets.LineEdit(value="type something") text.changed.connect(lambda val: print(f"Text changed to: {val}")) ``` @@ -32,9 +32,10 @@ widget's `changed` event: ```python from magicgui import magicgui + @magicgui - def my_function(text: str): - ... + def my_function(text: str): ... + my_function.text.changed.connect(lambda val: print(f"Text changed to: {val}")) ``` @@ -44,12 +45,14 @@ widget's `changed` event: ```python from magicgui import magic_factory + def _on_init(widget): widget.text.changed.connect(lambda val: print(f"Text changed to: {val}")) + @magic_factory(widget_init=_on_init) - def my_function(text: str): - ... + def my_function(text: str): ... + my_widget = my_function() ``` @@ -67,11 +70,12 @@ widget's `changed` event: use it as a decorator if you prefer. ```python - text = widgets.LineEdit(value='type something') + text = widgets.LineEdit(value="type something") # this works text.changed.connect(lambda val: print(f"Text changed to: {val}")) + # so does this @text.changed.connect def on_text_changed(val): diff --git a/docs/index.md b/docs/index.md index 1c848860c..3bdb4857b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,8 +53,7 @@ dataclasses simply by annotating them with standard python `str`, and the return value as a `list`.* ``` python - def my_function(param_a: int, param_b: str) -> list: - ... + def my_function(param_a: int, param_b: str) -> list: ... ``` If you are new to type annotations in Python, here are a few resources to get @@ -104,13 +103,14 @@ from magicgui import magicgui @magicgui def my_function( param_a: int, - param_b: Annotated[int, {'widget_type': "Slider", 'max': 100}] = 42, - param_c: Literal["First", "Second", "Third"] = "Second" + param_b: Annotated[int, {"widget_type": "Slider", "max": 100}] = 42, + param_c: Literal["First", "Second", "Third"] = "Second", ): print("param_a:", param_a) print("param_b:", param_b) print("param_c:", param_c) + # my_function now IS a widget, in addition to being a callable function my_function.show() ``` @@ -129,17 +129,19 @@ when the `gui` attribute is accessed for the first time.) ```python from magicgui.experimental import guiclass, button + @guiclass class MyDataclass: a: int = 0 - b: str = 'hello' + b: str = "hello" c: bool = True @button def compute(self): print(self.a, self.b, self.c) -obj = MyDataclass(a=10, b='foo') + +obj = MyDataclass(a=10, b="foo") obj.gui.show() ``` @@ -175,10 +177,12 @@ b = widgets.Slider(value=20, min=0, max=100, label="b") result = widgets.LineEdit(value=a.value * b.value, label="result") button = widgets.PushButton(text="multiply") + @button.clicked.connect def on_button_click(): result.value = a.value * b.value + container = widgets.Container(widgets=[a, b, result, button]) container.show() ``` diff --git a/docs/type_map.md b/docs/type_map.md index 255ea40c0..9e6ed325e 100644 --- a/docs/type_map.md +++ b/docs/type_map.md @@ -46,17 +46,29 @@ import pint import enum types = [ - bool, int, float, str, range, slice, list, - pathlib.Path, os.PathLike, Sequence[pathlib.Path], - datetime.time, datetime.timedelta, datetime.date, datetime.datetime, - Literal['a', 'b'], Set[Literal['a', 'b']], enum.Enum, - widgets.ProgressBar, pint.Quantity, + bool, + int, + float, + str, + range, + slice, + list, + pathlib.Path, + os.PathLike, + Sequence[pathlib.Path], + datetime.time, + datetime.timedelta, + datetime.date, + datetime.datetime, + Literal["a", "b"], + Set[Literal["a", "b"]], + enum.Enum, + widgets.ProgressBar, + pint.Quantity, ] wdg = widgets.Container( - widgets=[ - widgets.create_widget(annotation=t, label=str(t)) for t in types - ] + widgets=[widgets.create_widget(annotation=t, label=str(t)) for t in types] ) wdg.show() ``` @@ -109,6 +121,7 @@ Create a widget using standard type map: ```python from magicgui import magicgui + @magicgui def my_widget(x: int = 42): return x @@ -119,10 +132,12 @@ Create a widget using standard type map: ```python from magicgui.experimental import guiclass + @guiclass class MyObject: x: int = 42 + obj = MyObject() my_widget = obj.gui ``` @@ -134,7 +149,7 @@ Customize a widget using [`typing.Annotated`][typing.Annotated]: ```python from typing import Annotated - Int10_50 = Annotated[int, (('widget_type', 'Slider'),('step', 10),('max', 50))] + Int10_50 = Annotated[int, (("widget_type", "Slider"), ("step", 10), ("max", 50))] wdg2 = widgets.create_widget(value=42, annotation=Int10_50) ``` @@ -144,11 +159,11 @@ Customize a widget using [`typing.Annotated`][typing.Annotated]: from magicgui import magicgui from typing import Annotated - Int10_50 = Annotated[int, (('widget_type', 'Slider'),('step', 10),('max', 50))] + Int10_50 = Annotated[int, (("widget_type", "Slider"), ("step", 10), ("max", 50))] + @magicgui - def my_widget(x: Int10_50 = 42): - ... + def my_widget(x: Int10_50 = 42): ... ``` === "guiclass decorator" @@ -157,12 +172,14 @@ Customize a widget using [`typing.Annotated`][typing.Annotated]: from magicgui.experimental import guiclass from typing import Annotated - Int10_50 = Annotated[int, (('widget_type', 'Slider'),('step', 10),('max', 50))] + Int10_50 = Annotated[int, (("widget_type", "Slider"), ("step", 10), ("max", 50))] + @guiclass class MyObject: x: Int10_50 = 42 + obj = MyObject() my_widget = obj.gui ``` @@ -174,7 +191,7 @@ Note that you may also customize widget creation with kwargs to from typing import Annotated from magicgui.widgets import Slider -options = {'step': 10, 'max': 50} +options = {"step": 10, "max": 50} wdg3 = widgets.create_widget(value=42, widget_type=Slider, options=options) wdg3.show() ``` @@ -182,9 +199,9 @@ wdg3.show() ... or to the [`magicgui`][magicgui.magicgui] decorator: ```python -@magicgui(x={'widget_type': 'Slider', 'step': 10, 'max': 50}) -def my_widget(x: int = 42): - ... +@magicgui(x={"widget_type": "Slider", "step": 10, "max": 50}) +def my_widget(x: int = 42): ... + my_widget.show() ``` @@ -213,8 +230,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from mymodule import MyType -def my_function(x: 'MyType') -> None: - ... + +def my_function(x: "MyType") -> None: ... ``` ### :warning: `__future__.annotations` @@ -233,9 +250,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from mymodule import MyType + # no longer necessary to use quotes around 'MyType' -def my_function(x: MyType) -> None: - ... +def my_function(x: MyType) -> None: ... ``` While this is a useful feature for developers, it does make it significantly @@ -263,9 +280,9 @@ As a general rule, if you *must* use forward references or if TYPE_CHECKING: import mymodule + # this is easier for magicgui to resolve - def my_function(x: mymodule.MyType) -> None: - ... + def my_function(x: mymodule.MyType) -> None: ... ``` ## Registering Support for Custom Types diff --git a/docs/widgets.md b/docs/widgets.md index 829ef0380..c3ad9bc89 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -11,7 +11,7 @@ directly: ```python from magicgui.widgets import LineEdit -line_edit = LineEdit(value='hello!') +line_edit = LineEdit(value="hello!") line_edit.show() ``` @@ -21,7 +21,7 @@ widgets that comprise other widgets: ```python from magicgui.widgets import LineEdit, SpinBox, Container -line_edit = LineEdit(value='hello!') +line_edit = LineEdit(value="hello!") spin_box = SpinBox(value=400) container = Container(widgets=[line_edit, spin_box]) container.show() @@ -34,7 +34,7 @@ example that yields the same result as the one above: ```python from magicgui.widgets import create_widget -x = 'hello!' +x = "hello!" y = 400 container = Container(widgets=[create_widget(i) for i in (x, y)]) container.show() @@ -136,11 +136,11 @@ wdg_list = [ widgets.RangeEdit(value=range(0, 10, 2), label="RangeEdit:"), widgets.SliceEdit(value=slice(0, 10, 2), label="SliceEdit:"), widgets.DateTimeEdit( - value=datetime.datetime(1999, 12, 31, 11, 30), label="DateTimeEdit:" + value=datetime.datetime(1999, 12, 31, 11, 30), label="DateTimeEdit:" ), widgets.DateEdit(value=datetime.date(81, 2, 18), label="DateEdit:"), widgets.TimeEdit(value=datetime.time(12, 20), label="TimeEdit:"), - widgets.QuantityEdit(value='12 seconds', label="Quantity:") + widgets.QuantityEdit(value="12 seconds", label="Quantity:"), ] container = widgets.Container(widgets=wdg_list) container.max_height = 300 @@ -166,8 +166,8 @@ In addition to all of the `ValueWidget` attributes, `RangedWidget` attributes in | `range` | `tuple of float` | A convenience attribute for getting/setting the (min, max) simultaneously | ```python -w1 = widgets.SpinBox(value=10, max=20, label='SpinBox:') -w2 = widgets.FloatSpinBox(value=380, step=0.5, label='FloatSpinBox:') +w1 = widgets.SpinBox(value=10, max=20, label="SpinBox:") +w2 = widgets.FloatSpinBox(value=380, step=0.5, label="FloatSpinBox:") container = widgets.Container(widgets=[w1, w2]) container.show() ``` @@ -191,9 +191,9 @@ In addition to all of the `RangedWidget` attributes, `SliderWidget` attributes i | `readout` | `bool` | Whether to show the value of the slider. By default, `True`. | ```python -w1 = widgets.Slider(value=10, max=25, label='Slider:') -w2 = widgets.FloatSlider(value=10.5, max=18.5, label='FloatSlider:') -w3 = widgets.ProgressBar(value=80, max=100, label='ProgressBar:') +w1 = widgets.Slider(value=10, max=25, label="Slider:") +w2 = widgets.FloatSlider(value=10.5, max=18.5, label="FloatSlider:") +w3 = widgets.ProgressBar(value=80, max=100, label="ProgressBar:") container = widgets.Container(widgets=[w1, w2, w3]) container.show() ``` @@ -214,8 +214,8 @@ In addition to all of the `ValueWidget` attributes, `ButtonWidget` attributes in | `text` | `str` | The text to display on the button. If not provided, will use `name`. | ```python -w1 = widgets.PushButton(value=True, text='PushButton Text') -w2 = widgets.CheckBox(value=False, text='CheckBox Text') +w1 = widgets.PushButton(value=True, text="PushButton Text") +w2 = widgets.CheckBox(value=False, text="CheckBox Text") container = widgets.Container(widgets=[w1, w2]) container.show() ``` @@ -244,10 +244,10 @@ In addition to all of the `ValueWidget` attributes, `CategoricalWidget` attribut | `current_choice` | `str` | The name associated with the current choice. For instance, if `choices` was provided as `choices=[('one', 1), ('two', 2)]`, then an example `value` would be `1`, and an example `current_choice` would be `'one'`. | ```python -choices = ['one', 'two', 'three'] -w1 = widgets.ComboBox(choices=choices, value='two', label='ComboBox:') -w2 = widgets.RadioButtons(choices=choices, label='RadioButtons:') -w3 = widgets.Select(choices=choices, label='Select:') +choices = ["one", "two", "three"] +w1 = widgets.ComboBox(choices=choices, value="two", label="ComboBox:") +w2 = widgets.RadioButtons(choices=choices, label="RadioButtons:") +w3 = widgets.Select(choices=choices, label="Select:") container = widgets.Container(widgets=[w1, w2, w3]) container.max_height = 220 container.show() @@ -278,8 +278,8 @@ You can add and remove widgets from it just as you would add or remove items fro from magicgui.widgets import Container, Slider, FloatSlider, ProgressBar container = widgets.Container() -container.append(widgets.LineEdit(value='Mookie', label='Your Name:')) -container.append(widgets.FloatSlider(value=10.5, label='FloatSlider:')) +container.append(widgets.LineEdit(value="Mookie", label="Your Name:")) +container.append(widgets.FloatSlider(value=10.5, label="FloatSlider:")) container.show() ``` @@ -307,8 +307,10 @@ parameters in a decorated function. ```python from magicgui import magicgui + @magicgui -def my_function(x='hello', y=400): ... +def my_function(x="hello", y=400): ... + my_function.show() ``` @@ -319,13 +321,12 @@ to [`@magicgui`][magicgui.magicgui]. ```python from inspect import signature -def my_function(x='hello', y=400): - ... + +def my_function(x="hello", y=400): ... + params = signature(my_function).parameters.values() -container = Container( - widgets=[create_widget(p.default, name=p.name) for p in params] -) +container = Container(widgets=[create_widget(p.default, name=p.name) for p in params]) container.show() ```