diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index a56e0acc1..c172c3ee9 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -10,13 +10,17 @@ on: jobs: docs: - runs-on: macos-latest # for the screenshots + runs-on: macos-latest # for the screenshots steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-python@v4 with: - python-version: '3.x' - - run: pip install -e .[docs] + python-version: "3.x" + - run: | + python -m pip install --upgrade pip + python -m pip install -e .[docs] - name: Deploy docs to GitHub Pages if: github.event_name == 'push' diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 6a530cdce..643fa8b82 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -37,7 +37,7 @@ jobs: backend: pyside2 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.11" @@ -84,8 +84,8 @@ jobs: name: napari tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: repository: napari/napari path: napari-from-github @@ -111,8 +111,8 @@ jobs: name: magic-class tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: repository: hanjinliu/magic-class path: magic-class @@ -139,8 +139,8 @@ jobs: name: stardist tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: repository: stardist/stardist-napari path: stardist-napari @@ -167,8 +167,8 @@ jobs: name: partseg tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: repository: 4DNucleome/PartSeg path: PartSeg @@ -192,7 +192,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'pyapp-kit/magicgui' && contains(github.ref, 'tags') }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python diff --git a/.gitignore b/.gitignore index 3348e7b06..b49f9f631 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ venv.bak/ # mkdocs documentation /site +docs/generated* # mypy .mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e40fd78f8..a9a71763b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.282 + rev: v0.0.287 hooks: - id: ruff args: ["--fix"] @@ -25,12 +25,12 @@ repos: - id: black - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.13 + rev: v0.14 hooks: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.1 hooks: - id: mypy files: "^src/" diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 000000000..0db024071 --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,3 @@ +# Getting started + +A gallery of examples for magicgui. diff --git a/docs/examples/applications/README.md b/docs/examples/applications/README.md new file mode 100644 index 000000000..532860fc7 --- /dev/null +++ b/docs/examples/applications/README.md @@ -0,0 +1,3 @@ +# Demo applications + +Example applications built with magicgui. diff --git a/examples/callable.py b/docs/examples/applications/callable.py similarity index 66% rename from examples/callable.py rename to docs/examples/applications/callable.py index 8bf76c5ff..a5d3c4b9c 100644 --- a/examples/callable.py +++ b/docs/examples/applications/callable.py @@ -1,20 +1,28 @@ +"""# Callable functions demo + +This example demonstrates handling callable functions with magicgui. +""" from magicgui import magicgui def f(x: int, y="a string") -> str: + """Example function F.""" return f"{y} {x}" def g(x: int = 6, y="another string") -> str: + """Example function G.""" return f"{y} asdfsdf {x}" @magicgui(call_button=True, func={"choices": ["f", "g"]}) def example(func="f"): + """Ëxample function.""" pass def update(f: str): + """Update function.""" if len(example) > 2: del example[1] example.insert(1, magicgui(globals()[f])) diff --git a/examples/chaining.py b/docs/examples/applications/chaining.py similarity index 86% rename from examples/chaining.py rename to docs/examples/applications/chaining.py index 43c9eace6..0147a0f35 100644 --- a/examples/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,14 +1,20 @@ +"""# Chaining functions together + +This example demonstrates chaining multiple functions together. +""" from magicgui import magicgui, widgets @magicgui(auto_call=True) def func_a(x: int = 64, y: int = 64): + """Callable function A.""" print("calling func_a") return x + y @magicgui(auto_call=True, input={"visible": False, "label": " ", "max": 100000}) def func_b(input: int, mult=1.0): + """Callable function B.""" print("calling func_b") result = input * mult # since these function defs live in globals(), you can update them directly @@ -30,6 +36,7 @@ def _on_func_a(value: str): labels=False, ) def func_c(input: int, format: str = "({} + {}) * {} is {}") -> str: + """Callable function C.""" print("calling func_c\n") return format.format(func_a.x.value, func_a.y.value, func_b.mult.value, input) diff --git a/examples/main_window.py b/docs/examples/applications/hotdog.py similarity index 86% rename from examples/main_window.py rename to docs/examples/applications/hotdog.py index d63554f24..f5329a9d6 100644 --- a/examples/main_window.py +++ b/docs/examples/applications/hotdog.py @@ -1,3 +1,7 @@ +"""# Hotdog or not app + +Demo app to upload an image and classify if it's an hotdog or not. +""" import pathlib from enum import Enum @@ -5,7 +9,7 @@ class HotdogOptions(Enum): - """All hotdog possibilities""" + """All hotdog possibilities.""" Hotdog = 1 NotHotdog = 0 diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py new file mode 100644 index 000000000..9fa811355 --- /dev/null +++ b/docs/examples/applications/pint_quantity.py @@ -0,0 +1,20 @@ +"""# Quantities with pint + +Pint is a Python package to define, operate and manipulate physical quantities: +the product of a numerical value and a unit of measurement. +It allows arithmetic operations between them and conversions +from and to different units. +https://pint.readthedocs.io/en/stable/ +""" +from pint import Quantity + +from magicgui import magicgui + + +@magicgui +def widget(q=Quantity("1 ms")): + """Widget allowing users to input quantity measurements.""" + print(q) + + +widget.show(run=True) diff --git a/examples/snells_law.py b/docs/examples/applications/snells_law.py similarity index 85% rename from examples/snells_law.py rename to docs/examples/applications/snells_law.py index 205d68299..481f7f9b4 100644 --- a/examples/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,4 +1,7 @@ -"""Simple demonstration of magicgui.""" +"""# Snell's law demonstration using magicgui + +Demo app for calculating angles of refraction according to Snell's law. +""" import math from enum import Enum diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py new file mode 100644 index 000000000..451557005 --- /dev/null +++ b/docs/examples/applications/values_dialog.py @@ -0,0 +1,21 @@ +"""# Input values dialog + +A basic example of a user input dialog. + +This will pause code execution until the user responds. + +# ![Values input dialog](../../images/values_input.png){ width=50% } + +```python linenums="1" +from magicgui.widgets import request_values + +vals = request_values( + age=int, + name={"annotation": str, "label": "Enter your name:"}, + title="Hi, who are you?", +) +print(repr(vals)) +``` +""" + +# %% diff --git a/docs/examples/basic.md b/docs/examples/basic.md deleted file mode 100644 index 814acf68a..000000000 --- a/docs/examples/basic.md +++ /dev/null @@ -1,55 +0,0 @@ -# Basic Widget Demo - -This code demonstrates a few of the widget types that magicgui can -create based on the parameter types in your function - -```python -import datetime -from enum import Enum -from pathlib import Path - -from magicgui import magicgui - - -class Medium(Enum): - """Using Enums is a great way to make a dropdown menu.""" - Glass = 1.520 - Oil = 1.515 - Water = 1.333 - Air = 1.0003 - - -@magicgui( - call_button="Calculate", - layout="vertical", - result_widget=True, - # numbers default to spinbox widgets, but we can make - # them sliders using the 'widget_type' option - slider_float={"widget_type": "FloatSlider", "max": 100}, - slider_int={"widget_type": "Slider", "readout": False}, - radio_option={ - "widget_type": "RadioButtons", - "orientation": "horizontal", - "choices": [("first option", 1), ("second option", 2)], - }, - filename={"label": "Pick a file:"}, # custom label -) -def widget_demo( - boolean=True, - integer=1, - spin_float=3.14159, - slider_float=43.5, - slider_int=550, - string="Text goes here", - dropdown=Medium.Glass, - radio_option=2, - date=datetime.date(1999, 12, 31), - time=datetime.time(1, 30, 20), - datetime=datetime.datetime.now(), - filename=Path.home(), # path objects are provided a file picker -): - """Run some computation.""" - return locals().values() - -widget_demo.show() -``` diff --git a/examples/basic.py b/docs/examples/basic_example.py similarity index 61% rename from examples/basic.py rename to docs/examples/basic_example.py index eeaec9706..6e06da627 100644 --- a/examples/basic.py +++ b/docs/examples/basic_example.py @@ -1,8 +1,13 @@ +"""# Basic example + +A basic example using magicgui. +""" from magicgui import magicgui @magicgui def example(x: int, y="hi"): + """Basic example function.""" return x, y diff --git a/examples/widget_demo.py b/docs/examples/basic_widgets_demo.py similarity index 92% rename from examples/widget_demo.py rename to docs/examples/basic_widgets_demo.py index 428dc4c0a..b3ae376cf 100644 --- a/examples/widget_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,5 +1,10 @@ -"""Widget demonstration of magicgui.""" +"""# Basic widgets demo +Widget demonstration with magicgui. + +This code demonstrates a few of the widget types that magicgui can create +based on the parameter types in your function. +""" import datetime from enum import Enum from pathlib import Path diff --git a/docs/examples/demo_widgets/README.md b/docs/examples/demo_widgets/README.md new file mode 100644 index 000000000..2ac4101bb --- /dev/null +++ b/docs/examples/demo_widgets/README.md @@ -0,0 +1,3 @@ +# Demo widget types + +Example gallery demonstrating the available widget types in magicgui. diff --git a/examples/change_label.py b/docs/examples/demo_widgets/change_label.py similarity index 63% rename from examples/change_label.py rename to docs/examples/demo_widgets/change_label.py index aab8a90fd..4e3b660dc 100644 --- a/examples/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,9 +1,14 @@ +"""# Custom text labels for widgets + +An example showing how to create custom text labels for your widgets. +""" from magicgui import magicgui # use a different label than the default (the parameter name) in the UI @magicgui(x={"label": "widget to set x"}) def example(x=1, y="hi"): + """Example function.""" return x, y diff --git a/examples/choices.py b/docs/examples/demo_widgets/choices.py similarity index 70% rename from examples/choices.py rename to docs/examples/demo_widgets/choices.py index a02ae3814..74b3857f3 100644 --- a/examples/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,4 +1,7 @@ -"""Choices for dropdowns can be provided in a few different ways.""" +"""# Dropdown selection widget + +Choices for dropdowns can be provided in a few different ways. +""" from enum import Enum from magicgui import magicgui, widgets @@ -14,11 +17,13 @@ class Medium(Enum): @magicgui(ri={"choices": ["Oil", "Water", "Air"]}, auto_call=True) def as_list(ri="Water"): + """Function decorated with magicgui list of choices.""" print("refractive index is", Medium[ri].value) @magicgui(auto_call=True) def as_enum(ri: Medium = Medium.Water): + """Function decorated with magicgui and enumeration.""" print("refractive index is", ri.value) @@ -26,15 +31,18 @@ def as_enum(ri: Medium = Medium.Water): ri={"choices": [("Oil", 1.515), ("Water", 1.33), ("Air", 1.0)]}, auto_call=True ) def as_2tuple(ri=1.33): + """Function decorated with magicgui tuple of choices.""" print("refractive index is", ri) def get_choices(gui): + """Function returning tuple of material and refractive index value.""" return [("Oil", 1.515), ("Water", 1.33), ("Air", 1.0)] @magicgui(ri={"choices": get_choices}, auto_call=True) def as_function(ri: float): + """Function to calculate refractive index.""" print("refractive index is", ri) diff --git a/examples/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py similarity index 95% rename from examples/file_dialog.py rename to docs/examples/demo_widgets/file_dialog.py index 38783df2f..ff7230ebd 100644 --- a/examples/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,4 +1,7 @@ -"""FileDialog with magicgui.""" +"""# File dialog widget + +A file dialog widget example. +""" from pathlib import Path from typing import Sequence diff --git a/examples/image.py b/docs/examples/demo_widgets/image.py similarity index 52% rename from examples/image.py rename to docs/examples/demo_widgets/image.py index 2d66196c7..87cd5c86c 100644 --- a/examples/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,12 +1,12 @@ -"""Example of creating an Image Widget from a file. +"""# Image widget + +Example of creating an Image Widget from a file. (This requires pillow, or that magicgui was installed as ``magicgui[image]``) """ -from pathlib import Path from magicgui.widgets import Image -img = Path(__file__).parent.parent / "tests" / "_test.jpg" -image = Image(value=img) +image = Image(value="../../images/_test.jpg") image.scale_widget_to_image_size() image.show(run=True) diff --git a/examples/log_slider.py b/docs/examples/demo_widgets/log_slider.py similarity index 69% rename from examples/log_slider.py rename to docs/examples/demo_widgets/log_slider.py index a8b01e101..bd3385fdf 100644 --- a/examples/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,4 +1,7 @@ -"""Simple demonstration of magicgui.""" +"""# Log slider widget + +A logarithmic scale range slider widget. +""" from magicgui import magicgui @@ -8,6 +11,7 @@ input={"widget_type": "LogSlider", "max": 10000, "min": 1, "tracking": False}, ) def slider(input=1): + """Logarithmic scale slider.""" return round(input, 4) diff --git a/examples/login.py b/docs/examples/demo_widgets/login.py similarity index 81% rename from examples/login.py rename to docs/examples/demo_widgets/login.py index 0d555b55f..3cefafb42 100644 --- a/examples/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,3 +1,7 @@ +"""# Password login + +A password login field widget. +""" from magicgui import magicgui @@ -8,6 +12,7 @@ # (unless you override "widget_type") @magicgui(password2={"widget_type": "Password"}) def login(username: str, password: str, password2: str): + """User login credentials.""" ... diff --git a/examples/optional.py b/docs/examples/demo_widgets/optional.py similarity index 66% rename from examples/optional.py rename to docs/examples/demo_widgets/optional.py index c2e58e81e..8cae102c9 100644 --- a/examples/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,3 +1,7 @@ +"""# Optional user choice + +Optional user input using a dropdown selection widget. +""" from typing import Optional from magicgui import magicgui @@ -6,6 +10,7 @@ # Using optional will add a '----' to the combobox, which returns "None" @magicgui(path={"choices": ["a", "b"]}) def f(path: Optional[str] = None): + """Öptional user input function.""" print(path, type(path)) diff --git a/examples/range_slider.py b/docs/examples/demo_widgets/range_slider.py similarity index 69% rename from examples/range_slider.py rename to docs/examples/demo_widgets/range_slider.py index 129e8bdd3..9d25d72e2 100644 --- a/examples/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,3 +1,7 @@ +"""# Range slider widget + +A double ended range slider widget. +""" from typing import Tuple from magicgui import magicgui @@ -5,6 +9,7 @@ @magicgui(auto_call=True, range_value={"widget_type": "RangeSlider", "max": 500}) def func(range_value: Tuple[int, int] = (20, 380)): + """Double ended range slider.""" print(range_value) diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py new file mode 100644 index 000000000..8f92052c7 --- /dev/null +++ b/docs/examples/demo_widgets/selection.py @@ -0,0 +1,19 @@ +"""# Multiple selection widget + +A selection widget allowing multiple selections by the user. +""" +from magicgui import magicgui + + +@magicgui( + pick_some={ + "choices": ("first", "second", "third", "fourth"), + "allow_multiple": True, + } +) +def my_widget(pick_some=("first")): + """Dropdown selection function.""" + print("you selected", pick_some) + + +my_widget.show(run=True) diff --git a/examples/table.py b/docs/examples/demo_widgets/table.py similarity index 96% rename from examples/table.py rename to docs/examples/demo_widgets/table.py index 769dbcc7c..4af4b870c 100644 --- a/examples/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,4 +1,7 @@ -"""Demonstrating a few ways to input tables.""" +"""# Table widget + +Demonstrating a few ways to input tables. +""" import numpy as np from magicgui.widgets import Table diff --git a/docs/examples/matplotlib/README.md b/docs/examples/matplotlib/README.md new file mode 100644 index 000000000..aaf4e03a0 --- /dev/null +++ b/docs/examples/matplotlib/README.md @@ -0,0 +1,3 @@ +# matplotlib and magicgui + +Examples involving matplotlib graphs and magicgui. diff --git a/examples/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py similarity index 77% rename from examples/mpl_figure.py rename to docs/examples/matplotlib/mpl_figure.py index db5ef2199..ea29a0af0 100644 --- a/examples/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,6 +1,8 @@ -""""Basic example of adding a generic QWidget to a container. +"""# matplotlib figure example -main lesson: add your QWidget to container.native.layout() +Basic example of adding a generic QWidget to a container. + +Main lesson: add your QWidget to container.native.layout() as shown on line 30 """ @@ -21,6 +23,7 @@ @magicgui(position={"widget_type": "Slider", "max": 255}, auto_call=True) def f(position: int): + """Function demonstrating magicgui combined with matplotlib.""" line.set_ydata(data[position]) line.figure.canvas.draw() diff --git a/examples/waveform.py b/docs/examples/matplotlib/waveform.py similarity index 88% rename from examples/waveform.py rename to docs/examples/matplotlib/waveform.py index 6d808a865..146096232 100644 --- a/examples/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,3 +1,7 @@ +"""# Waveforms example + +Simple waveform generator widget, with plotting. +""" from dataclasses import dataclass, field from enum import Enum from functools import partial @@ -21,7 +25,7 @@ @dataclass class Signal: - """Constructs a 1D signal + """Constructs a 1D signal. As is, this class is not very useful, but one could add callbacks or more functionality here @@ -45,21 +49,20 @@ class Signal: data: np.ndarray = field(init=False) def __post_init__(self): - """evaluate the function at instantiation time""" + """Evaluate the function at instantiation time.""" self.time = np.linspace(0, self.duration, self.size) self.data = self.func(self.time) def plot(self, ax=None, **kwargs): - """Plots the data + """Plots the data. Parameters ---------- ax: matplotlib.axes.Axes instance, default None if provided the plot is done on this axes instance. If None a new ax is created - - - **kwargs are passed to matplotib ax.plot method + **kwargs: Keyword arguments that are passed on to + the matplotib ax.plot method Returns ------- @@ -77,18 +80,19 @@ def plot(self, ax=None, **kwargs): def sine( duration: Time = 10.0, size: int = 500, freq: Freq = 0.5, phase: Phase = 0.0 ) -> Signal: - """Returns a 1D sine wave + """Returns a 1D sine wave. Parameters ---------- duration: float the duration of the signal in seconds + size: int + the number of samples in the signal time array freq: float the frequency of the signal in Hz phase: Phase the phase of the signal (in degrees) """ - sig = Signal( duration=duration, size=size, @@ -105,7 +109,7 @@ def chirp( f1: float = 2.0, phase: Phase = 0.0, ) -> Signal: - """Frequency-swept cosine generator + """Frequency-swept cosine generator. See scipy.signal.chirp """ @@ -156,6 +160,7 @@ def square( def on_off( duration: Time = 10.0, size: int = 500, t_on: Time = 0.01, t_off: Time = 0.01 ) -> Signal: + """On/Off signal function.""" data = np.ones(size) data[: int(size * t_on / duration)] = -1 if t_off > 0: @@ -174,6 +179,8 @@ def on_off( class Select(Enum): + """Enumeration to select signal type.""" + OnOff = "on_off" Sine = "sine" Chirp = "chirp" @@ -185,7 +192,7 @@ class WaveForm(widgets.Container): """Simple waveform generator widget, with plotting.""" def __init__(self): - """Creates the widget""" + """Creates the widget.""" super().__init__() self.fig, self.ax = plt.subplots() self.native.layout().addWidget(FigureCanvas(self.fig)) @@ -197,13 +204,13 @@ def __init__(self): @magicgui(auto_call=True) def signal_widget(self, select: Select = Select.Sine) -> widgets.Container: - """Waveform selection, from the WAVEFORMS dict""" + """Waveform selection, from the WAVEFORMS dict.""" self.waveform = WAVEFORMS[select.value] self.update_controls() self.update_graph(self.waveform()) def update_controls(self): - """Reset controls according to the new function""" + """Reset controls according to the new function.""" if self.controls is not None: self.remove(self.controls) self.controls = magicgui(auto_call=True)(self.waveform) @@ -211,7 +218,7 @@ def update_controls(self): self.controls.called.connect(self.update_graph) def update_graph(self, sig: Signal): - """Re-plot when a parameter changes + """Re-plot when a parameter changes. Note ---- diff --git a/docs/examples/napari/README.md b/docs/examples/napari/README.md new file mode 100644 index 000000000..e4f34aa65 --- /dev/null +++ b/docs/examples/napari/README.md @@ -0,0 +1,3 @@ +# napari and magicgui + +Examples integrating magicgui with napari. diff --git a/examples/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py similarity index 96% rename from examples/napari_combine_qt.py rename to docs/examples/napari/napari_combine_qt.py index 144bad15c..9a4351e27 100644 --- a/examples/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,4 +1,5 @@ -""" +"""# napari Qt demo + Napari provides a few conveniences with magicgui, and one of the most commonly used is the layer combo box that gets created when a parameter is annotated as napari.layers.Layer. @@ -16,6 +17,8 @@ class CustomWidget(QWidget): + """A custom widget class.""" + def __init__(self) -> None: super().__init__() self.setLayout(QVBoxLayout()) diff --git a/examples/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py similarity index 92% rename from examples/napari_forward_refs.py rename to docs/examples/napari/napari_forward_refs.py index 9a52c3a17..1700c0d3c 100644 --- a/examples/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,4 +1,6 @@ -"""Example of using a ForwardRef to avoid importing a module that provides a widget. +"""# napari forward reference demo + +Example of using a ForwardRef to avoid importing a module that provides a widget. In this example, one might want to create a widget that takes as an argument a napari Image layer, and returns an Image. In order to avoid needing to import napari (and diff --git a/docs/examples/napari_img_math.md b/docs/examples/napari/napari_img_math.py similarity index 59% rename from docs/examples/napari_img_math.md rename to docs/examples/napari/napari_img_math.py index 94e299b0e..c14e27cbf 100644 --- a/docs/examples/napari_img_math.md +++ b/docs/examples/napari/napari_img_math.py @@ -1,4 +1,4 @@ -# napari image arithmetic widget +"""# napari image arithmetic widget [napari](https://github.com/napari/napari) is a fast, interactive, multi-dimensional image viewer for python. It uses Qt for the GUI, so it's easy @@ -9,7 +9,7 @@ For napari-specific magicgui documentation, see the [napari docs](https://napari.org/guides/magicgui.html) -![napari image arithmetic widget](../images/imagemath.gif){ width=80% } +![napari image arithmetic widget](../../images/imagemath.gif){ width=80% } ## outline @@ -17,32 +17,32 @@ 1. Create a `magicgui` widget that can be used in another program (napari) -1. Use an `Enum` to create a dropdown menu +2. Use an `Enum` to create a dropdown menu -1. Connect some event listeners to create interactivity. +3. Connect some event listeners to create interactivity. ## code *Code follows, with explanation below... You can also [get this example at -github](https://github.com/pyapp-kit/magicgui/blob/main/examples/napari_image_arithmetic.py).* +github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_image_arithmetic.py).* ```python linenums="1" hl_lines="25 38" from enum import Enum -import numpy import napari +import numpy from napari.types import ImageData from magicgui import magicgui -class Operation(Enum): - """A set of valid arithmetic operations for image_arithmetic. - To create nice dropdown menus with magicgui, it's best - (but not required) to use Enums. Here we make an Enum - class for all of the image math operations we want to - allow. - """ +class Operation(Enum): + # A set of valid arithmetic operations for image_arithmetic. + # + # To create nice dropdown menus with magicgui, it's best + # (but not required) to use Enums. Here we make an Enum + # class for all of the image math operations we want to + # allow. add = numpy.add subtract = numpy.subtract multiply = numpy.multiply @@ -55,7 +55,7 @@ class for all of the image math operations we want to def image_arithmetic( layerA: ImageData, operation: Operation, layerB: ImageData ) -> ImageData: - """Add, subtracts, multiplies, or divides to image layers.""" + # Add, subtracts, multiplies, or divides to image layers. return operation.value(layerA, layerB) # create a viewer and add a couple image layers @@ -76,15 +76,14 @@ def image_arithmetic( ## walkthrough We're going to go a little out of order so that the other code makes more sense. -Let's start with the actual function we'd like to write to do some image -arithmetic. +Let's start with the actual function we'd like to write to do some image arithmetic. ### the function -Our function takes two `numpy` arrays (in this case, from [Image -layers](https://napari.org/howtos/layers/image.html)), and some mathematical -operation (we'll restrict the options using an `enum.Enum`). When called, our -function calls the selected operation on the data. +Our function takes two `numpy` arrays (in this case, from [Image layers](https://napari.org/howtos/layers/image.html)), +and some mathematical operation +(we'll restrict the options using an `enum.Enum`). +When called, ourfunction calls the selected operation on the data. ```python def image_arithmetic(array1, operation, array2): @@ -93,19 +92,17 @@ def image_arithmetic(array1, operation, array2): #### type annotations -`magicgui` works particularly well with [type -annotations](https://docs.python.org/3/library/typing.html), and allows -third-party libraries to register widgets and behavior for handling their custom -types (using [`magicgui.type_map.register_type`][]). `napari` [provides support -for -`magicgui`](https://github.com/napari/napari/blob/main/napari/utils/_magicgui.py) +`magicgui` works particularly well with [type annotations](https://docs.python.org/3/library/typing.html), +and allows third-party libraries to register widgets and behavior for handling +their custom types (using [`magicgui.type_map.register_type`][]). +`napari` [provides support for `magicgui`](https://github.com/napari/napari/blob/main/napari/utils/_magicgui.py) by registering a dropdown menu whenever a function parameter is annotated as one -of the basic napari [`Layer` -types](https://napari.org/howtos/layers/index.html), or, in this case, -`ImageData` indicates we just the `data` attribute of the layer. Furthermore, it -recognizes when a function has a `napari.layers.Layer` or `LayerData` return -type annotation, and will add the result to the viewer. So we gain a *lot* by -annotating the above function with the appropriate `napari` types. +of the basic napari [`Layer` types](https://napari.org/howtos/layers/index.html), +or, in this case, `ImageData` indicates we just the `data` attribute of the layer. +Furthermore, it recognizes when a function has a `napari.layers.Layer` +or `LayerData` return type annotation, and will add the result to the viewer. +So we gain a *lot* by annotating the above function with the appropriate +`napari` types. ```python from napari.types import ImageData @@ -118,7 +115,7 @@ def image_arithmetic( ### the magic part - Finally, we decorate the function with `@magicgui` and tell it we'd like to +Finally, we decorate the function with `@magicgui` and tell it we'd like to have a `call_button` that we can click to execute the function. ```python hl_lines="1" @@ -184,16 +181,68 @@ class Operation(enum.Enum): original function* are called, the result will be passed to your callback function: - ```python - @image_arithmetic.called.connect - def print_mean(value): - """Callback function that accepts an event""" - # the value attribute has the result of calling the function - print(np.mean(value)) +```python +@image_arithmetic.called.connect +def print_mean(value): + # Callback function that accepts an event + # the value attribute has the result of calling the function + print(np.mean(value)) +``` - ``` +``` +>>> image_arithmetic() +1.0060037881040373 +``` + +## Code + +Here's the full code example again. + +""" + +# %% +from enum import Enum - ```python - >>> image_arithmetic() - 1.0060037881040373 - ``` +import napari +import numpy +from napari.types import ImageData + +from magicgui import magicgui + + +class Operation(Enum): + # A set of valid arithmetic operations for image_arithmetic. + # + # To create nice dropdown menus with magicgui, it's best + # (but not required) to use Enums. Here we make an Enum + # class for all of the image math operations we want to + # allow. + add = numpy.add + subtract = numpy.subtract + multiply = numpy.multiply + divide = numpy.divide + + +# here's the magicgui! We also use the additional +# `call_button` option +@magicgui(call_button="execute") +def image_arithmetic( + layerA: ImageData, operation: Operation, layerB: ImageData +) -> ImageData: + # Add, subtracts, multiplies, or divides to image layers. + return operation.value(layerA, layerB) + + +# create a viewer and add a couple image layers +viewer = napari.Viewer() +viewer.add_image(numpy.random.rand(20, 20), name="Layer 1") +viewer.add_image(numpy.random.rand(20, 20), name="Layer 2") + +# add our new magicgui widget to the viewer +viewer.window.add_dock_widget(image_arithmetic) + +# keep the dropdown menus in the gui in sync with the layer model +viewer.layers.events.inserted.connect(image_arithmetic.reset_choices) +viewer.layers.events.removed.connect(image_arithmetic.reset_choices) + +napari.run() diff --git a/docs/examples/napari_parameter_sweep.md b/docs/examples/napari/napari_parameter_sweep.py similarity index 64% rename from docs/examples/napari_parameter_sweep.md rename to docs/examples/napari/napari_parameter_sweep.py index 35460657b..acca30f39 100644 --- a/docs/examples/napari_parameter_sweep.md +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -1,4 +1,4 @@ -# napari parameter sweeps +"""# napari parameter sweeps [napari](https://github.com/napari/napari) is a fast, interactive, multi-dimensional image viewer for python. It uses Qt for the GUI, so it's easy @@ -9,10 +9,10 @@ For napari-specific magicgui documentation, see the [napari docs](https://napari.org/guides/magicgui.html) -![napari image arithmetic widget](../images/param_sweep.gif){ width=80% } +![napari parameter sweep widget](../../images/param_sweep.gif){ width=80% } -*See also:* Some of this tutorial overlaps with topics covered in the [napari -image arithmetic example](napari_img_math) +*See also:* Some of this tutorial overlaps with topics covered in the +[napari image arithmetic example](napari_img_math.py). ## outline @@ -21,21 +21,21 @@ 1. Create a `magicgui` widget that can be used in another program (`napari`) -1. Automatically call our function when a parameter changes +2. Automatically call our function when a parameter changes -1. Provide `magicgui` with a custom widget for a specific +3. Provide `magicgui` with a custom widget for a specific argument -1. Use the `choices` option to create a dropdown +4. Use the `choices` option to create a dropdown -1. Connect some event listeners to create interactivity. +5. Connect some event listeners to create interactivity. ## code *Code follows, with explanation below... You can also [get this example at -github](https://github.com/pyapp-kit/magicgui/blob/main/examples/napari_param_sweep.py).* +github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* -```python linenums="1" +```python linenums="1" hl_lines="14-19 31" import napari import skimage.data import skimage.filters @@ -53,12 +53,10 @@ auto_call=True, sigma={"widget_type": "FloatSlider", "max": 6}, mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, - layout='horizontal' + layout="horizontal", ) -def gaussian_blur( - layer: ImageData, sigma: float = 1.0, mode="nearest" -) -> ImageData: - """Apply a gaussian blur to 'layer'.""" +def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> ImageData: + # Apply a gaussian blur to 'layer'. if layer is not None: return skimage.filters.gaussian(layer, sigma=sigma, mode=mode) @@ -77,8 +75,9 @@ def gaussian_blur( ## walkthrough -We're going to go a little out of order so that the other code makes more sense. Let's -start with the actual function we'd like to write to apply a gaussian filter to an image. +We're going to go a little out of order so that the other code makes more sense. +Let's start with the actual function we'd like to write to apply a gaussian +filter to an image. ### the function @@ -97,14 +96,15 @@ def gaussian_blur( The reasons we are wrapping it here are: -1. `filters.gaussian` accepts a `numpy` array, but we want to work with `napari` layers +1. `filters.gaussian` accepts a `numpy` array, + but we want to work with `napari` layers that store the data in a `layer.data` attribute. So we need an adapter. -2. We'd like to add some [type annotations](type-inference) to the +2. We'd like to add some [type annotations](../../type_map.md) to the signature that were not provided by `filters.gaussian` #### type annotations -As described in the [image arithmetic tutorial](./napari_img_math.md), we take +As described in the [image arithmetic example](napari_img_math.py), we take advantage of napari's built in support for `magicgui` by annotating our function parameters and return value as napari `Layer` types. `napari` will then tell `magicgui` what to do with them, creating a dropdown with a list of current @@ -127,7 +127,7 @@ def gaussian_blur( def gaussian_blur( layer: ImageData, sigma: float = 1.0, mode="nearest" ) -> ImageData: - """Apply a gaussian blur to ``layer``.""" + # Apply a gaussian blur to ``layer``. if layer is not None: return skimage.filters.gaussian(layer, sigma=sigma, mode=mode) ``` @@ -135,7 +135,8 @@ def gaussian_blur( - `auto_call=True` makes it so that the `gaussian_blur` function will be called whenever one of the parameters changes (with the current parameters set in the GUI). -- We then provide keyword arguments to modify the look & behavior of `sigma` and `mode`: +- We then provide keyword arguments to modify the look & behavior of `sigma` + and `mode`: - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard (`float`) widget for the `sigma` widget, but rather to use a slider widget. @@ -146,7 +147,7 @@ def gaussian_blur( ### connecting events -As described in the [Events documentation](../events.md), we can +As described in the [Events documentation](../../events.md), we can also connect any callback to the `gaussian_blur.called` signal that will receive the result of our decorated function anytime it is called. @@ -156,3 +157,46 @@ def do_something_with_result(result): gaussian_blur.called.connect(do_something_with_result) ``` + +## Code + +Here's the full code example again. +""" + +# %% +import napari +import skimage.data +import skimage.filters +from napari.types import ImageData + +from magicgui import magicgui + + +# turn the gaussian blur function into a magicgui +# - 'auto_call' tells magicgui to call the function when a parameter changes +# - we use 'widget_type' to override the default "float" widget on sigma, +# and provide a maximum valid value. +# - we contstrain the possible choices for 'mode' +@magicgui( + auto_call=True, + sigma={"widget_type": "FloatSlider", "max": 6}, + mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, + layout="horizontal", +) +def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> ImageData: + # Apply a gaussian blur to 'layer'. + if layer is not None: + return skimage.filters.gaussian(layer, sigma=sigma, mode=mode) + + +# create a viewer and add some images +viewer = napari.Viewer() +viewer.add_image(skimage.data.astronaut().mean(-1), name="astronaut") +viewer.add_image(skimage.data.grass().astype("float"), name="grass") + +# Add it to the napari viewer +viewer.window.add_dock_widget(gaussian_blur) +# update the layer dropdown menu when the layer list changes +viewer.layers.events.changed.connect(gaussian_blur.reset_choices) + +napari.run() diff --git a/docs/examples/notebooks/README.md b/docs/examples/notebooks/README.md new file mode 100644 index 000000000..aef08795f --- /dev/null +++ b/docs/examples/notebooks/README.md @@ -0,0 +1,3 @@ +# Jupyter notebooks and magicgui + +Examples using jupyter notebooks together with magicgui. diff --git a/examples/magicgui_jupyter.ipynb b/docs/examples/notebooks/magicgui_jupyter.ipynb similarity index 100% rename from examples/magicgui_jupyter.ipynb rename to docs/examples/notebooks/magicgui_jupyter.ipynb diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py new file mode 100644 index 000000000..a108f232b --- /dev/null +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -0,0 +1,45 @@ +"""# Jupyter notebooks and magicgui + +This example shows magicgui widgets embedded in a jupyter notebook. + +The key function here is `use_app("ipynb")`. + +You can also [get this example at github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/notebooks/magicgui_jupyter.ipynb). + +```python hl_lines="4-5" +import math +from enum import Enum + +from magicgui import magicgui, use_app +use_app("ipynb") + +class Medium(Enum): + # Various media and their refractive indices. + Glass = 1.520 + Oil = 1.515 + Water = 1.333 + Air = 1.0003 + + +@magicgui( + call_button="calculate", result_widget=True, layout='vertical', auto_call=True +) +def snells_law(aoi=1.0, n1=Medium.Glass, n2=Medium.Water, degrees=True): + # Calculate the angle of refraction given two media and an angle of incidence. + if degrees: + aoi = math.radians(aoi) + try: + n1 = n1.value + n2 = n2.value + result = math.asin(n1 * math.sin(aoi) / n2) + return round(math.degrees(result) if degrees else result, 2) + except ValueError: # math domain error + return "TIR!" + + +snells_law +``` +""" + +# %% +# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) diff --git a/docs/examples/progress_bars/README.md b/docs/examples/progress_bars/README.md new file mode 100644 index 000000000..ac8e62cfc --- /dev/null +++ b/docs/examples/progress_bars/README.md @@ -0,0 +1,3 @@ +# Progress bars examples + +Examples of progress bars in magicgui. diff --git a/examples/progress.py b/docs/examples/progress_bars/progress.py similarity index 90% rename from examples/progress.py rename to docs/examples/progress_bars/progress.py index a04090606..6f064b5e5 100644 --- a/examples/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,3 +1,7 @@ +"""# Simple progress bar + +A simple progress bar demo with magicgui. +""" from time import sleep from magicgui import magicgui diff --git a/examples/progress_manual.py b/docs/examples/progress_bars/progress_manual.py similarity index 81% rename from examples/progress_manual.py rename to docs/examples/progress_bars/progress_manual.py index 61776360e..ccc907862 100644 --- a/examples/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,3 +1,8 @@ +"""# Manual progress bar + +Example of a progress bar being updated manually. + +""" from magicgui import magicgui from magicgui.widgets import ProgressBar diff --git a/examples/progress_nested.py b/docs/examples/progress_bars/progress_nested.py similarity index 93% rename from examples/progress_nested.py rename to docs/examples/progress_bars/progress_nested.py index 51c4228e6..6bb9a9b43 100644 --- a/examples/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,3 +1,9 @@ +"""# Nested progress bars + +Example using nested progress bars in magicgui. + +""" + import random from time import sleep diff --git a/docs/examples/under_the_hood/README.md b/docs/examples/under_the_hood/README.md new file mode 100644 index 000000000..0953445b0 --- /dev/null +++ b/docs/examples/under_the_hood/README.md @@ -0,0 +1,5 @@ +# Under the hood + +Learn more advanced usage patterns for magicgui, +including self referencing widgets and +decorating class methods with magicgui. diff --git a/examples/class_method.py b/docs/examples/under_the_hood/class_method.py similarity index 83% rename from examples/class_method.py rename to docs/examples/under_the_hood/class_method.py index 27586b115..7ca336755 100644 --- a/examples/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,4 +1,6 @@ -"""Demonstrates decorating a method. +"""# Deocrate class methods with magicgui + +Demonstrates decorating a class method with magicgui. Once the class is instantiated, `instance.method_name` will return a FunctionGui in which the instance will always be provided as the first argument (i.e. "self") when @@ -9,12 +11,15 @@ class MyObject: + """Example object class.""" + def __init__(self, name): self.name = name self.counter = 0.0 @magicgui(auto_call=True) def method(self, sigma: float = 0): + """Example class method.""" print(f"instance: {self.name}, counter: {self.counter}, sigma: {sigma}") self.counter = self.counter + sigma return self.name diff --git a/examples/self_reference.py b/docs/examples/under_the_hood/self_reference.py similarity index 64% rename from examples/self_reference.py rename to docs/examples/under_the_hood/self_reference.py index 0fa407671..58f2a68f6 100644 --- a/examples/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,8 +1,13 @@ +"""# Self reference magicgui widgets + +Widgets created with magicgui can reference themselves, and use the widget API. +""" from magicgui import magicgui @magicgui(auto_call=True, width={"max": 800, "min": 100}, x={"widget_type": "Slider"}) def function(width=400, x: int = 50): + """Example function.""" # the widget can reference itself, and use the widget API function.x.width = width diff --git a/docs/gallery_conf.py b/docs/gallery_conf.py new file mode 100644 index 000000000..3c8bc3490 --- /dev/null +++ b/docs/gallery_conf.py @@ -0,0 +1,89 @@ +import warnings +from pathlib import Path + +import napari +import qtgallery +from mkdocs_gallery.gen_data_model import GalleryScript +from mkdocs_gallery.scrapers import figure_md_or_html, matplotlib_scraper +from qtpy.QtWidgets import QApplication + +warnings.filterwarnings("ignore", category=DeprecationWarning) + + +def qt_window_scraper(block, script: GalleryScript): + """Scrape screenshots from open Qt windows. + + Parameters + ---------- + block : tuple + A tuple containing the (label, content, line_number) of the block. + script : GalleryScript + Script being run + + Returns + ------- + md : str + The ReSTructuredText that will be rendered to HTML containing + the images. This is often produced by :func:`figure_md_or_html`. + """ + imgpath_iter = script.run_vars.image_path_iterator + + app = QApplication.instance() + if app is None: + app = QApplication([]) + app.processEvents() + + # get top-level widgets that aren't hidden + widgets = [w for w in app.topLevelWidgets() if not w.isHidden()] + + image_paths = [] + for widg, imgpath in zip(widgets, imgpath_iter): + pixmap = widg.grab() + pixmap.save(str(imgpath)) + image_paths.append(imgpath) + widg.close() + + return figure_md_or_html(image_paths, script) + + +def napari_image_scraper(block, script: GalleryScript): + """Scrape screenshots from napari windows. + + Parameters + ---------- + block : tuple + A tuple containing the (label, content, line_number) of the block. + script : GalleryScript + Script being run + + Returns + ------- + md : str + The ReSTructuredText that will be rendered to HTML containing + the images. This is often produced by :func:`figure_md_or_html`. + """ + viewer = napari.current_viewer() + if viewer is not None: + image_path = next(script.run_vars.image_path_iterator) + viewer.screenshot(canvas_only=False, flash=False, path=image_path) + viewer.close() + return figure_md_or_html([image_path], script) + else: + return "" + + +def _reset_napari(gallery_conf, file: Path): + # Close all open napari windows and reset theme + while napari.current_viewer() is not None: + napari.current_viewer().close() + settings = napari.settings.get_settings() + settings.appearance.theme = "dark" + # qtgallery manages the event loop so it + # is not completely blocked by napari.run() + qtgallery.reset_qapp(gallery_conf, file) + + +conf = { + "image_scrapers": [napari_image_scraper, qt_window_scraper, matplotlib_scraper], + "reset_modules": [_reset_napari, qtgallery.reset_qapp], +} diff --git a/docs/images/_test.jpg b/docs/images/_test.jpg new file mode 100644 index 000000000..62f90a4cb Binary files /dev/null and b/docs/images/_test.jpg differ diff --git a/docs/images/jupyter_magicgui_widget.png b/docs/images/jupyter_magicgui_widget.png new file mode 100644 index 000000000..5f2ea2efa Binary files /dev/null and b/docs/images/jupyter_magicgui_widget.png differ diff --git a/docs/images/values_input.png b/docs/images/values_input.png new file mode 100644 index 000000000..a3c92afea Binary files /dev/null and b/docs/images/values_input.png differ diff --git a/docs/scripts/_hooks.py b/docs/scripts/_hooks.py index 812e4d34d..d7922e5f7 100644 --- a/docs/scripts/_hooks.py +++ b/docs/scripts/_hooks.py @@ -123,7 +123,7 @@ def _replace_type_to_widget(md: str) -> str: _name = name.split("[")[0] name_link = f"[`{name}`][typing.{_name}]" else: - name_link = f"[`{name}`][]" + name_link = f"[`{name}`][{name}]" table.append(f"| {name_link} | {wdg_link} | {kwargs} | ") lines[start:last_line] = table diff --git a/examples/napari_image_arithmetic.py b/examples/napari_image_arithmetic.py deleted file mode 100644 index 28844a285..000000000 --- a/examples/napari_image_arithmetic.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Basic example of using magicgui to create an Image Arithmetic GUI in napari.""" -from enum import Enum - -import napari -import numpy -from napari.layers import Image -from napari.types import ImageData - -from magicgui import magicgui - - -class Operation(Enum): - """A set of valid arithmetic operations for image_arithmetic. - - To create nice dropdown menus with magicgui, it's best (but not required) to use - Enums. Here we make an Enum class for all of the image math operations we want to - allow. - """ - - add = numpy.add - subtract = numpy.subtract - multiply = numpy.multiply - divide = numpy.divide - - -# create a viewer and add a couple image layers -viewer = napari.Viewer() -viewer.add_image(numpy.random.rand(20, 20), name="Layer 1") -viewer.add_image(numpy.random.rand(20, 20), name="Layer 2") - - -# for details on why the `-> ImageData` return annotation works: -# https://napari.org/guides/magicgui.html#return-annotations -@magicgui(call_button="execute", layout="horizontal") -def image_arithmetic(layerA: Image, operation: Operation, layerB: Image) -> ImageData: - """Add, subtracts, multiplies, or divides to image layers with equal shape.""" - return operation.value(layerA.data, layerB.data) - - -# add our new magicgui widget to the viewer -viewer.window.add_dock_widget(image_arithmetic, area="bottom") - - -# note: the function may still be called directly as usual! -# new_image = image_arithmetic(img_a, Operation.add, img_b) - -napari.run() diff --git a/examples/napari_param_sweep.py b/examples/napari_param_sweep.py deleted file mode 100644 index aee075854..000000000 --- a/examples/napari_param_sweep.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Example showing how to accomplish a napari parameter sweep with magicgui. - -It demonstrates: -1. overriding the default widget type with a custom class -2. the `auto_call` option, which calls the function whenever a parameter changes - -""" -import napari -import skimage.data -import skimage.filters -from napari.layers import Image -from napari.types import ImageData - -from magicgui import magicgui - -# create a viewer and add some images -viewer = napari.Viewer() -viewer.add_image(skimage.data.astronaut().mean(-1), name="astronaut") -viewer.add_image(skimage.data.grass().astype("float"), name="grass") - - -# turn the gaussian blur function into a magicgui -# for details on why the `-> ImageData` return annotation works: -# https://napari.org/guides/magicgui.html#return-annotations -@magicgui( - # tells magicgui to call the function whenever a parameter changes - auto_call=True, - # `widget_type` to override the default (spinbox) "float" widget - sigma={"widget_type": "FloatSlider", "max": 6}, - # contstrain the possible choices for `mode` - mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, - layout="horizontal", -) -def gaussian_blur(layer: Image, sigma: float = 1.0, mode="nearest") -> ImageData: - """Apply a gaussian blur to ``layer``.""" - if layer: - return skimage.filters.gaussian(layer.data, sigma=sigma, mode=mode) - - -# Add it to the napari viewer -viewer.window.add_dock_widget(gaussian_blur, area="bottom") - -napari.run() diff --git a/examples/pint_quantity.py b/examples/pint_quantity.py deleted file mode 100644 index aa1e8a2b7..000000000 --- a/examples/pint_quantity.py +++ /dev/null @@ -1,11 +0,0 @@ -from pint import Quantity - -from magicgui import magicgui - - -@magicgui -def widget(q=Quantity("1 ms")): - print(q) - - -widget.show(run=True) diff --git a/examples/selection.py b/examples/selection.py deleted file mode 100644 index e362a638d..000000000 --- a/examples/selection.py +++ /dev/null @@ -1,14 +0,0 @@ -from magicgui import magicgui - - -@magicgui( - pick_some={ - "choices": ["first", "second", "third", "fourth"], - "allow_multiple": True, - } -) -def my_widget(pick_some=["first"]): - print("you selected", pick_some) - - -my_widget.show(run=True) diff --git a/examples/values_dialog.py b/examples/values_dialog.py deleted file mode 100644 index 95cc212c0..000000000 --- a/examples/values_dialog.py +++ /dev/null @@ -1,8 +0,0 @@ -from magicgui.widgets import request_values - -vals = request_values( - age=int, - name={"annotation": str, "label": "Enter your name:"}, - title="Hi, who are you?", -) -print(repr(vals)) diff --git a/mkdocs.yml b/mkdocs.yml index 4031974b8..65e662f91 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ theme: features: - content.code.annotate - navigation.sections + - navigation.indexes - toc.follow - search.suggest - search.share @@ -52,10 +53,7 @@ nav: - events.md - decorators.md - dataclasses.md - - Examples: - - examples/basic.md - - examples/napari_img_math.md - - examples/napari_parameter_sweep.md + - Examples: generated_examples # This node will automatically be named and have sub-nodes. - API: - magicgui: api/magicgui.md - magic_factory: api/magic_factory.md @@ -105,6 +103,13 @@ plugins: scripts: - docs/scripts/_gen_screenshots.py - docs/scripts/_gen_widgets.py + - gallery: + conf_script: docs/gallery_conf.py + examples_dirs: [docs/examples] + gallery_dirs: [docs/generated_examples] + filename_pattern: /*.py # which scripts will be executed for the docs + ignore_pattern: /__init__.py # ignore these example files completely + run_stale_examples: True - spellcheck: backends: # the backends you want to use - codespell: # or nested configs diff --git a/pyproject.toml b/pyproject.toml index 9757caeba..e44e8a427 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,16 +112,21 @@ dev = [ ] docs = [ "mkdocs", - "mkdocs-material", - "mkdocstrings-python", + "mkdocs-material ~=9.2", + "mkdocstrings ==0.22.0", + "mkdocstrings-python ==1.6.2", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-spellcheck[all]", + "mkdocs-gallery", + "qtgallery", # extras for all the widgets + "napari ==0.4.18", + "pyside6 ==6.4.2", # 6.4.3 gives segfault for some reason "pint", - "ipywidgets>=8.0.0", + "matplotlib", + "ipywidgets >=8.0.0", "ipykernel", - "pyside6==6.4.2", # 6.4.3 gives segfault for some reason ] [project.urls] @@ -160,7 +165,6 @@ matrix.backend.features = [ ] - # https://github.com/charliermarsh/ruff [tool.ruff] line-length = 88 @@ -194,7 +198,9 @@ ignore = [ [tool.ruff.per-file-ignores] "tests/*.py" = ["D", "E501"] -"examples/*.py" = ["D", "B"] +"docs/examples/*.py" = ["D", "B"] +"docs/examples/napari/*" = ["E501"] +"docs/examples/notebooks/*" = ["E501"] "src/magicgui/widgets/_image/*.py" = ["D"] "setup.py" = ["F821"] "docs/*.py" = ["B"] @@ -225,7 +231,6 @@ pretty = true [[tool.mypy.overrides]] module = [ "_pytest.*", - ".examples/", ".docs/", "magicgui.widgets._image.*", "magicgui.backends.*", @@ -254,7 +259,7 @@ omit = [ "src/magicgui/widgets/_image/_mpl_image.py", "src/magicgui/widgets/_bases/*", "tests/*", - "examples/*", + "docs/*", ] diff --git a/src/magicgui/widgets/_image/_mpl_image.py b/src/magicgui/widgets/_image/_mpl_image.py index 46f14e2e1..5cf4f93c5 100644 --- a/src/magicgui/widgets/_image/_mpl_image.py +++ b/src/magicgui/widgets/_image/_mpl_image.py @@ -120,7 +120,7 @@ class Colormap: def __init__( self, colors: Collection = [[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0]], - controls: Collection = np.zeros((0, 4)), # noqa: B008 + controls: Collection = np.zeros((0, 4)), interpolation: str = "linear", ) -> None: self.interpolation = interpolation @@ -544,8 +544,7 @@ def set_data( self._A.dtype, float, "same_kind" ): raise TypeError( - "Image data of dtype {} cannot be converted to " - "float".format(self._A.dtype) + f"Image data of dtype {self._A.dtype} cannot be converted to " "float" ) if self._A.ndim == 3 and self._A.shape[-1] == 1: diff --git a/tests/test_docs.py b/tests/test_docs.py index d5185fcf2..d80e45c8f 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -33,7 +33,14 @@ def test_doc_code_cells(fname): exec(cell, globalns) -example_files = [f for f in glob("examples/*.py") if "napari" not in f] +EXAMPLES = Path(__file__).parent.parent / "docs" / "examples" +# leaving out image only because finding the image file in both +# tests and docs is a pain... +# range_slider has periodic segfaults +EXCLUDED = {"napari", "image", "range_slider"} +example_files = [ + str(f) for f in EXAMPLES.rglob("*.py") if all(x not in str(f) for x in EXCLUDED) +] # if os is Linux and python version is 3.9 and backend is PyQt5 LINUX = sys.platform.startswith("linux")