From e4638e3459e2eb64bb1f263c9a6687dc47661971 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:20:16 +1000 Subject: [PATCH 01/27] mkdocs-gallery skeleton --- .gitignore | 1 + docs/examples_gallery/README.md | 3 ++ docs/gallery_conf.py | 89 +++++++++++++++++++++++++++++++++ mkdocs.yml | 9 ++++ 4 files changed, 102 insertions(+) create mode 100644 docs/examples_gallery/README.md create mode 100644 docs/gallery_conf.py diff --git a/.gitignore b/.gitignore index 3348e7b06..53728ab7e 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ venv.bak/ # mkdocs documentation /site +docs/generated # mypy .mypy_cache/ diff --git a/docs/examples_gallery/README.md b/docs/examples_gallery/README.md new file mode 100644 index 000000000..48813d7c3 --- /dev/null +++ b/docs/examples_gallery/README.md @@ -0,0 +1,3 @@ +# Examples gallery + +A gallery of examples for magicgui. 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/mkdocs.yml b/mkdocs.yml index 4031974b8..88ab85a68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,8 +54,10 @@ nav: - dataclasses.md - Examples: - examples/basic.md + - examples/very_basic.md - examples/napari_img_math.md - examples/napari_parameter_sweep.md + - 'Examples gallery': generated/gallery # This node will automatically be named and have sub-nodes. - API: - magicgui: api/magicgui.md - magic_factory: api/magic_factory.md @@ -105,6 +107,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] + gallery_dirs: [docs/generated/gallery] + filename_pattern: /*.py # which scripts will be executed for the docs + ignore_pattern: /__init__.py # ignore these examle files completely + run_stale_examples: True - spellcheck: backends: # the backends you want to use - codespell: # or nested configs From e7361d5cae031107541f62bf41cac87fac7e3b8b Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:55:05 +1000 Subject: [PATCH 02/27] Move example scripts into example gallery docs location --- {examples => docs/examples_gallery/applications}/basic.py | 0 {examples => docs/examples_gallery/applications}/callable.py | 0 {examples => docs/examples_gallery/applications}/chaining.py | 0 {examples => docs/examples_gallery/applications}/main_window.py | 0 {examples => docs/examples_gallery/applications}/pint_quantity.py | 0 {examples => docs/examples_gallery/applications}/snells_law.py | 0 {examples => docs/examples_gallery/applications}/values_dialog.py | 0 {examples => docs/examples_gallery/demo_widgets}/change_label.py | 0 {examples => docs/examples_gallery/demo_widgets}/choices.py | 0 {examples => docs/examples_gallery/demo_widgets}/file_dialog.py | 0 {examples => docs/examples_gallery/demo_widgets}/image.py | 0 {examples => docs/examples_gallery/demo_widgets}/log_slider.py | 0 {examples => docs/examples_gallery/demo_widgets}/login.py | 0 {examples => docs/examples_gallery/demo_widgets}/optional.py | 0 {examples => docs/examples_gallery/demo_widgets}/range_slider.py | 0 {examples => docs/examples_gallery/demo_widgets}/selection.py | 0 {examples => docs/examples_gallery/demo_widgets}/table.py | 0 {examples => docs/examples_gallery/demo_widgets}/widget_demo.py | 0 {examples => docs/examples_gallery/matplotlib}/mpl_figure.py | 0 {examples => docs/examples_gallery/matplotlib}/waveform.py | 0 .../examples_gallery/napari_examples}/napari_combine_qt.py | 0 .../examples_gallery/napari_examples}/napari_forward_refs.py | 0 .../examples_gallery/napari_examples}/napari_image_arithmetic.py | 0 .../examples_gallery/napari_examples}/napari_param_sweep.py | 0 .../examples_gallery/notebooks}/magicgui_jupyter.ipynb | 0 {examples => docs/examples_gallery/progess_bars}/progress.py | 0 .../examples_gallery/progess_bars}/progress_manual.py | 0 .../examples_gallery/progess_bars}/progress_nested.py | 0 .../examples_gallery/under_the_hood}/class_method.py | 0 .../examples_gallery/under_the_hood}/self_reference.py | 0 30 files changed, 0 insertions(+), 0 deletions(-) rename {examples => docs/examples_gallery/applications}/basic.py (100%) rename {examples => docs/examples_gallery/applications}/callable.py (100%) rename {examples => docs/examples_gallery/applications}/chaining.py (100%) rename {examples => docs/examples_gallery/applications}/main_window.py (100%) rename {examples => docs/examples_gallery/applications}/pint_quantity.py (100%) rename {examples => docs/examples_gallery/applications}/snells_law.py (100%) rename {examples => docs/examples_gallery/applications}/values_dialog.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/change_label.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/choices.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/file_dialog.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/image.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/log_slider.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/login.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/optional.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/range_slider.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/selection.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/table.py (100%) rename {examples => docs/examples_gallery/demo_widgets}/widget_demo.py (100%) rename {examples => docs/examples_gallery/matplotlib}/mpl_figure.py (100%) rename {examples => docs/examples_gallery/matplotlib}/waveform.py (100%) rename {examples => docs/examples_gallery/napari_examples}/napari_combine_qt.py (100%) rename {examples => docs/examples_gallery/napari_examples}/napari_forward_refs.py (100%) rename {examples => docs/examples_gallery/napari_examples}/napari_image_arithmetic.py (100%) rename {examples => docs/examples_gallery/napari_examples}/napari_param_sweep.py (100%) rename {examples => docs/examples_gallery/notebooks}/magicgui_jupyter.ipynb (100%) rename {examples => docs/examples_gallery/progess_bars}/progress.py (100%) rename {examples => docs/examples_gallery/progess_bars}/progress_manual.py (100%) rename {examples => docs/examples_gallery/progess_bars}/progress_nested.py (100%) rename {examples => docs/examples_gallery/under_the_hood}/class_method.py (100%) rename {examples => docs/examples_gallery/under_the_hood}/self_reference.py (100%) diff --git a/examples/basic.py b/docs/examples_gallery/applications/basic.py similarity index 100% rename from examples/basic.py rename to docs/examples_gallery/applications/basic.py diff --git a/examples/callable.py b/docs/examples_gallery/applications/callable.py similarity index 100% rename from examples/callable.py rename to docs/examples_gallery/applications/callable.py diff --git a/examples/chaining.py b/docs/examples_gallery/applications/chaining.py similarity index 100% rename from examples/chaining.py rename to docs/examples_gallery/applications/chaining.py diff --git a/examples/main_window.py b/docs/examples_gallery/applications/main_window.py similarity index 100% rename from examples/main_window.py rename to docs/examples_gallery/applications/main_window.py diff --git a/examples/pint_quantity.py b/docs/examples_gallery/applications/pint_quantity.py similarity index 100% rename from examples/pint_quantity.py rename to docs/examples_gallery/applications/pint_quantity.py diff --git a/examples/snells_law.py b/docs/examples_gallery/applications/snells_law.py similarity index 100% rename from examples/snells_law.py rename to docs/examples_gallery/applications/snells_law.py diff --git a/examples/values_dialog.py b/docs/examples_gallery/applications/values_dialog.py similarity index 100% rename from examples/values_dialog.py rename to docs/examples_gallery/applications/values_dialog.py diff --git a/examples/change_label.py b/docs/examples_gallery/demo_widgets/change_label.py similarity index 100% rename from examples/change_label.py rename to docs/examples_gallery/demo_widgets/change_label.py diff --git a/examples/choices.py b/docs/examples_gallery/demo_widgets/choices.py similarity index 100% rename from examples/choices.py rename to docs/examples_gallery/demo_widgets/choices.py diff --git a/examples/file_dialog.py b/docs/examples_gallery/demo_widgets/file_dialog.py similarity index 100% rename from examples/file_dialog.py rename to docs/examples_gallery/demo_widgets/file_dialog.py diff --git a/examples/image.py b/docs/examples_gallery/demo_widgets/image.py similarity index 100% rename from examples/image.py rename to docs/examples_gallery/demo_widgets/image.py diff --git a/examples/log_slider.py b/docs/examples_gallery/demo_widgets/log_slider.py similarity index 100% rename from examples/log_slider.py rename to docs/examples_gallery/demo_widgets/log_slider.py diff --git a/examples/login.py b/docs/examples_gallery/demo_widgets/login.py similarity index 100% rename from examples/login.py rename to docs/examples_gallery/demo_widgets/login.py diff --git a/examples/optional.py b/docs/examples_gallery/demo_widgets/optional.py similarity index 100% rename from examples/optional.py rename to docs/examples_gallery/demo_widgets/optional.py diff --git a/examples/range_slider.py b/docs/examples_gallery/demo_widgets/range_slider.py similarity index 100% rename from examples/range_slider.py rename to docs/examples_gallery/demo_widgets/range_slider.py diff --git a/examples/selection.py b/docs/examples_gallery/demo_widgets/selection.py similarity index 100% rename from examples/selection.py rename to docs/examples_gallery/demo_widgets/selection.py diff --git a/examples/table.py b/docs/examples_gallery/demo_widgets/table.py similarity index 100% rename from examples/table.py rename to docs/examples_gallery/demo_widgets/table.py diff --git a/examples/widget_demo.py b/docs/examples_gallery/demo_widgets/widget_demo.py similarity index 100% rename from examples/widget_demo.py rename to docs/examples_gallery/demo_widgets/widget_demo.py diff --git a/examples/mpl_figure.py b/docs/examples_gallery/matplotlib/mpl_figure.py similarity index 100% rename from examples/mpl_figure.py rename to docs/examples_gallery/matplotlib/mpl_figure.py diff --git a/examples/waveform.py b/docs/examples_gallery/matplotlib/waveform.py similarity index 100% rename from examples/waveform.py rename to docs/examples_gallery/matplotlib/waveform.py diff --git a/examples/napari_combine_qt.py b/docs/examples_gallery/napari_examples/napari_combine_qt.py similarity index 100% rename from examples/napari_combine_qt.py rename to docs/examples_gallery/napari_examples/napari_combine_qt.py diff --git a/examples/napari_forward_refs.py b/docs/examples_gallery/napari_examples/napari_forward_refs.py similarity index 100% rename from examples/napari_forward_refs.py rename to docs/examples_gallery/napari_examples/napari_forward_refs.py diff --git a/examples/napari_image_arithmetic.py b/docs/examples_gallery/napari_examples/napari_image_arithmetic.py similarity index 100% rename from examples/napari_image_arithmetic.py rename to docs/examples_gallery/napari_examples/napari_image_arithmetic.py diff --git a/examples/napari_param_sweep.py b/docs/examples_gallery/napari_examples/napari_param_sweep.py similarity index 100% rename from examples/napari_param_sweep.py rename to docs/examples_gallery/napari_examples/napari_param_sweep.py diff --git a/examples/magicgui_jupyter.ipynb b/docs/examples_gallery/notebooks/magicgui_jupyter.ipynb similarity index 100% rename from examples/magicgui_jupyter.ipynb rename to docs/examples_gallery/notebooks/magicgui_jupyter.ipynb diff --git a/examples/progress.py b/docs/examples_gallery/progess_bars/progress.py similarity index 100% rename from examples/progress.py rename to docs/examples_gallery/progess_bars/progress.py diff --git a/examples/progress_manual.py b/docs/examples_gallery/progess_bars/progress_manual.py similarity index 100% rename from examples/progress_manual.py rename to docs/examples_gallery/progess_bars/progress_manual.py diff --git a/examples/progress_nested.py b/docs/examples_gallery/progess_bars/progress_nested.py similarity index 100% rename from examples/progress_nested.py rename to docs/examples_gallery/progess_bars/progress_nested.py diff --git a/examples/class_method.py b/docs/examples_gallery/under_the_hood/class_method.py similarity index 100% rename from examples/class_method.py rename to docs/examples_gallery/under_the_hood/class_method.py diff --git a/examples/self_reference.py b/docs/examples_gallery/under_the_hood/self_reference.py similarity index 100% rename from examples/self_reference.py rename to docs/examples_gallery/under_the_hood/self_reference.py From 12065efe0319dc6c976d0d40e64002e0b98344d4 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:56:56 +1000 Subject: [PATCH 03/27] Add required README files for mkdocs-gallery --- docs/examples_gallery/applications/README.md | 3 +++ docs/examples_gallery/demo_widgets/README.md | 3 +++ docs/examples_gallery/matplotlib/README.md | 3 +++ docs/examples_gallery/napari_examples/README.md | 3 +++ docs/examples_gallery/notebooks/README.md | 3 +++ docs/examples_gallery/progess_bars/README.md | 3 +++ docs/examples_gallery/under_the_hood/README.md | 3 +++ 7 files changed, 21 insertions(+) create mode 100644 docs/examples_gallery/applications/README.md create mode 100644 docs/examples_gallery/demo_widgets/README.md create mode 100644 docs/examples_gallery/matplotlib/README.md create mode 100644 docs/examples_gallery/napari_examples/README.md create mode 100644 docs/examples_gallery/notebooks/README.md create mode 100644 docs/examples_gallery/progess_bars/README.md create mode 100644 docs/examples_gallery/under_the_hood/README.md diff --git a/docs/examples_gallery/applications/README.md b/docs/examples_gallery/applications/README.md new file mode 100644 index 000000000..473d343ff --- /dev/null +++ b/docs/examples_gallery/applications/README.md @@ -0,0 +1,3 @@ +# Example applications + +Example appliations built with magicgui. diff --git a/docs/examples_gallery/demo_widgets/README.md b/docs/examples_gallery/demo_widgets/README.md new file mode 100644 index 000000000..2ac4101bb --- /dev/null +++ b/docs/examples_gallery/demo_widgets/README.md @@ -0,0 +1,3 @@ +# Demo widget types + +Example gallery demonstrating the available widget types in magicgui. diff --git a/docs/examples_gallery/matplotlib/README.md b/docs/examples_gallery/matplotlib/README.md new file mode 100644 index 000000000..aaf4e03a0 --- /dev/null +++ b/docs/examples_gallery/matplotlib/README.md @@ -0,0 +1,3 @@ +# matplotlib and magicgui + +Examples involving matplotlib graphs and magicgui. diff --git a/docs/examples_gallery/napari_examples/README.md b/docs/examples_gallery/napari_examples/README.md new file mode 100644 index 000000000..e4f34aa65 --- /dev/null +++ b/docs/examples_gallery/napari_examples/README.md @@ -0,0 +1,3 @@ +# napari and magicgui + +Examples integrating magicgui with napari. diff --git a/docs/examples_gallery/notebooks/README.md b/docs/examples_gallery/notebooks/README.md new file mode 100644 index 000000000..aef08795f --- /dev/null +++ b/docs/examples_gallery/notebooks/README.md @@ -0,0 +1,3 @@ +# Jupyter notebooks and magicgui + +Examples using jupyter notebooks together with magicgui. diff --git a/docs/examples_gallery/progess_bars/README.md b/docs/examples_gallery/progess_bars/README.md new file mode 100644 index 000000000..ac8e62cfc --- /dev/null +++ b/docs/examples_gallery/progess_bars/README.md @@ -0,0 +1,3 @@ +# Progress bars examples + +Examples of progress bars in magicgui. diff --git a/docs/examples_gallery/under_the_hood/README.md b/docs/examples_gallery/under_the_hood/README.md new file mode 100644 index 000000000..2b546ca9f --- /dev/null +++ b/docs/examples_gallery/under_the_hood/README.md @@ -0,0 +1,3 @@ +# Under the hood + +Learn more advanced usage patterns for magicgui, including self referencing widgets and creating new class methods. From 3be3c67cb9482b57591c7f8745f8247793dee7d8 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:03:58 +1000 Subject: [PATCH 04/27] Move example files into docs subdirectory --- docs/{examples_gallery => examples}/README.md | 0 .../applications/README.md | 0 .../applications/basic_example.py} | 0 .../applications/callable.py | 0 .../applications/chaining.py | 0 .../applications/main_window.py | 0 .../applications/pint_quantity.py | 0 .../applications/snells_law.py | 0 .../applications/values_dialog.md} | 0 docs/examples/basic.md | 55 ------------------- .../demo_widgets/README.md | 0 .../demo_widgets/change_label.py | 0 .../demo_widgets/choices.py | 0 .../demo_widgets/file_dialog.py | 0 .../demo_widgets/image.py | 0 .../demo_widgets/log_slider.py | 0 .../demo_widgets/login.py | 0 .../demo_widgets/optional.py | 0 .../demo_widgets/range_slider.py | 0 .../demo_widgets/selection.py | 0 .../demo_widgets/table.py | 0 .../matplotlib/README.md | 0 .../matplotlib/mpl_figure.py | 0 .../matplotlib/waveform.py | 0 .../napari}/README.md | 0 .../napari}/napari_combine_qt.py | 0 .../napari}/napari_forward_refs.py | 0 .../napari}/napari_image_arithmetic.py | 0 .../napari}/napari_param_sweep.py | 0 ...{napari_img_math.md => napari_img_math.py} | 0 ...ter_sweep.md => napari_parameter_sweep.py} | 0 .../notebooks/README.md | 0 .../notebooks/magicgui_jupyter.ipynb | 0 .../progress_bars}/README.md | 0 .../progress_bars}/progress.py | 0 .../progress_bars}/progress_manual.py | 0 .../progress_bars}/progress_nested.py | 0 .../under_the_hood/README.md | 0 .../under_the_hood/class_method.py | 0 .../under_the_hood/self_reference.py | 0 40 files changed, 55 deletions(-) rename docs/{examples_gallery => examples}/README.md (100%) rename docs/{examples_gallery => examples}/applications/README.md (100%) rename docs/{examples_gallery/applications/basic.py => examples/applications/basic_example.py} (100%) rename docs/{examples_gallery => examples}/applications/callable.py (100%) rename docs/{examples_gallery => examples}/applications/chaining.py (100%) rename docs/{examples_gallery => examples}/applications/main_window.py (100%) rename docs/{examples_gallery => examples}/applications/pint_quantity.py (100%) rename docs/{examples_gallery => examples}/applications/snells_law.py (100%) rename docs/{examples_gallery/applications/values_dialog.py => examples/applications/values_dialog.md} (100%) delete mode 100644 docs/examples/basic.md rename docs/{examples_gallery => examples}/demo_widgets/README.md (100%) rename docs/{examples_gallery => examples}/demo_widgets/change_label.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/choices.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/file_dialog.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/image.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/log_slider.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/login.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/optional.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/range_slider.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/selection.py (100%) rename docs/{examples_gallery => examples}/demo_widgets/table.py (100%) rename docs/{examples_gallery => examples}/matplotlib/README.md (100%) rename docs/{examples_gallery => examples}/matplotlib/mpl_figure.py (100%) rename docs/{examples_gallery => examples}/matplotlib/waveform.py (100%) rename docs/{examples_gallery/napari_examples => examples/napari}/README.md (100%) rename docs/{examples_gallery/napari_examples => examples/napari}/napari_combine_qt.py (100%) rename docs/{examples_gallery/napari_examples => examples/napari}/napari_forward_refs.py (100%) rename docs/{examples_gallery/napari_examples => examples/napari}/napari_image_arithmetic.py (100%) rename docs/{examples_gallery/napari_examples => examples/napari}/napari_param_sweep.py (100%) rename docs/examples/{napari_img_math.md => napari_img_math.py} (100%) rename docs/examples/{napari_parameter_sweep.md => napari_parameter_sweep.py} (100%) rename docs/{examples_gallery => examples}/notebooks/README.md (100%) rename docs/{examples_gallery => examples}/notebooks/magicgui_jupyter.ipynb (100%) rename docs/{examples_gallery/progess_bars => examples/progress_bars}/README.md (100%) rename docs/{examples_gallery/progess_bars => examples/progress_bars}/progress.py (100%) rename docs/{examples_gallery/progess_bars => examples/progress_bars}/progress_manual.py (100%) rename docs/{examples_gallery/progess_bars => examples/progress_bars}/progress_nested.py (100%) rename docs/{examples_gallery => examples}/under_the_hood/README.md (100%) rename docs/{examples_gallery => examples}/under_the_hood/class_method.py (100%) rename docs/{examples_gallery => examples}/under_the_hood/self_reference.py (100%) diff --git a/docs/examples_gallery/README.md b/docs/examples/README.md similarity index 100% rename from docs/examples_gallery/README.md rename to docs/examples/README.md diff --git a/docs/examples_gallery/applications/README.md b/docs/examples/applications/README.md similarity index 100% rename from docs/examples_gallery/applications/README.md rename to docs/examples/applications/README.md diff --git a/docs/examples_gallery/applications/basic.py b/docs/examples/applications/basic_example.py similarity index 100% rename from docs/examples_gallery/applications/basic.py rename to docs/examples/applications/basic_example.py diff --git a/docs/examples_gallery/applications/callable.py b/docs/examples/applications/callable.py similarity index 100% rename from docs/examples_gallery/applications/callable.py rename to docs/examples/applications/callable.py diff --git a/docs/examples_gallery/applications/chaining.py b/docs/examples/applications/chaining.py similarity index 100% rename from docs/examples_gallery/applications/chaining.py rename to docs/examples/applications/chaining.py diff --git a/docs/examples_gallery/applications/main_window.py b/docs/examples/applications/main_window.py similarity index 100% rename from docs/examples_gallery/applications/main_window.py rename to docs/examples/applications/main_window.py diff --git a/docs/examples_gallery/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py similarity index 100% rename from docs/examples_gallery/applications/pint_quantity.py rename to docs/examples/applications/pint_quantity.py diff --git a/docs/examples_gallery/applications/snells_law.py b/docs/examples/applications/snells_law.py similarity index 100% rename from docs/examples_gallery/applications/snells_law.py rename to docs/examples/applications/snells_law.py diff --git a/docs/examples_gallery/applications/values_dialog.py b/docs/examples/applications/values_dialog.md similarity index 100% rename from docs/examples_gallery/applications/values_dialog.py rename to docs/examples/applications/values_dialog.md 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/docs/examples_gallery/demo_widgets/README.md b/docs/examples/demo_widgets/README.md similarity index 100% rename from docs/examples_gallery/demo_widgets/README.md rename to docs/examples/demo_widgets/README.md diff --git a/docs/examples_gallery/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py similarity index 100% rename from docs/examples_gallery/demo_widgets/change_label.py rename to docs/examples/demo_widgets/change_label.py diff --git a/docs/examples_gallery/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py similarity index 100% rename from docs/examples_gallery/demo_widgets/choices.py rename to docs/examples/demo_widgets/choices.py diff --git a/docs/examples_gallery/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py similarity index 100% rename from docs/examples_gallery/demo_widgets/file_dialog.py rename to docs/examples/demo_widgets/file_dialog.py diff --git a/docs/examples_gallery/demo_widgets/image.py b/docs/examples/demo_widgets/image.py similarity index 100% rename from docs/examples_gallery/demo_widgets/image.py rename to docs/examples/demo_widgets/image.py diff --git a/docs/examples_gallery/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py similarity index 100% rename from docs/examples_gallery/demo_widgets/log_slider.py rename to docs/examples/demo_widgets/log_slider.py diff --git a/docs/examples_gallery/demo_widgets/login.py b/docs/examples/demo_widgets/login.py similarity index 100% rename from docs/examples_gallery/demo_widgets/login.py rename to docs/examples/demo_widgets/login.py diff --git a/docs/examples_gallery/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py similarity index 100% rename from docs/examples_gallery/demo_widgets/optional.py rename to docs/examples/demo_widgets/optional.py diff --git a/docs/examples_gallery/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py similarity index 100% rename from docs/examples_gallery/demo_widgets/range_slider.py rename to docs/examples/demo_widgets/range_slider.py diff --git a/docs/examples_gallery/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py similarity index 100% rename from docs/examples_gallery/demo_widgets/selection.py rename to docs/examples/demo_widgets/selection.py diff --git a/docs/examples_gallery/demo_widgets/table.py b/docs/examples/demo_widgets/table.py similarity index 100% rename from docs/examples_gallery/demo_widgets/table.py rename to docs/examples/demo_widgets/table.py diff --git a/docs/examples_gallery/matplotlib/README.md b/docs/examples/matplotlib/README.md similarity index 100% rename from docs/examples_gallery/matplotlib/README.md rename to docs/examples/matplotlib/README.md diff --git a/docs/examples_gallery/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py similarity index 100% rename from docs/examples_gallery/matplotlib/mpl_figure.py rename to docs/examples/matplotlib/mpl_figure.py diff --git a/docs/examples_gallery/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py similarity index 100% rename from docs/examples_gallery/matplotlib/waveform.py rename to docs/examples/matplotlib/waveform.py diff --git a/docs/examples_gallery/napari_examples/README.md b/docs/examples/napari/README.md similarity index 100% rename from docs/examples_gallery/napari_examples/README.md rename to docs/examples/napari/README.md diff --git a/docs/examples_gallery/napari_examples/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py similarity index 100% rename from docs/examples_gallery/napari_examples/napari_combine_qt.py rename to docs/examples/napari/napari_combine_qt.py diff --git a/docs/examples_gallery/napari_examples/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py similarity index 100% rename from docs/examples_gallery/napari_examples/napari_forward_refs.py rename to docs/examples/napari/napari_forward_refs.py diff --git a/docs/examples_gallery/napari_examples/napari_image_arithmetic.py b/docs/examples/napari/napari_image_arithmetic.py similarity index 100% rename from docs/examples_gallery/napari_examples/napari_image_arithmetic.py rename to docs/examples/napari/napari_image_arithmetic.py diff --git a/docs/examples_gallery/napari_examples/napari_param_sweep.py b/docs/examples/napari/napari_param_sweep.py similarity index 100% rename from docs/examples_gallery/napari_examples/napari_param_sweep.py rename to docs/examples/napari/napari_param_sweep.py diff --git a/docs/examples/napari_img_math.md b/docs/examples/napari_img_math.py similarity index 100% rename from docs/examples/napari_img_math.md rename to docs/examples/napari_img_math.py diff --git a/docs/examples/napari_parameter_sweep.md b/docs/examples/napari_parameter_sweep.py similarity index 100% rename from docs/examples/napari_parameter_sweep.md rename to docs/examples/napari_parameter_sweep.py diff --git a/docs/examples_gallery/notebooks/README.md b/docs/examples/notebooks/README.md similarity index 100% rename from docs/examples_gallery/notebooks/README.md rename to docs/examples/notebooks/README.md diff --git a/docs/examples_gallery/notebooks/magicgui_jupyter.ipynb b/docs/examples/notebooks/magicgui_jupyter.ipynb similarity index 100% rename from docs/examples_gallery/notebooks/magicgui_jupyter.ipynb rename to docs/examples/notebooks/magicgui_jupyter.ipynb diff --git a/docs/examples_gallery/progess_bars/README.md b/docs/examples/progress_bars/README.md similarity index 100% rename from docs/examples_gallery/progess_bars/README.md rename to docs/examples/progress_bars/README.md diff --git a/docs/examples_gallery/progess_bars/progress.py b/docs/examples/progress_bars/progress.py similarity index 100% rename from docs/examples_gallery/progess_bars/progress.py rename to docs/examples/progress_bars/progress.py diff --git a/docs/examples_gallery/progess_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py similarity index 100% rename from docs/examples_gallery/progess_bars/progress_manual.py rename to docs/examples/progress_bars/progress_manual.py diff --git a/docs/examples_gallery/progess_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py similarity index 100% rename from docs/examples_gallery/progess_bars/progress_nested.py rename to docs/examples/progress_bars/progress_nested.py diff --git a/docs/examples_gallery/under_the_hood/README.md b/docs/examples/under_the_hood/README.md similarity index 100% rename from docs/examples_gallery/under_the_hood/README.md rename to docs/examples/under_the_hood/README.md diff --git a/docs/examples_gallery/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py similarity index 100% rename from docs/examples_gallery/under_the_hood/class_method.py rename to docs/examples/under_the_hood/class_method.py diff --git a/docs/examples_gallery/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py similarity index 100% rename from docs/examples_gallery/under_the_hood/self_reference.py rename to docs/examples/under_the_hood/self_reference.py From 715e30b7a3ec6fb6726bae603321daf0b0416d00 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:05:53 +1000 Subject: [PATCH 05/27] Working examples gallery for magicgui --- .gitignore | 2 +- docs/examples/README.md | 4 +- docs/examples/applications/README.md | 4 +- docs/examples/applications/basic_example.py | 6 + docs/examples/applications/callable.py | 6 + docs/examples/applications/chaining.py | 6 + docs/examples/applications/main_window.py | 8 +- docs/examples/applications/pint_quantity.py | 9 + docs/examples/applications/snells_law.py | 5 +- docs/examples/applications/values_dialog.md | 8 - docs/examples/applications/values_dialog.py | 23 ++ docs/examples/basic_example.py | 16 ++ .../basic_widgets_demo.py} | 9 +- .../demo_widgets/basic_widgets_demo.py | 87 +++++++ docs/examples/demo_widgets/change_label.py | 4 + docs/examples/demo_widgets/choices.py | 7 +- docs/examples/demo_widgets/file_dialog.py | 5 +- docs/examples/demo_widgets/image.py | 9 +- docs/examples/demo_widgets/log_slider.py | 5 +- docs/examples/demo_widgets/login.py | 4 + docs/examples/demo_widgets/optional.py | 4 + docs/examples/demo_widgets/range_slider.py | 6 + docs/examples/demo_widgets/selection.py | 4 + docs/examples/demo_widgets/table.py | 7 +- docs/examples/magicgui_jupyter.py | 47 ++++ docs/examples/matplotlib/mpl_figure.py | 8 +- docs/examples/matplotlib/waveform.py | 23 +- docs/examples/napari/napari_combine_qt.py | 3 + docs/examples/napari/napari_forward_refs.py | 6 +- .../napari/napari_image_arithmetic.py | 47 ---- docs/examples/napari/napari_img_math.py | 208 ++++++++++++++++ docs/examples/napari/napari_param_sweep.py | 43 ---- .../examples/napari/napari_parameter_sweep.py | 172 +++++++++++++ docs/examples/napari_img_math.py | 199 --------------- docs/examples/napari_parameter_sweep.py | 226 ++++++++++-------- docs/examples/notebooks/magicgui_jupyter.py | 47 ++++ docs/examples/progress_bars/progress.py | 6 + .../examples/progress_bars/progress_manual.py | 7 + .../examples/progress_bars/progress_nested.py | 8 + docs/examples/under_the_hood/class_method.py | 6 +- .../examples/under_the_hood/self_reference.py | 6 + 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 mkdocs.yml | 14 +- pyproject.toml | 2 + 46 files changed, 886 insertions(+), 440 deletions(-) delete mode 100644 docs/examples/applications/values_dialog.md create mode 100644 docs/examples/applications/values_dialog.py create mode 100644 docs/examples/basic_example.py rename docs/{examples_gallery/demo_widgets/widget_demo.py => examples/basic_widgets_demo.py} (91%) create mode 100644 docs/examples/demo_widgets/basic_widgets_demo.py create mode 100644 docs/examples/magicgui_jupyter.py delete mode 100644 docs/examples/napari/napari_image_arithmetic.py create mode 100644 docs/examples/napari/napari_img_math.py delete mode 100644 docs/examples/napari/napari_param_sweep.py create mode 100644 docs/examples/napari/napari_parameter_sweep.py delete mode 100644 docs/examples/napari_img_math.py create mode 100644 docs/examples/notebooks/magicgui_jupyter.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 diff --git a/.gitignore b/.gitignore index 53728ab7e..b49f9f631 100644 --- a/.gitignore +++ b/.gitignore @@ -121,7 +121,7 @@ venv.bak/ # mkdocs documentation /site -docs/generated +docs/generated* # mypy .mypy_cache/ diff --git a/docs/examples/README.md b/docs/examples/README.md index 48813d7c3..3aff5175f 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,3 +1,5 @@ -# Examples gallery +# Featured examples A gallery of examples for magicgui. + +Featured examples: diff --git a/docs/examples/applications/README.md b/docs/examples/applications/README.md index 473d343ff..532860fc7 100644 --- a/docs/examples/applications/README.md +++ b/docs/examples/applications/README.md @@ -1,3 +1,3 @@ -# Example applications +# Demo applications -Example appliations built with magicgui. +Example applications built with magicgui. diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py index eeaec9706..a33a0d213 100644 --- a/docs/examples/applications/basic_example.py +++ b/docs/examples/applications/basic_example.py @@ -1,3 +1,9 @@ +""" +Basic example +============= + +A basic example using magicgui. +""" from magicgui import magicgui diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index 8bf76c5ff..f41f20afe 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -1,3 +1,9 @@ +""" +Callable functions demo +======================= + +This example demonstrates handling callable functions with magicgui. +""" from magicgui import magicgui diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index 43c9eace6..0f4337798 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,3 +1,9 @@ +""" +Chaining functions together +=========================== + +This example demonstrates chaining multiple functions together. +""" from magicgui import magicgui, widgets diff --git a/docs/examples/applications/main_window.py b/docs/examples/applications/main_window.py index d63554f24..e31b6075b 100644 --- a/docs/examples/applications/main_window.py +++ b/docs/examples/applications/main_window.py @@ -1,3 +1,9 @@ +""" +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 +11,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 index aa1e8a2b7..fb9dc68da 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,3 +1,12 @@ +""" +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 diff --git a/docs/examples/applications/snells_law.py b/docs/examples/applications/snells_law.py index 205d68299..3ad558a8f 100644 --- a/docs/examples/applications/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,4 +1,7 @@ -"""Simple demonstration of magicgui.""" +""" +Snell's law demonstration using magicgui +======================================== +""" import math from enum import Enum diff --git a/docs/examples/applications/values_dialog.md b/docs/examples/applications/values_dialog.md deleted file mode 100644 index 95cc212c0..000000000 --- a/docs/examples/applications/values_dialog.md +++ /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/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py new file mode 100644 index 000000000..dbc03f9ec --- /dev/null +++ b/docs/examples/applications/values_dialog.py @@ -0,0 +1,23 @@ +""" +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_example.py b/docs/examples/basic_example.py new file mode 100644 index 000000000..a33a0d213 --- /dev/null +++ b/docs/examples/basic_example.py @@ -0,0 +1,16 @@ +""" +Basic example +============= + +A basic example using magicgui. +""" +from magicgui import magicgui + + +@magicgui +def example(x: int, y="hi"): + return x, y + + +example.changed.connect(print) +example.show(run=True) diff --git a/docs/examples_gallery/demo_widgets/widget_demo.py b/docs/examples/basic_widgets_demo.py similarity index 91% rename from docs/examples_gallery/demo_widgets/widget_demo.py rename to docs/examples/basic_widgets_demo.py index 428dc4c0a..69e8ee0ca 100644 --- a/docs/examples_gallery/demo_widgets/widget_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,5 +1,12 @@ -"""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/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py new file mode 100644 index 000000000..69e8ee0ca --- /dev/null +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -0,0 +1,87 @@ +""" +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 + +from magicgui import magicgui + + +class Medium(Enum): + """Enum for various media and their refractive indices.""" + + Glass = 1.520 + Oil = 1.515 + Water = 1.333 + Air = 1.0003 + + +@magicgui( + main_window=True, + call_button="Calculate", + layout="vertical", + result_widget=True, + 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:"}, +) +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(), +): + """We can use numpy docstrings to provide tooltips. + + Parameters + ---------- + boolean : bool, optional + A checkbox for booleans, by default True + integer : int, optional + Some integer, by default 1 + spin_float : float, optional + This one is a float, by default "pi" + slider_float : float, optional + Hey look! I'm a slider, by default 43.5 + slider_int : float, optional + I only take integers, and I've hidden my readout, by default 550 + string : str, optional + We'll use this string carefully, by default "Text goes here" + dropdown : Enum, optional + Pick a medium, by default Medium.Glass + radio_option : int + A set of radio buttons. + date : datetime.date, optional + Your birthday, by default datetime.date(1999, 12, 31) + time : datetime.time, optional + Some time, by default datetime.time(1, 30, 20) + datetime : datetime.datetime, optional + A very specific time and date, by default ``datetime.datetime.now()`` + filename : str, optional + Pick a path, by default Path.home() + """ + return locals().values() + + +widget_demo.changed.connect(print) +widget_demo.show(run=True) diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index aab8a90fd..76a587b2c 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,3 +1,7 @@ +""" +Custom text labels for widgets +============================== +""" from magicgui import magicgui diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index a02ae3814..888ab9171 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,4 +1,9 @@ -"""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 diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py index 38783df2f..2e9f7cf1b 100644 --- a/docs/examples/demo_widgets/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,4 +1,7 @@ -"""FileDialog with magicgui.""" +""" +File dialog widget +================== +""" from pathlib import Path from typing import Sequence diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index 2d66196c7..e29994e87 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,12 +1,15 @@ -"""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" +img = "../../images/_test.jpg" image = Image(value=img) image.scale_widget_to_image_size() image.show(run=True) diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index a8b01e101..6e954409d 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,4 +1,7 @@ -"""Simple demonstration of magicgui.""" +""" +Log slider widget +================= +""" from magicgui import magicgui diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index 0d555b55f..cdc392811 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,3 +1,7 @@ +""" +Password login +============== +""" from magicgui import magicgui diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index c2e58e81e..4fb52ab57 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,3 +1,7 @@ +""" +Optional user choice +===================== +""" from typing import Optional from magicgui import magicgui diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index 129e8bdd3..bdee48893 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,3 +1,9 @@ +""" +Range slider widget +=================== + +A double ended range slider widget. +""" from typing import Tuple from magicgui import magicgui diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py index e362a638d..0afd6df22 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -1,3 +1,7 @@ +""" +Multiple selection widget +========================= +""" from magicgui import magicgui diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py index 769dbcc7c..8dbd92bcc 100644 --- a/docs/examples/demo_widgets/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,4 +1,9 @@ -"""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/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py new file mode 100644 index 000000000..28b7613c9 --- /dev/null +++ b/docs/examples/magicgui_jupyter.py @@ -0,0 +1,47 @@ +""" +Jupyter notebooks and magicgui +============================== + +This example shows magicgui widgets embedded in a jupyter notebook. + +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): +# """Enum for 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 AOI.""" +# 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/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index db5ef2199..be1f01903 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,6 +1,10 @@ -""""Basic example of adding a generic QWidget to a container. +""" +matplotlib figure example +========================= + +Basic example of adding a generic QWidget to a container. -main lesson: add your QWidget to container.native.layout() +Main lesson: add your QWidget to container.native.layout() as shown on line 30 """ diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index 6d808a865..6e5836801 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,3 +1,7 @@ +""" +Waveforms example +================= +""" 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,12 +49,12 @@ 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 ---------- @@ -77,7 +81,7 @@ 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 ---------- @@ -88,7 +92,6 @@ def sine( phase: Phase the phase of the signal (in degrees) """ - sig = Signal( duration=duration, size=size, @@ -105,7 +108,7 @@ def chirp( f1: float = 2.0, phase: Phase = 0.0, ) -> Signal: - """Frequency-swept cosine generator + """Frequency-swept cosine generator. See scipy.signal.chirp """ @@ -185,7 +188,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 +200,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 +214,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/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index 144bad15c..b152ae9ae 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,4 +1,7 @@ """ +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. diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py index 9a52c3a17..833f8e38e 100644 --- a/docs/examples/napari/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,4 +1,8 @@ -"""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/napari_image_arithmetic.py b/docs/examples/napari/napari_image_arithmetic.py deleted file mode 100644 index 28844a285..000000000 --- a/docs/examples/napari/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/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py new file mode 100644 index 000000000..eed940f79 --- /dev/null +++ b/docs/examples/napari/napari_img_math.py @@ -0,0 +1,208 @@ +""" +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 +to extend napari with small, composable widgets created with `magicgui`. Here +we're going to build this simple image arithmetic widget with a few additional +lines of code. + +For napari-specific magicgui documentation, see the [napari +docs](https://napari.org/guides/magicgui.html) + +![napari image arithmetic widget](../../images/imagemath.gif){ width=80% } + +## outline + +**This example demonstrates how to:** + +1. Create a `magicgui` widget that can be used in another program (napari) + +2. Use an `Enum` to create a dropdown menu + +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/docs/examples/napari/napari_image_arithmetic.py).* + + +from enum import Enum + +import numpy +import napari +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() + +# %% +# ## 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. + +# %% +# ### 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, ourfunction calls the selected operation on the data. + +# %% +# ```python +# def image_arithmetic(array1, operation, array2): +# return operation.value(array1, 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) +# 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. + +# %% +# ```python +# from napari.types import ImageData + +# def image_arithmetic( +# layerA: ImageData, operation: Operation, layerB: ImageData +# ) -> ImageData: +# return operation.value(layerA, layerB) +# ``` +# %% +# ### the magic part + +# 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" +# @magicgui(call_button="execute") +# def image_arithmetic(layerA: ImageData, operation: Operation, layerB: ImageData): +# return operation.value(layerA, layerB) +# ``` + +# %% +# That's it! The `image_arithmetic` function is now a +# [FunctionGui][magicgui.widgets.FunctionGui] that can be shown, or incorporated +# into other GUIs (such as the napari GUI shown in this example) + +# %% +# !!! note While [type hints](https://docs.python.org/3/library/typing.html) +# aren't always required in `magicgui`, they are recommended ... and they +# *are* required for certain things, like the `Operation(Enum)` [used here for +# the dropdown](#create-dropdowns-with-enums) and the `napari.types.ImageData` +# annotations that `napari` has registered with `magicgui`. + +# %% +# ### create dropdowns with Enums +# We'd like the user to be able to select the operation (`add`, `subtract`, +# `multiply`, `divide`) using a dropdown menu. [`enum.Enum`][] offers a convenient +# way to restrict values to a strict set of options, while providing `name: value` +# pairs for each of the options. Here, the value for each choice is the actual +# function we would like to have called when that option is selected. + +# %% +# ```python +# class Operation(enum.Enum): +# add = numpy.add +# subtract = numpy.subtract +# multiply = numpy.multiply +# divide = numpy.divide +# ``` + +# %% +# ### add it to napari +# When we decorated the `image_arithmetic` function above, it became a +# [FunctionGui][magicgui.widgets.FunctionGui]. Napari recognizes this type, so we +# can simply add it to the napari viewer as follows: + +# %% +# ```python +# viewer.window.add_dock_widget(image_arithmetic) +# ``` + +# %% +# ### connect event listeners for interactivity +# What fun is a GUI without some interactivity? Let's make stuff happen. +# +# We connect the `image_arithmetic.reset_choices` function to the +# `viewer.layers.events.inserted/removed` event from `napari`, to make sure that +# the dropdown menus stay in sync if a layer gets added or removed from the napari +# window: + +# %% +# ```python +# viewer.layers.events.inserted.connect(image_arithmetic.reset_choices) +# viewer.layers.events.removed.connect(image_arithmetic.reset_choices) +# ``` + +# %% +# !!!tip +# An additional offering from `magicgui` here is that the decorated +# function also acquires a new attribute "`called`" that can be connected to +# callback functions of your choice. Then, whenever the gui widget *or the +# 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)) +# ``` + +# %% +# ``` +# >>> image_arithmetic() +# 1.0060037881040373 +# ``` diff --git a/docs/examples/napari/napari_param_sweep.py b/docs/examples/napari/napari_param_sweep.py deleted file mode 100644 index aee075854..000000000 --- a/docs/examples/napari/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/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py new file mode 100644 index 000000000..c0a7d7c49 --- /dev/null +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -0,0 +1,172 @@ +""" +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 +to extend napari with small, composable widgets created with `magicgui`. Here, +we demonstrate how to build a interactive widget that lets you immediately see +the effect of changing one of the parameters of your function. + +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% } + +*See also:* Some of this tutorial overlaps with topics covered in the [napari +image arithmetic example](napari_img_math) +""" + +# %% +# ## outline +# +# **This example demonstrates how to:** +# +# 1. Create a `magicgui` widget that can be used in another +# program (`napari`) +# +# 2. Automatically call our function when a parameter changes +# +# 3. Provide `magicgui` with a custom widget for a specific +# argument +# +# 4. Use the `choices` option to create a dropdown +# +# 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/docs/examples/napari/napari_param_sweep.py).* + +# %% +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() + +# %% +# ## 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. + +# %% +# ### the function +# +# Our function is a very thin wrapper around +# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). +# It takes a `napari` [Image +# layer](https://napari.org/howtos/layers/image.html), a `sigma` to control +# the blur radius, and a `mode` that determines how edges are handled. + +# %% +# ```python +# def gaussian_blur( +# layer: Image, sigma: float = 1, mode="nearest" +# ) -> Image: +# return filters.gaussian(layer.data, sigma=sigma, mode=mode) +# ``` + +# %% +# The reasons we are wrapping it here are: +# +# 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 +# signature that were not provided by `filters.gaussian` + +# %% +# #### type annotations +# +# As described in the [image arithmetic tutorial](./napari_img_math.md), 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 +# layers for our `layer` parameter, and automatically adding the result of our +# function to the viewer when called. +# +# For documentation on napari types with magicgui, see the +# [napari docs](https://napari.org/guides/magicgui.html) + +# %% +# ### the magic part +# +# Finally, we decorate the function with `@magicgui` and provide some options. + +# %% +# ```python +# @magicgui( +# auto_call=True, +# sigma={"widget_type": "FloatSlider", "max": 6}, +# mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, +# ) +# 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) +# ``` + +# %% +# - `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`: +# +# - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard +# (`float`) widget for the `sigma` widget, but rather to use a slider widget. +# - we then set an upper limit on the slider values for `sigma`. +# +# - finally, we specify valid `choices` for the `mode` argument. This turns that +# parameter into a categorical/dropdown type widget, and sets the options. + +# %% +# ### connecting events +# +# 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. + +# %% +# ```python +# def do_something_with_result(result): +# ... + +# gaussian_blur.called.connect(do_something_with_result) +# ``` diff --git a/docs/examples/napari_img_math.py b/docs/examples/napari_img_math.py deleted file mode 100644 index 94e299b0e..000000000 --- a/docs/examples/napari_img_math.py +++ /dev/null @@ -1,199 +0,0 @@ -# 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 -to extend napari with small, composable widgets created with `magicgui`. Here -we're going to build this simple image arithmetic widget with a few additional -lines of code. - -For napari-specific magicgui documentation, see the [napari -docs](https://napari.org/guides/magicgui.html) - -![napari image arithmetic widget](../images/imagemath.gif){ width=80% } - -## outline - -**This example demonstrates how to:** - -1. Create a `magicgui` widget that can be used in another program (napari) - -1. Use an `Enum` to create a dropdown menu - -1. 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).* - -```python linenums="1" hl_lines="25 38" -from enum import Enum - -import numpy -import napari -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() -``` - -## 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. - -### 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. - -```python -def image_arithmetic(array1, operation, array2): - return operation.value(array1, 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) -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. - -```python -from napari.types import ImageData - -def image_arithmetic( - layerA: ImageData, operation: Operation, layerB: ImageData -) -> ImageData: - return operation.value(layerA, layerB) -``` - -### the magic part - - 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" -@magicgui(call_button="execute") -def image_arithmetic(layerA: ImageData, operation: Operation, layerB: ImageData): - return operation.value(layerA, layerB) -``` - -That's it! The `image_arithmetic` function is now a -[FunctionGui][magicgui.widgets.FunctionGui] that can be shown, or incorporated -into other GUIs (such as the napari GUI shown in this example) - -!!! note While [type hints](https://docs.python.org/3/library/typing.html) - aren't always required in `magicgui`, they are recommended ... and they - *are* required for certain things, like the `Operation(Enum)` [used here for - the dropdown](#create-dropdowns-with-enums) and the `napari.types.ImageData` - annotations that `napari` has registered with `magicgui`. - -### create dropdowns with Enums - -We'd like the user to be able to select the operation (`add`, `subtract`, -`multiply`, `divide`) using a dropdown menu. [`enum.Enum`][] offers a convenient -way to restrict values to a strict set of options, while providing `name: value` -pairs for each of the options. Here, the value for each choice is the actual -function we would like to have called when that option is selected. - -```python -class Operation(enum.Enum): - add = numpy.add - subtract = numpy.subtract - multiply = numpy.multiply - divide = numpy.divide -``` - -### add it to napari - -When we decorated the `image_arithmetic` function above, it became a -[FunctionGui][magicgui.widgets.FunctionGui]. Napari recognizes this type, so we -can simply add it to the napari viewer as follows: - -```python -viewer.window.add_dock_widget(image_arithmetic) -``` - -### connect event listeners for interactivity - -What fun is a GUI without some interactivity? Let's make stuff happen. - -We connect the `image_arithmetic.reset_choices` function to the -`viewer.layers.events.inserted/removed` event from `napari`, to make sure that -the dropdown menus stay in sync if a layer gets added or removed from the napari -window: - -```python -viewer.layers.events.inserted.connect(image_arithmetic.reset_choices) -viewer.layers.events.removed.connect(image_arithmetic.reset_choices) -``` - -!!!tip - An additional offering from `magicgui` here is that the decorated - function also acquires a new attribute "`called`" that can be connected to - callback functions of your choice. Then, whenever the gui widget *or the - 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() - 1.0060037881040373 - ``` diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index 35460657b..f039410b8 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/napari_parameter_sweep.py @@ -1,4 +1,6 @@ -# 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 @@ -13,29 +15,32 @@ *See also:* Some of this tutorial overlaps with topics covered in the [napari image arithmetic example](napari_img_math) - -## outline - -**This example demonstrates how to:** - -1. Create a `magicgui` widget that can be used in another -program (`napari`) - -1. Automatically call our function when a parameter changes - -1. Provide `magicgui` with a custom widget for a specific -argument - -1. Use the `choices` option to create a dropdown - -1. 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).* - -```python linenums="1" +""" + +# %% +# ## outline +# +# **This example demonstrates how to:** +# +# 1. Create a `magicgui` widget that can be used in another +# program (`napari`) +# +# 2. Automatically call our function when a parameter changes +# +# 3. Provide `magicgui` with a custom widget for a specific +# argument +# +# 4. Use the `choices` option to create a dropdown +# +# 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/docs/examples/napari/napari_param_sweep.py).* + +# %% import napari import skimage.data import skimage.filters @@ -73,86 +78,95 @@ def gaussian_blur( viewer.layers.events.changed.connect(gaussian_blur.reset_choices) napari.run() -``` - -## 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. - -### the function - -Our function is a very thin wrapper around -[`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). -It takes a `napari` [Image -layer](https://napari.org/howtos/layers/image.html), a `sigma` to control -the blur radius, and a `mode` that determines how edges are handled. - -```python -def gaussian_blur( - layer: Image, sigma: float = 1, mode="nearest" -) -> Image: - return filters.gaussian(layer.data, sigma=sigma, mode=mode) -``` - -The reasons we are wrapping it here are: - -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 - signature that were not provided by `filters.gaussian` - -#### type annotations - -As described in the [image arithmetic tutorial](./napari_img_math.md), 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 -layers for our `layer` parameter, and automatically adding the result of our -function to the viewer when called. - -For documentation on napari types with magicgui, see the -[napari docs](https://napari.org/guides/magicgui.html) - -### the magic part - -Finally, we decorate the function with `@magicgui` and provide some options. - -```python -@magicgui( - auto_call=True, - sigma={"widget_type": "FloatSlider", "max": 6}, - mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, -) -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) -``` - -- `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`: - - - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard - (`float`) widget for the `sigma` widget, but rather to use a slider widget. - - we then set an upper limit on the slider values for `sigma`. - -- finally, we specify valid `choices` for the `mode` argument. This turns that - parameter into a categorical/dropdown type widget, and sets the options. - -### connecting events - -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. - -```python -def do_something_with_result(result): - ... -gaussian_blur.called.connect(do_something_with_result) -``` +# %% +# ## 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. + +# %% +# ### the function +# +# Our function is a very thin wrapper around +# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). +# It takes a `napari` [Image +# layer](https://napari.org/howtos/layers/image.html), a `sigma` to control +# the blur radius, and a `mode` that determines how edges are handled. + +# %% +# ```python +# def gaussian_blur( +# layer: Image, sigma: float = 1, mode="nearest" +# ) -> Image: +# return filters.gaussian(layer.data, sigma=sigma, mode=mode) +# ``` + +# %% +# The reasons we are wrapping it here are: +# +# 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 +# signature that were not provided by `filters.gaussian` + +# %% +# #### type annotations +# +# As described in the [image arithmetic tutorial](./napari_img_math.md), 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 +# layers for our `layer` parameter, and automatically adding the result of our +# function to the viewer when called. +# +# For documentation on napari types with magicgui, see the +# [napari docs](https://napari.org/guides/magicgui.html) + +# %% +# ### the magic part +# +# Finally, we decorate the function with `@magicgui` and provide some options. + +# %% +# ```python +# @magicgui( +# auto_call=True, +# sigma={"widget_type": "FloatSlider", "max": 6}, +# mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, +# ) +# 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) +# ``` + +# %% +# - `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`: +# +# - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard +# (`float`) widget for the `sigma` widget, but rather to use a slider widget. +# - we then set an upper limit on the slider values for `sigma`. +# +# - finally, we specify valid `choices` for the `mode` argument. This turns that +# parameter into a categorical/dropdown type widget, and sets the options. + +# %% +# ### connecting events +# +# 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. + +# %% +# ```python +# def do_something_with_result(result): +# ... + +# gaussian_blur.called.connect(do_something_with_result) +# ``` diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py new file mode 100644 index 000000000..28b7613c9 --- /dev/null +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -0,0 +1,47 @@ +""" +Jupyter notebooks and magicgui +============================== + +This example shows magicgui widgets embedded in a jupyter notebook. + +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): +# """Enum for 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 AOI.""" +# 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/progress.py b/docs/examples/progress_bars/progress.py index a04090606..3eedcc13b 100644 --- a/docs/examples/progress_bars/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,3 +1,9 @@ +""" +Simple progress bar +=================== + +A simple progress bar demo with magicgui. +""" from time import sleep from magicgui import magicgui diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py index 61776360e..8c88b7a70 100644 --- a/docs/examples/progress_bars/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,3 +1,10 @@ +""" +Manual progress bar +=================== + +Example of a progress bar being updated manually. + +""" from magicgui import magicgui from magicgui.widgets import ProgressBar diff --git a/docs/examples/progress_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py index 51c4228e6..06f101d16 100644 --- a/docs/examples/progress_bars/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,3 +1,11 @@ +""" +Nested progress bars +==================== + +Example using nested progress bars in magicgui. + +""" + import random from time import sleep diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index 27586b115..4fbec040a 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,4 +1,8 @@ -"""Demonstrates decorating a method. +""" +Deocrate class methods with magicgui +==================================== + +Demonstrates decorating a method. 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 diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index 0fa407671..f071f005a 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,3 +1,9 @@ +""" +Self reference magicgui widgets +=============================== + +Widgets created with magicgui can reference themselves, and use the widget API. +""" from magicgui import magicgui diff --git a/docs/images/_test.jpg b/docs/images/_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62f90a4cbfb9cf70914f9b2a17fb0cee15ff5b64 GIT binary patch literal 14989 zcmbXJ2{@Et+Xsx_wnmhF-=?AzNkR&lkgbwb2r)_aE!i1vODJm+N@b*wEK`pIupd1CxxOaVNX^^NoaCT0L& zf`0(UD4+|lv9PkTvarD~Y;0`%5C_;1@Wai)x&Ht!HyhB~>@KD4)1Q$Cymy`gnfYkr%hw%;IWe3`T1s0|w05dNW z3ojF+2|&X8WMlfr1N_&+#LU79?~;8#2PfR2f(Kw`Vqsy1_YV^Q_YQ@>2UvO8_zo*+ z@8dVMLmcrII1`cbmR(xs^LN1;y+j%1dk-V`a|j8Gh>9IOCVTvZoQmpMwR7jybua1Z z8yFg0zIn^c+~T&SmHmAOM<-_&S07(L|A4@t;HXE@F|m)I#63%Wo|c~R;$>#eyZ50*Yjg%$CaE+*yx_?Ly3mF=+7K0a+zgq=74kuwqO0y-&g zKY!mZt$c$hc<*5^hmefQ^ik4Z(*8l&|9^x<{(qwEzX|&vx<-HlEKKm_vG4*YfP#C3 z<L|9BiVzA@Ms%6+iSY&kKgeYZ?K zDV!^ugM?j?L_pzI77&vGObfxm4Y#a?>?=|k=f3{RxYRndgRMk5f$ygg$GN$_ueWz) zAwiSIcCW53{2`QUUS z*T`gJCBi7iKj>&u=Oww-*Pe$jwtr)ek4}D%n!ZIl#{k|c>ob6Fc4%Q}jAxXpIFpCX zrCJS*UM@>DN^*)}7dYV~6J9A@0?-QkOp<7K_nAE20#M7Ji@XF)TxK-fuF%1i;9Tmn z+m9-}-4N=#@RTMRkiL@n$SB-6h=$Kg?D+ z4D~W&6$4(@h_I9(ep@1c)gu`|wp1E?ccy>uZXnRfG`UerBB)mNLVenW=4s2$XYY-V zd}Mz6W%dxC_m;|?lc6vha^z39()VYoK`b0QXv$JWKZIP;5{z$9zxFaWcSTL%fpl47 zWt!6J-Z%0XsJ;MBtCM2s2ZEF7kg9G|0AJucBkN?|$6G-j`L`n0r7aU&e>6v~Aw=a+ zhxHm-f52S#h8hb64}((&P&DI^^%b3-!0nl{xV%8zwSGShLzTR&b8}^hI`~&&^S@cj z#9r+ov|%!Y{+o=RKto)t0&bnmez&jWmjS=A3rExM{W1k|>i4-We1I9s>)#n_3jDGb zolWJQu220KcvHKN+)-6w7HK41u75J?R;)zS7)Qas^V7$Xu0n9egKufH$pT>>$^1-D za^{H{(q9euHZ_%k4L&8L1)t^r67m5C;Bdj*OPJ=}tHRA|wYWXv(QwTnVkL*^OT&u# z@e3DKe}Q)wP-NylJdpzh9V>BB8l{H)t}y27>b_PVcXnerx5g}Fzwfw?{0l+Zm~ma8i%DWPZnM*RMltEg zmwhPyrON@RBj~F$RjX<(kQ_1a#p^0_?Tnfy0? z^2ai7S&w_oBBFiYn9bIFt{KRQ3%noA(e{3WTW}fsOP!kzl;dd3$qe905d+|N$#h7L z^^@t5mOI^dcnC=dorNuJSPV zE8DfHb2;@))SwwOuPwE7XlT=R(DTsK(}BNhQwHmA4)90mDM_p+GJrX!8kljx1x~bH zpgeSmj)ZDw7(kC+NM$1`0?AQ(g~$MO2!6QzqvK1$UzDzRD;pm=H5xp1R>LBT8hy8? zVsusfYFT+?OG`jyWpbPOuo$1f{e&z;@KxsqoF?f!l9;)SNv2ZaRr(AIdn?{to83FI zxo!TAE_F?>nq}LhB1kw~lQWkZ)i6|DvGwO2ZJdOv zEle;Se+)Lcw@yrs)KW2uJ*`*h)(n6d+#U3~LA7}MUBx*wy;M6!?ay-6brY}PeSV`E zdySigBZa3r6kmX?AGaBROJNr{eS2Y&%r^*22rPxg<~T^mIYr^Kjfth!eWotYvZ&Pl zWY>%T99w+B|8}ZT@5?zeQ|DnBGM${(5hzUwOKlO>@GY#fRN(#68KTh6Yp{esDu-}4 z#Vv#>gmv4B5WRbfS1Y=CY>d1GI#r(8-1XE?X1j<~oYbnTCZB4k>It-%W&m+C{U@q< zSqci`xt+^FmkmCkYr&Htgb&Vm*78O-&rGP!+xH$~+dur`>(@_>I*>mtexivbEBJ-G zB;o0>$$455z6oroDTS3`R1-RS55#V$!z{x~@lPX8ezSTQ!4;AJvyr-HkTv-#)kZS! z&3mguSFGhl1blg$;t+H4Q|nXC}YU_5a^oW=lB2-M{NY|$DI9AnxZ8bz-)?r`LW zym!thDBZl)za}iw^hr8-z@p(l_JhZJ)_S6*ZHB>~?=y;x*S$7rdw$R%?ovOm7%8R! zU9VmRkJk)9@|_4f1E^<5=6VM+fbYloS`2GZGeQ^p_On|2kP!tEyzcN4zN&6o(Bx|r zcSSVysOZWaD72aZ@WM)StpkeT<_!~Gg}}CD+C{0T85|-owUO!lAbCirg8?XBS9`^E z!AJr}W9R5B_w^7W7Z$M19Eu{WTSzbft`$QLKL)_V({y4V{qdovDBQ*}&33#ntj(4^ z^pqhP%R^1-TL`yz@qCmr=Pw7$F1Xwd&?G!$0D)nC{_PoiXBfaAq`wveSdE+0v><9g zydIZsFZ;Oh)bRhlT&b7yO0xHJch6Mv<$Nb%*`;TFq^|s;C<)hZoF&{{2+fclgiRG# zi&9To%vedKHPWpEC?$%JU=l2c+6w4@^0H&cv~8@4n)`10=NH)%_(hxT4`j0a#g36) zq@zS~oh;OOdxA|G;y0|4R78=}^f_WaE$>ybsrC_CKiZG{)v_c5jSY*nRS!OK0V`HSmh{tfn3$_RQ zi~PiItLb`6(c7*trKFRO#pPVvoARPP$b(&)R;|+-jfx^mnZuEP}W#8XPlA`N`GQ| zF%{8%p+G4otBe60itpat2I;H>RiYPU^ykf1Dg)4UUD+zuzLp$}DqD&1n48lFt3#Jx zQx6yHAP<)R!k3lrd~(?N9qWUfP`jB4EXYwR7ywcUB-EmJ0oYP~F`zU1;%G3D%Y&V0 z3r=ql=tn>IuBO4U1MpF8zgNzZ0gTSE6yVILZp1zlAp`rOWgTk|sdEAb+$W9>v%cOf z3O&Cx{^@)H#7$MeSSPw!_qpHeoxyw;(=8uY&&tPc-DLoi2BWwhj*lM&FPgCcu;Y8_ zsmqXgwcl%V|Lb~QUVPC>=LY0HNQMi}BFh)La;NK2zY`}nqhL`@)?MA2fg52fkr6{> z0O@LA7xyM~;W$fM_1-+(0Zph6v&Yqg=uPZD-r;v(N}#&UP07$r2*fQ;GxQ+wSZn6aLkPECIW>=pple0QJ53Ee3FY8y=)Ym_*I*{?CwqHo@z`9pKU7T=noe zD8kk{_Qmo#c9RamjY;)8bg=86?-&5gqd0gg-9LeZu>0pB)p-*9X?J^jr~W6pS}Tq8 z3i*-?Oa!qo30wcw_?*cA-CI5$a5cktu=q%I#Zye%EdCz%b%T57ajzliz>Om3(GjDWRs@vq@O2j)eLqpe}2TulAV}4ku(h-=+<0v5EoIJe&)b zwEBSo5OC1I<+b_~f%niX=$4^ihs2in2csUp)A`mp!EQD8O{jfspL$YIqK3smFAP{( z{{73SrZ!>w2>~0ddwyv;Oo;)sY3MP4PvxkgI@I2Mut)YX?3{v7T@V9k88<2WAB=3j)7TA@<-NIb|866VJAy!h!?i!=?lcdeRY?lD8>k7gOKeejR`b;v2{+YNgICD>PKJF*GYO~Q&y zwG$)G0Gne~72kn2I4rc&Ck6S;ruin{n{c^|& z;$3DAVUih^?O!MKPjc33FaY-Ln8R1%1K$puGIOZmos0I!W@Vjwn$5QSD~rrDgRhuu z#tC9zJMhT1^}F}t)6wxpxmusmpomK$%+cRPM2^*RLgL)B2poJXL|c$rz0LrpagAVP6a_&i!F~^$>^%j1T1V-= zZdsHsxV3BXGi?c9=0!`$#L|P3=?AVOY8U_(7DKG;G6Z(-Xk!0LEEcNz`vI1QaLLfW z+OZpc`TG?FLbV&<-~PMvXb}F>eVqjd_tU~dwX{~a1?FH_0O$Xp{*Fd;&8~71P z6NW>BCX2T;9awiGt>BHF0LlFDPEg0MBffx*w=f>C6Y{Tq=CQFT7Y2~9opre1<4n;y)U&2ieEG0K#m|sC z0aA~Gd4doF7C%NNB0{D^-K={RleQyAeQ(S%J0}!Wv=StCdGtbkkh>MT9Gl%qT2j{Q z2aSqHEp`2Z@|&7xG0A5S#Q6Pq`tWRw1VyWyX;bw9UIcb`C36_SFDvA-ZB})06rdlG z|1=2k^A!V#$DydBpxddJZ`sMOt~{)6l6fA)HQkNykvef;X<4=fyK#I8KIY$wP`Fzm zH1HWFIIIzZj2S@lP;%@^6|}IM|Agp(iu2*}fhU!jSArb)EG|w#o;hXGUXR`yulX|W{JrintOy%A(YJ9ra|1juE zB*&Cf5Y3GT8bb5MM#WT7klaC7*9}d4X?uc47dT{2Y8-G~^u@PpNNhCqU+dhe>@3X0 z6K{0yum>taRW{(c<=KxEaD;P4?Xi}u3&cNwxtOkGD4lU{q^WJ+pX)y9=NIJT(ED{T zE~g?WmA{?_)q8e@{aLYB{rGLjb7-ZWZu+X#^ztg%3x;?6oy2Vm_s+t)tR40N2ruA+ zY4%RvJ@7ElL-po^@0nDqmC+XV<;+#hqXH1;AV%BUPQ|C9w2|vr!gN$hH>JDodINpY;HqQn_C5k z2!25dvpz?&ET!%DVxW@@ppr}vb;~<{>WsAk$v~!TOdtR|wfiREudKtywB(s4c9_4KZy-n8frhz7ep^QCybNQ5``KK)Cy1^+h5S72YY(bAof=rr489>o_SeYDH z`rj&H?kJ(c*Sdw<5reHhaIm{0D-87BL0WmyWYnt&;@9@IF}cff&sF+6w5aWI6NYl< zUNrUnrkT2FeX9rqzcT=vYpws%F8?((s9Hsus1Sa1%I|l~x7stc_oT(|&gf*%2lPN; zYzlIj^dif{KP7=q4MfElY0^C#!&4s04^jl# zIQ1VgttZLh%ao*g`@aoNt$|%^G|evUi7eC8}xt{XtY?i)!@-3qAAdtiSU1 zGUCz40{t@El{YEXEjRpf^-#fbE6ZVroDq#u$^+#NHeX2Xf2>HY&uArDuW__}weMz; zw5W3Q9;YtdZLId@M;mJX_g%Hbt&ZCcRc=+}_WI5j7MzG|1QyBn_yj2om;40>G19Q5PkjpuOB z+YbYOE~V5r7zHo2$K}Q6!^ZP3ZSw^63mgA0I*i&BHf@Buz+`eqv9gP zLafgeI1#1y_#6XpZsQDlPw0YehD0Q=;5m^<5dj6@+LQ5ptljK$S;cO z(5IjktGRK~E$Z?GIR(PVGv}S0R>+8kqKKh?+X%s@jSPd8S4ldy(=CS@OKdPNq!qiE90tv^#Hx4>p z_FtcQQ08hOZ*gvf<4?bWBU!uWo}C_QlFOO5gZQ^_Ir&Xhn1ENyy;x-{xe(QU;@Nw9 zy$qmEc2k9W%LrCuuuGt$8dsf^eJb-X#ob^tbZFjNcIXXdRTpq1#Efr?6SV5L7H@Q) z_KACbe|4cduY;yObD#tEmc`;8q!(=J1*~-tP)l1VIKt>*y98r1YV@cK2B0sNd$cQi z`9V!_>>I+I<^u$-Ld6)P>NpT!?R9qMU=7!4k=I+>MZaI!y-vr4P2}fua|XWD!xa`6 zlMG5XH_pS|Mria3te7A&tg5Eg3fZm!=qyznlX`L;wt2C=3&wu$&8_(L#EAZ;> z>suob)~CqD-<+@efQ$3ej}FnJUlM#eF@L1BzBTaYo^WdyS=qj}Liscy>CIUz78euu z5ESsQsM*#-Ccxe(*<^QNveTLR8;-MA<)Lc)hM-S#rbG8*tKR&tar~}LC2j3p-UQi^ zWD%DPsiTo2;_;8Od6e$nIOCJD7Fu``&2v}oq`^r@%i4L@(H`M5w+QI$I?3nB-v z4&X?rzY#k`M(;8e(3D}p2m^O1*xdmsnYek2KIvYEXCku|_nYEk6{2_d`pdVf%ZP;w z?8NeGNijOH0i9XH+qid)eObo}n{QY=Di~It^A^1(yWhx8UjCZkslh#E#VIrmT*7X( zqh6Nyn8W;^tVn-k0L91`6FnY5Vp%h{Ws`i=qYw8a+}%S4cUKdayG!beJO}T)&{iY0 zlwZ#lKQxhRt`(Hi%p%_(9RyzzuDwdqs&FFyIH72byhpYA?uB<)HGRyssyJ7zyZCt^ zl>uPZa9LVs!2ga3>$B@KgT(Fmk(+zxJ5GgEhALdxwnE-k5;2|RO2 z+LOyWT7vKsX5@*F;1kH{b41?abU}mld%Sc>5mpRrdshf6yyy5`rV<<&i=U5yKZ39h zoN;wSaXV)zVIkg-AV2XFeyVN`UsImxC6s1e4p%0(l_(eO=uU&Qq#+a$MQ80rUw1{8 z22#O%o4^`~|0YPs@`7dbsE$c&hIr*`KXT58)W!Ax!~48JoL}|Iqaqu310LlO^~P5+ z9f}^PHY{;OUOj%E^V2M@_~b}S>dX`CrZ9)qe9Fr`bjsYxEEs8e<+(b0u1h(>C{ZN1 zn--~vS{`QrWPVFzSt~8P7It;*&f&@LEv^(d=YsYVt97-qaKtFChim{BbA2XRtS?tm zL*Ch3@d>Ux-Q_Uy^u*PorsP+F()oA14Mi!4c+{LfdJmxpCAi{lQa{9ya!#K|80(P!mwLs;jVZLIA;pSh5M;E0kEwP_mTG0R8Md%i|H2FrUP#C?+#S(Fo36z z7{HqBR)O2n1JoJ`^||%}%yM4)A>?4RfT@kaaGC;sma<8Qg{eo+^5f7XX)g%cU;qn8 ziU^I4==GahvIrXLHJ+R&V5;XWsefmZwBrrI2z-y;(Ubr7K5VZMzajq?5&#uQC|Z&d zYBu8!JR(H)QE2Mkaj+c$?(Wv$QtRbmSC+GJlEPOB>DRxbzgiROr??MzzIb}rZM*H9 zCbQe0^@PtZybJYG)^4u|-XFh77w6pzIv$q28|E$t>zr#JDhUq~J z3aQ5}t>-LVoEvk#t^w9nmF@m1FPuk8(u@bRB&nQN>xs1V%tyVp^{+|e-@dWGP+4d^ z!2bIU$T5AGUO)?wwULHHYy1t6 z#g}K$o`-ntvB4g zjSjw3j%%nr*l8xAv#GFA^Sv7LGNVCCZs5R2q;?*hyL}^6j6fnz|UGxxd9yS4`Zd7uBS~ zWM{jamP{oC5hK=U-hLk%eKgG@d!_H%C;P89$NH>V(;X48QO8-DY+hNDgnb}nC3amA zij?aN_`(1V2z1CqSw9sulnH(99g%E>=WclZG?|*!hFbjvlFT+mehOL*{yw+z>(iLe z^9G@Z>FX=?#aT{z!A&Scj!)Fgegk?9HzZfwNW3PB(2{-)j(&vGzh<5ESLjkT7}+?J z{3ioq?q+`lp-!PDkh|>faiJ!|lrBpt-I@5OcOt!A>%)Wh%)a8C#D!Zh76EV?E*?e` zES83~f*v(Yxm^D+sdB?ampHac_faZ-oJ;&+jMMLavw+JzLVxMIqfV_Rf}@%6PCXTe z-HgGV*Iowns(hGzY%_K{BG#bUj@@D%WQ;|J{ho~L`Ezcs+KX=m?>v*k_GA)G62J|v zUT{T$dkg0~|N_O|=d=*lDyEb{>wBJ$bsYTXOM@IWTe0 z?=cID`%UM0Ssdyz9OsFetGac055uR)- zFn0jm@Yb$Y5sNG16r6~Zl%hiP67Pk&ubIOsYNb!l44*?rl-TS3#zw<>S_9_YZ+|U= zUPvx(1wXsaTt>xN4EUaRFXE{!WL@jh-U(y_)|d56Wrxu$;M~(dYv}uDP;RGV20=>} zo1}xCu#(AKs%6?UqA3uQBYnLNE?dRCm3o^$tao53{pym)aZO%A@x$Gk#d_(%Qx$ZS z^(Udsgkm2c1`3G&o-@m_aNwTnnRl43hJp>O$x@A(~Tb0?ow%ZQRT@j!@k;>kS zGaM>!X|8>ZNR|DKTCso;lD-pK5{o}aqKgd9U`mkE+ldSBukQjO)^FfX;E0tt-xb6C?Pg7mYgC%|?+;Ubht}zi@6i>6vzw`Qj%LDmA z%!w&bkQ#4nY@pq@FW-6m=$D#Ey5Mh22N>ASPJURD&--Gn?mB<;K;Ijq+uKwvVVcezr-lstHboISRqI9xCvwNnkj$}y z6HipJyF5qNWD=!WLi|EU4Rl+4R?&`2XmJ%gqWa{FLZp0WaHP1~S@#s*Kqc(9q0An4 z@J{3(6i?u$Rj6B2MnBr-@_Ua42ki*SEtx&>pF5L-f{);byaejt0QRh=MQs)?6}K8tO%_;I zUbfnzZEtULSsCTIr>#`3b0LJh(0PqRL9P|14grUJ7xY5U`w@@Jhcq828A} zZsx*yKS|~vvp37VcHBH<&M>V!pdszi``)O{?Nt;^DGCRsP7|Kw5NN8wl+Ap;OF>D# zYe{vj8>^POyrLRXN%Dsa^(ETq%n_>5EL!4&`1Kbs{8x=<9eFmZB{9eV)*t`<+($K< z4Moi$!^!Ici{FlnA3GL%{`8Mpb>v&Rl8uLN7N>ddYm8X*k{@yHGXn^lg#~1ZY&Aei zz{hTdBhcfm@F5ZO?jTy$c_rl=d|ZYyl(fMQ()uNH(9$W^W2#f>U?1Fo4Ck19sd zd`ngdXNDJxJdfSx(|`K0psVK=GstV`)Qc8}mwZl*g6d+Sm~xBwz(!co+-5<}B`x7z z)R(ZyP_8!2-svY#wddB(O^Lw}h~)oI2n2U-uzo-77U|&PP&s;^t8jJbn0GrXsnjFI zpG)6bbtO!k_@y9^3Yzq9AwE~se5o|dkFjoRY+1?{l-A_YJEr3Ff8xQtr-b$E?1S=N zTek()CSc)YYC83BuCWhs)Hv;w`*nUrKAR(IyDcl%oLRC<*1v_F2KP}L#9ziso>?1V z$36EccNf3UGWgJE#iYUq^(z3mxswM@xr4J7s-b+-Ytl#7jgbo^E-z{1w+o9*Tg~OY zV;56(mPpkx`Zu3`BOe+_n?~)!ILXEeDRKqIK%M3D57L$6-}|ueH+e)g@b_drn$1&M zYs|f#9E10|aP80f3?=Qo;T@}Ai*7Qm!9ppc-Y~LsD@^dW7%Tgz4_Y^Da^hf>{aY%f zkB?+s@Tp#!%H+}enbK~D^+u~YkAz=5kIVicr=x*Ydvo)3J%4#t%VD>#_r9(M3yMd( zw-^4A5Grx9uD3vBQb=2i=bQsY{WyLEzcGpSEWv)?y_T?jYDm|#s`~bm9kU-z*en4o zRWM|`@!;7Bv&W~crc@@Q)mt+5G+b=YJriZ?seiWX%xzYdW@-jAw|gVfvD@_*HDu<+ z>TJSJxV_}^^pZwr8JWZ7gG_ghAM)U^!MpwXKhqX%*+^Oqtd^${!RsX$f2r0EW$$FG z4erNb(pSQlimP6QBV;loyH8M~LV~=f>sU-OrziS;tZf~7Og5gl@)>z!FysBD_Xh|? zn1@uOLaN&oAsSja1v+D` zrV2&(CN+IayeyvRyBTdsG$#(mejJZMWKO@`pI<-`?NU4_XCARdpz)WPeobxElJB#5 zF*<77sB$+;pe;chxxHdB=r@J@G#TYtt4z!?MqiJ9TTLj@8u~DP>99^~D_(X%FJ!!l zWZlqzby>oYQ{(fu>*~)!5{?Ao;Cf4KT-WVGde%4jyr?w-XD z=qTwZAzpRd&8pfwQHs#<vEY5z@EdXUUs=0g1`>JJmq35BGL5D%$X&mEh7HnnT0 zgeaOc)gY|?#VmRC!q5awyhizzJBUN?EIj}D^YZ`7?@I5f!ddLK4%ko&eMKgoH4%FC zHr3mGXK)%R^Y$|VR{{77{%4g%ALxju(cwZooxuwMjhX4V9JEXD^~$>X>MzfZE6lvS z()RlrAi8Ym)QRGR^?^Uk1rf08bS#d{%|R2UzMC#~Rq(PQdcL*Kw<=WG<~!qYQUA8dVGa)c>hRC^%tK+tHA=JIYB zZS_k}f_UYeMdBCok=n9mrL(vhV}o1$w(^DYH7}1v-cP&Sme5ql70d$zqR;;m5UoE5 zFQwUEUkWaG*ko1Yq7O=uUsy0tT!lz~i!0jeC8+a_BeIW9lE-y|`eu&o4ceXDcRLHO zdr_*`q5?$H>W`@!zPZiuDAY5`;Ev1UPE*jau|b{!5}}0ghhWQ#>Ro)l;?#*Fb7u?_ z)FV-+^)U9NE0o)$nAi`-K~qNjHht|U)4LBP{W(mZcz|4D*Ba;s!g{|*ad{ui{oOh7 z!dlT@<&=O^fqrXSXmv;c&b^FYRL^7VCQycU{j#AR*{d4$EWcm+5G43s?lKq2(d-mJH?>V?Q#GLpbwGj}v8!)M{Ea9+&7gO#^+I32W(|#A720^2 z?fe*29ew9F9qsc*#$(3lfUbJWvkzeM>Vm->7kGIK9Ol<1wkFa7KDY{3CV%XFYjCj1 zSz6xx{=@GkotsKFRdY;OE9y}F6Z(_- zR8C7S^))?MAVZyy#UbD~?6CC>V@E<0lFxShaHY3Q@FjPdGpFZ*iq4H@n&(k;Vm_k{J7+lL+t(e==I6I z+K?7J(p?iBEIYU*p2Bg~t~_qMNhxHOn!IN~Y<94u-6CTAGg2xBd(N~7g1xUwwoq+m zWr>nzw*9EtQByc1M$wXk5J)KFBeLD$xq{72F;B-^D0Q8?qg&`Ai!&K(5& z%o=DgAoD3SzUO!6k4JK*ns*7c8h?-V~UX67Yr{iaWalf53o+3=)U&(X{V3IwFK#l zTt-0Z4u|fA&DmJmvt%BS@Cnu!Lvd4KGBigjdIrtxx_c~oRzZEwv^vWDENOjg?({iP zsqo%gMmxX%546Y@F)N8Q$2M6gYv)dMh(D5B^!)kW-S!a&Wpclpx-mYQToaObt?Pt+ zMWUpX!-4n5S$VS#9#ipcVm_^}7u5aa?xVmOGt&ci_n$g_HVM@;tvUjFEr4VZcz1aC ze;iT^s3uJ#S)ak8#gTpaL3v|A8@hgMWwkd>lt0lnXJK_P+>O%NXXNDJL19HX#jhc! zkmSBF{sY!wadoI~YeV98_Si|JB)G9k5hz?{@Fa$H6yn;Els zLB0yy7x<4ZgaQDDi+u1o_R^?>^NFNRFH2@bq{pZwXGvONa ztGHs+)NAj>_2R(Z3ijeRLPI@K>IIyeSF$SFgOCRpz!DnP;QchWQlewZyJdBo0WF^X zqV@bZ2e;_{X-m;t1tZp8?okQ7aIv{9ZU&6mHAH#T_n5`f$`WtD$60@if7e0~hp`~W zS$3R?nsyk_th}`;8=)}U!EA8-Hoc#Ar1m97d?s21eQ+u60^7Uf#p_Fy04uI|IkyK| znFL|p7kWJ#2@&STzs3`0A9N4ERqbBQS&BoNhPZ=$w}-7z`$m3@U1BFY?zL{RjC^)q zaZlB&<#Yi!NC2yS#Ab+j0447wY(&ISdk}us+E(Ibv6bzwMOIf~bWLjemm?M<``a5f zBT{zyMAIl&&^we^h$_)sZMAr8XZ_=`c0I;eV9;xS*zw6DR2i-*p;0npCvTw_L5j z0Aee|Is^WvFE>aHn8xv129^#jZE6oHWG5&E z9W6@{47)jy5&@%FlUVlpLsW5Ns>Af?L0y~D$fbi?GHH^x)9$MK;<`c6Kp&eLl7Cr% zTdqw9D`B&pzsTu8p8lcaZ@qc|TxHjAV#Z{bc>Kmk{cw)bH;aY*6AA^ z$~xCIEI>XIcbp{_zTTTnSTb83tnD1@7wJhfo9S+L8t>;`jM2yDJXhZFF%9iApY9gz zAR;E<0l)Cmt{AvT`fX0f21vxkpxmAFNM0?YolcDpItmuoojl_;k6%tVoAc2VV44Mq zae=8&cqM*&g+Rc6N~(EJmHs|)q@3h1v&cU-*(!75K@sPNWYz?3tK-?+@BVY{B+#?) zajgf5|I&cwaclF6C?lSZqt$$Zv|+CP6ap(uD`pGE_Vb^h7C1v-?4UDLZI8`WC<`&9 zX{i|RpnfU9pVHw;VE<-l_I_+1!acbQ-v2T;)e6gvUvMs^VQKQe@uS^lpGi$&0#ba~ zy*neS-6L9EMVPt?+p#&Wuc$PE>xd5IztK<|)^ti!lw`Yoi*&g?*HSA@MqXt>C*#y3 zF2o%cm|zN)reG2~`Co@WjrubGq+C&ycC5UwR^dbI$BDcGfzAUQ&5^7ROr!_mV3OnT z(*h(}I+GW;K8(9c&nWW3sFFGwGlyziF2ua?I2q&i#QyMgkrxLb8JOPCzv*yJlj(K( zK_N7pjf@VmShbr)9SF>yGOx)A46%qc9dL3!U`{L)@+gy*@czXNa=?p3nnY_F!A0TL zlY11mLMkf9b=L$&EnS0?Uio-9xfEI(Yj%r{JT5)qsP*k{|2jwkbBSnPNSw9$)KAlO 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/mkdocs.yml b/mkdocs.yml index 88ab85a68..dc15a32b5 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,12 +53,7 @@ nav: - events.md - decorators.md - dataclasses.md - - Examples: - - examples/basic.md - - examples/very_basic.md - - examples/napari_img_math.md - - examples/napari_parameter_sweep.md - - 'Examples gallery': generated/gallery # This node will automatically be named and have sub-nodes. + - 'Examples': generated_examples # This node will automatically be named and have sub-nodes. - API: - magicgui: api/magicgui.md - magic_factory: api/magic_factory.md @@ -109,10 +105,10 @@ plugins: - docs/scripts/_gen_widgets.py - gallery: conf_script: docs/gallery_conf.py - examples_dirs: [docs/examples_gallery] - gallery_dirs: [docs/generated/gallery] + 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 examle files completely + ignore_pattern: /__init__.py # ignore these example files completely run_stale_examples: True - spellcheck: backends: # the backends you want to use diff --git a/pyproject.toml b/pyproject.toml index 88ebca509..fd572f377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,8 @@ docs = [ "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-spellcheck[all]", + "mkdocs-gallery", + "qtgallery", # extras for all the widgets "pint", "ipywidgets>=8.0.0", From 323334ccde5a43aa009af3660925e7996b7c1214 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 15:16:43 +1000 Subject: [PATCH 06/27] Remove period after markdown formatting heading ==== --- docs/examples/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/basic_widgets_demo.py | 2 +- docs/examples/napari/napari_img_math.py | 7 +++++-- docs/examples/napari/napari_parameter_sweep.py | 7 +++---- docs/examples/napari_parameter_sweep.py | 7 +++---- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index 69e8ee0ca..12c228d2f 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -4,7 +4,7 @@ Widget demonstration with magicgui. -This code demonstrates a few of the widget types that magicgui can create +This code demonstrates a few of the widget types that magicgui can create based on the parameter types in your function. """ import datetime diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py index 69e8ee0ca..12c228d2f 100644 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -4,7 +4,7 @@ Widget demonstration with magicgui. -This code demonstrates a few of the widget types that magicgui can create +This code demonstrates a few of the widget types that magicgui can create based on the parameter types in your function. """ import datetime diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index eed940f79..77c7e074a 100644 --- a/docs/examples/napari/napari_img_math.py +++ b/docs/examples/napari/napari_img_math.py @@ -27,18 +27,19 @@ # %% # ## code # -# *Code follows, with explanation below... You can also [get this example at +# *Code follows, with explanation below... You can also [get this example at # github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_image_arithmetic.py).* 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. @@ -47,6 +48,7 @@ class Operation(Enum): class for all of the image math operations we want to allow. """ + add = numpy.add subtract = numpy.subtract multiply = numpy.multiply @@ -62,6 +64,7 @@ def image_arithmetic( """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") diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index c0a7d7c49..a578af2f0 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -58,15 +58,14 @@ 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: +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") diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index f039410b8..b3825601e 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/napari_parameter_sweep.py @@ -58,15 +58,14 @@ 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: +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") From b0261ea9580a64a37ee2f64edade4dc3ce020318 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 05:43:02 +0000 Subject: [PATCH 07/27] style(pre-commit.ci): auto fixes [...] --- docs/examples/applications/basic_example.py | 2 +- docs/examples/applications/callable.py | 2 +- docs/examples/applications/chaining.py | 2 +- docs/examples/applications/main_window.py | 2 +- docs/examples/applications/pint_quantity.py | 2 +- docs/examples/applications/snells_law.py | 2 +- docs/examples/applications/values_dialog.py | 2 +- docs/examples/basic_example.py | 2 +- docs/examples/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/change_label.py | 2 +- docs/examples/demo_widgets/choices.py | 2 +- docs/examples/demo_widgets/file_dialog.py | 2 +- docs/examples/demo_widgets/image.py | 2 +- docs/examples/demo_widgets/log_slider.py | 2 +- docs/examples/demo_widgets/login.py | 2 +- docs/examples/demo_widgets/optional.py | 2 +- docs/examples/demo_widgets/range_slider.py | 2 +- docs/examples/demo_widgets/selection.py | 2 +- docs/examples/demo_widgets/table.py | 2 +- docs/examples/magicgui_jupyter.py | 2 +- docs/examples/matplotlib/mpl_figure.py | 2 +- docs/examples/matplotlib/waveform.py | 2 +- docs/examples/napari/napari_combine_qt.py | 2 +- docs/examples/napari/napari_forward_refs.py | 2 +- docs/examples/napari/napari_img_math.py | 2 +- docs/examples/napari/napari_parameter_sweep.py | 2 +- docs/examples/napari_parameter_sweep.py | 2 +- docs/examples/notebooks/magicgui_jupyter.py | 2 +- docs/examples/progress_bars/progress.py | 2 +- docs/examples/progress_bars/progress_manual.py | 2 +- docs/examples/progress_bars/progress_nested.py | 2 +- docs/examples/under_the_hood/class_method.py | 2 +- docs/examples/under_the_hood/self_reference.py | 2 +- 34 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py index a33a0d213..d36557ddd 100644 --- a/docs/examples/applications/basic_example.py +++ b/docs/examples/applications/basic_example.py @@ -1,6 +1,6 @@ """ Basic example -============= +=============. A basic example using magicgui. """ diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index f41f20afe..312c7af77 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -1,6 +1,6 @@ """ Callable functions demo -======================= +=======================. This example demonstrates handling callable functions with magicgui. """ diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index 0f4337798..d54a3ff66 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,6 +1,6 @@ """ Chaining functions together -=========================== +===========================. This example demonstrates chaining multiple functions together. """ diff --git a/docs/examples/applications/main_window.py b/docs/examples/applications/main_window.py index e31b6075b..ee0f5fb5f 100644 --- a/docs/examples/applications/main_window.py +++ b/docs/examples/applications/main_window.py @@ -1,6 +1,6 @@ """ Hotdog or not app -================= +=================. Demo app to upload an image and classify if it's an hotdog or not. """ diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index fb9dc68da..f581df0d8 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,6 +1,6 @@ """ 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. diff --git a/docs/examples/applications/snells_law.py b/docs/examples/applications/snells_law.py index 3ad558a8f..50da41151 100644 --- a/docs/examples/applications/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,6 +1,6 @@ """ Snell's law demonstration using magicgui -======================================== +========================================. """ import math diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index dbc03f9ec..162582d37 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -1,6 +1,6 @@ """ Input values dialog -=================== +===================. A basic example of a user input dialog. diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py index a33a0d213..d36557ddd 100644 --- a/docs/examples/basic_example.py +++ b/docs/examples/basic_example.py @@ -1,6 +1,6 @@ """ Basic example -============= +=============. A basic example using magicgui. """ diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index 12c228d2f..3aeb113fa 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,6 +1,6 @@ """ Basic widgets demo -================== +==================. Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py index 12c228d2f..3aeb113fa 100644 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -1,6 +1,6 @@ """ Basic widgets demo -================== +==================. Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index 76a587b2c..5e41b7aea 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,6 +1,6 @@ """ Custom text labels for widgets -============================== +==============================. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index 888ab9171..60a356c99 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,6 +1,6 @@ """ Dropdown selection widget -========================= +=========================. Choices for dropdowns can be provided in a few different ways. """ diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py index 2e9f7cf1b..0906ede31 100644 --- a/docs/examples/demo_widgets/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,6 +1,6 @@ """ File dialog widget -================== +==================. """ from pathlib import Path from typing import Sequence diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index e29994e87..dc6f9ac7b 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,6 +1,6 @@ """ Image widget -============ +============. Example of creating an Image Widget from a file. diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index 6e954409d..381f63f6a 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,6 +1,6 @@ """ Log slider widget -================= +=================. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index cdc392811..572a5a133 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,6 +1,6 @@ """ Password login -============== +==============. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index 4fb52ab57..41b96881b 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,6 +1,6 @@ """ Optional user choice -===================== +=====================. """ from typing import Optional diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index bdee48893..a79675df8 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,6 +1,6 @@ """ Range slider widget -=================== +===================. A double ended range slider widget. """ diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py index 0afd6df22..65126ff01 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -1,6 +1,6 @@ """ Multiple selection widget -========================= +=========================. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py index 8dbd92bcc..f6b41c83b 100644 --- a/docs/examples/demo_widgets/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,6 +1,6 @@ """ Table widget -============ +============. Demonstrating a few ways to input tables. """ diff --git a/docs/examples/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py index 28b7613c9..2160d4bd8 100644 --- a/docs/examples/magicgui_jupyter.py +++ b/docs/examples/magicgui_jupyter.py @@ -1,6 +1,6 @@ """ Jupyter notebooks and magicgui -============================== +==============================. This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index be1f01903..117ce10b4 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,6 +1,6 @@ """ matplotlib figure example -========================= +=========================. Basic example of adding a generic QWidget to a container. diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index 6e5836801..671496d20 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,6 +1,6 @@ """ Waveforms example -================= +=================. """ from dataclasses import dataclass, field from enum import Enum diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index b152ae9ae..29d131ef0 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,6 +1,6 @@ """ 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 diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py index 833f8e38e..290cbf648 100644 --- a/docs/examples/napari/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,6 +1,6 @@ """ napari forward reference demo -============================= +=============================. Example of using a ForwardRef to avoid importing a module that provides a widget. diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index 77c7e074a..9fe50eddc 100644 --- a/docs/examples/napari/napari_img_math.py +++ b/docs/examples/napari/napari_img_math.py @@ -1,6 +1,6 @@ """ 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 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index a578af2f0..938fb35f0 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -1,6 +1,6 @@ """ 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 diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index b3825601e..c7dffcc99 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/napari_parameter_sweep.py @@ -1,6 +1,6 @@ """ 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 diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index 28b7613c9..2160d4bd8 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -1,6 +1,6 @@ """ Jupyter notebooks and magicgui -============================== +==============================. This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/progress_bars/progress.py b/docs/examples/progress_bars/progress.py index 3eedcc13b..d27027f02 100644 --- a/docs/examples/progress_bars/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,6 +1,6 @@ """ Simple progress bar -=================== +===================. A simple progress bar demo with magicgui. """ diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py index 8c88b7a70..be5f7a82b 100644 --- a/docs/examples/progress_bars/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,6 +1,6 @@ """ Manual progress bar -=================== +===================. Example of a progress bar being updated manually. diff --git a/docs/examples/progress_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py index 06f101d16..abe5a30aa 100644 --- a/docs/examples/progress_bars/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,6 +1,6 @@ """ Nested progress bars -==================== +====================. Example using nested progress bars in magicgui. diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index 4fbec040a..c6f4b4b36 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,6 +1,6 @@ """ Deocrate class methods with magicgui -==================================== +====================================. Demonstrates decorating a method. diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index f071f005a..10fb646f2 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,6 +1,6 @@ """ Self reference magicgui widgets -=============================== +===============================. Widgets created with magicgui can reference themselves, and use the widget API. """ From 04dc2308d7adc38b91602821024ce5ed21bf2929 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:08:55 +1000 Subject: [PATCH 08/27] Fix formatting of python docstrings so pre-commit doesn't overwrite --- docs/examples/applications/basic_example.py | 4 +--- docs/examples/applications/callable.py | 4 +--- docs/examples/applications/chaining.py | 4 +--- docs/examples/applications/main_window.py | 4 +--- docs/examples/applications/pint_quantity.py | 4 +--- docs/examples/applications/snells_law.py | 6 +++--- docs/examples/applications/values_dialog.py | 4 +--- docs/examples/basic_example.py | 4 +--- docs/examples/basic_widgets_demo.py | 4 +--- docs/examples/demo_widgets/basic_widgets_demo.py | 4 +--- docs/examples/demo_widgets/change_label.py | 6 +++--- docs/examples/demo_widgets/choices.py | 4 +--- docs/examples/demo_widgets/file_dialog.py | 6 +++--- docs/examples/demo_widgets/image.py | 4 +--- docs/examples/demo_widgets/log_slider.py | 6 +++--- docs/examples/demo_widgets/login.py | 6 +++--- docs/examples/demo_widgets/optional.py | 6 +++--- docs/examples/demo_widgets/range_slider.py | 4 +--- docs/examples/demo_widgets/selection.py | 6 +++--- docs/examples/demo_widgets/table.py | 4 +--- docs/examples/magicgui_jupyter.py | 4 +--- docs/examples/matplotlib/mpl_figure.py | 4 +--- docs/examples/matplotlib/waveform.py | 6 +++--- docs/examples/napari/napari_combine_qt.py | 4 +--- docs/examples/napari/napari_forward_refs.py | 4 +--- docs/examples/napari/napari_img_math.py | 4 +--- docs/examples/napari/napari_parameter_sweep.py | 4 +--- docs/examples/napari_parameter_sweep.py | 4 +--- docs/examples/notebooks/magicgui_jupyter.py | 4 +--- docs/examples/progress_bars/progress.py | 4 +--- docs/examples/progress_bars/progress_manual.py | 4 +--- docs/examples/progress_bars/progress_nested.py | 4 +--- docs/examples/under_the_hood/class_method.py | 4 +--- docs/examples/under_the_hood/self_reference.py | 4 +--- 34 files changed, 50 insertions(+), 102 deletions(-) diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py index d36557ddd..e44ef1d73 100644 --- a/docs/examples/applications/basic_example.py +++ b/docs/examples/applications/basic_example.py @@ -1,6 +1,4 @@ -""" -Basic example -=============. +"""# Basic example A basic example using magicgui. """ diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index 312c7af77..909063eab 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -1,6 +1,4 @@ -""" -Callable functions demo -=======================. +"""# Callable functions demo This example demonstrates handling callable functions with magicgui. """ diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index d54a3ff66..666e3fc76 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,6 +1,4 @@ -""" -Chaining functions together -===========================. +"""# Chaining functions together This example demonstrates chaining multiple functions together. """ diff --git a/docs/examples/applications/main_window.py b/docs/examples/applications/main_window.py index ee0f5fb5f..f5329a9d6 100644 --- a/docs/examples/applications/main_window.py +++ b/docs/examples/applications/main_window.py @@ -1,6 +1,4 @@ -""" -Hotdog or not app -=================. +"""# Hotdog or not app Demo app to upload an image and classify if it's an hotdog or not. """ diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index f581df0d8..6d9e86bda 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,6 +1,4 @@ -""" -Quantities with pint -====================. +"""# 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. diff --git a/docs/examples/applications/snells_law.py b/docs/examples/applications/snells_law.py index 50da41151..481f7f9b4 100644 --- a/docs/examples/applications/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,6 +1,6 @@ -""" -Snell's law demonstration using magicgui -========================================. +"""# Snell's law demonstration using magicgui + +Demo app for calculating angles of refraction according to Snell's law. """ import math diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index 162582d37..fc95b33af 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -1,6 +1,4 @@ -""" -Input values dialog -===================. +"""# Input values dialog A basic example of a user input dialog. diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py index d36557ddd..e44ef1d73 100644 --- a/docs/examples/basic_example.py +++ b/docs/examples/basic_example.py @@ -1,6 +1,4 @@ -""" -Basic example -=============. +"""# Basic example A basic example using magicgui. """ diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index 3aeb113fa..b3ae376cf 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,6 +1,4 @@ -""" -Basic widgets demo -==================. +"""# Basic widgets demo Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py index 3aeb113fa..b3ae376cf 100644 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -1,6 +1,4 @@ -""" -Basic widgets demo -==================. +"""# Basic widgets demo Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index 5e41b7aea..37abb096f 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,6 +1,6 @@ -""" -Custom text labels for widgets -==============================. +"""# Custom text labels for widgets + +An example showing how to create custom text labels for your widgets. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index 60a356c99..dd2f874d9 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,6 +1,4 @@ -""" -Dropdown selection widget -=========================. +"""# Dropdown selection widget Choices for dropdowns can be provided in a few different ways. """ diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py index 0906ede31..ff7230ebd 100644 --- a/docs/examples/demo_widgets/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,6 +1,6 @@ -""" -File dialog widget -==================. +"""# File dialog widget + +A file dialog widget example. """ from pathlib import Path from typing import Sequence diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index dc6f9ac7b..e890b5f4b 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,6 +1,4 @@ -""" -Image widget -============. +"""# Image widget Example of creating an Image Widget from a file. diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index 381f63f6a..28fa8ffdf 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,6 +1,6 @@ -""" -Log slider widget -=================. +"""# Log slider widget + +A logarithmic scale range slider widget. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index 572a5a133..94488cb1c 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,6 +1,6 @@ -""" -Password login -==============. +"""# Password login + +A password login field widget. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index 41b96881b..9a7b00bcd 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,6 +1,6 @@ -""" -Optional user choice -=====================. +"""# Optional user choice + +Optional user input using a dropdown selection widget. """ from typing import Optional diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index a79675df8..042abf347 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,6 +1,4 @@ -""" -Range slider widget -===================. +"""# Range slider widget A double ended range slider widget. """ diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py index 65126ff01..58e55bc92 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -1,6 +1,6 @@ -""" -Multiple selection widget -=========================. +"""# Multiple selection widget + +A selection widget allowing multiple selections by the user. """ from magicgui import magicgui diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py index f6b41c83b..4af4b870c 100644 --- a/docs/examples/demo_widgets/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,6 +1,4 @@ -""" -Table widget -============. +"""# Table widget Demonstrating a few ways to input tables. """ diff --git a/docs/examples/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py index 2160d4bd8..504e386c1 100644 --- a/docs/examples/magicgui_jupyter.py +++ b/docs/examples/magicgui_jupyter.py @@ -1,6 +1,4 @@ -""" -Jupyter notebooks and magicgui -==============================. +"""# Jupyter notebooks and magicgui This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index 117ce10b4..3ae2aeb8e 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,6 +1,4 @@ -""" -matplotlib figure example -=========================. +"""# matplotlib figure example Basic example of adding a generic QWidget to a container. diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index 671496d20..fd06ff1eb 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,6 +1,6 @@ -""" -Waveforms example -=================. +"""# Waveforms example + +Simple waveform generator widget, with plotting. """ from dataclasses import dataclass, field from enum import Enum diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index 29d131ef0..6a3b99e3b 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,6 +1,4 @@ -""" -napari Qt demo -==============. +"""# 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 diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py index 290cbf648..1700c0d3c 100644 --- a/docs/examples/napari/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,6 +1,4 @@ -""" -napari forward reference demo -=============================. +"""# napari forward reference demo Example of using a ForwardRef to avoid importing a module that provides a widget. diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index 9fe50eddc..6eddad833 100644 --- a/docs/examples/napari/napari_img_math.py +++ b/docs/examples/napari/napari_img_math.py @@ -1,6 +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 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index 938fb35f0..1a53ade15 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -1,6 +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 diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index c7dffcc99..c029428f7 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/napari_parameter_sweep.py @@ -1,6 +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 diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index 2160d4bd8..504e386c1 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -1,6 +1,4 @@ -""" -Jupyter notebooks and magicgui -==============================. +"""# Jupyter notebooks and magicgui This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/progress_bars/progress.py b/docs/examples/progress_bars/progress.py index d27027f02..6f064b5e5 100644 --- a/docs/examples/progress_bars/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,6 +1,4 @@ -""" -Simple progress bar -===================. +"""# Simple progress bar A simple progress bar demo with magicgui. """ diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py index be5f7a82b..ccc907862 100644 --- a/docs/examples/progress_bars/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,6 +1,4 @@ -""" -Manual progress bar -===================. +"""# Manual progress bar Example of a progress bar being updated manually. diff --git a/docs/examples/progress_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py index abe5a30aa..6bb9a9b43 100644 --- a/docs/examples/progress_bars/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,6 +1,4 @@ -""" -Nested progress bars -====================. +"""# Nested progress bars Example using nested progress bars in magicgui. diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index c6f4b4b36..9dfa6ce34 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,6 +1,4 @@ -""" -Deocrate class methods with magicgui -====================================. +"""# Deocrate class methods with magicgui Demonstrates decorating a method. diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index 10fb646f2..b70065447 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,6 +1,4 @@ -""" -Self reference magicgui widgets -===============================. +"""# Self reference magicgui widgets Widgets created with magicgui can reference themselves, and use the widget API. """ From 9bc2d31dec5538f59465cd4651ba9851e0003225 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:10:22 +1000 Subject: [PATCH 09/27] More meaningful file name for hotdog app example --- docs/examples/{applications/main_window.py => hotdog.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/examples/{applications/main_window.py => hotdog.py} (100%) diff --git a/docs/examples/applications/main_window.py b/docs/examples/hotdog.py similarity index 100% rename from docs/examples/applications/main_window.py rename to docs/examples/hotdog.py From 5c79ffcbd5b49d8f1fc40afee5b5b96fdbaa6621 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:13:36 +1000 Subject: [PATCH 10/27] Minor clarifications for examples text --- docs/examples/notebooks/magicgui_jupyter.py | 2 ++ docs/examples/under_the_hood/README.md | 4 +++- docs/examples/under_the_hood/class_method.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index 504e386c1..da9299d0a 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -2,6 +2,8 @@ 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). """ diff --git a/docs/examples/under_the_hood/README.md b/docs/examples/under_the_hood/README.md index 2b546ca9f..0953445b0 100644 --- a/docs/examples/under_the_hood/README.md +++ b/docs/examples/under_the_hood/README.md @@ -1,3 +1,5 @@ # Under the hood -Learn more advanced usage patterns for magicgui, including self referencing widgets and creating new class methods. +Learn more advanced usage patterns for magicgui, +including self referencing widgets and +decorating class methods with magicgui. diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index 9dfa6ce34..490b66d52 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,6 +1,6 @@ """# Deocrate class methods with magicgui -Demonstrates decorating a method. +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 From 9ef0a6894dc0466592e97c5fad8e0c2c8736d372 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 06:14:04 +0000 Subject: [PATCH 11/27] style(pre-commit.ci): auto fixes [...] --- docs/examples/applications/basic_example.py | 2 +- docs/examples/applications/callable.py | 2 +- docs/examples/applications/chaining.py | 2 +- docs/examples/applications/pint_quantity.py | 2 +- docs/examples/applications/snells_law.py | 2 +- docs/examples/applications/values_dialog.py | 2 +- docs/examples/basic_example.py | 2 +- docs/examples/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/change_label.py | 2 +- docs/examples/demo_widgets/choices.py | 2 +- docs/examples/demo_widgets/file_dialog.py | 2 +- docs/examples/demo_widgets/image.py | 2 +- docs/examples/demo_widgets/log_slider.py | 2 +- docs/examples/demo_widgets/login.py | 2 +- docs/examples/demo_widgets/optional.py | 2 +- docs/examples/demo_widgets/range_slider.py | 2 +- docs/examples/demo_widgets/selection.py | 2 +- docs/examples/demo_widgets/table.py | 2 +- docs/examples/hotdog.py | 2 +- docs/examples/magicgui_jupyter.py | 2 +- docs/examples/matplotlib/mpl_figure.py | 2 +- docs/examples/matplotlib/waveform.py | 2 +- docs/examples/napari/napari_combine_qt.py | 2 +- docs/examples/napari/napari_forward_refs.py | 2 +- docs/examples/napari/napari_img_math.py | 2 +- docs/examples/napari/napari_parameter_sweep.py | 2 +- docs/examples/napari_parameter_sweep.py | 2 +- docs/examples/notebooks/magicgui_jupyter.py | 2 +- docs/examples/progress_bars/progress.py | 2 +- docs/examples/progress_bars/progress_manual.py | 2 +- docs/examples/progress_bars/progress_nested.py | 2 +- docs/examples/under_the_hood/class_method.py | 2 +- docs/examples/under_the_hood/self_reference.py | 2 +- 34 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py index e44ef1d73..be2148a7f 100644 --- a/docs/examples/applications/basic_example.py +++ b/docs/examples/applications/basic_example.py @@ -1,4 +1,4 @@ -"""# Basic example +"""# Basic example. A basic example using magicgui. """ diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index 909063eab..82d3db3aa 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -1,4 +1,4 @@ -"""# Callable functions demo +"""# Callable functions demo. This example demonstrates handling callable functions with magicgui. """ diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index 666e3fc76..aec82f5de 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,4 +1,4 @@ -"""# Chaining functions together +"""# Chaining functions together. This example demonstrates chaining multiple functions together. """ diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index 6d9e86bda..e8a61873d 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,4 +1,4 @@ -"""# Quantities with pint +"""# 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. diff --git a/docs/examples/applications/snells_law.py b/docs/examples/applications/snells_law.py index 481f7f9b4..ab3bf6be6 100644 --- a/docs/examples/applications/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,4 +1,4 @@ -"""# Snell's law demonstration using magicgui +"""# Snell's law demonstration using magicgui. Demo app for calculating angles of refraction according to Snell's law. """ diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index fc95b33af..b83e0c6c5 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -1,4 +1,4 @@ -"""# Input values dialog +"""# Input values dialog. A basic example of a user input dialog. diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py index e44ef1d73..be2148a7f 100644 --- a/docs/examples/basic_example.py +++ b/docs/examples/basic_example.py @@ -1,4 +1,4 @@ -"""# Basic example +"""# Basic example. A basic example using magicgui. """ diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index b3ae376cf..51b0514e4 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,4 +1,4 @@ -"""# Basic widgets demo +"""# Basic widgets demo. Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py index b3ae376cf..51b0514e4 100644 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -1,4 +1,4 @@ -"""# Basic widgets demo +"""# Basic widgets demo. Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index 37abb096f..9980983ea 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,4 +1,4 @@ -"""# Custom text labels for widgets +"""# Custom text labels for widgets. An example showing how to create custom text labels for your widgets. """ diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index dd2f874d9..8ceba6d90 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,4 +1,4 @@ -"""# Dropdown selection widget +"""# Dropdown selection widget. Choices for dropdowns can be provided in a few different ways. """ diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py index ff7230ebd..84f06d99a 100644 --- a/docs/examples/demo_widgets/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,4 +1,4 @@ -"""# File dialog widget +"""# File dialog widget. A file dialog widget example. """ diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index e890b5f4b..238f8bae6 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,4 +1,4 @@ -"""# Image widget +"""# Image widget. Example of creating an Image Widget from a file. diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index 28fa8ffdf..5d1de45f6 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,4 +1,4 @@ -"""# Log slider widget +"""# Log slider widget. A logarithmic scale range slider widget. """ diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index 94488cb1c..398f92d91 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,4 +1,4 @@ -"""# Password login +"""# Password login. A password login field widget. """ diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index 9a7b00bcd..981ffa865 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,4 +1,4 @@ -"""# Optional user choice +"""# Optional user choice. Optional user input using a dropdown selection widget. """ diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index 042abf347..696020ab7 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,4 +1,4 @@ -"""# Range slider widget +"""# Range slider widget. A double ended range slider widget. """ diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py index 58e55bc92..a55f21531 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -1,4 +1,4 @@ -"""# Multiple selection widget +"""# Multiple selection widget. A selection widget allowing multiple selections by the user. """ diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py index 4af4b870c..94cbd2cca 100644 --- a/docs/examples/demo_widgets/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,4 +1,4 @@ -"""# Table widget +"""# Table widget. Demonstrating a few ways to input tables. """ diff --git a/docs/examples/hotdog.py b/docs/examples/hotdog.py index f5329a9d6..f3673f1ea 100644 --- a/docs/examples/hotdog.py +++ b/docs/examples/hotdog.py @@ -1,4 +1,4 @@ -"""# Hotdog or not app +"""# Hotdog or not app. Demo app to upload an image and classify if it's an hotdog or not. """ diff --git a/docs/examples/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py index 504e386c1..b8935ec8a 100644 --- a/docs/examples/magicgui_jupyter.py +++ b/docs/examples/magicgui_jupyter.py @@ -1,4 +1,4 @@ -"""# Jupyter notebooks and magicgui +"""# Jupyter notebooks and magicgui. This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index 3ae2aeb8e..7162f5274 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,4 +1,4 @@ -"""# matplotlib figure example +"""# matplotlib figure example. Basic example of adding a generic QWidget to a container. diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index fd06ff1eb..601e13319 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,4 +1,4 @@ -"""# Waveforms example +"""# Waveforms example. Simple waveform generator widget, with plotting. """ diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index 6a3b99e3b..03fa7800f 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,4 +1,4 @@ -"""# napari Qt demo +"""# 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 diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py index 1700c0d3c..afbaa1741 100644 --- a/docs/examples/napari/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,4 +1,4 @@ -"""# napari forward reference demo +"""# napari forward reference demo. Example of using a ForwardRef to avoid importing a module that provides a widget. diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index 6eddad833..a53240a80 100644 --- a/docs/examples/napari/napari_img_math.py +++ 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 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index 1a53ade15..5d9ae5860 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ 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 diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index c029428f7..1d72cbdbd 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/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 diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index da9299d0a..8f195201b 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -1,4 +1,4 @@ -"""# Jupyter notebooks and magicgui +"""# Jupyter notebooks and magicgui. This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/progress_bars/progress.py b/docs/examples/progress_bars/progress.py index 6f064b5e5..8db6a7a59 100644 --- a/docs/examples/progress_bars/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,4 +1,4 @@ -"""# Simple progress bar +"""# Simple progress bar. A simple progress bar demo with magicgui. """ diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py index ccc907862..7659cc399 100644 --- a/docs/examples/progress_bars/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,4 +1,4 @@ -"""# Manual progress bar +"""# Manual progress bar. Example of a progress bar being updated manually. diff --git a/docs/examples/progress_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py index 6bb9a9b43..4dac4563e 100644 --- a/docs/examples/progress_bars/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,4 +1,4 @@ -"""# Nested progress bars +"""# Nested progress bars. Example using nested progress bars in magicgui. diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index 490b66d52..5dd5b96c9 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,4 +1,4 @@ -"""# Deocrate class methods with magicgui +"""# Deocrate class methods with magicgui. Demonstrates decorating a class method with magicgui. diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index b70065447..7e230ef0b 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,4 +1,4 @@ -"""# Self reference magicgui widgets +"""# Self reference magicgui widgets. Widgets created with magicgui can reference themselves, and use the widget API. """ From 9065af90e7385d9910e568b095b725b00fe79fb3 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:31:07 +1000 Subject: [PATCH 12/27] Fix formatting errors (lines too long) --- docs/examples/{ => applications}/hotdog.py | 2 +- docs/examples/applications/pint_quantity.py | 6 +++-- docs/examples/magicgui_jupyter.py | 2 +- docs/examples/napari/napari_img_math.py | 25 ++++++++++--------- .../examples/napari/napari_parameter_sweep.py | 19 ++++++++------ docs/examples/napari_parameter_sweep.py | 19 ++++++++------ docs/examples/notebooks/magicgui_jupyter.py | 4 +-- 7 files changed, 43 insertions(+), 34 deletions(-) rename docs/examples/{ => applications}/hotdog.py (97%) diff --git a/docs/examples/hotdog.py b/docs/examples/applications/hotdog.py similarity index 97% rename from docs/examples/hotdog.py rename to docs/examples/applications/hotdog.py index f5329a9d6..f3673f1ea 100644 --- a/docs/examples/hotdog.py +++ b/docs/examples/applications/hotdog.py @@ -1,4 +1,4 @@ -"""# Hotdog or not app +"""# Hotdog or not app. Demo app to upload an image and classify if it's an hotdog or not. """ diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index 6d9e86bda..f7123f2bf 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,8 +1,9 @@ -"""# Quantities with pint +"""# 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. +It allows arithmetic operations between them and conversions +from and to different units. https://pint.readthedocs.io/en/stable/ """ from pint import Quantity @@ -12,6 +13,7 @@ @magicgui def widget(q=Quantity("1 ms")): + """Widget allowing users to input quantity measurements.""" print(q) diff --git a/docs/examples/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py index 504e386c1..cfd052cbd 100644 --- a/docs/examples/magicgui_jupyter.py +++ b/docs/examples/magicgui_jupyter.py @@ -42,4 +42,4 @@ # ``` # %% -# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) +# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) # noqa: E501 diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index 6eddad833..c6aecd8e6 100644 --- a/docs/examples/napari/napari_img_math.py +++ 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 @@ -26,7 +26,7 @@ # ## code # # *Code follows, with explanation below... You can also [get this example at -# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_image_arithmetic.py).* +# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_image_arithmetic.py).* # noqa: E501 from enum import Enum @@ -84,7 +84,7 @@ def image_arithmetic( # %% # ### the function -# Our function takes two `numpy` arrays (in this case, from [Image layers](https://napari.org/howtos/layers/image.html)), +# Our function takes two `numpy` arrays (in this case, from [Image layers](https://napari.org/howtos/layers/image.html)), # noqa: E501 # and some mathematical operation # (we'll restrict the options using an `enum.Enum`). # When called, ourfunction calls the selected operation on the data. @@ -97,16 +97,17 @@ def image_arithmetic( # %% # #### 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), # noqa: E501 +# 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) # noqa: E501 # 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 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index 1a53ade15..235e11609 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ 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,7 +9,7 @@ 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) @@ -36,7 +36,7 @@ # ## code # # *Code follows, with explanation below... You can also [get this example at -# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* +# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* # noqa: E501 # %% import napari @@ -79,14 +79,15 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # ## 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 # # Our function is a very thin wrapper around -# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). +# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). # noqa: E501 # It takes a `napari` [Image # layer](https://napari.org/howtos/layers/image.html), a `sigma` to control # the blur radius, and a `mode` that determines how edges are handled. @@ -102,7 +103,8 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # 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 # signature that were not provided by `filters.gaussian` @@ -144,7 +146,8 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # - `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. diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py index c029428f7..a214b31b4 100644 --- a/docs/examples/napari_parameter_sweep.py +++ b/docs/examples/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,7 +9,7 @@ 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) @@ -36,7 +36,7 @@ # ## code # # *Code follows, with explanation below... You can also [get this example at -# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* +# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* # noqa: E501 # %% import napari @@ -79,14 +79,15 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # ## 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 # # Our function is a very thin wrapper around -# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). +# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). # noqa: E501 # It takes a `napari` [Image # layer](https://napari.org/howtos/layers/image.html), a `sigma` to control # the blur radius, and a `mode` that determines how edges are handled. @@ -102,7 +103,8 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # 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 # signature that were not provided by `filters.gaussian` @@ -144,7 +146,8 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # - `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. diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index da9299d0a..b79a5e74d 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -1,4 +1,4 @@ -"""# Jupyter notebooks and magicgui +"""# Jupyter notebooks and magicgui. This example shows magicgui widgets embedded in a jupyter notebook. @@ -44,4 +44,4 @@ # ``` # %% -# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) +# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) # noqa: E501 From b17c686e8d117a237092fd211f6d1d09bb10f3fe Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:03:32 +1000 Subject: [PATCH 13/27] Ruff formatting rules --- docs/examples/applications/basic_example.py | 1 + docs/examples/applications/callable.py | 4 ++++ docs/examples/applications/chaining.py | 3 +++ docs/examples/applications/pint_quantity.py | 2 +- docs/examples/basic_example.py | 1 + docs/examples/basic_widgets_demo.py | 6 +++--- docs/examples/demo_widgets/basic_widgets_demo.py | 6 +++--- docs/examples/demo_widgets/change_label.py | 1 + docs/examples/demo_widgets/choices.py | 5 +++++ docs/examples/demo_widgets/log_slider.py | 1 + docs/examples/demo_widgets/login.py | 1 + docs/examples/demo_widgets/optional.py | 1 + docs/examples/demo_widgets/range_slider.py | 1 + docs/examples/demo_widgets/selection.py | 5 +++-- docs/examples/matplotlib/mpl_figure.py | 1 + docs/examples/matplotlib/waveform.py | 10 +++++++--- docs/examples/napari/napari_combine_qt.py | 2 ++ docs/examples/under_the_hood/class_method.py | 3 +++ docs/examples/under_the_hood/self_reference.py | 1 + 19 files changed, 43 insertions(+), 12 deletions(-) diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py index be2148a7f..ebbec4c8f 100644 --- a/docs/examples/applications/basic_example.py +++ b/docs/examples/applications/basic_example.py @@ -7,6 +7,7 @@ @magicgui def example(x: int, y="hi"): + """Basic example function.""" return x, y diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index 82d3db3aa..9f819fbc7 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -6,19 +6,23 @@ 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/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index aec82f5de..da69989fb 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -7,12 +7,14 @@ @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 @@ -34,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/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index f7123f2bf..3d1653991 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -12,7 +12,7 @@ @magicgui -def widget(q=Quantity("1 ms")): +def widget(q=Quantity("1 ms")): # noqa: B008 """Widget allowing users to input quantity measurements.""" print(q) diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py index be2148a7f..ebbec4c8f 100644 --- a/docs/examples/basic_example.py +++ b/docs/examples/basic_example.py @@ -7,6 +7,7 @@ @magicgui def example(x: int, y="hi"): + """Basic example function.""" return x, y diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index 51b0514e4..bc23cdbb0 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -45,9 +45,9 @@ def widget_demo( 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(), + time=datetime.time(1, 30, 20), # noqa: B008 + datetime=datetime.datetime.now(), # noqa: B008 + filename=Path.home(), # noqa: B008 ): """We can use numpy docstrings to provide tooltips. diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py index 51b0514e4..bc23cdbb0 100644 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ b/docs/examples/demo_widgets/basic_widgets_demo.py @@ -45,9 +45,9 @@ def widget_demo( 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(), + time=datetime.time(1, 30, 20), # noqa: B008 + datetime=datetime.datetime.now(), # noqa: B008 + filename=Path.home(), # noqa: B008 ): """We can use numpy docstrings to provide tooltips. diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index 9980983ea..e2439e934 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -8,6 +8,7 @@ # 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/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index 8ceba6d90..d035a97b4 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -17,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) @@ -29,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/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index 5d1de45f6..65b12ed42 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -11,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/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index 398f92d91..899a229d3 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -12,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/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index 981ffa865..ce136c013 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -10,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/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index 696020ab7..f5fa0ad22 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -9,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 index a55f21531..362a2361f 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -7,11 +7,12 @@ @magicgui( pick_some={ - "choices": ["first", "second", "third", "fourth"], + "choices": ("first", "second", "third", "fourth"), "allow_multiple": True, } ) -def my_widget(pick_some=["first"]): +def my_widget(pick_some=("first")): + """Dropdown selection function.""" print("you selected", pick_some) diff --git a/docs/examples/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index 7162f5274..b67bcf023 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -23,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/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index 601e13319..16a6bf168 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -61,9 +61,8 @@ def plot(self, ax=None, **kwargs): 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 ------- @@ -87,6 +86,8 @@ def sine( ---------- 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 @@ -159,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: @@ -177,6 +179,8 @@ def on_off( class Select(Enum): + """Enumeration to select signal type.""" + OnOff = "on_off" Sine = "sine" Chirp = "chirp" diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index 03fa7800f..d44201726 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -17,6 +17,8 @@ class CustomWidget(QWidget): + """A custom widget class.""" + def __init__(self) -> None: super().__init__() self.setLayout(QVBoxLayout()) diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index 5dd5b96c9..e2ec35928 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -11,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/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index 7e230ef0b..690fff112 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -7,6 +7,7 @@ @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 From 90f339ba9a5b4a824f3c0b3afae527e88d3e504b Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:46:50 +1000 Subject: [PATCH 14/27] Fix mkdocs build --strict warnings --- docs/examples/README.md | 4 +- docs/examples/applications/basic_example.py | 15 -- docs/examples/applications/values_dialog.py | 2 +- .../demo_widgets/basic_widgets_demo.py | 85 --------- docs/examples/magicgui_jupyter.py | 45 ----- .../examples/napari/napari_parameter_sweep.py | 8 +- docs/examples/napari_parameter_sweep.py | 172 ------------------ 7 files changed, 6 insertions(+), 325 deletions(-) delete mode 100644 docs/examples/applications/basic_example.py delete mode 100644 docs/examples/demo_widgets/basic_widgets_demo.py delete mode 100644 docs/examples/magicgui_jupyter.py delete mode 100644 docs/examples/napari_parameter_sweep.py diff --git a/docs/examples/README.md b/docs/examples/README.md index 3aff5175f..0db024071 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,5 +1,3 @@ -# Featured examples +# Getting started A gallery of examples for magicgui. - -Featured examples: diff --git a/docs/examples/applications/basic_example.py b/docs/examples/applications/basic_example.py deleted file mode 100644 index ebbec4c8f..000000000 --- a/docs/examples/applications/basic_example.py +++ /dev/null @@ -1,15 +0,0 @@ -"""# Basic example. - -A basic example using magicgui. -""" -from magicgui import magicgui - - -@magicgui -def example(x: int, y="hi"): - """Basic example function.""" - return x, y - - -example.changed.connect(print) -example.show(run=True) diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index b83e0c6c5..ad7fd53bd 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -4,7 +4,7 @@ This will pause code execution until the user responds. -![Values input dialog](../images/values_input.png){ width=50% } +![Values input dialog](../../images/values_input.png){ width=50% } """ diff --git a/docs/examples/demo_widgets/basic_widgets_demo.py b/docs/examples/demo_widgets/basic_widgets_demo.py deleted file mode 100644 index bc23cdbb0..000000000 --- a/docs/examples/demo_widgets/basic_widgets_demo.py +++ /dev/null @@ -1,85 +0,0 @@ -"""# 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 - -from magicgui import magicgui - - -class Medium(Enum): - """Enum for various media and their refractive indices.""" - - Glass = 1.520 - Oil = 1.515 - Water = 1.333 - Air = 1.0003 - - -@magicgui( - main_window=True, - call_button="Calculate", - layout="vertical", - result_widget=True, - 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:"}, -) -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), # noqa: B008 - datetime=datetime.datetime.now(), # noqa: B008 - filename=Path.home(), # noqa: B008 -): - """We can use numpy docstrings to provide tooltips. - - Parameters - ---------- - boolean : bool, optional - A checkbox for booleans, by default True - integer : int, optional - Some integer, by default 1 - spin_float : float, optional - This one is a float, by default "pi" - slider_float : float, optional - Hey look! I'm a slider, by default 43.5 - slider_int : float, optional - I only take integers, and I've hidden my readout, by default 550 - string : str, optional - We'll use this string carefully, by default "Text goes here" - dropdown : Enum, optional - Pick a medium, by default Medium.Glass - radio_option : int - A set of radio buttons. - date : datetime.date, optional - Your birthday, by default datetime.date(1999, 12, 31) - time : datetime.time, optional - Some time, by default datetime.time(1, 30, 20) - datetime : datetime.datetime, optional - A very specific time and date, by default ``datetime.datetime.now()`` - filename : str, optional - Pick a path, by default Path.home() - """ - return locals().values() - - -widget_demo.changed.connect(print) -widget_demo.show(run=True) diff --git a/docs/examples/magicgui_jupyter.py b/docs/examples/magicgui_jupyter.py deleted file mode 100644 index 5186a8fd4..000000000 --- a/docs/examples/magicgui_jupyter.py +++ /dev/null @@ -1,45 +0,0 @@ -"""# Jupyter notebooks and magicgui. - -This example shows magicgui widgets embedded in a jupyter notebook. - -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): -# """Enum for 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 AOI.""" -# 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) # noqa: E501 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index 235e11609..de0dbb8f2 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -11,8 +11,8 @@ ![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). """ # %% @@ -112,7 +112,7 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # #### 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 @@ -159,7 +159,7 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image # %% # ### 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. diff --git a/docs/examples/napari_parameter_sweep.py b/docs/examples/napari_parameter_sweep.py deleted file mode 100644 index a214b31b4..000000000 --- a/docs/examples/napari_parameter_sweep.py +++ /dev/null @@ -1,172 +0,0 @@ -"""# 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 -to extend napari with small, composable widgets created with `magicgui`. Here, -we demonstrate how to build a interactive widget that lets you immediately see -the effect of changing one of the parameters of your function. - -For napari-specific magicgui documentation, see the -[napari docs](https://napari.org/guides/magicgui.html) - -![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) -""" - -# %% -# ## outline -# -# **This example demonstrates how to:** -# -# 1. Create a `magicgui` widget that can be used in another -# program (`napari`) -# -# 2. Automatically call our function when a parameter changes -# -# 3. Provide `magicgui` with a custom widget for a specific -# argument -# -# 4. Use the `choices` option to create a dropdown -# -# 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/docs/examples/napari/napari_param_sweep.py).* # noqa: E501 - -# %% -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() - -# %% -# ## 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. - -# %% -# ### the function -# -# Our function is a very thin wrapper around -# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). # noqa: E501 -# It takes a `napari` [Image -# layer](https://napari.org/howtos/layers/image.html), a `sigma` to control -# the blur radius, and a `mode` that determines how edges are handled. - -# %% -# ```python -# def gaussian_blur( -# layer: Image, sigma: float = 1, mode="nearest" -# ) -> Image: -# return filters.gaussian(layer.data, sigma=sigma, mode=mode) -# ``` - -# %% -# The reasons we are wrapping it here are: -# -# 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 -# signature that were not provided by `filters.gaussian` - -# %% -# #### type annotations -# -# As described in the [image arithmetic tutorial](./napari_img_math.md), 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 -# layers for our `layer` parameter, and automatically adding the result of our -# function to the viewer when called. -# -# For documentation on napari types with magicgui, see the -# [napari docs](https://napari.org/guides/magicgui.html) - -# %% -# ### the magic part -# -# Finally, we decorate the function with `@magicgui` and provide some options. - -# %% -# ```python -# @magicgui( -# auto_call=True, -# sigma={"widget_type": "FloatSlider", "max": 6}, -# mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, -# ) -# 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) -# ``` - -# %% -# - `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`: -# -# - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard -# (`float`) widget for the `sigma` widget, but rather to use a slider widget. -# - we then set an upper limit on the slider values for `sigma`. -# -# - finally, we specify valid `choices` for the `mode` argument. This turns that -# parameter into a categorical/dropdown type widget, and sets the options. - -# %% -# ### connecting events -# -# 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. - -# %% -# ```python -# def do_something_with_result(result): -# ... - -# gaussian_blur.called.connect(do_something_with_result) -# ``` From b4781c297f0c9eed4439ec408a6213743151deb6 Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Wed, 26 Jul 2023 18:10:28 +1000 Subject: [PATCH 15/27] Must have napari in docs requirements to build examples in docs --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fd572f377..201614959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ docs = [ "mkdocs-gallery", "qtgallery", # extras for all the widgets + "napari[pyqt]", "pint", "ipywidgets>=8.0.0", "ipykernel", From 32e07d08e9aa40268ed88e809f087586b854f18a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Jul 2023 14:18:55 -0400 Subject: [PATCH 16/27] update deps --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 201614959..e00b47f65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,8 +120,9 @@ docs = [ "mkdocs-gallery", "qtgallery", # extras for all the widgets - "napari[pyqt]", + "napari", "pint", + "matplotlib", "ipywidgets>=8.0.0", "ipykernel", "pyside6==6.4.2", # 6.4.3 gives segfault for some reason From 93955a13588797a454a132f139f12a7a3d81774c Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Jul 2023 15:17:51 -0400 Subject: [PATCH 17/27] pin napari --- .github/workflows/deploy_docs.yml | 2 ++ pyproject.toml | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index a56e0acc1..7ec9c9c09 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -13,6 +13,8 @@ jobs: runs-on: macos-latest # for the screenshots steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - uses: actions/setup-python@v4 with: python-version: '3.x' diff --git a/pyproject.toml b/pyproject.toml index e00b47f65..8260e389b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,12 +120,12 @@ docs = [ "mkdocs-gallery", "qtgallery", # extras for all the widgets - "napari", + "napari ==0.4.18", + "pyside6 ==6.4.2", # 6.4.3 gives segfault for some reason "pint", "matplotlib", - "ipywidgets>=8.0.0", + "ipywidgets >=8.0.0", "ipykernel", - "pyside6==6.4.2", # 6.4.3 gives segfault for some reason ] [project.urls] From 12bbfdb791627bf635bc81107c8174969c473d90 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Jul 2023 15:26:31 -0400 Subject: [PATCH 18/27] update pip --- .github/workflows/deploy_docs.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 7ec9c9c09..2202eba1b 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -10,15 +10,17 @@ on: jobs: docs: - runs-on: macos-latest # for the screenshots + runs-on: macos-latest # for the screenshots steps: - uses: actions/checkout@v3 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' From f4af6d51345e560baaa5106a258dcc5e94f2fedf Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Jul 2023 15:43:15 -0400 Subject: [PATCH 19/27] fix ruff --- docs/examples/applications/pint_quantity.py | 2 +- docs/examples/basic_widgets_demo.py | 6 +++--- magicgui/_version.py | 4 ---- mkdocs.yml | 2 +- pyproject.toml | 5 ++--- 5 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 magicgui/_version.py diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index 3d1653991..f7123f2bf 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -12,7 +12,7 @@ @magicgui -def widget(q=Quantity("1 ms")): # noqa: B008 +def widget(q=Quantity("1 ms")): """Widget allowing users to input quantity measurements.""" print(q) diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index bc23cdbb0..51b0514e4 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -45,9 +45,9 @@ def widget_demo( dropdown=Medium.Glass, radio_option=2, date=datetime.date(1999, 12, 31), - time=datetime.time(1, 30, 20), # noqa: B008 - datetime=datetime.datetime.now(), # noqa: B008 - filename=Path.home(), # noqa: B008 + time=datetime.time(1, 30, 20), + datetime=datetime.datetime.now(), + filename=Path.home(), ): """We can use numpy docstrings to provide tooltips. diff --git a/magicgui/_version.py b/magicgui/_version.py deleted file mode 100644 index b921eea20..000000000 --- a/magicgui/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# file generated by setuptools_scm -# don't change, don't track in version control -__version__ = version = "0.5.2.dev30+ga5e272f.d20220824" -__version_tuple__ = version_tuple = (0, 5, 2, "dev30", "ga5e272f.d20220824") diff --git a/mkdocs.yml b/mkdocs.yml index dc15a32b5..65e662f91 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,7 +53,7 @@ nav: - events.md - decorators.md - dataclasses.md - - 'Examples': generated_examples # This node will automatically be named and have sub-nodes. + - Examples: generated_examples # This node will automatically be named and have sub-nodes. - API: - magicgui: api/magicgui.md - magic_factory: api/magic_factory.md diff --git a/pyproject.toml b/pyproject.toml index 8260e389b..831d1bec8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,7 +201,7 @@ extend-ignore = [ [tool.ruff.per-file-ignores] "tests/*.py" = ["D", "E501"] -"examples/*.py" = ["D", "B"] +"docs/examples/*.py" = ["D", "B"] "src/magicgui/widgets/_image/*.py" = ["D"] "setup.py" = ["F821"] @@ -232,7 +232,6 @@ pretty = true [[tool.mypy.overrides]] module = [ "_pytest.*", - ".examples/", ".docs/", "magicgui.widgets._image.*", "magicgui.backends.*", @@ -261,7 +260,7 @@ omit = [ "src/magicgui/widgets/_image/_mpl_image.py", "src/magicgui/widgets/_bases/*", "tests/*", - "examples/*", + "docs/*", ] From 7f39ccb8126e87308dbdede7ceb026b1ccf4dfdd Mon Sep 17 00:00:00 2001 From: Genevieve Buckley <30920819+GenevieveBuckley@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:53:41 +1000 Subject: [PATCH 20/27] Examples jupytext - put more info in docstring so users see fewer # symbols when looking at plain text --- docs/examples/applications/values_dialog.py | 22 +- docs/examples/napari/napari_img_math.py | 296 ++++++++++-------- .../examples/napari/napari_parameter_sweep.py | 246 ++++++++------- docs/examples/notebooks/magicgui_jupyter.py | 72 +++-- pyproject.toml | 2 + 5 files changed, 353 insertions(+), 285 deletions(-) diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index ad7fd53bd..8c18f82fc 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -4,18 +4,18 @@ This will pause code execution until the user responds. -![Values input dialog](../../images/values_input.png){ width=50% } +# ![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)) +``` """ # %% -# ```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/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index c6aecd8e6..575ae6712 100644 --- a/docs/examples/napari/napari_img_math.py +++ b/docs/examples/napari/napari_img_math.py @@ -20,15 +20,13 @@ 2. Use an `Enum` to create a dropdown menu 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/docs/examples/napari/napari_image_arithmetic.py).* # noqa: E501 +## code +*Code follows, with explanation below... You can also [get this example at +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 napari @@ -39,14 +37,12 @@ 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. - """ - + # 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 @@ -59,10 +55,9 @@ 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 viewer = napari.Viewer() viewer.add_image(numpy.random.rand(20, 20), name="Layer 1") @@ -76,135 +71,178 @@ def image_arithmetic( viewer.layers.events.removed.connect(image_arithmetic.reset_choices) napari.run() +``` -# %% -# ## 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. +## walkthrough -# %% -# ### the function -# Our function takes two `numpy` arrays (in this case, from [Image layers](https://napari.org/howtos/layers/image.html)), # noqa: E501 -# and some mathematical operation -# (we'll restrict the options using an `enum.Enum`). -# When called, ourfunction calls the selected operation on the data. +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. -# %% -# ```python -# def image_arithmetic(array1, operation, array2): -# return operation.value(array1, array2) -# ``` +### the function -# %% -# #### type annotations -# `magicgui` works particularly well with [type annotations](https://docs.python.org/3/library/typing.html), # noqa: E501 -# 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) # noqa: E501 -# 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. +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 -# from napari.types import ImageData - -# def image_arithmetic( -# layerA: ImageData, operation: Operation, layerB: ImageData -# ) -> ImageData: -# return operation.value(layerA, layerB) -# ``` -# %% -# ### the magic part +```python +def image_arithmetic(array1, operation, array2): + return operation.value(array1, array2) +``` -# 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. +#### type annotations -# %% -# ```python hl_lines="1" -# @magicgui(call_button="execute") -# def image_arithmetic(layerA: ImageData, operation: Operation, layerB: ImageData): -# return operation.value(layerA, layerB) -# ``` +`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. -# %% -# That's it! The `image_arithmetic` function is now a -# [FunctionGui][magicgui.widgets.FunctionGui] that can be shown, or incorporated -# into other GUIs (such as the napari GUI shown in this example) +```python +from napari.types import ImageData -# %% -# !!! note While [type hints](https://docs.python.org/3/library/typing.html) -# aren't always required in `magicgui`, they are recommended ... and they -# *are* required for certain things, like the `Operation(Enum)` [used here for -# the dropdown](#create-dropdowns-with-enums) and the `napari.types.ImageData` -# annotations that `napari` has registered with `magicgui`. +def image_arithmetic( + layerA: ImageData, operation: Operation, layerB: ImageData +) -> ImageData: + return operation.value(layerA, layerB) +``` -# %% -# ### create dropdowns with Enums -# We'd like the user to be able to select the operation (`add`, `subtract`, -# `multiply`, `divide`) using a dropdown menu. [`enum.Enum`][] offers a convenient -# way to restrict values to a strict set of options, while providing `name: value` -# pairs for each of the options. Here, the value for each choice is the actual -# function we would like to have called when that option is selected. +### the magic part -# %% -# ```python -# class Operation(enum.Enum): -# add = numpy.add -# subtract = numpy.subtract -# multiply = numpy.multiply -# divide = numpy.divide -# ``` +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. -# %% -# ### add it to napari -# When we decorated the `image_arithmetic` function above, it became a -# [FunctionGui][magicgui.widgets.FunctionGui]. Napari recognizes this type, so we -# can simply add it to the napari viewer as follows: +```python hl_lines="1" +@magicgui(call_button="execute") +def image_arithmetic(layerA: ImageData, operation: Operation, layerB: ImageData): + return operation.value(layerA, layerB) +``` -# %% -# ```python -# viewer.window.add_dock_widget(image_arithmetic) -# ``` +That's it! The `image_arithmetic` function is now a +[FunctionGui][magicgui.widgets.FunctionGui] that can be shown, or incorporated +into other GUIs (such as the napari GUI shown in this example) -# %% -# ### connect event listeners for interactivity -# What fun is a GUI without some interactivity? Let's make stuff happen. -# -# We connect the `image_arithmetic.reset_choices` function to the -# `viewer.layers.events.inserted/removed` event from `napari`, to make sure that -# the dropdown menus stay in sync if a layer gets added or removed from the napari -# window: +!!! note While [type hints](https://docs.python.org/3/library/typing.html) + aren't always required in `magicgui`, they are recommended ... and they + *are* required for certain things, like the `Operation(Enum)` [used here for + the dropdown](#create-dropdowns-with-enums) and the `napari.types.ImageData` + annotations that `napari` has registered with `magicgui`. -# %% -# ```python -# viewer.layers.events.inserted.connect(image_arithmetic.reset_choices) -# viewer.layers.events.removed.connect(image_arithmetic.reset_choices) -# ``` +### create dropdowns with Enums -# %% -# !!!tip -# An additional offering from `magicgui` here is that the decorated -# function also acquires a new attribute "`called`" that can be connected to -# callback functions of your choice. Then, whenever the gui widget *or the -# original function* are called, the result will be passed to your callback -# function: +We'd like the user to be able to select the operation (`add`, `subtract`, +`multiply`, `divide`) using a dropdown menu. [`enum.Enum`][] offers a convenient +way to restrict values to a strict set of options, while providing `name: value` +pairs for each of the options. Here, the value for each choice is the actual +function we would like to have called when that option is selected. -# %% -# ```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 +class Operation(enum.Enum): + add = numpy.add + subtract = numpy.subtract + multiply = numpy.multiply + divide = numpy.divide +``` + +### add it to napari + +When we decorated the `image_arithmetic` function above, it became a +[FunctionGui][magicgui.widgets.FunctionGui]. Napari recognizes this type, so we +can simply add it to the napari viewer as follows: + +```python +viewer.window.add_dock_widget(image_arithmetic) +``` + +### connect event listeners for interactivity + +What fun is a GUI without some interactivity? Let's make stuff happen. + +We connect the `image_arithmetic.reset_choices` function to the +`viewer.layers.events.inserted/removed` event from `napari`, to make sure that +the dropdown menus stay in sync if a layer gets added or removed from the napari +window: + +```python +viewer.layers.events.inserted.connect(image_arithmetic.reset_choices) +viewer.layers.events.removed.connect(image_arithmetic.reset_choices) +``` + +!!!tip + An additional offering from `magicgui` here is that the decorated + function also acquires a new attribute "`called`" that can be connected to + callback functions of your choice. Then, whenever the gui widget *or the + 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)) +``` + +``` +>>> image_arithmetic() +1.0060037881040373 +``` + +## Code + +Here's the full code example again. + +""" # %% -# ``` -# >>> image_arithmetic() -# 1.0060037881040373 -# ``` +from enum import Enum + +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/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index de0dbb8f2..8e735a1fa 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -13,32 +13,29 @@ *See also:* Some of this tutorial overlaps with topics covered in the [napari image arithmetic example](napari_img_math.py). -""" -# %% -# ## outline -# -# **This example demonstrates how to:** -# -# 1. Create a `magicgui` widget that can be used in another -# program (`napari`) -# -# 2. Automatically call our function when a parameter changes -# -# 3. Provide `magicgui` with a custom widget for a specific -# argument -# -# 4. Use the `choices` option to create a dropdown -# -# 5. Connect some event listeners to create interactivity. +## outline -# %% -# ## code -# -# *Code follows, with explanation below... You can also [get this example at -# github](https://github.com/pyapp-kit/magicgui/blob/main/docs/examples/napari/napari_param_sweep.py).* # noqa: E501 +**This example demonstrates how to:** -# %% +1. Create a `magicgui` widget that can be used in another +program (`napari`) + +2. Automatically call our function when a parameter changes + +3. Provide `magicgui` with a custom widget for a specific +argument + +4. Use the `choices` option to create a dropdown + +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/docs/examples/napari/napari_param_sweep.py).* + +```python linenums="1" hl_lines="14-19 31" import napari import skimage.data import skimage.filters @@ -59,11 +56,10 @@ layout="horizontal", ) 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) - # create a viewer and add some images viewer = napari.Viewer() viewer.add_image(skimage.data.astronaut().mean(-1), name="astronaut") @@ -75,98 +71,132 @@ def gaussian_blur(layer: ImageData, sigma: float = 1.0, mode="nearest") -> Image viewer.layers.events.changed.connect(gaussian_blur.reset_choices) napari.run() +``` -# %% -# ## 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. +## walkthrough -# %% -# ### the function -# -# Our function is a very thin wrapper around -# [`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). # noqa: E501 -# It takes a `napari` [Image -# layer](https://napari.org/howtos/layers/image.html), a `sigma` to control -# the blur radius, and a `mode` that determines how edges are handled. +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. -# %% -# ```python -# def gaussian_blur( -# layer: Image, sigma: float = 1, mode="nearest" -# ) -> Image: -# return filters.gaussian(layer.data, sigma=sigma, mode=mode) -# ``` +### the function -# %% -# The reasons we are wrapping it here are: -# -# 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 -# signature that were not provided by `filters.gaussian` +Our function is a very thin wrapper around +[`skimage.filters.gaussian`](https://scikit-image.org/docs/dev/api/skimage.filters.html#skimage.filters.gaussian). +It takes a `napari` [Image +layer](https://napari.org/howtos/layers/image.html), a `sigma` to control +the blur radius, and a `mode` that determines how edges are handled. -# %% -# #### type annotations -# -# 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 -# layers for our `layer` parameter, and automatically adding the result of our -# function to the viewer when called. -# -# For documentation on napari types with magicgui, see the -# [napari docs](https://napari.org/guides/magicgui.html) +```python +def gaussian_blur( + layer: Image, sigma: float = 1, mode="nearest" +) -> Image: + return filters.gaussian(layer.data, sigma=sigma, mode=mode) +``` -# %% -# ### the magic part -# -# Finally, we decorate the function with `@magicgui` and provide some options. +The reasons we are wrapping it here are: -# %% -# ```python -# @magicgui( -# auto_call=True, -# sigma={"widget_type": "FloatSlider", "max": 6}, -# mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, -# ) -# 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) -# ``` +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 + signature that were not provided by `filters.gaussian` -# %% -# - `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`: -# -# - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard -# (`float`) widget for the `sigma` widget, but rather to use a slider widget. -# - we then set an upper limit on the slider values for `sigma`. -# -# - finally, we specify valid `choices` for the `mode` argument. This turns that -# parameter into a categorical/dropdown type widget, and sets the options. +#### type annotations -# %% -# ### connecting events -# -# 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. +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 +layers for our `layer` parameter, and automatically adding the result of our +function to the viewer when called. + +For documentation on napari types with magicgui, see the +[napari docs](https://napari.org/guides/magicgui.html) + +### the magic part + +Finally, we decorate the function with `@magicgui` and provide some options. + +```python +@magicgui( + auto_call=True, + sigma={"widget_type": "FloatSlider", "max": 6}, + mode={"choices": ["reflect", "constant", "nearest", "mirror", "wrap"]}, +) +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) +``` + +- `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`: + + - `"widget_type": "FloatSlider"` tells `magicgui` not to use the standard + (`float`) widget for the `sigma` widget, but rather to use a slider widget. + - we then set an upper limit on the slider values for `sigma`. + +- finally, we specify valid `choices` for the `mode` argument. This turns that + parameter into a categorical/dropdown type widget, and sets the options. + +### connecting events + +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. + +```python +def do_something_with_result(result): + ... + +gaussian_blur.called.connect(do_something_with_result) +``` + +## Code + +Here's the full code example again. +""" # %% -# ```python -# def do_something_with_result(result): -# ... +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) + -# gaussian_blur.called.connect(do_something_with_result) -# ``` +# 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/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index b79a5e74d..d15ce9c92 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -5,43 +5,41 @@ 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): -# """Enum for 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 AOI.""" -# 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 -# ``` +```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) # noqa: E501 +# ![magicgui widget embedded in the jupyter notebook](../../images/jupyter_magicgui_widget.png) diff --git a/pyproject.toml b/pyproject.toml index 831d1bec8..b8b3b40c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -202,6 +202,8 @@ extend-ignore = [ [tool.ruff.per-file-ignores] "tests/*.py" = ["D", "E501"] "docs/examples/*.py" = ["D", "B"] +"docs/examples/napari/*" = ["E501"] +"docs/examples/notebooks/*" = ["E501"] "src/magicgui/widgets/_image/*.py" = ["D"] "setup.py" = ["F821"] From e6d876e46705cc0d701220fac1b070a9420bcf3b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 5 Sep 2023 20:04:58 -0400 Subject: [PATCH 21/27] change deps --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 80635731c..eb130ed7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,8 +112,9 @@ 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]", @@ -121,7 +122,7 @@ docs = [ "qtgallery", # extras for all the widgets "napari ==0.4.18", - "pyside6 ==6.4.2", # 6.4.3 gives segfault for some reason + "pyside6 ==6.5.2", # 6.4.3 gives segfault for some reason "pint", "matplotlib", "ipywidgets >=8.0.0", @@ -164,7 +165,6 @@ matrix.backend.features = [ ] - # https://github.com/charliermarsh/ruff [tool.ruff] line-length = 88 From 2fd984399a66e82c159a2d6af71672a0b74d9167 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 5 Sep 2023 20:14:00 -0400 Subject: [PATCH 22/27] fix example tests --- docs/examples/demo_widgets/image.py | 6 ++++-- tests/test_docs.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index 238f8bae6..b36d0c3ee 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -5,9 +5,11 @@ (This requires pillow, or that magicgui was installed as ``magicgui[image]``) """ +from pathlib import Path + from magicgui.widgets import Image -img = "../../images/_test.jpg" -image = Image(value=img) +img = Path(__file__).parent.parent.parent / "images" / "_test.jpg" +image = Image(value=str(img)) image.scale_widget_to_image_size() image.show(run=True) diff --git a/tests/test_docs.py b/tests/test_docs.py index d5185fcf2..7db549fc8 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -33,7 +33,11 @@ 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" +EXCLUDED = {"napari"} +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") From 5d573675f9aa4fd7ec9d12b02888b1ad1f729aed Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 5 Sep 2023 20:19:26 -0400 Subject: [PATCH 23/27] revert pyside change --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eb130ed7e..e44e8a427 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,7 @@ docs = [ "qtgallery", # extras for all the widgets "napari ==0.4.18", - "pyside6 ==6.5.2", # 6.4.3 gives segfault for some reason + "pyside6 ==6.4.2", # 6.4.3 gives segfault for some reason "pint", "matplotlib", "ipywidgets >=8.0.0", From 939ac1fb5c9ba081f7cdabf41b503b95cd55a8ce Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 6 Sep 2023 07:26:30 -0400 Subject: [PATCH 24/27] update image path --- docs/examples/demo_widgets/image.py | 5 +---- tests/test_docs.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index b36d0c3ee..42a60419a 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -5,11 +5,8 @@ (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.parent / "images" / "_test.jpg" -image = Image(value=str(img)) +image = Image(value="../../images/_test.jpg") image.scale_widget_to_image_size() image.show(run=True) diff --git a/tests/test_docs.py b/tests/test_docs.py index 7db549fc8..d4e448a04 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -34,7 +34,9 @@ def test_doc_code_cells(fname): EXAMPLES = Path(__file__).parent.parent / "docs" / "examples" -EXCLUDED = {"napari"} +# leaving out image only because finding the image file in both +# tests and docs is a pain... +EXCLUDED = {"napari", "image"} example_files = [ str(f) for f in EXAMPLES.rglob("*.py") if all(x not in str(f) for x in EXCLUDED) ] From 2a76160dada01e604a5155e1a529d1a83436db0d Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 6 Sep 2023 07:29:00 -0400 Subject: [PATCH 25/27] remove periods --- docs/examples/applications/callable.py | 2 +- docs/examples/applications/chaining.py | 2 +- docs/examples/applications/hotdog.py | 2 +- docs/examples/applications/pint_quantity.py | 2 +- docs/examples/applications/snells_law.py | 2 +- docs/examples/applications/values_dialog.py | 2 +- docs/examples/basic_example.py | 2 +- docs/examples/basic_widgets_demo.py | 2 +- docs/examples/demo_widgets/change_label.py | 2 +- docs/examples/demo_widgets/choices.py | 2 +- docs/examples/demo_widgets/file_dialog.py | 2 +- docs/examples/demo_widgets/image.py | 2 +- docs/examples/demo_widgets/log_slider.py | 2 +- docs/examples/demo_widgets/login.py | 2 +- docs/examples/demo_widgets/optional.py | 2 +- docs/examples/demo_widgets/range_slider.py | 2 +- docs/examples/demo_widgets/selection.py | 2 +- docs/examples/demo_widgets/table.py | 2 +- docs/examples/matplotlib/mpl_figure.py | 2 +- docs/examples/matplotlib/waveform.py | 2 +- docs/examples/napari/napari_combine_qt.py | 2 +- docs/examples/napari/napari_forward_refs.py | 2 +- docs/examples/napari/napari_img_math.py | 2 +- docs/examples/napari/napari_parameter_sweep.py | 2 +- docs/examples/notebooks/magicgui_jupyter.py | 2 +- docs/examples/progress_bars/progress.py | 2 +- docs/examples/progress_bars/progress_manual.py | 2 +- docs/examples/progress_bars/progress_nested.py | 2 +- docs/examples/under_the_hood/class_method.py | 2 +- docs/examples/under_the_hood/self_reference.py | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py index 9f819fbc7..a5d3c4b9c 100644 --- a/docs/examples/applications/callable.py +++ b/docs/examples/applications/callable.py @@ -1,4 +1,4 @@ -"""# Callable functions demo. +"""# Callable functions demo This example demonstrates handling callable functions with magicgui. """ diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py index da69989fb..0147a0f35 100644 --- a/docs/examples/applications/chaining.py +++ b/docs/examples/applications/chaining.py @@ -1,4 +1,4 @@ -"""# Chaining functions together. +"""# Chaining functions together This example demonstrates chaining multiple functions together. """ diff --git a/docs/examples/applications/hotdog.py b/docs/examples/applications/hotdog.py index f3673f1ea..f5329a9d6 100644 --- a/docs/examples/applications/hotdog.py +++ b/docs/examples/applications/hotdog.py @@ -1,4 +1,4 @@ -"""# Hotdog or not app. +"""# Hotdog or not app Demo app to upload an image and classify if it's an hotdog or not. """ diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py index f7123f2bf..9fa811355 100644 --- a/docs/examples/applications/pint_quantity.py +++ b/docs/examples/applications/pint_quantity.py @@ -1,4 +1,4 @@ -"""# Quantities with pint. +"""# 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. diff --git a/docs/examples/applications/snells_law.py b/docs/examples/applications/snells_law.py index ab3bf6be6..481f7f9b4 100644 --- a/docs/examples/applications/snells_law.py +++ b/docs/examples/applications/snells_law.py @@ -1,4 +1,4 @@ -"""# Snell's law demonstration using magicgui. +"""# Snell's law demonstration using magicgui Demo app for calculating angles of refraction according to Snell's law. """ diff --git a/docs/examples/applications/values_dialog.py b/docs/examples/applications/values_dialog.py index 8c18f82fc..451557005 100644 --- a/docs/examples/applications/values_dialog.py +++ b/docs/examples/applications/values_dialog.py @@ -1,4 +1,4 @@ -"""# Input values dialog. +"""# Input values dialog A basic example of a user input dialog. diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py index ebbec4c8f..6e06da627 100644 --- a/docs/examples/basic_example.py +++ b/docs/examples/basic_example.py @@ -1,4 +1,4 @@ -"""# Basic example. +"""# Basic example A basic example using magicgui. """ diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py index 51b0514e4..b3ae376cf 100644 --- a/docs/examples/basic_widgets_demo.py +++ b/docs/examples/basic_widgets_demo.py @@ -1,4 +1,4 @@ -"""# Basic widgets demo. +"""# Basic widgets demo Widget demonstration with magicgui. diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py index e2439e934..4e3b660dc 100644 --- a/docs/examples/demo_widgets/change_label.py +++ b/docs/examples/demo_widgets/change_label.py @@ -1,4 +1,4 @@ -"""# Custom text labels for widgets. +"""# Custom text labels for widgets An example showing how to create custom text labels for your widgets. """ diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py index d035a97b4..74b3857f3 100644 --- a/docs/examples/demo_widgets/choices.py +++ b/docs/examples/demo_widgets/choices.py @@ -1,4 +1,4 @@ -"""# Dropdown selection widget. +"""# Dropdown selection widget Choices for dropdowns can be provided in a few different ways. """ diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py index 84f06d99a..ff7230ebd 100644 --- a/docs/examples/demo_widgets/file_dialog.py +++ b/docs/examples/demo_widgets/file_dialog.py @@ -1,4 +1,4 @@ -"""# File dialog widget. +"""# File dialog widget A file dialog widget example. """ diff --git a/docs/examples/demo_widgets/image.py b/docs/examples/demo_widgets/image.py index 42a60419a..87cd5c86c 100644 --- a/docs/examples/demo_widgets/image.py +++ b/docs/examples/demo_widgets/image.py @@ -1,4 +1,4 @@ -"""# Image widget. +"""# Image widget Example of creating an Image Widget from a file. diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py index 65b12ed42..bd3385fdf 100644 --- a/docs/examples/demo_widgets/log_slider.py +++ b/docs/examples/demo_widgets/log_slider.py @@ -1,4 +1,4 @@ -"""# Log slider widget. +"""# Log slider widget A logarithmic scale range slider widget. """ diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py index 899a229d3..3cefafb42 100644 --- a/docs/examples/demo_widgets/login.py +++ b/docs/examples/demo_widgets/login.py @@ -1,4 +1,4 @@ -"""# Password login. +"""# Password login A password login field widget. """ diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py index ce136c013..8cae102c9 100644 --- a/docs/examples/demo_widgets/optional.py +++ b/docs/examples/demo_widgets/optional.py @@ -1,4 +1,4 @@ -"""# Optional user choice. +"""# Optional user choice Optional user input using a dropdown selection widget. """ diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py index f5fa0ad22..9d25d72e2 100644 --- a/docs/examples/demo_widgets/range_slider.py +++ b/docs/examples/demo_widgets/range_slider.py @@ -1,4 +1,4 @@ -"""# Range slider widget. +"""# Range slider widget A double ended range slider widget. """ diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py index 362a2361f..8f92052c7 100644 --- a/docs/examples/demo_widgets/selection.py +++ b/docs/examples/demo_widgets/selection.py @@ -1,4 +1,4 @@ -"""# Multiple selection widget. +"""# Multiple selection widget A selection widget allowing multiple selections by the user. """ diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py index 94cbd2cca..4af4b870c 100644 --- a/docs/examples/demo_widgets/table.py +++ b/docs/examples/demo_widgets/table.py @@ -1,4 +1,4 @@ -"""# Table widget. +"""# Table widget Demonstrating a few ways to input tables. """ diff --git a/docs/examples/matplotlib/mpl_figure.py b/docs/examples/matplotlib/mpl_figure.py index b67bcf023..ea29a0af0 100644 --- a/docs/examples/matplotlib/mpl_figure.py +++ b/docs/examples/matplotlib/mpl_figure.py @@ -1,4 +1,4 @@ -"""# matplotlib figure example. +"""# matplotlib figure example Basic example of adding a generic QWidget to a container. diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py index 16a6bf168..146096232 100644 --- a/docs/examples/matplotlib/waveform.py +++ b/docs/examples/matplotlib/waveform.py @@ -1,4 +1,4 @@ -"""# Waveforms example. +"""# Waveforms example Simple waveform generator widget, with plotting. """ diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py index d44201726..9a4351e27 100644 --- a/docs/examples/napari/napari_combine_qt.py +++ b/docs/examples/napari/napari_combine_qt.py @@ -1,4 +1,4 @@ -"""# napari Qt demo. +"""# 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 diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py index afbaa1741..1700c0d3c 100644 --- a/docs/examples/napari/napari_forward_refs.py +++ b/docs/examples/napari/napari_forward_refs.py @@ -1,4 +1,4 @@ -"""# napari forward reference demo. +"""# napari forward reference demo Example of using a ForwardRef to avoid importing a module that provides a widget. diff --git a/docs/examples/napari/napari_img_math.py b/docs/examples/napari/napari_img_math.py index 575ae6712..c14e27cbf 100644 --- a/docs/examples/napari/napari_img_math.py +++ 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 diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index 8e735a1fa..a90b7c28c 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ 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 diff --git a/docs/examples/notebooks/magicgui_jupyter.py b/docs/examples/notebooks/magicgui_jupyter.py index d15ce9c92..a108f232b 100644 --- a/docs/examples/notebooks/magicgui_jupyter.py +++ b/docs/examples/notebooks/magicgui_jupyter.py @@ -1,4 +1,4 @@ -"""# Jupyter notebooks and magicgui. +"""# Jupyter notebooks and magicgui This example shows magicgui widgets embedded in a jupyter notebook. diff --git a/docs/examples/progress_bars/progress.py b/docs/examples/progress_bars/progress.py index 8db6a7a59..6f064b5e5 100644 --- a/docs/examples/progress_bars/progress.py +++ b/docs/examples/progress_bars/progress.py @@ -1,4 +1,4 @@ -"""# Simple progress bar. +"""# Simple progress bar A simple progress bar demo with magicgui. """ diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py index 7659cc399..ccc907862 100644 --- a/docs/examples/progress_bars/progress_manual.py +++ b/docs/examples/progress_bars/progress_manual.py @@ -1,4 +1,4 @@ -"""# Manual progress bar. +"""# Manual progress bar Example of a progress bar being updated manually. diff --git a/docs/examples/progress_bars/progress_nested.py b/docs/examples/progress_bars/progress_nested.py index 4dac4563e..6bb9a9b43 100644 --- a/docs/examples/progress_bars/progress_nested.py +++ b/docs/examples/progress_bars/progress_nested.py @@ -1,4 +1,4 @@ -"""# Nested progress bars. +"""# Nested progress bars Example using nested progress bars in magicgui. diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py index e2ec35928..7ca336755 100644 --- a/docs/examples/under_the_hood/class_method.py +++ b/docs/examples/under_the_hood/class_method.py @@ -1,4 +1,4 @@ -"""# Deocrate class methods with magicgui. +"""# Deocrate class methods with magicgui Demonstrates decorating a class method with magicgui. diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py index 690fff112..58f2a68f6 100644 --- a/docs/examples/under_the_hood/self_reference.py +++ b/docs/examples/under_the_hood/self_reference.py @@ -1,4 +1,4 @@ -"""# Self reference magicgui widgets. +"""# Self reference magicgui widgets Widgets created with magicgui can reference themselves, and use the widget API. """ From 60dda824fa0c43988a30c87a44b384566e1f6e2d Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 6 Sep 2023 07:52:44 -0400 Subject: [PATCH 26/27] fix links --- docs/examples/napari/napari_parameter_sweep.py | 2 +- docs/scripts/_hooks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/napari/napari_parameter_sweep.py b/docs/examples/napari/napari_parameter_sweep.py index a90b7c28c..acca30f39 100644 --- a/docs/examples/napari/napari_parameter_sweep.py +++ b/docs/examples/napari/napari_parameter_sweep.py @@ -99,7 +99,7 @@ def gaussian_blur( 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 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 From 2caf03d87472b21f27226fe3c4f19df2ec93e34f Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 6 Sep 2023 08:22:17 -0400 Subject: [PATCH 27/27] test: skip rangeslider --- tests/test_docs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_docs.py b/tests/test_docs.py index d4e448a04..d80e45c8f 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -36,7 +36,8 @@ def test_doc_code_cells(fname): EXAMPLES = Path(__file__).parent.parent / "docs" / "examples" # leaving out image only because finding the image file in both # tests and docs is a pain... -EXCLUDED = {"napari", "image"} +# 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) ]