Skip to content

Commit

Permalink
Merge pull request #99 from imagej/98-settings-refactor
Browse files Browse the repository at this point in the history
Use dynaconf for user preferences
  • Loading branch information
gselzer authored Sep 13, 2022
2 parents 99a115c + f901740 commit d47796a
Show file tree
Hide file tree
Showing 17 changed files with 723 additions and 191 deletions.
2 changes: 2 additions & 0 deletions dev-environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ channels:
- defaults
dependencies:
# Project dependencies
- confuse
- labeling >= 0.1.12
- magicgui >= 0.5.1
- napari
Expand All @@ -31,6 +32,7 @@ dependencies:
- pyqt5-sip
- pytest
- pytest-cov
- pytest-env
- pytest-qt
- qtpy
# Project from source
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ channels:
- defaults
dependencies:
# Project depenencies
- confuse
- labeling >= 0.1.12
- magicgui >= 0.5.1
- napari
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"
]
15 changes: 0 additions & 15 deletions settings.yml

This file was deleted.

2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ package_dir =

# add your package requirements here
install_requires =
confuse
labeling >= 0.1.12
napari
magicgui >= 0.5.1
Expand Down Expand Up @@ -64,6 +65,7 @@ dev =
pyqt5
pytest
pytest-cov
pytest-env
pytest-qt
qtpy
numpy
Expand Down
58 changes: 58 additions & 0 deletions src/napari_imagej/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,62 @@
napari-imagej is built upon the PyImageJ project:
https://pyimagej.readthedocs.io/en/latest/
"""
import os
import sys
from typing import Any

import confuse

from napari_imagej.utilities.logging import log_debug

__version__ = "0.0.1.dev0"


class _NapariImageJSettings(confuse.Configuration):
"""Napari-ImageJ Settings object"""

def __init__(self, read=True, **kwargs):
super().__init__(appname="napari-imagej", modname=__name__, read=read)

def read(self, user: bool = True, defaults: bool = True):
"""Override of Configuration.read(), performs validation on each setting"""
# Don't use user settings during the tests
testing = os.environ.get("NAPARI_IMAGEJ_TESTING", "no") == "yes"
super().read(user=user and not testing, defaults=defaults)
# -- VALIDATE SETTINGS -- #
for key, value in self.items():
self._validate_setting(key, value.get(), strict=False)

def _validate_setting(self, setting: str, value: Any, strict=True):
"""
Helper function to perform validation on a particular setting.
By and large, this validation consists of an if block for each setting,
checking the specifics of that setting.
:param setting: The setting (key) to check
:param value: The value assigned to a particular setting
:param strict: If true, raise an Error. If false, assign a reasonable default.
"""
if setting == "jvm_mode":
# Ensure a valid jvm mode choice
self["jvm_mode"].as_choice(["interactive", "headless"])
# Ensure headless chosen on MacOS
if value == "interactive" and sys.platform == "darwin":
if strict: # Report the failure
raise ValueError(
"ImageJ2 must be run headlessly on MacOS. Visit "
'<a href="https://pyimagej.readthedocs.io/en/latest/'
'Initialization.html#interactive-mode">this site</a> '
"for more information."
)
else: # Assign a reasonable default
log_debug(
"ImageJ2 must be run headlessly on MacOS. Reconfiguring "
"jvm_mode to headless"
)
self["jvm_mode"] = "headless"


# napari-imagej uses confuse (https://confuse.readthedocs.io/en/latest/) to configure
# user settings.
settings = _NapariImageJSettings()
31 changes: 31 additions & 0 deletions src/napari_imagej/config_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# napari-imagej Default Settings

# USERS BEWARE:
# This yaml file will soon be replaced with napari's contribution configuration.

# Path to a local ImageJ2 installation (e.g. /Applications/Fiji.app),
# OR version of net.imagej:imagej artifact to launch (e.g. 2.3.0),
# OR endpoint of another artifact built on ImageJ2 (e.g. sc.fiji:fiji),
# OR list of Maven artifacts to include (e.g.
# ['net.imagej:imagej:2.3.0', 'net.imagej:imagej-legacy', 'net.preibisch:BigStitcher']).
# The default ('net.imagej:image') will use the latest version of ImageJ2,
# downloading it if needed.
imagej_directory_or_endpoint: 'net.imagej:imagej'

# This can be used to include original ImageJ functionality.
# 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.
# By setting this value to false, a popup will be shown instead.
choose_active_layer: true
110 changes: 53 additions & 57 deletions src/napari_imagej/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
A module encapsulating access to Java functionality.
Notable functions included in the module:
* ij()
- used to access the ImageJ instance
* ij_init()
- used to begin the creation of the ImageJ instance.
* ensure_jvm_started()
- used to block execution until the ImageJ instance is ready
* running_headless()
- reports whether the JVM is being run headlessly
* setting()
- used to obtain values of configuration settings
* ij()
- used to access the ImageJ instance
* log_debug()
- used for logging in a standardized way
Expand All @@ -18,16 +16,15 @@
- object whose fields are lazily-loaded Java Class instances.
"""
import os
import sys
from functools import lru_cache
from multiprocessing.pool import AsyncResult, ThreadPool
from typing import Any, Callable, Dict
from threading import Lock
from typing import Callable

import imagej
import yaml
from jpype import JClass
from scyjava import config, jimport

from napari_imagej import settings
from napari_imagej.utilities.logging import log_debug

# -- ImageJ API -- #
Expand All @@ -38,36 +35,14 @@ def ij():
Returns the ImageJ instance.
If it isn't ready yet, blocks until it is ready.
"""
return ij_future.get()
return ij_init().get()


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


def setting(name: str):
"""Gets the value of setting name"""
return settings().get(name, None)


@lru_cache(maxsize=None)
def settings() -> Dict[Any, Any]:
"""Gets all plugin settings as a dictionary"""
return yaml.safe_load(open("settings.yml", "r"))


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


def running_headless() -> bool:
return get_mode() == "headless"
ij_init().wait()


def _imagej_init():
Expand All @@ -85,36 +60,57 @@ def _imagej_init():
config.endpoints.append("io.scif:scifio:0.43.1")
log_debug("Completed JVM Configuration")

# Configure PyImageJ settings
settings = {
"ij_dir_or_version_or_endpoint": setting("imagej_installation"),
"mode": get_mode(),
"add_legacy": False,
}

# Launch PyImageJ
_ij = imagej.init(**settings)
_ij = imagej.init(
ij_dir_or_version_or_endpoint=settings["imagej_directory_or_endpoint"].get(str),
mode=settings["jvm_mode"].get(str),
add_legacy=settings["include_imagej_legacy"].get(bool),
)
log_debug(f"Initialized at version {_ij.getVersion()}")

# Return the ImageJ gateway
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 ij_init() -> AsyncResult:
"""
Initializes the singular ImageJ2 instance.
This function returns BEFORE the ImageJ2 instance has been created!
To block until the ImageJ2 instance is ready, use ij() instead.
This function will only create ONE ImageJ2 instance. This ImageJ2 instance
will be created in the first call to this function. Later calls to the function
will return the same AsyncResult generated from the first call to the function.
This function also tries to be thread-safe.
:return: An AsyncResult that will be populated with the ImageJ2
instance once it has been created.
"""
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
16 changes: 16 additions & 0 deletions src/napari_imagej/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
A module used to help find napari-imagej widget resources
"""
from pathlib import Path

PATH = Path(__file__).parent.resolve()
RESOURCES = {x.stem: str(x) for x in PATH.iterdir() if x.suffix != ".py"}


def resource_path(name: str) -> str:
"""Return path to a resource in this folder."""
if name not in RESOURCES:
raise ValueError(
f"{name} is not a known resource! Known resources: {RESOURCES}"
)
return RESOURCES[name]
Loading

0 comments on commit d47796a

Please sign in to comment.