From bf58bd60e78d8295cede64aacc56fcf300dc8176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 19:24:00 -0400 Subject: [PATCH 1/3] ci(dependabot): bump actions/checkout from 3 to 4 (#578) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy_docs.yml | 2 +- .github/workflows/test_and_deploy.yml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index a56e0acc1..c4f79edab 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -12,7 +12,7 @@ jobs: docs: runs-on: macos-latest # for the screenshots steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.x' 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 From 6e6f0dae83299a69b12df0fc2c1335ed074bc8db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 19:24:09 -0400 Subject: [PATCH 2/3] ci(pre-commit.ci): autoupdate (#580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci(pre-commit.ci): autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.282 → v0.0.287](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.282...v0.0.287) - [github.com/abravalheri/validate-pyproject: v0.13 → v0.14](https://github.com/abravalheri/validate-pyproject/compare/v0.13...v0.14) - [github.com/pre-commit/mirrors-mypy: v1.4.1 → v1.5.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.4.1...v1.5.1) * style(pre-commit.ci): auto fixes [...] --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- src/magicgui/widgets/_image/_mpl_image.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) 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/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: From 4158d0af2edc7664e04f53f3d8ca44bd8342956c Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:31:06 +1000 Subject: [PATCH 3/3] Auto-generated examples gallery (#571) * mkdocs-gallery skeleton * Move example scripts into example gallery docs location * Add required README files for mkdocs-gallery * Move example files into docs subdirectory * Working examples gallery for magicgui * Remove period after markdown formatting heading ==== * style(pre-commit.ci): auto fixes [...] * Fix formatting of python docstrings so pre-commit doesn't overwrite * More meaningful file name for hotdog app example * Minor clarifications for examples text * style(pre-commit.ci): auto fixes [...] * Fix formatting errors (lines too long) * Ruff formatting rules * Fix mkdocs build --strict warnings * Must have napari in docs requirements to build examples in docs * update deps * pin napari * update pip * fix ruff * Examples jupytext - put more info in docstring so users see fewer # symbols when looking at plain text * change deps * fix example tests * revert pyside change * update image path * remove periods * fix links * test: skip rangeslider --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Talley Lambert --- .github/workflows/deploy_docs.yml | 10 +- .gitignore | 1 + docs/examples/README.md | 3 + docs/examples/applications/README.md | 3 + .../examples/applications}/callable.py | 8 + .../examples/applications}/chaining.py | 7 + .../examples/applications/hotdog.py | 6 +- docs/examples/applications/pint_quantity.py | 20 +++ .../examples/applications}/snells_law.py | 5 +- docs/examples/applications/values_dialog.py | 21 +++ docs/examples/basic.md | 55 ------- .../examples/basic_example.py | 5 + .../examples/basic_widgets_demo.py | 7 +- docs/examples/demo_widgets/README.md | 3 + .../examples/demo_widgets}/change_label.py | 5 + .../examples/demo_widgets}/choices.py | 10 +- .../examples/demo_widgets}/file_dialog.py | 5 +- .../examples/demo_widgets}/image.py | 8 +- .../examples/demo_widgets}/log_slider.py | 6 +- .../examples/demo_widgets}/login.py | 5 + .../examples/demo_widgets}/optional.py | 5 + .../examples/demo_widgets}/range_slider.py | 5 + docs/examples/demo_widgets/selection.py | 19 +++ .../examples/demo_widgets}/table.py | 5 +- docs/examples/matplotlib/README.md | 3 + .../examples/matplotlib}/mpl_figure.py | 7 +- .../examples/matplotlib}/waveform.py | 33 +++-- docs/examples/napari/README.md | 3 + .../examples/napari}/napari_combine_qt.py | 5 +- .../examples/napari}/napari_forward_refs.py | 4 +- .../napari_img_math.py} | 137 ++++++++++++------ .../napari_parameter_sweep.py} | 90 +++++++++--- docs/examples/notebooks/README.md | 3 + .../notebooks}/magicgui_jupyter.ipynb | 0 docs/examples/notebooks/magicgui_jupyter.py | 45 ++++++ docs/examples/progress_bars/README.md | 3 + .../examples/progress_bars}/progress.py | 4 + .../progress_bars}/progress_manual.py | 5 + .../progress_bars}/progress_nested.py | 6 + docs/examples/under_the_hood/README.md | 5 + .../examples/under_the_hood}/class_method.py | 7 +- .../under_the_hood}/self_reference.py | 5 + docs/gallery_conf.py | 89 ++++++++++++ docs/images/_test.jpg | Bin 0 -> 14989 bytes docs/images/jupyter_magicgui_widget.png | Bin 0 -> 16374 bytes docs/images/values_input.png | Bin 0 -> 45622 bytes docs/scripts/_hooks.py | 2 +- examples/napari_image_arithmetic.py | 47 ------ examples/napari_param_sweep.py | 43 ------ examples/pint_quantity.py | 11 -- examples/selection.py | 14 -- examples/values_dialog.py | 8 - mkdocs.yml | 13 +- pyproject.toml | 21 ++- tests/test_docs.py | 9 +- 55 files changed, 558 insertions(+), 291 deletions(-) create mode 100644 docs/examples/README.md create mode 100644 docs/examples/applications/README.md rename {examples => docs/examples/applications}/callable.py (66%) rename {examples => docs/examples/applications}/chaining.py (86%) rename examples/main_window.py => docs/examples/applications/hotdog.py (86%) create mode 100644 docs/examples/applications/pint_quantity.py rename {examples => docs/examples/applications}/snells_law.py (85%) create mode 100644 docs/examples/applications/values_dialog.py delete mode 100644 docs/examples/basic.md rename examples/basic.py => docs/examples/basic_example.py (61%) rename examples/widget_demo.py => docs/examples/basic_widgets_demo.py (92%) create mode 100644 docs/examples/demo_widgets/README.md rename {examples => docs/examples/demo_widgets}/change_label.py (63%) rename {examples => docs/examples/demo_widgets}/choices.py (70%) rename {examples => docs/examples/demo_widgets}/file_dialog.py (95%) rename {examples => docs/examples/demo_widgets}/image.py (52%) rename {examples => docs/examples/demo_widgets}/log_slider.py (69%) rename {examples => docs/examples/demo_widgets}/login.py (81%) rename {examples => docs/examples/demo_widgets}/optional.py (66%) rename {examples => docs/examples/demo_widgets}/range_slider.py (69%) create mode 100644 docs/examples/demo_widgets/selection.py rename {examples => docs/examples/demo_widgets}/table.py (96%) create mode 100644 docs/examples/matplotlib/README.md rename {examples => docs/examples/matplotlib}/mpl_figure.py (77%) rename {examples => docs/examples/matplotlib}/waveform.py (88%) create mode 100644 docs/examples/napari/README.md rename {examples => docs/examples/napari}/napari_combine_qt.py (96%) rename {examples => docs/examples/napari}/napari_forward_refs.py (92%) rename docs/examples/{napari_img_math.md => napari/napari_img_math.py} (59%) rename docs/examples/{napari_parameter_sweep.md => napari/napari_parameter_sweep.py} (64%) create mode 100644 docs/examples/notebooks/README.md rename {examples => docs/examples/notebooks}/magicgui_jupyter.ipynb (100%) create mode 100644 docs/examples/notebooks/magicgui_jupyter.py create mode 100644 docs/examples/progress_bars/README.md rename {examples => docs/examples/progress_bars}/progress.py (90%) rename {examples => docs/examples/progress_bars}/progress_manual.py (81%) rename {examples => docs/examples/progress_bars}/progress_nested.py (93%) create mode 100644 docs/examples/under_the_hood/README.md rename {examples => docs/examples/under_the_hood}/class_method.py (83%) rename {examples => docs/examples/under_the_hood}/self_reference.py (64%) create mode 100644 docs/gallery_conf.py create mode 100644 docs/images/_test.jpg create mode 100644 docs/images/jupyter_magicgui_widget.png create mode 100644 docs/images/values_input.png delete mode 100644 examples/napari_image_arithmetic.py delete mode 100644 examples/napari_param_sweep.py delete mode 100644 examples/pint_quantity.py delete mode 100644 examples/selection.py delete mode 100644 examples/values_dialog.py diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index c4f79edab..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@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/.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/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 0000000000000000000000000000000000000000..62f90a4cbfb9cf70914f9b2a17fb0cee15ff5b64 GIT binary patch literal 14989 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 Zx$tv{nxyoVFMwstK3fIe2gn%ye*m#+p}YV9 literal 0 HcmV?d00001 diff --git a/docs/images/jupyter_magicgui_widget.png b/docs/images/jupyter_magicgui_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..5f2ea2efa1c7b760a018b519ca71a8961938aa04 GIT binary patch literal 16374 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 oWc*XS35Nw@!To>mt2djUv<@ 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

2*S!OdP2E#H#5c?QgMOWR^avci6yix^GIFCR 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 C+DR1v literal 0 HcmV?d00001 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/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")