Skip to content

Commit

Permalink
Allow add_legacy configuration in settings
Browse files Browse the repository at this point in the history
  • Loading branch information
gselzer committed Sep 9, 2022
1 parent a1d84e6 commit fb645e8
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 39 deletions.
1 change: 1 addition & 0 deletions dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies:
- pyqt5-sip
- pytest
- pytest-cov
- pytest-env
- pytest-qt
- qtpy
# Project from source
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ requires = [
build-backend = "setuptools.build_meta"

[tool.isort]
profile = "black"
profile = "black"

[tool.pytest.ini_options]
env = [
"NAPARI_IMAGEJ_TESTING=yes"
]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dev =
pyqt5
pytest
pytest-cov
pytest-env
pytest-qt
qtpy
numpy
Expand Down
15 changes: 14 additions & 1 deletion src/napari_imagej/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,23 @@
napari-imagej is built upon the PyImageJ project:
https://pyimagej.readthedocs.io/en/latest/
"""
import os
import sys

import confuse

__version__ = "0.0.1.dev0"

# napari-imagej uses confuse (https://confuse.readthedocs.io/en/latest/) to configure
# user settings.
settings = confuse.Configuration(appname="napari-imagej", modname=__name__)
settings = confuse.Configuration(appname="napari-imagej", modname=__name__, read=False)
# Don't use user settings during the tests
use_user_settings = os.environ["NAPARI_IMAGEJ_TESTING"] != "yes"
settings.read(user=use_user_settings)

# -- SETTING VALIDATION -- #

# Ensure that the jvm mode is valid
jvm_mode: str = settings["jvm_mode"].as_choice(["interactive", "headless"])
if jvm_mode == "interactive" and sys.platform == "darwin":
settings["jvm_mode"] = "headless"
8 changes: 8 additions & 0 deletions src/napari_imagej/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ imagej_directory_or_endpoint: 'net.imagej:imagej'
# Iff True, original ImageJ functionality (ij.* packages) will be available.
include_imagej_legacy: false

# Designates the mode of execution for ImageJ2.
# Allowed options are 'headless' and 'interactive'.
# NB 'interactive' mode is unavailable on MacOS. More details can be found at
# https://pyimagej.readthedocs.io/en/latest/Initialization.html#interactive-mode
# If napari-imagej is launched on MacOS with this setting set to "interactive",
# the setting will silently be reassigned to "headless"
jvm_mode: 'interactive'

# This can be used to identify whether transferred data between ImageJ2 and napari
# should be selected via activation or by user selection via a dialog.
# By default, the active layer/window is chosen for transfer between applications.
Expand Down
61 changes: 31 additions & 30 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
- object whose fields are lazily-loaded Java Class instances.
"""
import os
import sys
from multiprocessing.pool import AsyncResult, ThreadPool
from threading import Lock
from typing import Callable

import imagej
Expand All @@ -35,25 +35,14 @@ def ij():
Returns the ImageJ instance.
If it isn't ready yet, blocks until it is ready.
"""
return ij_future.get()
return imagej_init().get()


def ensure_jvm_started() -> None:
"""
Blocks until the ImageJ instance is ready.
"""
ij_future.wait()


def _get_mode() -> str:
"""
Returns the mode ImageJ will be run in
"""
return "headless" if sys.platform == "darwin" else "interactive"


def jvm_is_headless() -> bool:
return _get_mode() == "headless"
imagej_init().wait()


def _imagej_init():
Expand All @@ -74,7 +63,7 @@ def _imagej_init():
# Launch PyImageJ
_ij = imagej.init(
ij_dir_or_version_or_endpoint=settings["imagej_directory_or_endpoint"].get(str),
mode=_get_mode(),
mode=settings["jvm_mode"].get(str),
add_legacy=settings["include_imagej_legacy"].get(bool),
)
log_debug(f"Initialized at version {_ij.getVersion()}")
Expand All @@ -83,21 +72,33 @@ def _imagej_init():
return _ij


# There is a good debate to be had whether to multithread or multiprocess.
# From what I (Gabe) have read, it seems that threading is preferrable for
# network / IO bottlenecking, while multiprocessing is preferrable for CPU
# bottlenecking.
# While multiprocessing might theoretically be a better choice for JVM startup,
# there are two reasons we instead choose multithreading:
# 1) Multiprocessing is not supported without additional libraries on MacOS.
# See https://docs.python.org/3/library/multiprocessing.html#introduction
# 2) JPype items cannot (currently) be passed between processes due to an
# issue with pickling. See
# https://github.com/imagej/napari-imagej/issues/27#issuecomment-1130102033
threadpool: ThreadPool = ThreadPool(processes=1)
# ij_future is not very pythonic, but we are dealing with a Java Object
# and it better conveys the object's meaning than e.g. ij_result
ij_future: AsyncResult = threadpool.apply_async(func=_imagej_init)
init_lock = Lock()
_ij_future: AsyncResult = None


def imagej_init() -> AsyncResult:
"""Function that"""
global _ij_future
if not _ij_future:
with init_lock:
if not _ij_future:
# There is a good debate to be had whether to multithread or
# multiprocess. From what I (Gabe) have read, it seems that threading
# is preferrable for network / IO bottlenecking, while multiprocessing
# is preferrable for CPU bottlenecking. While multiprocessing might
# theoretically be a better choice for JVM startup, there are two
# reasons we instead choose multithreading:
# 1) Multiprocessing is not supported without additional libraries on
# MacOS. See
# https://docs.python.org/3/library/multiprocessing.html#introduction
# 2) JPype items cannot (currently) be passed between processes due to
# an issue with pickling. See
# https://github.com/imagej/napari-imagej/issues/27#issuecomment-1130102033
threadpool: ThreadPool = ThreadPool(processes=1)
# ij_future is not very pythonic, but we are dealing with a Java Object
# and it better conveys the object's meaning than e.g. ij_result
_ij_future = threadpool.apply_async(func=_imagej_init)
return _ij_future


class JavaClasses(object):
Expand Down
6 changes: 3 additions & 3 deletions src/napari_imagej/widgets/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from qtpy.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QWidget

from napari_imagej import settings
from napari_imagej.java import ensure_jvm_started, ij, jc, jvm_is_headless, log_debug
from napari_imagej.java import ensure_jvm_started, ij, jc, log_debug
from napari_imagej.resources import resource_path
from napari_imagej.utilities._module_utils import _get_layers_hack

Expand All @@ -38,7 +38,7 @@ def __init__(self, viewer: Viewer):
self.settings_button: SettingsButton = SettingsButton(viewer)
self.layout().addWidget(self.settings_button)

if jvm_is_headless():
if settings["jvm_mode"].get(str) == "headless":
self.gui_button.clicked.connect(self.gui_button.disable_popup)
else:
self.gui_button.clicked.connect(self._showUI)
Expand Down Expand Up @@ -243,7 +243,7 @@ def __init__(self):
super().__init__()
self.setEnabled(False)

if jvm_is_headless():
if settings["jvm_mode"].get(str) == "headless":
self._setup_headless()
else:
self._setup_headful()
Expand Down
4 changes: 4 additions & 0 deletions src/napari_imagej/widgets/napari_imagej.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from napari import Viewer
from qtpy.QtWidgets import QTreeWidgetItem, QVBoxLayout, QWidget

from napari_imagej.java import imagej_init
from napari_imagej.widgets.menu import NapariImageJMenu
from napari_imagej.widgets.result_runner import ResultRunner
from napari_imagej.widgets.result_tree import SearchResultTree, SearchResultTreeItem
Expand All @@ -20,6 +21,9 @@ def __init__(self, napari_viewer: Viewer):
super().__init__()
self.setLayout(QVBoxLayout())

# First things first, let's start up imagej (in the background)
imagej_init()

# -- NapariImageJWidget construction -- #

# At the top: the napari-imagej menu
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@


@pytest.fixture(autouse=True)
def confuse_settings():
"""Fixture ensuring user settings are not used in tests"""
def install_default_settings():
"""Fixture ensuring any changes made earlier to the settings are reversed"""
napari_imagej.settings.clear()
napari_imagej.settings.read(user=False)

Expand Down
3 changes: 1 addition & 2 deletions tests/widgets/test_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from qtpy.QtWidgets import QApplication, QHBoxLayout, QMessageBox

from napari_imagej import settings
from napari_imagej.java import jvm_is_headless
from napari_imagej.resources import resource_path
from napari_imagej.widgets import menu
from napari_imagej.widgets.menu import (
Expand All @@ -27,7 +26,7 @@
from tests.utils import jc

# Determine whether we are testing headlessly
TESTING_HEADLESS: bool = jvm_is_headless()
TESTING_HEADLESS: bool = settings["jvm_mode"].get(str) == "headless"


@pytest.fixture(autouse=True)
Expand Down

0 comments on commit fb645e8

Please sign in to comment.