From 5ac03cda446f6cac89642733a512143c3027dac9 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 21 Apr 2026 16:10:47 -0500 Subject: [PATCH 1/3] add message routing info to contributing guide --- docs/developers/contributing/index.md | 59 ++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/docs/developers/contributing/index.md b/docs/developers/contributing/index.md index da59ea962..2a2a94650 100644 --- a/docs/developers/contributing/index.md +++ b/docs/developers/contributing/index.md @@ -145,7 +145,7 @@ the corresponding docstring to contain the `.. versionadded::` or Please also consider documenting any major features/changes in our [tutorials](tutorials) and other [usage documentation](usage). -#### Deprecation Warnings +#### napari Deprecations When deprecating a feature, use `DeprecationWarning` instead of `FutureWarning`. `FutureWarning` is silenced by Python's default warning filters, making it invisible @@ -162,6 +162,63 @@ In the docstring below the short summary, use the [`.. deprecated::` directive]( :func:`old_function` is deprecated. Use :func:`new_function` instead. ``` +(message-routing)= +#### Notifications, warnings, and logging + +For an overview and visual demonstration of the different message routes in napari +check out the {ref}`sphx_glr_gallery_message_routing.py` example. + +- Let real exceptions propagate by default. This is the normal path for bugs, + invalid state, and operations that should fail. In a running napari session, + uncaught exceptions are surfaced with traceback UI. +- Use `show_info()`, `show_warning()`, or `show_error()` for explicit + user-facing messages after your code has already handled the condition. +- Use `warnings.warn()` for Python warning semantics, especially deprecations + and library-style warnings that should still exist outside the GUI. +- Use logging for developer diagnostics and post-hoc debugging, not as the main + user-facing channel. + +Catch an exception only when you need to recover, return a fallback value, or +replace a misleading higher-level failure with a more accurate one. If you do +catch a real exception and still want napari's traceback popup, forward the +original exception with `notification_manager.receive_error(...)` rather than +flattening it to `show_error(str(exc))`. + +Two practical cautions: + +- `warnings.warn()` is not a guaranteed repeatable GUI message channel. Python + warning filters apply first, and napari also deduplicates repeated warnings + while its warning hook is installed. +- Messages emitted before the main window is visible should not rely on GUI + notification display. + +Rule of thumb: + +- Let errors propagate unless you have a concrete recovery or translation step. +- Use napari notification helpers for explicit viewer messages. +- Use Python warnings for deprecations and library-style warnings. +- Use logging for diagnostics. + +For `warnings.warn()`, set `stacklevel` so the warning points at the code that +should change: + +- Use `stacklevel=2` when a public napari function or method warns directly. +- Increase it to `3` or more when the warning is emitted from a helper, + wrapper, descriptor, or decorator and you want the warning to land on the + external caller rather than inside napari. +- Check the rendered warning location in a real call site rather than assuming + `2` is always correct. + +Default visibility also matters: + +- `DeprecationWarning` is hidden by Python's default CLI warning filters unless + the user enables it, use `FutureWarning` if you want the warning to be displayed to users. +- `UserWarning` is shown by default in the CLI. +- In a running napari session, warnings may also be mirrored into the viewer + notification system while napari's warning hook is installed, but they are + still subject to Python warning filters and napari deduplicates repeated + identical warnings. + ### Tests We use unit tests, integration tests, and functional tests to ensure that From 6b954805fd1808b1970f1002bd07ab9c149a0c04 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 21 Apr 2026 16:12:00 -0500 Subject: [PATCH 2/3] update debug_plugins with more details on message routing --- .../building_a_plugin/debug_plugins.md | 192 +++++++++++++----- 1 file changed, 139 insertions(+), 53 deletions(-) diff --git a/docs/plugins/building_a_plugin/debug_plugins.md b/docs/plugins/building_a_plugin/debug_plugins.md index 089371c95..e0d4d30dc 100644 --- a/docs/plugins/building_a_plugin/debug_plugins.md +++ b/docs/plugins/building_a_plugin/debug_plugins.md @@ -62,11 +62,17 @@ For example if `pip list` does show the package is installed, and `npe2 validate ## Seeing tracebacks from plugin errors -By default, napari will output any traceback information from plugin related errors to the console or jupyter notebook that napari was launched from. +By default, uncaught exceptions that reach napari are printed to the console or Jupyter notebook that napari was launched from. Additionally, a popup will show in the bottom right corner of the napari viewer with a `View Traceback` button. -Inside of this popup, the full traceback can be seen, along with the option to drop into the debugger from here. +Inside that popup, the full traceback can be seen, along with the option to drop into the debugger from there. Dropping into the debugger will open the built in [python debugger](https://docs.python.org/3/library/pdb.html) at the point of failure. +If you catch an exception yourself, napari will only show that same traceback +experience if you forward the original exception object with +`notification_manager.receive_error(...)`. +If you instead convert the exception to `show_error(str(exc))` or +`warnings.warn(str(exc))`, the traceback UI is intentionally lost. + You can also configure napari not to catch error messages, or force napari to exit on error via the following environment variables, respectively: ```sh @@ -223,76 +229,153 @@ Then, for `python test_print.py` you can use any of your usual debugging tools - ## Logging and user messages in napari -### Set up plugin user messages and notifications - There are, generally speaking, three main methods for notifying users of problems in napari. 1. Raise an exception to indicate a breaking problem in the code (e.g. unexpected user input `raise ValueError("some error")`). 1. Indicate that something was handled, but may not be the behaviour the user was expecting using `warnings.warn("some warning")`. 1. Show an information popup in the napari GUI by using the `napari.utils.notifications.show_info("message")` command. -### Set up plugin log messages +Check out the {ref}`sphx_glr_gallery_message_routing.py` example for a visual +demonstration of the different routes in napari and how they interact with each other. +See also [](message-routing) for a discussion of when to use each method and how to use them effectively in your plugin code. -In addition to these user focused methods, you can set up plugin debug logs and messages during development. You can either use {mod}`napari specific functions `, or [built in Python logging](https://docs.python.org/3/library/logging.html). +### When to let an exception propagate -```{tip} -A logging library, like [loguru](https://github.com/Delgan/loguru), can be easier to get started with than the built in Python logging library. +Let an exception propagate when the current operation should fail and the caller +or napari itself should decide how to present that failure. +This is the default and usually the right choice for genuine bugs, invalid +internal state, and actions that should abort. + +### When to catch an exception + +Catch an exception when one of the following is true: + +1. You can recover and continue. +1. You want to replace a misleading higher-level error with a more accurate + user-facing one. +1. You need to downgrade the problem to a warning or informational message. + +If you catch a real exception but still want napari's traceback popup, forward +it with `notification_manager.receive_error(type(exc), exc, exc.__traceback__)`. +If you catch it and call `show_error(str(exc))`, you are intentionally choosing +to discard the traceback UI. + +### What to use for messages + +- Use `show_warning()` or related napari helpers for explicit GUI-visible + messages after your code has already handled the situation. +- Use `warnings.warn()` for deprecations and other Python warning semantics + that should also exist in tests, scripts, and headless usage. +- Use logging for developer diagnostics and **Help > Show logs**. + +Repeated identical Python warnings are not a good GUI message channel. +Python warning filters apply first, and napari also deduplicates repeated +warnings while its hook is installed. + +### Stacklevel and visibility for warnings + +When you call `warnings.warn()`, set `stacklevel` so the warning points to the +code that should change. + +- In napari internals, `stacklevel=2` is usually right for a direct public API + warning, but wrappers and helper layers often need `3` or more. +- In plugins, `stacklevel=2` is usually right when your own public helper or + API warns directly. +- If the warning is emitted from a plugin callback, decorator, or helper, + increase the stacklevel until the warning points at the plugin code or user + call site you actually want people to inspect. + +Default visibility is different across channels: + +- `DeprecationWarning` is hidden by Python's default CLI warning filters. +- `UserWarning` is shown by default in the CLI. +- In napari, warnings may also appear as viewer notifications while napari's + warning hook is installed, but that is not guaranteed before the window is + visible and repeated identical warnings are deduplicated. +- If you need a guaranteed viewer-side message, use `show_warning()` rather + than relying on `warnings.warn()`. + +### Recommended plugin patterns + +Use Python warnings for deprecations and API-style warnings: + +```python +import warnings + + +def old_function(): + warnings.warn( + "old_function() is deprecated; use new_function() instead", + DeprecationWarning, + stacklevel=2, + ) ``` -Below is an example of establishing debug messages and logs in your code and viewing them in napari by setting the preferences for GUI notifications and console notifications to be at the debug level. We modify the example function from before to have a debug log message: +Use napari notifications for explicit GUI-visible user messages: -```Python +```python +from napari.utils.notifications import show_warning + + +def export_with_defaults(path): + show_warning("No output path was chosen; using the default export location.") +``` + +Use logging for developer diagnostics and post-hoc debugging: + +```python import logging -import sys -from napari.utils.notifications import ( - notification_manager, - Notification, - NotificationSeverity, - show_console_notification, -) -my_plugin_logger = logging.getLogger("napari_simple_reload") -stdout_handler = logging.StreamHandler(sys.stderr) -stdout_handler.setFormatter( - logging.Formatter( - fmt="%(levelname)s: %(asctime)s %(message)s", - datefmt="%d/%m/%Y %I:%M:%S %p" - ) -) -my_plugin_logger.addHandler(stdout_handler) -my_plugin_logger.setLevel(logging.WARNING) - -def show_debug(message: str): - """ - Show a debug message in the notification manager. - """ - notification_ = Notification( - message, severity=NotificationSeverity.DEBUG) - # Show message in the console only - show_console_notification(notification_) - # Show message in console and the napari GUI - notification_manager.dispatch(notification_) - # Control level of shown messages via napari preferences -def example(input_string: str) -> str: - output_string = ( - f"You entered {input_string}!" - if input_string - else "Please enter something in the text box." - ) - show_debug(f"The input string was (napari): {input_string}") - my_plugin_logger.debug( - f"The input string was (logging): {input_string}") - print(output_string) - return output_string +logger = logging.getLogger(__name__) + + +def recompute_preview(shape): + logger.debug("Recomputing preview for shape %s", shape) +``` + +Use `receive_error(...)` only when you intentionally catch an exception but +still want napari's traceback UI: + +```python +from napari.utils.notifications import notification_manager + + +def run_plugin_action(): + try: + raise ValueError("Bad user input") + except ValueError as exc: + notification_manager.receive_error( + type(exc), exc, exc.__traceback__ + ) + return None ``` ### Viewing plugin log messages -Launch the viewer with the napari notification levels set to debug and your plugin logger level set to debug: +The simplest path is to use standard Python logging: + +```python +import logging + + +logger = logging.getLogger(__name__) +logger.debug("Widget state changed") +logger.warning("Falling back to a slower code path") +``` + +Then run napari from a terminal and open **Help > Show logs**. +The terminal and the log dock are both useful, but they are not the same +channel as napari's notification popup. + +```{tip} +A logging library, like [loguru](https://github.com/Delgan/loguru), can be easier to get started with than the built in Python logging library. +``` + +If you want explicit napari notifications to be echoed to the terminal while +you are debugging, lower the console notification threshold: ```Python -# example_notication.py import logging from napari.settings import get_settings from napari import run, Viewer @@ -300,6 +383,7 @@ from napari import run, Viewer settings = get_settings() settings.application.console_notification_level = "debug" settings.application.gui_notification_level = "debug" + viewer = Viewer() viewer.window.add_plugin_dock_widget( "napari-simple-reload", "Autogenerated" @@ -307,7 +391,6 @@ viewer.window.add_plugin_dock_widget( logging.getLogger("napari_simple_reload").setLevel(logging.DEBUG) run() ``` - Running this script with `python example_notification.py` and entering fast into the input text box and clicking run you should then see: ```text @@ -318,6 +401,9 @@ DEBUG: 20/09/2022 05:59:23 PM The input string was (logging): fast 'You entered fast!' ``` +With those settings, explicit napari notifications such as `show_info()` and +`show_warning()` will also be echoed to the terminal. + The full code changes and new files after applying the changes to the plugin in each step of the examples are [here](https://github.com/seankmartin/napari-plugin-debug/tree/full_code/napari-simple-reload). ## Debugging segfaults/memory violation errors From 0da4c1f27c0195f6b7e33098b3f8204f41e2dcb7 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 21 Apr 2026 21:22:02 -0500 Subject: [PATCH 3/3] create one shared message_routing guide --- docs/_toc.yml | 1 + docs/developers/contributing/index.md | 66 ++-------- docs/guides/index.md | 3 + docs/guides/message_routing.md | 116 +++++++++++++++++ .../building_a_plugin/debug_plugins.md | 123 ++---------------- 5 files changed, 141 insertions(+), 168 deletions(-) create mode 100644 docs/guides/message_routing.md diff --git a/docs/_toc.yml b/docs/_toc.yml index af1d6061f..ed6d1a714 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -55,6 +55,7 @@ subtrees: - file: guides/3D_interactivity - file: guides/axis-names - file: guides/handedness + - file: guides/message_routing - file: guides/triangulation - file: guides/rendering - file: guides/performance diff --git a/docs/developers/contributing/index.md b/docs/developers/contributing/index.md index 2a2a94650..2a1043d5a 100644 --- a/docs/developers/contributing/index.md +++ b/docs/developers/contributing/index.md @@ -165,59 +165,19 @@ In the docstring below the short summary, use the [`.. deprecated::` directive]( (message-routing)= #### Notifications, warnings, and logging -For an overview and visual demonstration of the different message routes in napari -check out the {ref}`sphx_glr_gallery_message_routing.py` example. - -- Let real exceptions propagate by default. This is the normal path for bugs, - invalid state, and operations that should fail. In a running napari session, - uncaught exceptions are surfaced with traceback UI. -- Use `show_info()`, `show_warning()`, or `show_error()` for explicit - user-facing messages after your code has already handled the condition. -- Use `warnings.warn()` for Python warning semantics, especially deprecations - and library-style warnings that should still exist outside the GUI. -- Use logging for developer diagnostics and post-hoc debugging, not as the main - user-facing channel. - -Catch an exception only when you need to recover, return a fallback value, or -replace a misleading higher-level failure with a more accurate one. If you do -catch a real exception and still want napari's traceback popup, forward the -original exception with `notification_manager.receive_error(...)` rather than -flattening it to `show_error(str(exc))`. - -Two practical cautions: - -- `warnings.warn()` is not a guaranteed repeatable GUI message channel. Python - warning filters apply first, and napari also deduplicates repeated warnings - while its warning hook is installed. -- Messages emitted before the main window is visible should not rely on GUI - notification display. - -Rule of thumb: - -- Let errors propagate unless you have a concrete recovery or translation step. -- Use napari notification helpers for explicit viewer messages. -- Use Python warnings for deprecations and library-style warnings. -- Use logging for diagnostics. - -For `warnings.warn()`, set `stacklevel` so the warning points at the code that -should change: - -- Use `stacklevel=2` when a public napari function or method warns directly. -- Increase it to `3` or more when the warning is emitted from a helper, - wrapper, descriptor, or decorator and you want the warning to land on the - external caller rather than inside napari. -- Check the rendered warning location in a real call site rather than assuming - `2` is always correct. - -Default visibility also matters: - -- `DeprecationWarning` is hidden by Python's default CLI warning filters unless - the user enables it, use `FutureWarning` if you want the warning to be displayed to users. -- `UserWarning` is shown by default in the CLI. -- In a running napari session, warnings may also be mirrored into the viewer - notification system while napari's warning hook is installed, but they are - still subject to Python warning filters and napari deduplicates repeated - identical warnings. +See [](napari-message-routing) for the canonical guidance on choosing between +exceptions, napari notifications, Python warnings, and logging. + +For `napari` contributions in particular: + +- Let real exceptions propagate as with normal Python, napari natively handles + them in the GUI, unless you have a concrete recovery or + translation step. +- If you want to raise information for the user without tracebacks, + use `show_info()` or `show_warning()` rather than `warnings.warn()`. +- If you catch a real exception but still want napari's traceback popup, + forward it with `notification_manager.receive_error(...)` instead of + flattening it to `show_error(str(exc))`. ### Tests diff --git a/docs/guides/index.md b/docs/guides/index.md index 973c67de7..287e7113c 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -15,6 +15,9 @@ computations, and would like to avoid having the viewer become unresponsive while you wait for a computation to finish, you may benefit from reading about {ref}`multithreading-in-napari`. +If you are deciding whether a condition should raise, warn, notify, or log, +see {ref}`napari-message-routing`. + If you are interested in using napari to explore 3D objects, see {ref}`3d-interactivity`. To understand how napari produces a 2- or 3-dimensional render in the canvas diff --git a/docs/guides/message_routing.md b/docs/guides/message_routing.md new file mode 100644 index 000000000..2d10909d8 --- /dev/null +++ b/docs/guides/message_routing.md @@ -0,0 +1,116 @@ +(napari-message-routing)= + +# Notifications, warnings, and logging + +napari exposes several ways to surface problems and status to users and +developers. The right route depends on whether the current operation should +fail, whether the condition has already been handled, and whether the message +needs to exist outside the GUI. + +## Choose the route + +- Let real exceptions propagate by default. This is the normal path for bugs, + invalid state, and operations that should fail. +- Use `show_info()`, `show_warning()`, or `show_error()` for explicit + user-facing messages after your code has already handled the condition. +- Use `warnings.warn()` for Python warning semantics, especially deprecations + and library-style warnings that should still exist in tests, scripts, and + headless usage. +- Use logging for developer diagnostics and post-hoc debugging, not as the + main user-facing channel. + +In a running napari session, uncaught exceptions are surfaced with traceback +UI. If you catch an exception and convert it to `show_error(str(exc))` or +`warnings.warn(str(exc))`, you are intentionally choosing a different route and +losing that traceback experience. + +## Catching exceptions without losing the traceback UI + +Catch an exception only when you can recover, return a fallback value, or +replace a misleading higher-level failure with a more accurate one. + +If you catch a real exception but still want napari's traceback popup, forward +the original exception with `notification_manager.receive_error(...)`: + +```python +from napari.utils.notifications import notification_manager + + +def run_plugin_action(): + try: + raise ValueError("Bad user input") + except ValueError as exc: + notification_manager.receive_error( + type(exc), exc, exc.__traceback__ + ) + return None +``` + +## Warnings are not a guaranteed GUI message channel + +Repeated identical Python warnings are not a reliable way to communicate with +the viewer UI. Python warning filters apply first, and while napari's warning +hook is installed, repeated warnings from the same call site are deduplicated. + +Default visibility also matters: + +- `DeprecationWarning` is hidden by Python's default CLI warning filters. +- `UserWarning` is shown by default in the CLI. +- In napari, warnings may also appear as viewer notifications while the warning + hook is installed, but that is not guaranteed before the window is visible. +- If you need a guaranteed viewer-side message, use `show_warning()` rather + than relying on `warnings.warn()`. + +## Set `stacklevel` deliberately + +When you call `warnings.warn()`, set `stacklevel` so the warning points at the +code that should change. + +- `stacklevel=2` is usually right when a public function or method warns + directly. +- Increase it to `3` or more when the warning is emitted from a helper, + wrapper, descriptor, or decorator and you want the warning to land on the + external caller. +- Check the rendered warning location in a real call site rather than assuming + `2` is always correct. + +## Practical patterns + +Use Python warnings for deprecations and other API-style warnings: + +```python +import warnings + + +def old_function(): + warnings.warn( + "old_function() is deprecated; use new_function() instead", + DeprecationWarning, + stacklevel=2, + ) +``` + +Use napari notifications for explicit GUI-visible user messages: + +```python +from napari.utils.notifications import show_warning + + +def export_with_defaults(path): + show_warning("No output path was chosen; using the default export location.") +``` + +Use logging for developer diagnostics and log review: + +```python +import logging + + +logger = logging.getLogger(__name__) + + +def recompute_preview(shape): + logger.debug("Recomputing preview for shape %s", shape) +``` + +You can inspect logs in a running viewer via **Help > Show logs**. \ No newline at end of file diff --git a/docs/plugins/building_a_plugin/debug_plugins.md b/docs/plugins/building_a_plugin/debug_plugins.md index e0d4d30dc..b2ccb4dc4 100644 --- a/docs/plugins/building_a_plugin/debug_plugins.md +++ b/docs/plugins/building_a_plugin/debug_plugins.md @@ -235,121 +235,14 @@ There are, generally speaking, three main methods for notifying users of problem 1. Indicate that something was handled, but may not be the behaviour the user was expecting using `warnings.warn("some warning")`. 1. Show an information popup in the napari GUI by using the `napari.utils.notifications.show_info("message")` command. -Check out the {ref}`sphx_glr_gallery_message_routing.py` example for a visual -demonstration of the different routes in napari and how they interact with each other. -See also [](message-routing) for a discussion of when to use each method and how to use them effectively in your plugin code. - -### When to let an exception propagate - -Let an exception propagate when the current operation should fail and the caller -or napari itself should decide how to present that failure. -This is the default and usually the right choice for genuine bugs, invalid -internal state, and actions that should abort. - -### When to catch an exception - -Catch an exception when one of the following is true: - -1. You can recover and continue. -1. You want to replace a misleading higher-level error with a more accurate - user-facing one. -1. You need to downgrade the problem to a warning or informational message. - -If you catch a real exception but still want napari's traceback popup, forward -it with `notification_manager.receive_error(type(exc), exc, exc.__traceback__)`. -If you catch it and call `show_error(str(exc))`, you are intentionally choosing -to discard the traceback UI. - -### What to use for messages - -- Use `show_warning()` or related napari helpers for explicit GUI-visible - messages after your code has already handled the situation. -- Use `warnings.warn()` for deprecations and other Python warning semantics - that should also exist in tests, scripts, and headless usage. -- Use logging for developer diagnostics and **Help > Show logs**. - -Repeated identical Python warnings are not a good GUI message channel. -Python warning filters apply first, and napari also deduplicates repeated -warnings while its hook is installed. - -### Stacklevel and visibility for warnings - -When you call `warnings.warn()`, set `stacklevel` so the warning points to the -code that should change. - -- In napari internals, `stacklevel=2` is usually right for a direct public API - warning, but wrappers and helper layers often need `3` or more. -- In plugins, `stacklevel=2` is usually right when your own public helper or - API warns directly. -- If the warning is emitted from a plugin callback, decorator, or helper, - increase the stacklevel until the warning points at the plugin code or user - call site you actually want people to inspect. - -Default visibility is different across channels: - -- `DeprecationWarning` is hidden by Python's default CLI warning filters. -- `UserWarning` is shown by default in the CLI. -- In napari, warnings may also appear as viewer notifications while napari's - warning hook is installed, but that is not guaranteed before the window is - visible and repeated identical warnings are deduplicated. -- If you need a guaranteed viewer-side message, use `show_warning()` rather - than relying on `warnings.warn()`. - -### Recommended plugin patterns - -Use Python warnings for deprecations and API-style warnings: - -```python -import warnings - - -def old_function(): - warnings.warn( - "old_function() is deprecated; use new_function() instead", - DeprecationWarning, - stacklevel=2, - ) -``` - -Use napari notifications for explicit GUI-visible user messages: - -```python -from napari.utils.notifications import show_warning - - -def export_with_defaults(path): - show_warning("No output path was chosen; using the default export location.") -``` - -Use logging for developer diagnostics and post-hoc debugging: - -```python -import logging - - -logger = logging.getLogger(__name__) - - -def recompute_preview(shape): - logger.debug("Recomputing preview for shape %s", shape) -``` - -Use `receive_error(...)` only when you intentionally catch an exception but -still want napari's traceback UI: - -```python -from napari.utils.notifications import notification_manager - - -def run_plugin_action(): - try: - raise ValueError("Bad user input") - except ValueError as exc: - notification_manager.receive_error( - type(exc), exc, exc.__traceback__ - ) - return None -``` +See [](napari-message-routing) for the canonical guidance on when to use each +route, how warning filtering behaves, and how to preserve napari's traceback +UI when you intentionally catch an exception. + +For plugins specifically, the main debugging rule is: use standard Python +logging for diagnostics, use napari notification helpers for handled +viewer-facing messages, and only call `receive_error(...)` when you are +catching an exception on purpose but still want napari to show the traceback. ### Viewing plugin log messages