napari-tmidas 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. napari_tmidas/__init__.py +35 -5
  2. napari_tmidas/_crop_anything.py +1520 -609
  3. napari_tmidas/_env_manager.py +76 -0
  4. napari_tmidas/_file_conversion.py +1646 -1131
  5. napari_tmidas/_file_selector.py +1455 -216
  6. napari_tmidas/_label_inspection.py +83 -8
  7. napari_tmidas/_processing_worker.py +309 -0
  8. napari_tmidas/_reader.py +6 -10
  9. napari_tmidas/_registry.py +2 -2
  10. napari_tmidas/_roi_colocalization.py +1221 -84
  11. napari_tmidas/_tests/test_crop_anything.py +123 -0
  12. napari_tmidas/_tests/test_env_manager.py +89 -0
  13. napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
  14. napari_tmidas/_tests/test_init.py +98 -0
  15. napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
  16. napari_tmidas/_tests/test_label_inspection.py +86 -0
  17. napari_tmidas/_tests/test_processing_basic.py +500 -0
  18. napari_tmidas/_tests/test_processing_worker.py +142 -0
  19. napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
  20. napari_tmidas/_tests/test_registry.py +70 -2
  21. napari_tmidas/_tests/test_scipy_filters.py +168 -0
  22. napari_tmidas/_tests/test_skimage_filters.py +259 -0
  23. napari_tmidas/_tests/test_split_channels.py +217 -0
  24. napari_tmidas/_tests/test_spotiflow.py +87 -0
  25. napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
  26. napari_tmidas/_tests/test_ui_utils.py +68 -0
  27. napari_tmidas/_tests/test_widget.py +30 -0
  28. napari_tmidas/_tests/test_windows_basic.py +66 -0
  29. napari_tmidas/_ui_utils.py +57 -0
  30. napari_tmidas/_version.py +16 -3
  31. napari_tmidas/_widget.py +41 -4
  32. napari_tmidas/processing_functions/basic.py +557 -20
  33. napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
  34. napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
  35. napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
  36. napari_tmidas/processing_functions/colocalization.py +513 -56
  37. napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
  38. napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
  39. napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
  40. napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
  41. napari_tmidas/processing_functions/sam2_mp4.py +274 -195
  42. napari_tmidas/processing_functions/scipy_filters.py +403 -8
  43. napari_tmidas/processing_functions/skimage_filters.py +424 -212
  44. napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
  45. napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
  46. napari_tmidas/processing_functions/timepoint_merger.py +334 -86
  47. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +70 -30
  48. napari_tmidas-0.2.4.dist-info/RECORD +63 -0
  49. napari_tmidas/_tests/__init__.py +0 -0
  50. napari_tmidas-0.2.2.dist-info/RECORD +0 -40
  51. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
  52. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
  53. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
  54. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,87 @@
1
+ """
2
+ Test module for Spotiflow processing functions.
3
+ """
4
+
5
+ import subprocess
6
+
7
+ import numpy as np
8
+ import pytest
9
+
10
+ from napari_tmidas.processing_functions.spotiflow_env_manager import (
11
+ SpotiflowEnvironmentManager,
12
+ is_env_created,
13
+ is_spotiflow_installed,
14
+ )
15
+
16
+
17
+ def test_spotiflow_env_manager_init():
18
+ """Test SpotiflowEnvironmentManager initialization."""
19
+ manager = SpotiflowEnvironmentManager()
20
+ assert manager.env_name == "spotiflow"
21
+ assert "spotiflow" in manager.env_dir
22
+
23
+
24
+ def test_is_spotiflow_installed():
25
+ """Test spotiflow installation check."""
26
+ # This will likely be False in most test environments
27
+ result = is_spotiflow_installed()
28
+ assert isinstance(result, bool)
29
+
30
+
31
+ def test_is_env_created():
32
+ """Test environment creation check."""
33
+ result = is_env_created()
34
+ assert isinstance(result, bool)
35
+
36
+
37
+ @pytest.mark.slow
38
+ def test_spotiflow_detection_import():
39
+ """Test importing the spotiflow detection module."""
40
+ try:
41
+ from napari_tmidas.processing_functions import spotiflow_detection
42
+
43
+ assert hasattr(spotiflow_detection, "spotiflow_detect_spots")
44
+ except ImportError:
45
+ pytest.skip("Spotiflow detection module not available")
46
+
47
+
48
+ @pytest.mark.slow
49
+ def test_spotiflow_detection_with_synthetic_data():
50
+ """Test spot detection with synthetic data."""
51
+ try:
52
+ from napari_tmidas.processing_functions.spotiflow_detection import (
53
+ spotiflow_detect_spots,
54
+ )
55
+
56
+ # Create synthetic 2D image with some bright spots
57
+ image = np.zeros((100, 100), dtype=np.uint16)
58
+ # Add some bright spots
59
+ image[25:27, 25:27] = 1000
60
+ image[75:77, 75:77] = 1200
61
+ image[50:52, 25:27] = 800
62
+
63
+ # Add some noise
64
+ image = image + np.random.normal(0, 50, image.shape).astype(np.uint16)
65
+
66
+ # Test detection (this will likely use the dedicated environment)
67
+ points = spotiflow_detect_spots(
68
+ image, pretrained_model="general", force_dedicated_env=True
69
+ )
70
+
71
+ # Should return an array of points
72
+ assert isinstance(points, np.ndarray)
73
+ assert points.ndim == 2
74
+ assert points.shape[1] == 2 # 2D coordinates
75
+
76
+ except ImportError:
77
+ pytest.skip("Spotiflow not available for testing")
78
+ except (RuntimeError, subprocess.CalledProcessError) as e:
79
+ pytest.skip(f"Spotiflow test failed: {e}")
80
+
81
+
82
+ if __name__ == "__main__":
83
+ # Run basic tests
84
+ test_spotiflow_env_manager_init()
85
+ test_is_spotiflow_installed()
86
+ test_is_env_created()
87
+ print("Basic Spotiflow tests passed!")
@@ -0,0 +1,142 @@
1
+ """Test TYX image display bug fix"""
2
+
3
+ import os
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
9
+
10
+
11
+ class TestTYXDisplayFix:
12
+ """Test the fix for TYX images being incorrectly displayed"""
13
+
14
+ def test_channel_detection_rgb(self):
15
+ """RGB images (3 channels) should be detected as multi-channel"""
16
+ shape = (3, 100, 100)
17
+ # Simulate the detection logic
18
+ is_multi_channel = len(shape) > 2 and shape[0] <= 4 and shape[0] > 1
19
+ assert (
20
+ is_multi_channel
21
+ ), "RGB image should be detected as multi-channel"
22
+
23
+ def test_channel_detection_rgba(self):
24
+ """RGBA images (4 channels) should be detected as multi-channel"""
25
+ shape = (4, 100, 100)
26
+ is_multi_channel = len(shape) > 2 and shape[0] <= 4 and shape[0] > 1
27
+ assert (
28
+ is_multi_channel
29
+ ), "RGBA image should be detected as multi-channel"
30
+
31
+ def test_channel_detection_tyx(self):
32
+ """TYX time series (5+ timepoints) should NOT be detected as multi-channel"""
33
+ shape = (5, 100, 100)
34
+ is_multi_channel = len(shape) > 2 and shape[0] <= 4 and shape[0] > 1
35
+ assert (
36
+ not is_multi_channel
37
+ ), "TYX time series should NOT be detected as multi-channel"
38
+
39
+ def test_channel_detection_zyx(self):
40
+ """ZYX z-stacks (10+ slices) should NOT be detected as multi-channel"""
41
+ shape = (10, 100, 100)
42
+ is_multi_channel = len(shape) > 2 and shape[0] <= 4 and shape[0] > 1
43
+ assert (
44
+ not is_multi_channel
45
+ ), "ZYX z-stack should NOT be detected as multi-channel"
46
+
47
+ def test_channel_detection_dual_channel(self):
48
+ """2-channel images should be detected as multi-channel"""
49
+ shape = (2, 100, 100)
50
+ is_multi_channel = len(shape) > 2 and shape[0] <= 4 and shape[0] > 1
51
+ assert (
52
+ is_multi_channel
53
+ ), "2-channel image should be detected as multi-channel"
54
+
55
+ def test_3d_view_not_enabled_for_tyx(self):
56
+ """TYX time series should use 2D view, not 3D view"""
57
+ shape = (5, 100, 100)
58
+
59
+ # Simulate the 3D view detection logic
60
+ if shape[0] >= 2 and shape[0] <= 4:
61
+ meaningful_dims = shape[1:]
62
+ else:
63
+ meaningful_dims = shape
64
+
65
+ # Check if 3D view would be enabled
66
+ enable_3d = False
67
+ if len(meaningful_dims) >= 4:
68
+ z_dim = meaningful_dims[1]
69
+ enable_3d = z_dim > 1
70
+ elif len(meaningful_dims) == 3:
71
+ first_dim = meaningful_dims[0]
72
+ enable_3d = first_dim > 10
73
+
74
+ assert not enable_3d, "TYX with 5 timepoints should NOT enable 3D view"
75
+
76
+ def test_3d_view_enabled_for_large_zyx(self):
77
+ """Large ZYX z-stacks should enable 3D view"""
78
+ shape = (20, 100, 100)
79
+
80
+ # Simulate the 3D view detection logic
81
+ if shape[0] >= 2 and shape[0] <= 4:
82
+ meaningful_dims = shape[1:]
83
+ else:
84
+ meaningful_dims = shape
85
+
86
+ enable_3d = False
87
+ if len(meaningful_dims) >= 4:
88
+ z_dim = meaningful_dims[1]
89
+ enable_3d = z_dim > 1
90
+ elif len(meaningful_dims) == 3:
91
+ first_dim = meaningful_dims[0]
92
+ enable_3d = first_dim > 10
93
+
94
+ assert enable_3d, "ZYX with 20 slices should enable 3D view"
95
+
96
+ def test_3d_view_enabled_for_tzyx(self):
97
+ """TZYX data should enable 3D view"""
98
+ shape = (10, 50, 100, 100)
99
+
100
+ # Simulate the 3D view detection logic
101
+ if shape[0] >= 2 and shape[0] <= 4:
102
+ meaningful_dims = shape[1:]
103
+ else:
104
+ meaningful_dims = shape
105
+
106
+ enable_3d = False
107
+ if len(meaningful_dims) >= 4:
108
+ z_dim = meaningful_dims[1]
109
+ enable_3d = z_dim > 1
110
+ elif len(meaningful_dims) == 3:
111
+ first_dim = meaningful_dims[0]
112
+ enable_3d = first_dim > 10
113
+
114
+ assert enable_3d, "TZYX data should enable 3D view"
115
+
116
+ def test_3d_view_not_enabled_for_rgb_channels(self):
117
+ """After splitting RGB, individual channels should use 2D view"""
118
+ shape = (
119
+ 3,
120
+ 100,
121
+ 100,
122
+ ) # This would be seen after channel splitting doesn't occur
123
+
124
+ # Simulate the 3D view detection logic
125
+ if shape[0] >= 2 and shape[0] <= 4:
126
+ meaningful_dims = shape[1:]
127
+ else:
128
+ meaningful_dims = shape
129
+
130
+ enable_3d = False
131
+ if len(meaningful_dims) >= 4:
132
+ z_dim = meaningful_dims[1]
133
+ enable_3d = z_dim > 1
134
+ elif len(meaningful_dims) == 3:
135
+ first_dim = meaningful_dims[0]
136
+ enable_3d = first_dim > 10
137
+
138
+ assert not enable_3d, "RGB channels should use 2D view"
139
+
140
+
141
+ if __name__ == "__main__":
142
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,68 @@
1
+ # src/napari_tmidas/_tests/test_ui_utils.py
2
+ from unittest.mock import Mock
3
+
4
+ from napari_tmidas._ui_utils import add_browse_button_to_folder_field
5
+
6
+
7
+ class TestUIUtils:
8
+ def test_add_browse_button_to_folder_field(self):
9
+ """Test adding browse button to a folder field"""
10
+ # Create mock widget
11
+ mock_widget = Mock()
12
+ mock_folder_field = Mock()
13
+ mock_folder_field.value = "/test/path"
14
+ mock_folder_field.native = Mock()
15
+ mock_parent = Mock()
16
+ mock_layout = Mock()
17
+ mock_parent.layout.return_value = mock_layout
18
+ mock_folder_field.native.parent.return_value = mock_parent
19
+
20
+ mock_widget.folder_path = mock_folder_field
21
+
22
+ # Call the function
23
+ result = add_browse_button_to_folder_field(mock_widget, "folder_path")
24
+ assert result == mock_widget
25
+
26
+ def test_add_browse_button_with_existing_value(self):
27
+ """Test adding browse button when folder field has existing value"""
28
+ # Create mock widget
29
+ mock_widget = Mock()
30
+ mock_folder_field = Mock()
31
+ mock_folder_field.value = "/existing/path"
32
+ mock_folder_field.native = Mock()
33
+ mock_parent = Mock()
34
+ mock_layout = Mock()
35
+ mock_parent.layout.return_value = mock_layout
36
+ mock_folder_field.native.parent.return_value = mock_parent
37
+
38
+ mock_widget.existing_path = mock_folder_field
39
+
40
+ # Call the function
41
+ result = add_browse_button_to_folder_field(
42
+ mock_widget, "existing_path"
43
+ )
44
+
45
+ # Check that the button was added to the layout
46
+ mock_layout.addWidget.assert_called_once()
47
+ assert result == mock_widget
48
+
49
+ def test_add_browse_button_with_empty_value(self):
50
+ """Test adding browse button when folder field is empty"""
51
+ # Create mock widget
52
+ mock_widget = Mock()
53
+ mock_folder_field = Mock()
54
+ mock_folder_field.value = "" # Empty value
55
+ mock_folder_field.native = Mock()
56
+ mock_parent = Mock()
57
+ mock_layout = Mock()
58
+ mock_parent.layout.return_value = mock_layout
59
+ mock_folder_field.native.parent.return_value = mock_parent
60
+
61
+ mock_widget.empty_path = mock_folder_field
62
+
63
+ # Call the function
64
+ result = add_browse_button_to_folder_field(mock_widget, "empty_path")
65
+
66
+ # Check that the button was added to the layout
67
+ mock_layout.addWidget.assert_called_once()
68
+ assert result == mock_widget
@@ -1,4 +1,8 @@
1
+ import os
2
+ import sys
3
+
1
4
  import numpy as np
5
+ import pytest
2
6
 
3
7
  from napari_tmidas._widget import (
4
8
  ExampleQWidget,
@@ -7,6 +11,14 @@ from napari_tmidas._widget import (
7
11
  threshold_magic_widget,
8
12
  )
9
13
 
14
+ # Check if pytest-qt is available
15
+ try:
16
+ import pytest_qt # noqa: F401
17
+
18
+ PYTEST_QT_AVAILABLE = True
19
+ except ImportError:
20
+ PYTEST_QT_AVAILABLE = False
21
+
10
22
 
11
23
  def test_threshold_autogenerate_widget():
12
24
  # because our "widget" is a pure function, we can call it and
@@ -20,6 +32,12 @@ def test_threshold_autogenerate_widget():
20
32
  # make_napari_viewer is a pytest fixture that returns a napari viewer object
21
33
  # you don't need to import it, as long as napari is installed
22
34
  # in your testing environment
35
+ @pytest.mark.skipif(
36
+ not PYTEST_QT_AVAILABLE
37
+ or (os.environ.get("DISPLAY", "") == "" and os.name != "nt")
38
+ or (sys.platform == "win32" and os.environ.get("CI") == "true"),
39
+ reason="Requires pytest-qt, X11 display in headless *nix CI or full napari install on Windows CI",
40
+ )
23
41
  def test_threshold_magic_widget(make_napari_viewer):
24
42
  viewer = make_napari_viewer()
25
43
  layer = viewer.add_image(np.random.random((100, 100)))
@@ -33,6 +51,12 @@ def test_threshold_magic_widget(make_napari_viewer):
33
51
  # etc.
34
52
 
35
53
 
54
+ @pytest.mark.skipif(
55
+ not PYTEST_QT_AVAILABLE
56
+ or (os.environ.get("DISPLAY", "") == "" and os.name != "nt")
57
+ or (sys.platform == "win32" and os.environ.get("CI") == "true"),
58
+ reason="Requires pytest-qt, X11 display in headless *nix CI or full napari install on Windows CI",
59
+ )
36
60
  def test_image_threshold_widget(make_napari_viewer):
37
61
  viewer = make_napari_viewer()
38
62
  layer = viewer.add_image(np.random.random((100, 100)))
@@ -50,6 +74,12 @@ def test_image_threshold_widget(make_napari_viewer):
50
74
 
51
75
 
52
76
  # capsys is a pytest fixture that captures stdout and stderr output streams
77
+ @pytest.mark.skipif(
78
+ not PYTEST_QT_AVAILABLE
79
+ or (os.environ.get("DISPLAY", "") == "" and os.name != "nt")
80
+ or (sys.platform == "win32" and os.environ.get("CI") == "true"),
81
+ reason="Requires pytest-qt, X11 display in headless *nix CI or full napari install on Windows CI",
82
+ )
53
83
  def test_example_q_widget(make_napari_viewer, capsys):
54
84
  # make viewer and add an image layer using our fixture
55
85
  viewer = make_napari_viewer()
@@ -0,0 +1,66 @@
1
+ # src/napari_tmidas/_tests/test_windows_basic.py
2
+ """
3
+ Basic Windows tests that don't require heavy dependencies.
4
+ This ensures the package structure is correct without testing full functionality.
5
+ """
6
+ import os
7
+ import platform
8
+ import sys
9
+
10
+
11
+ class TestWindowsBasic:
12
+ def test_python_version(self):
13
+ """Test that Python version is supported"""
14
+ assert sys.version_info >= (3, 9)
15
+
16
+ def test_platform_detection(self):
17
+ """Test that we can detect Windows platform"""
18
+ if platform.system() == "Windows":
19
+ assert True # We're on Windows, basic test passes
20
+ else:
21
+ assert True # Not on Windows, still pass
22
+
23
+ def test_basic_imports(self):
24
+ """Test that basic Python modules can be imported"""
25
+ import json
26
+ import pathlib
27
+ import tempfile
28
+
29
+ # Create a simple test
30
+ with tempfile.TemporaryDirectory() as temp_dir:
31
+ test_file = pathlib.Path(temp_dir) / "test.json"
32
+ test_data = {"test": "data"}
33
+
34
+ # Write and read JSON
35
+ with open(test_file, "w") as f:
36
+ json.dump(test_data, f)
37
+
38
+ with open(test_file) as f:
39
+ loaded_data = json.load(f)
40
+
41
+ assert loaded_data == test_data
42
+
43
+ def test_package_structure_exists(self):
44
+ """Test that the package structure exists"""
45
+ # Test that we can find the package directory
46
+ import napari_tmidas
47
+
48
+ package_dir = os.path.dirname(napari_tmidas.__file__)
49
+ assert os.path.exists(package_dir)
50
+ assert os.path.isdir(package_dir)
51
+
52
+ # Test that __version__ is available
53
+ assert hasattr(napari_tmidas, "__version__")
54
+ assert isinstance(napari_tmidas.__version__, str)
55
+
56
+ # On Windows CI with skip_install=true, version may be "unknown"
57
+ # This is expected and acceptable
58
+ if platform.system() == "Windows" and os.environ.get("CI") == "true":
59
+ assert (
60
+ napari_tmidas.__version__ in ["unknown", "0.0.0"]
61
+ or "+" in napari_tmidas.__version__
62
+ or napari_tmidas.__version__.startswith("0.")
63
+ )
64
+ else:
65
+ # On other platforms or local development, version should be meaningful
66
+ assert napari_tmidas.__version__ != "unknown"
@@ -0,0 +1,57 @@
1
+ """
2
+ Common UI utilities for napari widgets.
3
+ """
4
+
5
+ import os
6
+
7
+ # Lazy imports for optional heavy dependencies
8
+ try:
9
+ from qtpy.QtWidgets import QFileDialog, QPushButton
10
+
11
+ _HAS_QTPY = True
12
+ except ImportError:
13
+ QFileDialog = QPushButton = None
14
+ _HAS_QTPY = False
15
+
16
+
17
+ def add_browse_button_to_folder_field(widget, folder_field_name: str):
18
+ """
19
+ Add a browse button next to a folder path field in a magicgui widget.
20
+
21
+ Parameters
22
+ ----------
23
+ widget : magicgui widget
24
+ The widget containing the folder field
25
+ folder_field_name : str
26
+ The name of the folder field attribute
27
+
28
+ Returns
29
+ -------
30
+ QWidget
31
+ The modified widget with browse button
32
+ """
33
+ folder_field = getattr(widget, folder_field_name)
34
+
35
+ # Create browse button
36
+ browse_button = QPushButton("Browse...")
37
+
38
+ def on_browse_clicked():
39
+ current_value = folder_field.value
40
+ start_dir = current_value if current_value else os.path.expanduser("~")
41
+ folder = QFileDialog.getExistingDirectory(
42
+ None,
43
+ "Select Folder",
44
+ start_dir,
45
+ QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks,
46
+ )
47
+ if folder:
48
+ folder_field.value = folder
49
+
50
+ browse_button.clicked.connect(on_browse_clicked)
51
+
52
+ # Insert the browse button next to the folder_path field
53
+ field_layout = folder_field.native.parent().layout()
54
+ if field_layout:
55
+ field_layout.addWidget(browse_button)
56
+
57
+ return widget
napari_tmidas/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '0.2.2'
21
- __version_tuple__ = version_tuple = (0, 2, 2)
31
+ __version__ = version = '0.2.4'
32
+ __version_tuple__ = version_tuple = (0, 2, 4)
33
+
34
+ __commit_id__ = commit_id = None
napari_tmidas/_widget.py CHANGED
@@ -31,10 +31,47 @@ Replace code below according to your needs.
31
31
 
32
32
  from typing import TYPE_CHECKING
33
33
 
34
- from magicgui import magic_factory
35
- from magicgui.widgets import CheckBox, Container, create_widget
36
- from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget
37
- from skimage.util import img_as_float
34
+ # Lazy imports for optional heavy dependencies
35
+ try:
36
+ from magicgui import magic_factory
37
+ from magicgui.widgets import CheckBox, Container, create_widget
38
+
39
+ _HAS_MAGICGUI = True
40
+ except ImportError:
41
+ # Create stub decorator and stubs
42
+ def magic_factory(*args, **kwargs):
43
+ def decorator(func):
44
+ return func
45
+
46
+ if len(args) == 1 and callable(args[0]) and not kwargs:
47
+ return args[0]
48
+ return decorator
49
+
50
+ class Container:
51
+ pass
52
+
53
+ CheckBox = create_widget = None
54
+ _HAS_MAGICGUI = False
55
+
56
+ try:
57
+ from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget
58
+
59
+ _HAS_QTPY = True
60
+ except ImportError:
61
+
62
+ class QWidget:
63
+ pass
64
+
65
+ QHBoxLayout = QPushButton = None
66
+ _HAS_QTPY = False
67
+
68
+ try:
69
+ from skimage.util import img_as_float
70
+
71
+ _HAS_SKIMAGE = True
72
+ except ImportError:
73
+ img_as_float = None
74
+ _HAS_SKIMAGE = False
38
75
 
39
76
  if TYPE_CHECKING:
40
77
  import napari