flimkit 0.12.0__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 (104) hide show
  1. flimkit/FLIM/__init__.py +0 -0
  2. flimkit/FLIM/assemble.py +254 -0
  3. flimkit/FLIM/batch.py +681 -0
  4. flimkit/FLIM/bg_tools.py +51 -0
  5. flimkit/FLIM/fit_tools.py +244 -0
  6. flimkit/FLIM/fitters.py +1471 -0
  7. flimkit/FLIM/irf_tools.py +617 -0
  8. flimkit/FLIM/models.py +391 -0
  9. flimkit/GPU/__init__.py +85 -0
  10. flimkit/GPU/_base.py +391 -0
  11. flimkit/GPU/cuda.py +10 -0
  12. flimkit/GPU/mlx_backend.py +381 -0
  13. flimkit/GPU/mps.py +10 -0
  14. flimkit/GPU/rocm.py +10 -0
  15. flimkit/GPU/torch_backend.py +385 -0
  16. flimkit/UI/app_state.py +10 -0
  17. flimkit/UI/controller.py +139 -0
  18. flimkit/UI/expert_settings.py +248 -0
  19. flimkit/UI/fit_help.py +206 -0
  20. flimkit/UI/fov_preview.py +1085 -0
  21. flimkit/UI/gui.py +3919 -0
  22. flimkit/UI/icon.icns +0 -0
  23. flimkit/UI/icon.ico +0 -0
  24. flimkit/UI/icon.png +0 -0
  25. flimkit/UI/irf_widget.py +103 -0
  26. flimkit/UI/mode_controller.py +118 -0
  27. flimkit/UI/modes/__init__.py +0 -0
  28. flimkit/UI/modes/base.py +3 -0
  29. flimkit/UI/modes/batch_mode.py +312 -0
  30. flimkit/UI/modes/fov_mode.py +164 -0
  31. flimkit/UI/modes/irf_mode.py +80 -0
  32. flimkit/UI/modes/phasor_mode.py +131 -0
  33. flimkit/UI/modes/stitch_mode.py +254 -0
  34. flimkit/UI/phasor_panel.py +1087 -0
  35. flimkit/UI/progress_window.py +113 -0
  36. flimkit/UI/project_panel.py +262 -0
  37. flimkit/UI/results_panel.py +332 -0
  38. flimkit/UI/roi_tools.py +794 -0
  39. flimkit/UI/utils.py +217 -0
  40. flimkit/__init__.py +0 -0
  41. flimkit/_version.py +41 -0
  42. flimkit/cli.py +120 -0
  43. flimkit/configs.py +148 -0
  44. flimkit/dialogs.py +46 -0
  45. flimkit/formats/BH/__init__.py +0 -0
  46. flimkit/formats/BH/reader.py +296 -0
  47. flimkit/formats/BH/writer.py +86 -0
  48. flimkit/formats/ISS/__init__.py +0 -0
  49. flimkit/formats/ISS/fdflim.py +86 -0
  50. flimkit/formats/ISS/image.py +114 -0
  51. flimkit/formats/ISS/reader.py +223 -0
  52. flimkit/formats/PS/__init__.py +0 -0
  53. flimkit/formats/PS/reader.py +202 -0
  54. flimkit/formats/PTU/__init__.py +0 -0
  55. flimkit/formats/PTU/decode.py +27 -0
  56. flimkit/formats/PTU/phu.py +85 -0
  57. flimkit/formats/PTU/reader.py +235 -0
  58. flimkit/formats/PTU/series.py +258 -0
  59. flimkit/formats/PTU/stitch.py +1182 -0
  60. flimkit/formats/PTU/tools.py +94 -0
  61. flimkit/formats/__init__.py +2 -0
  62. flimkit/formats/flim_file.py +232 -0
  63. flimkit/formats/phasor.py +132 -0
  64. flimkit/formats/signal.py +170 -0
  65. flimkit/image/tools.py +124 -0
  66. flimkit/interactive.py +1857 -0
  67. flimkit/mpl_backend.py +22 -0
  68. flimkit/phasor/__init__.py +40 -0
  69. flimkit/phasor/filters.py +127 -0
  70. flimkit/phasor/fret.py +654 -0
  71. flimkit/phasor/interactive.py +556 -0
  72. flimkit/phasor/peaks.py +186 -0
  73. flimkit/phasor/signal.py +90 -0
  74. flimkit/phasor_launcher.py +314 -0
  75. flimkit/plugins/__init__.py +137 -0
  76. flimkit/plugins/bindings.py +116 -0
  77. flimkit/plugins/builtin/__init__.py +3 -0
  78. flimkit/plugins/builtin/core_tools.py +28 -0
  79. flimkit/plugins/loader.py +371 -0
  80. flimkit/plugins/registry.py +406 -0
  81. flimkit/project.py +197 -0
  82. flimkit/synth.py +145 -0
  83. flimkit/utils/__init__.py +0 -0
  84. flimkit/utils/batch_fit.py +301 -0
  85. flimkit/utils/config_manager.py +119 -0
  86. flimkit/utils/config_snapshot.py +30 -0
  87. flimkit/utils/crash_handler.py +183 -0
  88. flimkit/utils/display.py +197 -0
  89. flimkit/utils/enhanced_outputs.py +345 -0
  90. flimkit/utils/fancy.py +103 -0
  91. flimkit/utils/lifetime_image.py +243 -0
  92. flimkit/utils/misc.py +111 -0
  93. flimkit/utils/plotting.py +190 -0
  94. flimkit/utils/roi.py +370 -0
  95. flimkit/utils/session.py +51 -0
  96. flimkit/utils/update_check.py +198 -0
  97. flimkit/utils/xlsx_tools.py +97 -0
  98. flimkit/utils/xml_utils.py +219 -0
  99. flimkit-0.12.0.dist-info/METADATA +356 -0
  100. flimkit-0.12.0.dist-info/RECORD +104 -0
  101. flimkit-0.12.0.dist-info/WHEEL +5 -0
  102. flimkit-0.12.0.dist-info/entry_points.txt +2 -0
  103. flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
  104. flimkit-0.12.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,137 @@
1
+ from flimkit.plugins.bindings import (
2
+ export_rois_geojson,
3
+ get_current_images,
4
+ import_rois_geojson,
5
+ )
6
+ from flimkit.plugins.registry import (
7
+ API_VERSION,
8
+ Format,
9
+ PanelButton,
10
+ Startup,
11
+ PluginError,
12
+ PhasorFilter,
13
+ Sniffer,
14
+ Tool,
15
+ clear,
16
+ file_format,
17
+ format_sniffer,
18
+ formats,
19
+ get_format,
20
+ get_phasor_filter,
21
+ get_panel_button,
22
+ get_tool,
23
+ panel_button,
24
+ panel_buttons,
25
+ phasor_filter,
26
+ phasor_filters,
27
+ register_format,
28
+ register_panel_button,
29
+ register_phasor_filter,
30
+ register_sniffer,
31
+ register_startup,
32
+ run_startups,
33
+ startup,
34
+ startups,
35
+ register_tool,
36
+ sniffers,
37
+ sources,
38
+ tool,
39
+ tools,
40
+ version,
41
+ )
42
+ from flimkit.plugins.loader import (
43
+ PluginConfig,
44
+ allow_user_plugins,
45
+ ENTRY_POINT_GROUP,
46
+ add_archive,
47
+ archives,
48
+ candidates,
49
+ config_dirs,
50
+ disabled_plugins,
51
+ ensure_loaded,
52
+ entry_points,
53
+ extra_dirs,
54
+ failures,
55
+ load_module,
56
+ load_path,
57
+ load_report,
58
+ pending_user_plugins,
59
+ plugin_config,
60
+ plugins_enabled,
61
+ portable_wheel,
62
+ reset,
63
+ set_config_dirs,
64
+ set_plugin_disabled,
65
+ set_plugins_enabled,
66
+ short_name,
67
+ skipped,
68
+ user_dir,
69
+ user_plugins_allowed,
70
+ )
71
+
72
+ __all__ = [
73
+ 'startups',
74
+ 'startup',
75
+ 'run_startups',
76
+ 'register_startup',
77
+ 'register_panel_button',
78
+ 'panel_buttons',
79
+ 'panel_button',
80
+ 'get_panel_button',
81
+ 'Startup',
82
+ 'PanelButton',
83
+ 'API_VERSION',
84
+ 'ENTRY_POINT_GROUP',
85
+ 'Format',
86
+ 'PhasorFilter',
87
+ 'PluginConfig',
88
+ 'PluginError',
89
+ 'Sniffer',
90
+ 'Tool',
91
+ 'add_archive',
92
+ 'allow_user_plugins',
93
+ 'archives',
94
+ 'candidates',
95
+ 'clear',
96
+ 'config_dirs',
97
+ 'disabled_plugins',
98
+ 'ensure_loaded',
99
+ 'entry_points',
100
+ 'export_rois_geojson',
101
+ 'extra_dirs',
102
+ 'failures',
103
+ 'file_format',
104
+ 'format_sniffer',
105
+ 'formats',
106
+ 'get_current_images',
107
+ 'get_format',
108
+ 'get_phasor_filter',
109
+ 'get_tool',
110
+ 'import_rois_geojson',
111
+ 'load_module',
112
+ 'load_path',
113
+ 'load_report',
114
+ 'pending_user_plugins',
115
+ 'phasor_filter',
116
+ 'phasor_filters',
117
+ 'plugin_config',
118
+ 'plugins_enabled',
119
+ 'portable_wheel',
120
+ 'register_format',
121
+ 'register_phasor_filter',
122
+ 'register_sniffer',
123
+ 'register_tool',
124
+ 'reset',
125
+ 'set_config_dirs',
126
+ 'set_plugin_disabled',
127
+ 'set_plugins_enabled',
128
+ 'short_name',
129
+ 'skipped',
130
+ 'sniffers',
131
+ 'sources',
132
+ 'tool',
133
+ 'tools',
134
+ 'user_dir',
135
+ 'user_plugins_allowed',
136
+ 'version',
137
+ ]
@@ -0,0 +1,116 @@
1
+ import threading
2
+ from typing import Any, Callable, Dict, List, TypeVar, cast
3
+
4
+
5
+ _T = TypeVar('_T')
6
+
7
+
8
+ def _fov_preview(app):
9
+ preview = getattr(app, '_fov_preview', None)
10
+ if preview is None:
11
+ raise RuntimeError('FOV preview is not available')
12
+ return preview
13
+
14
+
15
+ def _roi_manager(preview):
16
+ manager = getattr(preview, '_roi_manager', None)
17
+ if manager is None:
18
+ raise RuntimeError('ROI manager is not available')
19
+ return manager
20
+
21
+
22
+ def _run_on_ui_thread(app, callback: Callable[[], _T]) -> _T:
23
+ if threading.current_thread() is threading.main_thread():
24
+ return callback()
25
+
26
+ root = getattr(app, 'root', None)
27
+ if root is None or not callable(getattr(root, 'after', None)):
28
+ raise RuntimeError('FLIMKit UI is not available')
29
+
30
+ done = threading.Event()
31
+ outcome = {}
32
+
33
+ def run():
34
+ try:
35
+ outcome['value'] = callback()
36
+ except BaseException as error:
37
+ outcome['error'] = error
38
+ finally:
39
+ done.set()
40
+
41
+ root.after(0, run)
42
+ done.wait()
43
+ if 'error' in outcome:
44
+ raise outcome['error']
45
+ return cast(_T, outcome.get('value'))
46
+
47
+
48
+ def get_current_images(app) -> Dict[str, Any]:
49
+ """Return copies of the intensity and lifetime images currently shown."""
50
+ import numpy as np
51
+
52
+ def snapshot():
53
+ preview = _fov_preview(app)
54
+ images = {}
55
+ for name, attribute in (
56
+ ('intensity', '_intensity_map'),
57
+ ('lifetime', '_lifetime_map'),
58
+ ):
59
+ image = getattr(preview, attribute, None)
60
+ if image is None:
61
+ continue
62
+ array = np.asarray(image)
63
+ if array.ndim < 2:
64
+ raise RuntimeError(f'{name} image must have at least two dimensions')
65
+ if name == 'intensity' and array.ndim > 2:
66
+ trailing_axes = tuple(range(2, array.ndim))
67
+ array = array.sum(axis=trailing_axes)
68
+ elif name == 'lifetime' and array.ndim != 2:
69
+ raise RuntimeError('lifetime image must be 2D')
70
+ images[name] = np.array(array, copy=True)
71
+ units = {
72
+ 'intensity': 'photons',
73
+ 'lifetime': 'ns',
74
+ }
75
+ return {
76
+ 'images': images,
77
+ 'units': {
78
+ name: units[name]
79
+ for name in images
80
+ },
81
+ }
82
+
83
+ return _run_on_ui_thread(app, snapshot)
84
+
85
+
86
+ def export_rois_geojson(app) -> Dict:
87
+ """Return the current FLIMKit ROIs as a GeoJSON FeatureCollection."""
88
+ def export():
89
+ manager = _roi_manager(_fov_preview(app))
90
+ return manager.to_geojson()
91
+
92
+ return _run_on_ui_thread(app, export)
93
+
94
+
95
+ def import_rois_geojson(app, payload: Dict, mode: str = 'append') -> List[int]:
96
+ """Import GeoJSON ROIs and refresh the FLIMKit ROI display."""
97
+ def import_rois():
98
+ preview = _fov_preview(app)
99
+ manager = _roi_manager(preview)
100
+ region_ids = manager.add_geojson(payload, mode=mode)
101
+ if region_ids or mode == 'replace':
102
+ redraw = getattr(preview, '_redraw_region_overlays', None)
103
+ if callable(redraw):
104
+ redraw()
105
+ save = getattr(preview, '_save_regions_update', None)
106
+ if callable(save):
107
+ save()
108
+ panel = getattr(preview, '_roi_analysis_panel', None)
109
+ if panel is None:
110
+ panel = getattr(app, '_roi_analysis_panel', None)
111
+ refresh = getattr(panel, '_refresh_region_list', None)
112
+ if callable(refresh):
113
+ refresh()
114
+ return region_ids
115
+
116
+ return _run_on_ui_thread(app, import_rois)
@@ -0,0 +1,3 @@
1
+ BUILTIN = (
2
+ 'flimkit.plugins.builtin.core_tools',
3
+ )
@@ -0,0 +1,28 @@
1
+ from flimkit.plugins import tool
2
+
3
+ FLIMKIT_PLUGIN_API = 1
4
+
5
+
6
+ @tool(id='irf_builder', label='Machine IRF Builder', menu='Tools', order=10)
7
+ def open_irf_builder(app):
8
+ app._menu_irf_builder()
9
+
10
+
11
+ @tool(id='synth_generator', label='Generate Synthetic PTU...', menu='Tools', order=20)
12
+ def open_synth_generator(app):
13
+ app._menu_synth_generator()
14
+
15
+
16
+ @tool(id='batch_tiled', label='Multi-Tile ROI Fit', menu='Tools/Batch Processing', order=110)
17
+ def open_batch_tiled(app):
18
+ app._menu_batch_processing('tiled')
19
+
20
+
21
+ @tool(id='batch_fov', label='Single FOV Fit', menu='Tools/Batch Processing', order=120)
22
+ def open_batch_fov(app):
23
+ app._menu_batch_processing('fov')
24
+
25
+
26
+ @tool(id='batch_timelapse', label='Timelapse Fit', menu='Tools/Batch Processing', order=130)
27
+ def open_batch_timelapse(app):
28
+ app._menu_batch_processing('timelapse')
@@ -0,0 +1,371 @@
1
+ import importlib
2
+ import importlib.util
3
+ import os
4
+ import sys
5
+ import threading
6
+ import traceback
7
+
8
+ from flimkit.plugins import registry
9
+ from flimkit.plugins.builtin import BUILTIN
10
+
11
+ _lock = threading.RLock()
12
+ _loaded = False
13
+ _report = []
14
+ _skipped = []
15
+
16
+
17
+ class LoadResult:
18
+
19
+ def __init__(self, source, ok, n_registered, error=None):
20
+ self.source = source
21
+ self.ok = ok
22
+ self.n_registered = n_registered
23
+ self.error = error
24
+
25
+ def __repr__(self):
26
+ state = 'ok' if self.ok else 'failed'
27
+ return f'<LoadResult {self.source!r} {state} n={self.n_registered}>'
28
+
29
+
30
+ def disabled():
31
+ return os.environ.get('FLIMKIT_NO_PLUGINS', '') not in ('', '0', 'false', 'False')
32
+
33
+
34
+ def _record(result):
35
+ _report.append(result)
36
+ return result
37
+
38
+
39
+ def _api_ok(module, source):
40
+ declared = getattr(module, 'FLIMKIT_PLUGIN_API', registry.API_VERSION)
41
+ if declared != registry.API_VERSION:
42
+ raise registry.PluginError(
43
+ f'plugin {source!r} declares FLIMKIT_PLUGIN_API {declared!r}, '
44
+ f'this FLIMKit provides {registry.API_VERSION}')
45
+
46
+
47
+ def _load(source, importer):
48
+ before = registry.count()
49
+ prev = registry._set_source(source)
50
+ print(f'[Plugins] loading {source}')
51
+ try:
52
+ module = importer()
53
+ _api_ok(module, source)
54
+ except KeyboardInterrupt:
55
+ registry._set_source(prev)
56
+ registry._rollback(source)
57
+ raise
58
+ except BaseException:
59
+ registry._set_source(prev)
60
+ dropped = registry._rollback(source)
61
+ err = traceback.format_exc()
62
+ print(f'[Plugins] {source} failed, {len(dropped)} registration(s) rolled back')
63
+ return _record(LoadResult(source, False, 0, err))
64
+ registry._set_source(prev)
65
+ return _record(LoadResult(source, True, registry.count() - before))
66
+
67
+
68
+ def load_module(dotted, source=None):
69
+ source = source or dotted
70
+
71
+ def importer():
72
+ cached = sys.modules.get(dotted)
73
+ if cached is not None:
74
+ return importlib.reload(cached)
75
+ return importlib.import_module(dotted)
76
+
77
+ return _load(source, importer)
78
+
79
+
80
+ def load_path(path, source=None):
81
+ path = os.path.abspath(path)
82
+ stem = os.path.basename(path.rstrip(os.sep))
83
+ if os.path.isdir(path):
84
+ init = os.path.join(path, '__init__.py')
85
+ submodules = [path]
86
+ else:
87
+ init = path
88
+ stem = os.path.splitext(stem)[0]
89
+ submodules = None
90
+ name = 'flimkit_plugin_' + stem
91
+ source = source or path
92
+
93
+ def importer():
94
+ spec = importlib.util.spec_from_file_location(
95
+ name, init, submodule_search_locations=submodules)
96
+ if spec is None or spec.loader is None:
97
+ raise registry.PluginError(f'cannot import plugin {path!r}')
98
+ module = importlib.util.module_from_spec(spec)
99
+ sys.modules[name] = module
100
+ try:
101
+ spec.loader.exec_module(module)
102
+ except BaseException:
103
+ sys.modules.pop(name, None)
104
+ raise
105
+ return module
106
+
107
+ return _load(source, importer)
108
+
109
+
110
+ def candidates(directory):
111
+ if not os.path.isdir(directory):
112
+ return []
113
+ found = []
114
+ for entry in sorted(os.listdir(directory)):
115
+ if entry.startswith(('_', '.')):
116
+ continue
117
+ path = os.path.join(directory, entry)
118
+ if os.path.isdir(path):
119
+ if os.path.isfile(os.path.join(path, '__init__.py')):
120
+ found.append(path)
121
+ elif entry.endswith('.py'):
122
+ found.append(path)
123
+ return found
124
+
125
+
126
+ def archives(directory):
127
+ if not os.path.isdir(directory):
128
+ return []
129
+ found = []
130
+ for entry in sorted(os.listdir(directory)):
131
+ if entry.startswith(('_', '.')):
132
+ continue
133
+ if entry.endswith(('.whl', '.zip')):
134
+ found.append(os.path.join(directory, entry))
135
+ return found
136
+
137
+
138
+ def portable_wheel(path):
139
+ name = os.path.basename(path)
140
+ if not name.endswith('.whl'):
141
+ return True
142
+ parts = name[:-4].split('-')
143
+ if len(parts) < 5:
144
+ return False
145
+ return parts[-1] == 'any' and parts[-2] == 'none'
146
+
147
+
148
+ def add_archive(path):
149
+ name = short_name(path)
150
+ if name in disabled_plugins():
151
+ print(f'[Plugins] {name} disabled, skipping')
152
+ _skipped.append(name)
153
+ return False
154
+ if not portable_wheel(path):
155
+ print(f'[Plugins] {name} is built for one platform and cannot be imported '
156
+ f'from a zip, install it with pip instead')
157
+ _record(LoadResult(path, False, 0,
158
+ 'wheel is not py3-none-any, so it cannot be imported from a zip'))
159
+ return False
160
+ if path not in sys.path:
161
+ sys.path.insert(0, path)
162
+ importlib.invalidate_caches()
163
+ print(f'[Plugins] added {path} to the import path')
164
+ return True
165
+
166
+
167
+ def user_dir():
168
+ return os.path.join(os.path.expanduser('~'), '.flimkit', 'plugins')
169
+
170
+
171
+ def extra_dirs():
172
+ raw = os.environ.get('FLIMKIT_PLUGIN_PATH', '')
173
+ found = [os.path.expanduser(p) for p in raw.split(os.pathsep) if p.strip()]
174
+ for p in config_dirs():
175
+ if p not in found:
176
+ found.append(p)
177
+ return found
178
+
179
+
180
+ def config_dirs():
181
+ from flimkit.utils.config_manager import cfg
182
+ found = cfg.get('plugins.paths', [])
183
+ if isinstance(found, str):
184
+ found = found.split(os.pathsep)
185
+ if isinstance(found, (list, tuple)) == False:
186
+ return []
187
+ return [os.path.expanduser(str(p)) for p in found if str(p).strip()]
188
+
189
+
190
+ def set_config_dirs(paths):
191
+ from flimkit.utils.config_manager import cfg
192
+ cfg.set('plugins.paths', [str(p) for p in paths if str(p).strip()])
193
+ cfg.save()
194
+
195
+
196
+ def plugins_enabled():
197
+ from flimkit.utils.config_manager import cfg
198
+ return cfg.get('plugins.enabled', True) != False
199
+
200
+
201
+ def set_plugins_enabled(enabled=True):
202
+ from flimkit.utils.config_manager import cfg
203
+ cfg.set('plugins.enabled', bool(enabled))
204
+ cfg.save()
205
+
206
+
207
+ def user_plugins_allowed():
208
+ from flimkit.utils.config_manager import cfg
209
+ return cfg.get('plugins.allow_user_plugins', False) == True
210
+
211
+
212
+ def pending_user_plugins():
213
+ if user_plugins_allowed():
214
+ return []
215
+ return candidates(user_dir())
216
+
217
+
218
+ def allow_user_plugins(allowed=True):
219
+ from flimkit.utils.config_manager import cfg
220
+ cfg.set('plugins.allow_user_plugins', bool(allowed))
221
+ cfg.save()
222
+
223
+
224
+ def short_name(source):
225
+ name = os.path.basename(str(source).rstrip(os.sep))
226
+ if name.endswith(('.whl', '.zip')):
227
+ return name.rsplit('.', 1)[0].split('-')[0]
228
+ if name.endswith('.py'):
229
+ name = name[:-3]
230
+ if '.' in name and os.sep not in name:
231
+ name = name.rsplit('.', 1)[-1]
232
+ return name
233
+
234
+
235
+ def disabled_plugins():
236
+ from flimkit.utils.config_manager import cfg
237
+ found = cfg.get('plugins.disabled', [])
238
+ if isinstance(found, (list, tuple)) == False:
239
+ return []
240
+ return [str(x) for x in found]
241
+
242
+
243
+ def set_plugin_disabled(name, disabled=True):
244
+ from flimkit.utils.config_manager import cfg
245
+ current = [n for n in disabled_plugins() if n != name]
246
+ if disabled:
247
+ current.append(name)
248
+ cfg.set('plugins.disabled', sorted(current))
249
+ cfg.save()
250
+ return current
251
+
252
+
253
+ def plugin_config(name):
254
+ return PluginConfig(name)
255
+
256
+
257
+ class PluginConfig:
258
+
259
+ def __init__(self, name):
260
+ self.section = 'plugin:' + short_name(name)
261
+
262
+ def get(self, key, default=None):
263
+ from flimkit.utils.config_manager import cfg
264
+ return cfg.get(self.section + '.' + key, default)
265
+
266
+ def set(self, key, value):
267
+ from flimkit.utils.config_manager import cfg
268
+ cfg.set(self.section + '.' + key, value)
269
+
270
+ def save(self):
271
+ from flimkit.utils.config_manager import cfg
272
+ cfg.save()
273
+
274
+ def all(self):
275
+ from flimkit.utils.config_manager import cfg
276
+ return cfg.get(self.section)
277
+
278
+ def __repr__(self):
279
+ return f'<PluginConfig {self.section!r}>'
280
+
281
+
282
+ ENTRY_POINT_GROUP = 'flimkit.plugins'
283
+
284
+
285
+ def entry_points():
286
+ try:
287
+ from importlib.metadata import entry_points as _eps
288
+ except ImportError:
289
+ return []
290
+ try:
291
+ return sorted(_eps(group=ENTRY_POINT_GROUP), key=lambda e: e.name)
292
+ except Exception as exc:
293
+ print(f'[Plugins] entry point lookup failed: {exc}')
294
+ return []
295
+
296
+
297
+ def _load_entry_point(ep):
298
+ if ep.name in disabled_plugins():
299
+ print(f'[Plugins] {ep.name} disabled, skipping')
300
+ _skipped.append(ep.name)
301
+ return None
302
+ source = f'{ENTRY_POINT_GROUP}:{ep.name}'
303
+ return _load(source, ep.load)
304
+
305
+
306
+ def _load_unless_disabled(target, is_module=False):
307
+ name = short_name(target)
308
+ if name in disabled_plugins():
309
+ print(f'[Plugins] {name} disabled, skipping')
310
+ _skipped.append(name)
311
+ return None
312
+ return load_module(target) if is_module else load_path(target)
313
+
314
+
315
+ def ensure_loaded():
316
+ global _loaded
317
+ with _lock:
318
+ if _loaded:
319
+ return list(_report)
320
+ _loaded = True
321
+ if disabled():
322
+ print('[Plugins] FLIMKIT_NO_PLUGINS set, loading nothing')
323
+ return list(_report)
324
+ if plugins_enabled() == False:
325
+ print('[Plugins] plugins.enabled is false in the config, loading nothing')
326
+ return list(_report)
327
+ for dotted in BUILTIN:
328
+ _load_unless_disabled(dotted, is_module=True)
329
+ home = user_dir()
330
+ allowed = user_plugins_allowed()
331
+ if allowed:
332
+ for path in archives(home):
333
+ add_archive(path)
334
+ for directory in extra_dirs():
335
+ for path in archives(directory):
336
+ add_archive(path)
337
+ for ep in entry_points():
338
+ _load_entry_point(ep)
339
+ if allowed:
340
+ for path in candidates(home):
341
+ _load_unless_disabled(path)
342
+ else:
343
+ waiting = candidates(home) + archives(home)
344
+ if waiting:
345
+ print(f'[Plugins] {len(waiting)} plugin(s) in {home} not loaded, '
346
+ f'set plugins.allow_user_plugins to enable them')
347
+ for directory in extra_dirs():
348
+ for path in candidates(directory):
349
+ _load_unless_disabled(path)
350
+ return list(_report)
351
+
352
+
353
+ def load_report():
354
+ return list(_report)
355
+
356
+
357
+ def failures():
358
+ return [r for r in _report if not r.ok]
359
+
360
+
361
+ def skipped():
362
+ return list(_skipped)
363
+
364
+
365
+ def reset():
366
+ global _loaded
367
+ with _lock:
368
+ _loaded = False
369
+ _report.clear()
370
+ _skipped.clear()
371
+ registry.clear()