Skip to content

Commit

Permalink
Add warnings mechanism for non-dealbreak problems
Browse files Browse the repository at this point in the history
  • Loading branch information
gselzer committed Jul 20, 2023
1 parent 94fb761 commit 89032c4
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 20 deletions.
12 changes: 11 additions & 1 deletion src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* jc
- object whose fields are lazily-loaded Java Class instances.
"""
from typing import Any, Callable, Dict
from typing import Any, Callable, Dict, List

import imagej
from jpype import JClass
Expand Down Expand Up @@ -39,6 +39,7 @@
# -- ImageJ API -- #

_ij = None
_init_warnings: List[str] = []


def ij():
Expand All @@ -49,6 +50,14 @@ def ij():
return _ij


def init_warnings():
if _ij is None:
raise Exception(
"The ImageJ instance has not yet been initialized! Please run init_ij()"
)
return _init_warnings


def init_ij() -> "jc.ImageJ":
"""
Creates the ImageJ instance
Expand Down Expand Up @@ -174,6 +183,7 @@ def _validate_imagej():
)
violations.insert(0, failure_str)
failure_str = "\n\t".join(violations)
_init_warnings.append(failure_str)
warn(failure_str)


Expand Down
7 changes: 7 additions & 0 deletions src/napari_imagej/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
Additional command line arguments to pass to the Java Virtual Machine (JVM).
For example, "-Xmx4g" to allow Java to use up to 4 GB of memory.
By default, no arguments are passed.
display_imagej_initialization_warnings: bool = True
When napari-imagej encounters a warnable issue pertaining to Java components
it will display the warnings iff this flag is true.
Defaults to True.
"""

import os
Expand All @@ -56,6 +61,7 @@
"include_imagej_legacy": True,
"enable_imagej_gui": True,
"jvm_command_line_arguments": "",
"display_java_warnings": True,
}

# -- Configuration options --
Expand All @@ -65,6 +71,7 @@
include_imagej_legacy: bool = defaults["include_imagej_legacy"]
enable_imagej_gui: bool = defaults["enable_imagej_gui"]
jvm_command_line_arguments: str = defaults["jvm_command_line_arguments"]
display_java_warnings: bool = defaults["display_java_warnings"]

_test_mode = bool(os.environ.get("NAPARI_IMAGEJ_TESTING", None))
_is_macos = sys.platform == "darwin"
Expand Down
33 changes: 30 additions & 3 deletions src/napari_imagej/widgets/napari_imagej.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
This Widget is made accessible to napari through napari.yml
"""
from traceback import format_exception
from typing import Callable
from typing import Callable, List

from jpype import JArray, JImplements, JOverride
from magicgui.widgets import FunctionGui, Widget
Expand All @@ -15,7 +15,8 @@
from qtpy.QtWidgets import QTreeWidgetItem, QVBoxLayout, QWidget
from scyjava import isjava, jstacktrace, when_jvm_stops

from napari_imagej.java import ij, init_ij, jc
from napari_imagej import settings
from napari_imagej.java import ij, init_ij, init_warnings, jc
from napari_imagej.utilities._module_utils import _non_layer_widget
from napari_imagej.utilities.event_subscribers import (
NapariEventSubscriber,
Expand All @@ -33,7 +34,10 @@
SearchResultTreeItem,
)
from napari_imagej.widgets.searchbar import JVMEnabledSearchbar
from napari_imagej.widgets.widget_utils import JavaErrorMessageBox
from napari_imagej.widgets.widget_utils import (
JavaErrorMessageBox,
JavaWarningMessageBox,
)


class NapariImageJWidget(QWidget):
Expand All @@ -42,6 +46,7 @@ class NapariImageJWidget(QWidget):
output_handler = Signal(object)
progress_handler = Signal(object)
ij_error_handler = Signal(object)
ij_warning_handler = Signal(object)

def __init__(self, napari_viewer: Viewer):
super().__init__()
Expand Down Expand Up @@ -118,6 +123,7 @@ def return_search_bar():
self.output_handler.connect(self._handle_output)
self.progress_handler.connect(self._update_progress)
self.ij_error_handler.connect(self._handle_ij_init_error)
self.ij_warning_handler.connect(self._handle_ij_init_warning)

# -- Final setup -- #

Expand Down Expand Up @@ -216,6 +222,24 @@ def _handle_ij_init_error(self, exc: Exception):
msg: JavaErrorMessageBox = JavaErrorMessageBox(title, exception_str)
msg.exec()

@Slot(object)
def _handle_ij_init_warning(self, warnings: List[Exception]):
"""
Handles warnings associated initializing ImageJ.
Initializing ImageJ can fail for all sorts of reasons,
so we give it special attention here.
NB: This MUST be done within this slot, as slot functions
are run on the GUI thread. napari-imagej runs ImageJ initialization
on a separate Qt thread, which isn't the GUI thread.
"""
# Print thet error
if not settings.display_java_warnings or len(warnings) == 0:
return
title = "During the initialization of ImageJ, warnings were raised:"
msg: JavaWarningMessageBox = JavaWarningMessageBox(title, "\n\n".join(warnings))
msg.exec()


class ImageJInitializer(QThread):
"""
Expand All @@ -236,6 +260,8 @@ def run(self):
try:
# Initialize ImageJ
init_ij()
# Log any warnings
self.widget.ij_warning_handler.emit(init_warnings())
# Finalize the menu
self.widget.menu.finalize()
# Finalize the search bar
Expand All @@ -249,6 +275,7 @@ def run(self):
except Exception as e:
# Handle the exception on the GUI thread
self.widget.ij_error_handler.emit(e)
return

def _finalize_results_tree(self):
"""
Expand Down
44 changes: 44 additions & 0 deletions src/napari_imagej/widgets/widget_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from qtpy.QtGui import QFontMetrics
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
Expand All @@ -20,6 +21,7 @@
QWidget,
)

from napari_imagej import settings
from napari_imagej.java import ij, jc
from napari_imagej.utilities._module_utils import (
execute_function_modally,
Expand Down Expand Up @@ -140,6 +142,48 @@ def __init__(self, title: str, error_message: str, *args, **kwargs):
self.layout().addWidget(btn_box, 2, 0, 1, self.layout().columnCount())


class JavaWarningMessageBox(QDialog):
def __init__(self, title: str, error_message: str, *args, **kwargs):
QDialog.__init__(self, *args, **kwargs)
self.setLayout(QGridLayout())
# Write the title to a Label
self.layout().addWidget(
QLabel(title, self), 0, 0, 1, self.layout().columnCount()
)

# Write the error message to a TextEdit
msg_edit = QTextEdit(self)
msg_edit.setReadOnly(True)
msg_edit.setText(error_message)
self.layout().addWidget(msg_edit, 1, 0, 1, self.layout().columnCount())
msg_edit.setLineWrapMode(0)

# Default size - size of the error message
font = msg_edit.document().defaultFont()
fontMetrics = QFontMetrics(font)
textSize = fontMetrics.size(0, error_message)
textWidth = textSize.width() + 100
textHeight = textSize.height() + 100
self.resize(textWidth, textHeight)
# Maximum size - ~80% of the user's screen
screen_size = QApplication.desktop().screenGeometry()
self.setMaximumSize(
int(screen_size.width() * 0.8), int(screen_size.height() * 0.8)
)
self.checkbox: QCheckBox = QCheckBox("Don't warn me again")
self.layout().addWidget(self.checkbox, 2, 0, 1, self.layout().columnCount())

btn_box = QDialogButtonBox(QDialogButtonBox.Ok)
btn_box.accepted.connect(self.accept)
self.layout().addWidget(btn_box, 3, 0, 1, self.layout().columnCount())

def accept(self):
super().accept()
if self.checkbox.isChecked():
settings.display_java_warnings = False
settings.save()


_IMAGE_LAYER_TYPES = (Image, Labels)
_ROI_LAYER_TYPES = (Points, Shapes)

Expand Down
22 changes: 7 additions & 15 deletions tests/test_java.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from scyjava import get_version, is_version_at_least, jimport

from napari_imagej import __version__, settings
from napari_imagej.java import _validate_imagej, minimum_versions
from napari_imagej.utilities.logging import logger
from napari_imagej.java import _validate_imagej, init_warnings, minimum_versions

version_checks = {
"io.scif:scifio": "io.scif.SCIFIO",
Expand Down Expand Up @@ -67,23 +66,16 @@ def test_recommended_version(ij):
existing = napari_imagej.java.recommended_versions
napari_imagej.java.recommended_versions = {"org.scijava:scijava-common": "999.0.0"}

# Setup log handler to capture warning
import io
import logging

log_capture_string = io.StringIO()
ch = logging.StreamHandler(log_capture_string)
ch.setLevel(logging.WARN)
logger().addHandler(ch)
# Validate ImageJ - capture lower-than-recommended version
_validate_imagej()
log_contents = log_capture_string.getvalue()
log_capture_string.close()
warnings = init_warnings()
assert len(warnings) == 1

# Assert warning given
assert log_contents == (
f"napari-imagej: napari-imagej v{__version__} recommends using the "
assert warnings[0] == (
f"napari-imagej v{__version__} recommends using the "
"following component versions:\n\torg.scijava:scijava-common : "
"999.0.0 (Installed: 2.94.1)\n"
"999.0.0 (Installed: 2.94.1)"
)

# restore recommended versions
Expand Down
32 changes: 31 additions & 1 deletion tests/widgets/test_napari_imagej.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
from napari_imagej.widgets.napari_imagej import NapariImageJWidget, ResultRunner
from napari_imagej.widgets.result_tree import SearchResultTree
from napari_imagej.widgets.searchbar import JVMEnabledSearchbar
from napari_imagej.widgets.widget_utils import JavaErrorMessageBox
from napari_imagej.widgets.widget_utils import (
JavaErrorMessageBox,
JavaWarningMessageBox,
)
from tests.utils import jc
from tests.widgets.widget_utils import _searcher_tree_named

Expand Down Expand Up @@ -307,3 +310,30 @@ def new_exec(self):

# Finally, restore JavaErrorMessageBox.exec
JavaErrorMessageBox.exec = old_exec


def test_handle_ij_init_warning(imagej_widget: NapariImageJWidget):
"""
Ensure that napari-imagej's ij init warnings are displayed correctly
"""
title = ""
contents = ""

# first, mock JavaErrorMessageBox.exec
old_exec = JavaWarningMessageBox.exec

def new_exec(self):
nonlocal title, contents
title = self.findChild(QLabel).text()
contents = self.findChild(QTextEdit).toPlainText()

JavaWarningMessageBox.exec = new_exec

# Then, test a Java exception is correctly configured
warnings = ["This is a warning"]
imagej_widget._handle_ij_init_warning(warnings)
assert title == "During the initialization of ImageJ, warnings were raised:"
assert contents == "This is a warning"

# Finally, restore JavaErrorMessageBox.exec
JavaWarningMessageBox.exec = old_exec

0 comments on commit 89032c4

Please sign in to comment.