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 da59ea962..2a1043d5a 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,23 @@ 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 + +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 We use unit tests, integration tests, and functional tests to ensure that 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 089371c95..b2ccb4dc4 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,46 @@ 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 +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. -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). +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. -```{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. -``` +### Viewing plugin log messages -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: +The simplest path is to use standard Python logging: -```Python +```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__) +logger.debug("Widget state changed") +logger.warning("Falling back to a slower code path") ``` -### Viewing plugin log messages +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. -Launch the viewer with the napari notification levels set to debug and your plugin logger level set to debug: +```{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 +276,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 +284,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 +294,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