diff --git a/dev-environment.yml b/dev-environment.yml index 452a3796..b0efafd4 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -31,6 +31,7 @@ dependencies: - pandas - pyimagej >= 1.4.1 - scyjava >= 1.8.1 + - xarray < 2024.10.0 # Version rules to avoid problems - qtconsole != 5.4.2 - typing_extensions != 4.6.0 diff --git a/environment.yml b/environment.yml index dd5aae2f..f9960e38 100644 --- a/environment.yml +++ b/environment.yml @@ -31,6 +31,7 @@ dependencies: - pandas - pyimagej >= 1.4.1 - scyjava >= 1.8.1 + - xarray < 2024.10.0 # Version rules to avoid problems - qtconsole != 5.4.2 - typing_extensions != 4.6.0 diff --git a/pyproject.toml b/pyproject.toml index feb874e3..dd6e42a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "pandas", "pyimagej >= 1.4.1", "scyjava >= 1.9.1", + "xarray < 2024.10.0", # Version rules to avoid problems "qtconsole != 5.4.2", # https://github.com/napari/napari/issues/5700 "typing_extensions != 4.6.0", # https://github.com/pydantic/pydantic/issues/5821 diff --git a/src/napari_imagej/types/widget_mappings.py b/src/napari_imagej/types/widget_mappings.py index e2a2fa8d..37578dd0 100644 --- a/src/napari_imagej/types/widget_mappings.py +++ b/src/napari_imagej/types/widget_mappings.py @@ -15,6 +15,7 @@ from napari_imagej.widgets.parameter_widgets import ( ShapeWidget, file_widget_for, + number_widget_for, numeric_type_widget_for, ) @@ -71,6 +72,14 @@ def _numeric_type_preference( return numeric_type_widget_for(item.getType()) +@_widget_preference +def _number_preference( + item: "jc.ModuleItem", type_hint: Union[type, str] +) -> Optional[Union[type, str]]: + if issubclass(item.getType(), jc.Number): + return number_widget_for(item.getType()) + + @_widget_preference def _mutable_output_preference( item: "jc.ModuleItem", type_hint: Union[type, str] diff --git a/src/napari_imagej/widgets/parameter_widgets.py b/src/napari_imagej/widgets/parameter_widgets.py index 9be7f057..b03bf2f5 100644 --- a/src/napari_imagej/widgets/parameter_widgets.py +++ b/src/napari_imagej/widgets/parameter_widgets.py @@ -27,6 +27,7 @@ from napari.layers import Layer from napari.utils._magicgui import get_layers from numpy import dtype +from scyjava import numeric_bounds from napari_imagej.java import jc @@ -83,6 +84,69 @@ def value(self): return Widget +@lru_cache(maxsize=None) +def number_widget_for(cls: type): + if not issubclass(cls, jc.Number): + return None + # Sensible default for Number iface + if cls == jc.Number: + cls = jc.Double + + # NB cls in this instance is jc.Byte.class_, NOT jc.Byte + # We want the latter for use in numeric_bounds and in the widget subclass + if cls == jc.Byte: + cls = ctor = jc.Byte + if cls == jc.Short: + cls = ctor = jc.Short + if cls == jc.Integer: + cls = ctor = jc.Integer + if cls == jc.Long: + cls = ctor = jc.Long + if cls == jc.Float: + cls = ctor = jc.Float + if cls == jc.Double: + cls = ctor = jc.Double + if cls == jc.BigInteger: + cls = jc.BigInteger + ctor = jc.BigInteger.valueOf + if cls == jc.BigDecimal: + cls = jc.BigDecimal + ctor = jc.BigDecimal.valueOf + + extra_args = {} + # HACK: Best attempt for BigDecimal/BigInteger + extra_args["min"], extra_args["max"] = ( + numeric_bounds(jc.Long) + if cls == jc.BigInteger + else numeric_bounds(jc.Double) + if cls == jc.BigDecimal + else numeric_bounds(cls) + ) + # Case logic for implementation-specific attributes + parent = ( + FloatSpinBox + if issubclass(cls, (jc.Double, jc.Float, jc.BigDecimal)) + else SpinBox + ) + + # Define the new widget + class Widget(parent): + def __init__(self, **kwargs): + for k, v in extra_args.items(): + kwargs.setdefault(k, v) + super().__init__(**kwargs) + + @property + def value(self): + return ctor(parent.value.fget(self)) + + @value.setter + def value(self, value: Any): + parent.value.fset(self, value) + + return Widget + + class MutableOutputWidget(Container): """ A ComboBox widget combined with a button that creates new layers. diff --git a/tests/utilities/test_module_utils.py b/tests/utilities/test_module_utils.py index 9b268071..d868fcdb 100644 --- a/tests/utilities/test_module_utils.py +++ b/tests/utilities/test_module_utils.py @@ -248,7 +248,6 @@ def test_add_scijava_metadata(metadata_module_item: DummyModuleItem): assert param_map["label"] == "bar" assert param_map["tooltip"] == "The foo." assert param_map["choices"] == ["a", "b", "c"] - assert param_map["widget_type"] == "FloatSpinBox" choiceList = [ diff --git a/tests/widgets/test_parameter_widgets.py b/tests/widgets/test_parameter_widgets.py index 26023966..5473fefd 100644 --- a/tests/widgets/test_parameter_widgets.py +++ b/tests/widgets/test_parameter_widgets.py @@ -19,6 +19,7 @@ ) from napari import current_viewer from napari.layers import Image +from scyjava import numeric_bounds from napari_imagej.widgets.parameter_widgets import ( DirectoryWidget, @@ -26,6 +27,7 @@ OpenFileWidget, SaveFileWidget, ShapeWidget, + number_widget_for, numeric_type_widget_for, ) from tests.utils import jc @@ -164,6 +166,32 @@ def test_mutable_output_add_new_image( assert foo is output_widget.value +def test_numbers(ij): + numbers = [ + jc.Byte, + jc.Short, + jc.Integer, + jc.Long, + jc.Float, + jc.Double, + jc.BigInteger, + jc.BigDecimal, + ] + for number in numbers: + # NB See HACK in number_widget_for + if number == jc.BigInteger: + min_val, max_val = numeric_bounds(jc.Long) + elif number == jc.BigDecimal: + min_val, max_val = numeric_bounds(jc.Double) + else: + min_val, max_val = numeric_bounds(number) + + widget = number_widget_for(number.class_)() + assert min_val == widget.min + assert max_val == widget.max + assert isinstance(widget.value, number) + + def test_realType(): real_types = [ (jc.BitType),