There are a number of good and bad practices that may not be immediately obvious when developing a plugin. This page covers some known practices that could affect the ability to install or use your plugin effectively.
PyQt5 in your plugin’s dependencies.¶
This is important!
Napari supports both PyQt and PySide backends for Qt. It is up to the
end-user to choose which one they want. If they installed napari with
pip install napari[all], then the
[all] extra will (currently) install
for them from pypi. If they installed via
conda install napari, then they’ll
PyQt5, but via anaconda cloud instead of pypi. Lastly, they may have
installed napari with PySide2.
Here’s what can go wrong if you also declare one of these backends in the
install_requires section of your plugin metadata:
If they installed via
conda install napariand then they install your plugin via
pip(or via the builtin plugin installer, which currently uses
pip), then there will be a binary incompatibility between their conda
pyqtinstallation, and the new pip “
PyQt5” installation. This will very likely lead to a broken environment, forcing the user to re-create their entire environment and re-install napari. This is an unfortunate consequence of package naming decisions, and it’s not something napari can fix.
Alternatively, they may end up with both PyQt and PySide in their environment, and while that’s not always guaranteed to break things, it can lead to unexpected and difficult to debug problems.
Don’t import from PyQt5 or PySide2 in your plugin: use
If you use
from PyQt5 import QtCore(or similar) in your plugin, but the end-user has chosen to use
PySide2for their Qt backend — or vice versa — then your plugin will fail to import. Instead use
from qtpy import QtCore.
qtpyis a Qt compatibility layer that will import from whatever backend is installed in the environment.
Try not to depend on packages that require C compilation if these packages do not offer wheels¶
This requires some awareness of how your dependencies are built and distributed…
Some python packages write a portion of their code in lower level languages like
C or C++ and compile that code into “C Extensions” that can be called by python
at runtime. This can greatly improve performance, but it means that the
package must be compiled for each platform (i.e. Windows, Mac, Linux) that the
package wants to support. Some packages do this compilation step ahead of time,
by distributing “wheels” on
PyPI… or by providing pre-compiled packages via
Other packages simply distribute the source code (as an “sdist”) and expect the
end-user to compile it on their own computer. Compiling C code requires
software that is not always installed on every computer. (If you’ve ever tried
python -m pip install a package and had it fail with a big wall of red text saying
gcc, then you’ve run into a package that doesn’t distribute
wheels, and you didn’t have the software required to compile it).
As a plugin developer, if you depend on a package that uses C extensions but doesn’t distribute a pre-compiled wheel, then it’s very likely that your users will run into difficulties installing your plugin:
What is a “wheel”?
Briefly, a wheel is a built distribution, containing code that is pre-compiled for a specific operating system.
For more detail, see What Are Python Wheels and Why Should You Care?
How do I know if my dependency offers a wheel
There are many ways, but a sure-fire way to know is to go to the respective package on PyPI, and click on the “Download Files” link. If the package offers wheels, you’ll see one or more files ending in
.whl. For example, napari offers a wheel. If a package doesn’t offer a wheel, it may still be ok if it’s just a pure python package that doesn’t have any C extensions…
How do I know if one of my dependencies uses C Extensions?
What about conda?
conda also distributes & installs pre-compiled packages, though they aren’t wheels. While this is definitely a fine way to install binary dependencies in a reliable way, the built-in napari plugin installer doesn’t currently work with conda. If your dependency is only available on conda, but does not offer wheels,you may guide your users in using conda to install your package or one of your dependencies. Just know that it may not work with the built-in plugin installer.
Don’t import heavy dependencies at the top of your module¶
This point will be less relevant when we move to the second generation manifest-based plugin declaration, but it’s still a good idea to delay importing your plugin-specific dependencies and modules until after your hookspec has been called. This helps napari stay quick and responsive at startup.
Consider the following example plugin:
[options.entry_points] napari.plugin = plugin-name = mypackage.napari_plugin
In this example,
my_heavy_dependency_like_tensorflow will be imported
immediately when napari is launched, and we search the entry_point
mypackage.napari_plugin for decorated hook specifications.
# mypackage/napari_plugin.py from napari_plugin_engine import napari_hook_specification from qtpy.QtWidgets import QWidget from my_heavy_dependency_like_tensorflow import something_amazing class MyWidget(QWidget): def do_something_amazing(self): return something_amazing() @napari_hook_specification def napari_experimental_provide_dock_widget(): return MyWidget
This can deterioate the end-user experience, and make napari feel slugish. Best practice is to delay heavy imports until right before they are used. The following slight modification will help napari load much faster:
# mypackage/napari_plugin.py from napari_plugin_engine import napari_hook_specification from qtpy.QtWidgets import QWidget class MyWidget(QWidget): def do_something_amazing(self): # import has been moved here, will happen only after the user # has opened and used this widget. from my_heavy_dependency_like_tensorflow import something_amazing return something_amazing()
(again, the second gen napari plugin engine will help improve this situation, but it’s still a good idea!)
Don’t leave resources open¶
It’s always good practice to clean up resources like open file handles and databases. As a napari plugin it’s particularly important to do this (and especially for Windows users). If someone tries to use the built-in plugin manager to uninstall your plugin, open file handles and resources may cause the process to fail or even leave your plugin in an “installed-but-unuseable” state.
Don’t do this:
# my_plugin/module.py import json data_file = open("some_data_in_my_plugin.json") data = json.load(data_file)
Instead, make sure to close your resource after grabbing the data (ideally by using a context manager, but manually otherwise):
with open("some_data_in_my_plugin.json") as data_file: data = json.load(data_file)
Write extensive tests for your plugin!¶
Programmer and author Bruce Eckel famously wrote:
“If it’s not tested, it’s broken”
It’s true. High test coverage is one way to show your users that you are dedicated to the stability of your plugin. Aim for 100%!
Of course, simply having 100% coverage doesn’t mean your code is bug-free, so make sure that you test all of the various ways that your code might be called.
See our Tips for testing napari plugins.
How to check test coverage?¶
python -m pip install pytest-cov
Run your tests with
pytest --cov=<your_package> --cov-report=html
Open the resulting report in your browser:
The report will show line-by-line what is being tested, and what is being missed. Continue writing tests until everything is covered! If you have lines that you know never need to be tested (like debugging code) you can exempt specific lines from coverage with the comment
# pragma: no cover
In the cookiecutter, coverage tests from github actions will be uploaded to codecov.io
Set style for additional windows in your plugin¶
In napari plugins we strongly advise additional widgets be docked in the main napari viewer,
but sometimes a separate window is required.
The best practice is to use
based windows with parent set to widget
already docked in the viewer.
from qtpy.QtWidgets import QDialog, QWidget, QSpinBox, QPushButton, QGridLayout, QLabel class MyInputDialog(QDialog): def __init__(self, parent: QWidget): super().__init__(parent) self.setWindowTitle("My Input Dialog") self.number = QSpinBox() self.ok_btn = QPushButton("OK") self.cancel_btn = QPushButton("Cancel") layout = QGridLayout() layout.addWidget(QLabel("Number:"), 0, 0) layout.addWidget(self.number, 0, 1) layout.addWidget(self.ok_btn, 1, 0) layout.addWidget(self.cancel_btn, 1, 1) self.setLayout(layout) self.ok_btn.clicked.connect(self.accept) self.cancel_btn.clicked.connect(self.reject) class MyWidget(QWidget): def __init__(self, viewer: "napari.Viewer"): super().__init__() self.viewer = viewer self.open_dialog = QPushButton("Open dialog") self.open_dialog.clicked.connect(self.open_dialog_clicked) def open_dialog_clicked(self): # setting parent to self allows the dialog to inherit its # style from the viewer by pass self as argument dialog = MyInputDialog(self) dialog.exec_() if dialog.result() == QDialog.Accepted: print(dialog.number.value())
If there is a particular reason that you need to use a separate window that
QDialog, then you could use the
get_stylesheet functions from the
Here is a
magicgui example (but could be easily generalised to native
qt based widgets):
from magicgui import magicgui from napari.qt import get_current_stylesheet from napari.settings import get_settings def sample_add(a: int, b: int) -> int: return a + b @magicgui def sample_add(a: int, b: int) -> int: return a + b def change_style(): sample_add.native.setStyleSheet(get_current_stylesheet()) get_settings().appearance.events.theme.connect(change_style) change_style()
Do not package your tests as a top-level package¶
If you are using the napari plugin cookiecutter template, your tests are already packaged in the correct way. No further action required!
# project structure suggested by the cookiecutter template src/ my_package/ _tests/ test_my_module.py __init__.py my_module.py pyproject.toml README.md
However, if your project structure is already following a different scheme, the testing logic might live outside your package, as a top-level directory:
# alternative structure, no src/ directory, testing logic outside the package my_package/ __init__.py my_module.py tests/ conftest.py test_my_module.py pyproject.toml README.md
Under these circumstances, your build backend (usually
setuptools) might include
tests as a
separate package that will be installed next to
Most of the time, this is not wanted; e.g. do you want to do
import tests? Probably not!
Additionally, this unwanted behavior might cause installation issues with other projects.
Ideally, you could change your project structure to follow the recommended skeleton followed in the cookiecutter template. Howevever, if that’s unfeasible, you can fix this in the project metadata files.
You need to explicitly exclude the top-level
tests directory from the packaged contents:
# pyproject.toml ... [options.packages.find] exclude = tests tests.*
# setup.py ... setup( ... packages=find_packages(exclude=("tests", "tests.*")), ... )
Note this also applies to other top-level directories, like
You can find more information in the
package discovery documentation for
License issues when including code from 3rd parties¶
Plugins will often depend on 3rd party packages beyond
These dependencies are usually included in the project metadata in
However, sometimes developers might include code from 3rd parties directly in their project.
Sometimes it will be just a little snippet, maybe slightly modified to suit the project needs.
Some other times, a whole project will be included entirely (vendoring).
This constitutes an act of source code redistribution, which is usually covered by many licensing schemes. Most of the time, this means you need to explicitly include the vendored project license in the source. This is the case for Apache, BSD and MIT-style licenses. Do note that some projects might NOT allow redistribution without explicit approval. Others will prevent it entirely… Be mindful and check the requirements before distributing your package!
If you are vendoring other projects, please add an acknowledgement in your README. The license details in your project metadata should also include this information!