napari-tmidas 0.2.1__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.
- napari_tmidas/__init__.py +35 -5
- napari_tmidas/_crop_anything.py +1458 -499
- napari_tmidas/_env_manager.py +76 -0
- napari_tmidas/_file_conversion.py +1646 -1131
- napari_tmidas/_file_selector.py +1464 -223
- napari_tmidas/_label_inspection.py +83 -8
- napari_tmidas/_processing_worker.py +309 -0
- napari_tmidas/_reader.py +6 -10
- napari_tmidas/_registry.py +15 -14
- napari_tmidas/_roi_colocalization.py +1221 -84
- napari_tmidas/_tests/test_crop_anything.py +123 -0
- napari_tmidas/_tests/test_env_manager.py +89 -0
- napari_tmidas/_tests/test_file_selector.py +90 -0
- napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
- napari_tmidas/_tests/test_init.py +98 -0
- napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
- napari_tmidas/_tests/test_label_inspection.py +86 -0
- napari_tmidas/_tests/test_processing_basic.py +500 -0
- napari_tmidas/_tests/test_processing_worker.py +142 -0
- napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
- napari_tmidas/_tests/test_registry.py +135 -0
- napari_tmidas/_tests/test_scipy_filters.py +168 -0
- napari_tmidas/_tests/test_skimage_filters.py +259 -0
- napari_tmidas/_tests/test_split_channels.py +217 -0
- napari_tmidas/_tests/test_spotiflow.py +87 -0
- napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
- napari_tmidas/_tests/test_ui_utils.py +68 -0
- napari_tmidas/_tests/test_widget.py +30 -0
- napari_tmidas/_tests/test_windows_basic.py +66 -0
- napari_tmidas/_ui_utils.py +57 -0
- napari_tmidas/_version.py +16 -3
- napari_tmidas/_widget.py +41 -4
- napari_tmidas/processing_functions/basic.py +557 -20
- napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
- napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
- napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
- napari_tmidas/processing_functions/colocalization.py +513 -56
- napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
- napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
- napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
- napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
- napari_tmidas/processing_functions/sam2_mp4.py +274 -195
- napari_tmidas/processing_functions/scipy_filters.py +403 -8
- napari_tmidas/processing_functions/skimage_filters.py +424 -212
- napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
- napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
- napari_tmidas/processing_functions/timepoint_merger.py +334 -86
- napari_tmidas/processing_functions/trackastra_tracking.py +24 -5
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +92 -39
- napari_tmidas-0.2.4.dist-info/RECORD +63 -0
- napari_tmidas/_tests/__init__.py +0 -0
- napari_tmidas-0.2.1.dist-info/RECORD +0 -38
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.2.1.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__ = [
|
|
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 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
|
-
|
|
35
|
-
|
|
36
|
-
from
|
|
37
|
-
from
|
|
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
|