Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion docs/developers/contributing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions docs/guides/message_routing.md
Original file line number Diff line number Diff line change
@@ -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**.
89 changes: 34 additions & 55 deletions docs/plugins/building_a_plugin/debug_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -223,91 +229,61 @@ 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 <napari.utils.notifications>`, 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

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"
)
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
Expand All @@ -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
Expand Down
Loading