Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recommended_versions #264

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
63 changes: 56 additions & 7 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
* 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
from scyjava import config, get_version, is_version_at_least, jimport, jvm_started

from napari_imagej import settings
from napari_imagej.utilities.logging import log_debug
from napari_imagej import __version__, settings
from napari_imagej.utilities.logging import log_debug, warn

# -- Constants --

Expand All @@ -34,9 +34,18 @@
"sc.fiji:TrackMate": "7.11.0",
}

recommended_versions = {
"net.imagej:imagej": "2.10.0",
"net.imagej:imagej-legacy": "1.2.1",
"net.imagej:imagej-ops": "2.0.1",
"org.scijava:scijava-search": "2.0.4",
"sc.fiji:fiji": "2.10.0",
}

# -- ImageJ API -- #

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


def ij():
Expand All @@ -47,6 +56,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 @@ -119,11 +136,13 @@ def _validate_imagej():
# If we want to require a minimum version for a java component, we need to
# be able to find our current version. We do that by querying a Java class
# within that component.
ImageJ = jimport("net.imagej.Main")
RGRAI = jimport("net.imglib2.python.ReferenceGuardingRandomAccessibleInterval")
SCIFIO = jimport("io.scif.SCIFIO")
UnsafeImg = jimport("net.imglib2.img.unsafe.UnsafeImg")
component_requirements = {
"io.scif:scifio": SCIFIO,
"net.imagej:imagej": ImageJ,
"net.imagej:imagej-common": jc.Dataset,
"net.imagej:imagej-ops": jc.OpInfo,
"net.imglib2:imglib2-unsafe": UnsafeImg,
Expand All @@ -135,6 +154,8 @@ def _validate_imagej():
# Find version that violate the minimum
violations = []
for component, cls in component_requirements.items():
if component not in minimum_versions:
continue
min_version = minimum_versions[component]
component_version = get_version(cls)
if not is_version_at_least(component_version, min_version):
Expand All @@ -152,17 +173,45 @@ def _validate_imagej():
)
raise RuntimeError(failure_str)

# Find versions below recommended
violations = []
for component, cls in component_requirements.items():
if component not in recommended_versions:
continue
recommended_version = recommended_versions[component]
component_version = get_version(cls)
if not is_version_at_least(component_version, recommended_version):
violations.append(
f"{component} : {recommended_version} (Installed: {component_version})"
)

# If there are older versions, warn the user
if violations:
failure_str = (
f"napari-imagej v{__version__} recommends using "
"the following component versions:"
)
violations.insert(0, failure_str)
failure_str = "\n\t".join(violations)
_init_warnings.append(failure_str)
warn(failure_str)


def _optional_requirements():
optionals = {}
# Add additional minimum versions for legacy components
if _ij.legacy and _ij.legacy.isActive():
optionals["net.imagej:imagej-legacy"] = _ij.legacy.getClass()
# Add additional minimum versions for fiji components
try:
optionals["sc.fiji:TrackMate"] = jimport("fiji.plugin.trackmate.TrackMate")
except Exception:
pass
optional_classes = {
"sc.fiji:TrackMate": "fiji.plugin.trackmate.TrackMate",
"sc.fiji:Fiji": "fiji.Main",
}
for artifact, cls in optional_classes.items():
try:
optionals[artifact] = jimport(cls)
except Exception:
pass

return optionals

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
29 changes: 27 additions & 2 deletions tests/test_java.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from scyjava import get_version, is_version_at_least, jimport

from napari_imagej import settings
from napari_imagej.java import minimum_versions
from napari_imagej import __version__, settings
from napari_imagej.java import _validate_imagej, init_warnings, minimum_versions

version_checks = {
"io.scif:scifio": "io.scif.SCIFIO",
Expand Down Expand Up @@ -57,3 +57,28 @@ def test_endpoint(ij):
version = gav[2]
exp_version = get_version(jimport(version_checks[ga]))
assert is_version_at_least(version, exp_version)


def test_recommended_version(ij):
# Save old recommended versions
import napari_imagej.java

existing_recommendations = napari_imagej.java.recommended_versions
existing_warnings = [w for w in napari_imagej.java._init_warnings]
napari_imagej.java.recommended_versions = {"org.scijava:scijava-common": "999.0.0"}

# Validate ImageJ - capture lower-than-recommended version
_validate_imagej()
warnings = init_warnings()
assert len(warnings) == 1

# Assert warning given
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)"
)

# restore recommended versions
napari_imagej.java.recommended_versions = existing_recommendations
napari_imagej.java._init_warnings = existing_warnings
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
Loading