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.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- 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,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()
|