cubevis 1.0.14__py3-none-any.whl → 1.0.21__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.
- cubevis/__js__/bokeh-3.6/cubevisjs.min.js +13 -10
- cubevis/__js__/bokeh-3.7/cubevisjs.min.js +12 -8
- cubevis/__js__/bokeh-3.8/cubevisjs.min.js +10 -8
- cubevis/__version__.py +1 -1
- cubevis/bokeh/models/_bokeh_app_context.py +48 -1
- cubevis/bokeh/models/_showable.py +108 -82
- cubevis/exe/_task.py +36 -16
- cubevis/private/apps/_createmask.py +43 -41
- cubevis/private/apps/_createregion.py +31 -29
- cubevis/private/apps/_interactiveclean.mustache +2 -1
- cubevis/private/apps/_interactiveclean.py +2 -1
- cubevis/private/apps/_interactivecleannotebook.mustache +24 -1
- cubevis/private/apps/_interactivecleannotebook.py +24 -1
- cubevis/toolbox/__init__.py +0 -1
- cubevis/toolbox/_cube.py +40 -23
- cubevis/toolbox/_interactive_clean_ui.mustache +71 -44
- cubevis/toolbox/_interactive_clean_ui.py +71 -44
- cubevis/utils/__init__.py +1 -0
- cubevis/utils/_mutual_exclusion.py +117 -0
- {cubevis-1.0.14.dist-info → cubevis-1.0.21.dist-info}/METADATA +2 -2
- {cubevis-1.0.14.dist-info → cubevis-1.0.21.dist-info}/RECORD +23 -22
- {cubevis-1.0.14.dist-info → cubevis-1.0.21.dist-info}/WHEEL +0 -0
- {cubevis-1.0.14.dist-info → cubevis-1.0.21.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,13 @@ import logging
|
|
|
2
2
|
from bokeh.core.properties import String, Dict, Any, Nullable, Instance
|
|
3
3
|
from bokeh.models.layouts import LayoutDOM
|
|
4
4
|
from bokeh.models.ui import UIElement
|
|
5
|
+
from bokeh.resources import CDN
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
5
7
|
from uuid import uuid4
|
|
8
|
+
import unicodedata
|
|
9
|
+
import webbrowser
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
6
12
|
|
|
7
13
|
logger = logging.getLogger(__name__)
|
|
8
14
|
|
|
@@ -29,9 +35,34 @@ class BokehAppContext(LayoutDOM):
|
|
|
29
35
|
cls._session_id = str(uuid4())
|
|
30
36
|
return cls._session_id
|
|
31
37
|
|
|
32
|
-
def
|
|
38
|
+
def _slugify(self, value, allow_unicode=False):
|
|
39
|
+
"""
|
|
40
|
+
Taken from https://github.com/django/django/blob/master/django/utils/text.py
|
|
41
|
+
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
|
|
42
|
+
dashes to single dashes. Remove characters that aren't alphanumerics,
|
|
43
|
+
underscores, or hyphens. Convert to lowercase. Also strip leading and
|
|
44
|
+
trailing whitespace, dashes, and underscores.
|
|
45
|
+
https://stackoverflow.com/a/295466/2903943
|
|
46
|
+
"""
|
|
47
|
+
value = str(value)
|
|
48
|
+
if allow_unicode:
|
|
49
|
+
value = unicodedata.normalize('NFKC', value)
|
|
50
|
+
else:
|
|
51
|
+
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
|
|
52
|
+
value = re.sub(r'[^\w\s-]', '', value.lower())
|
|
53
|
+
return re.sub(r'[-\s]+', '-', value).strip('-_')
|
|
54
|
+
|
|
55
|
+
def __init__( self, ui=None, title=str(uuid4( )), prefix=None, **kwargs ):
|
|
33
56
|
logger.debug(f"\tBokehAppContext::__init__(ui={type(ui).__name__ if ui else None}, {kwargs}): {id(self)}")
|
|
34
57
|
|
|
58
|
+
if prefix is None:
|
|
59
|
+
## create a prefix from the title
|
|
60
|
+
prefix = self._slugify(title)[:10]
|
|
61
|
+
|
|
62
|
+
self.__title = title
|
|
63
|
+
self.__workdir = TemporaryDirectory(prefix=prefix)
|
|
64
|
+
self.__htmlpath = os.path.join( self.__workdir.name, f'''{self._slugify(self.__title)}.html''' )
|
|
65
|
+
|
|
35
66
|
if ui is not None and 'ui' in kwargs:
|
|
36
67
|
raise RuntimeError( "'ui' supplied as both a positional parameter and a keyword parameter" )
|
|
37
68
|
|
|
@@ -61,3 +92,19 @@ class BokehAppContext(LayoutDOM):
|
|
|
61
92
|
current_state = dict(self.app_state)
|
|
62
93
|
current_state.update(state_updates)
|
|
63
94
|
self.app_state = current_state
|
|
95
|
+
|
|
96
|
+
def show( self ):
|
|
97
|
+
"""Always show plot in a new browser tab without changing output settings.
|
|
98
|
+
Jupyter display is handled by the Showable class. However, at some
|
|
99
|
+
point this function might need to support more than just independent
|
|
100
|
+
browser tab display.
|
|
101
|
+
"""
|
|
102
|
+
logger.debug(f"\tBokehAppContext::show( ): {id(self)}")
|
|
103
|
+
|
|
104
|
+
from bokeh.plotting import save
|
|
105
|
+
|
|
106
|
+
# Save the plot
|
|
107
|
+
save( self, filename=self.__htmlpath, resources=CDN, title=self.__title)
|
|
108
|
+
|
|
109
|
+
# Open in browser
|
|
110
|
+
webbrowser.open('file://' + os.path.abspath(self.__htmlpath))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from bokeh.models.layouts import LayoutDOM
|
|
3
3
|
from bokeh.models.ui import UIElement
|
|
4
|
-
from bokeh.core.properties import Instance
|
|
4
|
+
from bokeh.core.properties import Instance, String
|
|
5
|
+
|
|
5
6
|
from bokeh.io import curdoc
|
|
6
7
|
from .. import BokehInit
|
|
7
8
|
|
|
@@ -9,12 +10,20 @@ logger = logging.getLogger(__name__)
|
|
|
9
10
|
|
|
10
11
|
class Showable(LayoutDOM,BokehInit):
|
|
11
12
|
"""Wrap a UIElement to make any Bokeh UI component showable with show()
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
This class works by acting as a simple container that delegates to its UI element.
|
|
14
15
|
For Jupyter notebook display, use show(showable) - automatic display via _repr_mimebundle_
|
|
15
16
|
is not reliably supported by Bokeh's architecture.
|
|
16
17
|
"""
|
|
17
18
|
|
|
19
|
+
### _usage_mode is needed to prevent mixing "bokeh.plotting.show(showable)" with
|
|
20
|
+
### "showable.show( )" or just evaluating "showable". This is required because the
|
|
21
|
+
### latter use "bokeh.embed.components" to create the HTML that is rendered while
|
|
22
|
+
### "bokeh.plotting.show(showable)" uses internal Bokeh rendering that is
|
|
23
|
+
### incompatable with "bokeh.embed.components" usage. For this reason, the user
|
|
24
|
+
### can use either one, but not both.
|
|
25
|
+
_usage_mode = None
|
|
26
|
+
|
|
18
27
|
@property
|
|
19
28
|
def document(self):
|
|
20
29
|
"""Get the document this model is attached to."""
|
|
@@ -26,58 +35,52 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
26
35
|
Intercept when Bokeh tries to attach us to a document.
|
|
27
36
|
This is called by bokeh.plotting.show() when it adds us to a document.
|
|
28
37
|
"""
|
|
29
|
-
from bokeh.io.state import curstate
|
|
30
|
-
import traceback
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
def get_caller_class_name(frame):
|
|
40
|
+
"""Attempt to find the name of the class the calling method belongs to."""
|
|
41
|
+
# Check if 'self' is in the caller's local variables (conventional for instance methods)
|
|
42
|
+
if 'self' in frame.f_locals:
|
|
43
|
+
return frame.f_locals['self'].__class__.__name__
|
|
44
|
+
# Check for 'cls' (conventional for class methods)
|
|
45
|
+
elif 'cls' in frame.f_locals:
|
|
46
|
+
return frame.f_locals['cls'].__name__
|
|
47
|
+
else:
|
|
48
|
+
# It might be a regular function or static method without explicit 'self'/'cls'
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Allow None (detaching from document) without any further checking
|
|
33
52
|
if doc is None:
|
|
34
53
|
self._document = None
|
|
35
54
|
return
|
|
36
55
|
|
|
37
|
-
state
|
|
38
|
-
|
|
39
|
-
# Check calling context
|
|
40
|
-
stack = traceback.extract_stack()
|
|
41
|
-
|
|
42
|
-
# Detect if called from bokeh.plotting.show or bokeh.io.show
|
|
43
|
-
called_from_bokeh_show = any(
|
|
44
|
-
('bokeh/io/' in frame.filename or 'bokeh\\io\\' in frame.filename or
|
|
45
|
-
'bokeh/plotting/' in frame.filename or 'bokeh\\plotting\\' in frame.filename)
|
|
46
|
-
for frame in stack[:-2] # Exclude the last 2 frames (this setter and __setattr__)
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# Check if called from our own methods
|
|
50
|
-
called_from_our_methods = any(
|
|
51
|
-
'Showable' in str(frame.line) or frame.filename.endswith('showable.py')
|
|
52
|
-
for frame in stack[-5:-2] # Check recent frames
|
|
53
|
-
)
|
|
56
|
+
from bokeh.io.state import curstate
|
|
57
|
+
state = curstate( )
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
f"Please use one of these methods instead:\n"
|
|
60
|
-
f" • my_showable.show() # Custom show method\n"
|
|
61
|
-
f" • my_showable # Automatic display (evaluate in cell)\n\n"
|
|
62
|
-
f"Reason: bokeh.plotting.show() doesn't properly handle the custom\n"
|
|
63
|
-
f"sizing and backend requirements of Showable objects.\n"
|
|
64
|
-
f"{'='*70}\n"
|
|
65
|
-
)
|
|
59
|
+
# Validate environment (only one OUTPUT mode)
|
|
60
|
+
active_modes = []
|
|
61
|
+
if state.file: active_modes.append('file')
|
|
62
|
+
if state.notebook: active_modes.append('notebook')
|
|
66
63
|
|
|
67
|
-
#
|
|
68
|
-
if
|
|
64
|
+
# only allow a single GUI to be displayed since there is a backend
|
|
65
|
+
# this could be relaxed if the backend can manage events from two
|
|
66
|
+
# different GUIs
|
|
67
|
+
if len(active_modes) > 1:
|
|
69
68
|
raise RuntimeError(
|
|
70
|
-
f"{self.__class__.__name__} can only be displayed in
|
|
71
|
-
f"
|
|
72
|
-
f" from bokeh.io import output_notebook\n"
|
|
73
|
-
f" output_notebook()"
|
|
69
|
+
f"{self.__class__.__name__} can only be displayed in a single Bokeh\n"
|
|
70
|
+
f"display mode. Either file or notebook, but not both."
|
|
74
71
|
)
|
|
75
72
|
|
|
76
|
-
#
|
|
77
|
-
|
|
73
|
+
# For notebook display, fixed sizing is required. This selects between the
|
|
74
|
+
# fixed, notebook dimensions and the default browser dimensions based on
|
|
75
|
+
# the Bokeh output that has been selected.
|
|
76
|
+
if 'notebook' in active_modes and self._display_config['notebook']['mode'] == 'fixed':
|
|
78
77
|
self.sizing_mode = None
|
|
79
|
-
self.width = self.
|
|
80
|
-
self.height = self.
|
|
78
|
+
self.width = self._display_config['notebook']['width']
|
|
79
|
+
self.height = self._display_config['notebook']['height']
|
|
80
|
+
else:
|
|
81
|
+
self.sizing_mode = self._display_config['browser']['mode']
|
|
82
|
+
self.width = self._display_config['browser']['width']
|
|
83
|
+
self.height = self._display_config['browser']['height']
|
|
81
84
|
|
|
82
85
|
# Now set the document
|
|
83
86
|
self._document = doc
|
|
@@ -87,12 +90,23 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
87
90
|
self._start_backend()
|
|
88
91
|
self._backend_started = True
|
|
89
92
|
|
|
93
|
+
def to_serializable(self, *args, **kwargs):
|
|
94
|
+
if self._display_context:
|
|
95
|
+
self._display_context.on_to_serializable( )
|
|
96
|
+
|
|
97
|
+
# Call parent's to_serializable
|
|
98
|
+
return super().to_serializable(*args, **kwargs)
|
|
99
|
+
|
|
90
100
|
def __init__( self, ui_element=None, backend_func=None,
|
|
91
101
|
result_retrieval=None,
|
|
92
102
|
notebook_width=1200, notebook_height=800,
|
|
93
|
-
notebook_sizing='fixed',
|
|
103
|
+
notebook_sizing='fixed',
|
|
104
|
+
display_context=None,
|
|
105
|
+
**kwargs ):
|
|
94
106
|
logger.debug(f"\tShowable::__init__(ui_element={type(ui_element).__name__ if ui_element else None}, {kwargs}): {id(self)}")
|
|
95
|
-
|
|
107
|
+
|
|
108
|
+
self._display_context = display_context
|
|
109
|
+
|
|
96
110
|
# Set default sizing if not provided
|
|
97
111
|
sizing_params = {'sizing_mode', 'width', 'height'}
|
|
98
112
|
provided_sizing_params = set(kwargs.keys()) & sizing_params
|
|
@@ -102,11 +116,20 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
102
116
|
# CRITICAL FIX: Don't call _ensure_in_document during __init__
|
|
103
117
|
# Let Bokeh handle document management through the normal flow
|
|
104
118
|
super().__init__(**kwargs)
|
|
105
|
-
|
|
119
|
+
|
|
106
120
|
# Set the UI element
|
|
107
121
|
if ui_element is not None:
|
|
108
122
|
self.ui = ui_element
|
|
109
123
|
|
|
124
|
+
# Keep track of defaults based on display mode
|
|
125
|
+
### self._notebook_width = notebook_width
|
|
126
|
+
### self._notebook_height = notebook_height
|
|
127
|
+
### self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
|
|
128
|
+
self._display_config = {
|
|
129
|
+
'notebook': { 'mode': notebook_sizing, 'width': notebook_width, 'height': notebook_height },
|
|
130
|
+
'browser': { 'mode': self.sizing_mode, 'width': self.width, 'height': self.height }
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
# Set the function to be called upon display
|
|
111
134
|
if backend_func is not None:
|
|
112
135
|
self._backend_startup_callback = backend_func
|
|
@@ -114,14 +137,35 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
114
137
|
# result (if one is/will be available)...
|
|
115
138
|
self._result_retrieval = result_retrieval
|
|
116
139
|
|
|
117
|
-
self._notebook_width = notebook_width
|
|
118
|
-
self._notebook_height = notebook_height
|
|
119
|
-
self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
|
|
120
140
|
self._notebook_rendering = None
|
|
121
141
|
|
|
122
142
|
ui = Instance(UIElement, help="""
|
|
123
143
|
A UI element, which can be plots, layouts, widgets, or any other UIElement.
|
|
124
144
|
""")
|
|
145
|
+
###
|
|
146
|
+
### when 'disabled' is set to true, this message should be displayed over
|
|
147
|
+
### a grey-obscured GUI...
|
|
148
|
+
###
|
|
149
|
+
### May need to adjust message formatting to allow for more flexiblity,
|
|
150
|
+
### i.e. all of the message does not need to be in big green print. May
|
|
151
|
+
### need to allow something like:
|
|
152
|
+
###
|
|
153
|
+
### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
|
|
154
|
+
### Interaction Complete ✓
|
|
155
|
+
### </div>
|
|
156
|
+
### <div style="font-size: 14px; color: #666;">
|
|
157
|
+
### You can now close this GUI or continue working in your notebook
|
|
158
|
+
### </div>
|
|
159
|
+
###
|
|
160
|
+
### currently the message is displayed in showable.ts like:
|
|
161
|
+
###
|
|
162
|
+
### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
|
|
163
|
+
### ${this.model.disabled_message}
|
|
164
|
+
### </div>
|
|
165
|
+
###
|
|
166
|
+
disabled_message = String(default="Interaction Complete ✓", help="""
|
|
167
|
+
Message to show when disabled
|
|
168
|
+
""")
|
|
125
169
|
|
|
126
170
|
# FIXED: Remove the children property override
|
|
127
171
|
# Let LayoutDOM handle its own children management
|
|
@@ -138,7 +182,7 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
138
182
|
"""Ensure this Showable is in the current document"""
|
|
139
183
|
from bokeh.io import curdoc
|
|
140
184
|
current_doc = curdoc()
|
|
141
|
-
|
|
185
|
+
|
|
142
186
|
# FIXED: More careful document management
|
|
143
187
|
# Only add to document if we're not already in the right one
|
|
144
188
|
if self.document is None:
|
|
@@ -239,6 +283,9 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
239
283
|
if self.ui is None:
|
|
240
284
|
return '<div style="color: red; padding: 10px; border: 1px solid red;">Showable object with no UI set</div>'
|
|
241
285
|
|
|
286
|
+
if self._display_context:
|
|
287
|
+
self._display_context.on_show( )
|
|
288
|
+
|
|
242
289
|
if self._notebook_rendering:
|
|
243
290
|
# Return a lightweight reference instead of re-rendering the full GUI
|
|
244
291
|
return f'''
|
|
@@ -251,10 +298,10 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
251
298
|
'''
|
|
252
299
|
|
|
253
300
|
# Apply notebook sizing for Jupyter context
|
|
254
|
-
if self._notebook_sizing == 'fixed':
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
###if self._notebook_sizing == 'fixed':
|
|
302
|
+
### self.sizing_mode = None
|
|
303
|
+
### self.width = self._notebook_width
|
|
304
|
+
### self.height = self._notebook_height
|
|
258
305
|
|
|
259
306
|
script, div = components(self)
|
|
260
307
|
if start_backend:
|
|
@@ -263,32 +310,6 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
263
310
|
self._notebook_rendering = f'{script}\n{div}'
|
|
264
311
|
return self._notebook_rendering
|
|
265
312
|
|
|
266
|
-
def _repr_html_(self, start_backend=True):
|
|
267
|
-
"""
|
|
268
|
-
HTML representation for Jupyter display.
|
|
269
|
-
|
|
270
|
-
Note: Bokeh doesn't reliably support automatic display via _repr_mimebundle_.
|
|
271
|
-
This provides a helpful message directing users to use show().
|
|
272
|
-
"""
|
|
273
|
-
html = self._get_notebook_html(start_backend)
|
|
274
|
-
|
|
275
|
-
if html is not None:
|
|
276
|
-
return html
|
|
277
|
-
|
|
278
|
-
# Not in notebook environment
|
|
279
|
-
return f"<!-- error: non-notebook environment{' in ' + self.name if self.name else ''} -->" + '''
|
|
280
|
-
<div style="padding: 15px; border: 2px solid #4CAF50; border-radius: 5px; background: #f9fff9; margin: 10px 0;">
|
|
281
|
-
<strong>📊 Showable Widget Ready</strong><br>
|
|
282
|
-
<em>Notebook display is not enabled, run:</em>
|
|
283
|
-
<p><pre>
|
|
284
|
-
from bokeh.io import output_notebook
|
|
285
|
-
output_notebook()</pre>
|
|
286
|
-
<p><em>and try again.</em>
|
|
287
|
-
<hr>
|
|
288
|
-
<small>Contains: {}</small>
|
|
289
|
-
</div>
|
|
290
|
-
'''.format(type(self.ui).__name__ if self.ui else "None")
|
|
291
|
-
|
|
292
313
|
def _repr_mimebundle_(self, include=None, exclude=None):
|
|
293
314
|
"""
|
|
294
315
|
MIME bundle representation for Jupyter display.
|
|
@@ -301,6 +322,8 @@ output_notebook()</pre>
|
|
|
301
322
|
- Bokeh show(showable)
|
|
302
323
|
- showable.show()
|
|
303
324
|
"""
|
|
325
|
+
if self.__class__._usage_mode is None:
|
|
326
|
+
self.__class__._usage_mode = "custom"
|
|
304
327
|
from bokeh.io.state import curstate
|
|
305
328
|
|
|
306
329
|
state = curstate()
|
|
@@ -311,13 +334,16 @@ output_notebook()</pre>
|
|
|
311
334
|
return {
|
|
312
335
|
'text/html': html
|
|
313
336
|
}
|
|
314
|
-
|
|
337
|
+
|
|
315
338
|
# Fall back to default Bokeh behavior for non-notebook environments
|
|
316
339
|
# Return None to let Bokeh handle it
|
|
317
340
|
return None
|
|
318
341
|
|
|
319
342
|
def show(self, start_backend=True):
|
|
320
343
|
"""Explicitly show this Showable using inline display in Jupyter"""
|
|
344
|
+
if self.__class__._usage_mode is None:
|
|
345
|
+
self.__class__._usage_mode = "custom"
|
|
346
|
+
|
|
321
347
|
from bokeh.io.state import curstate
|
|
322
348
|
|
|
323
349
|
self._ensure_in_document()
|
|
@@ -327,7 +353,7 @@ output_notebook()</pre>
|
|
|
327
353
|
if state.notebook:
|
|
328
354
|
# In Jupyter, display directly using IPython.display
|
|
329
355
|
from IPython.display import display, HTML
|
|
330
|
-
|
|
356
|
+
|
|
331
357
|
html = self._get_notebook_html(start_backend)
|
|
332
358
|
if html:
|
|
333
359
|
display(HTML(html))
|
cubevis/exe/_task.py
CHANGED
|
@@ -67,31 +67,53 @@ class Task:
|
|
|
67
67
|
|
|
68
68
|
return threading_event
|
|
69
69
|
|
|
70
|
+
def _run_coroutine_sync(self, coro):
|
|
71
|
+
"""
|
|
72
|
+
Helper to run a coroutine synchronously, handling both CLI and Jupyter contexts.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# Check if there's already a running event loop (Jupyter/IPython)
|
|
76
|
+
loop = asyncio.get_running_loop()
|
|
77
|
+
except RuntimeError:
|
|
78
|
+
# No running loop - safe to use asyncio.run() (CLI context)
|
|
79
|
+
return asyncio.run(coro)
|
|
80
|
+
else:
|
|
81
|
+
# Running loop exists (Jupyter context)
|
|
82
|
+
try:
|
|
83
|
+
import nest_asyncio
|
|
84
|
+
nest_asyncio.apply()
|
|
85
|
+
return asyncio.run(coro)
|
|
86
|
+
except ImportError:
|
|
87
|
+
# Fallback: run in a new thread with its own event loop
|
|
88
|
+
import concurrent.futures
|
|
89
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
90
|
+
return pool.submit(asyncio.run, coro).result()
|
|
91
|
+
|
|
70
92
|
def run_sync(self) -> Any:
|
|
71
93
|
"""Run synchronously until completion or stop signal."""
|
|
72
94
|
if self.stop_condition is None:
|
|
73
95
|
# Handle async functions in sync context
|
|
74
96
|
if asyncio.iscoroutinefunction(self.server_func):
|
|
75
|
-
logger.debug(
|
|
76
|
-
return
|
|
97
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - asyncio no stop condition {self.server_func}")
|
|
98
|
+
return self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
|
|
77
99
|
else:
|
|
78
|
-
logger.debug(
|
|
100
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - no stop condition {self.server_func}")
|
|
79
101
|
return self.server_func(*self.args, **self.kwargs)
|
|
80
102
|
|
|
81
103
|
# Handle asyncio.Event in sync context by converting to threading.Event
|
|
82
104
|
stop_condition = self.stop_condition
|
|
83
105
|
if isinstance(self.stop_condition, asyncio.Event):
|
|
84
|
-
logger.debug(
|
|
106
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - asyncio bridge {self.server_func}")
|
|
85
107
|
# Convert asyncio.Event to threading.Event for sync execution
|
|
86
108
|
stop_condition = self._convert_asyncio_event_to_threading(self.stop_condition)
|
|
87
|
-
|
|
109
|
+
|
|
88
110
|
if callable(stop_condition):
|
|
89
111
|
# Poll-based stopping
|
|
90
|
-
logger.debug(
|
|
112
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - polling {self.server_func}")
|
|
91
113
|
while not stop_condition():
|
|
92
114
|
try:
|
|
93
115
|
if asyncio.iscoroutinefunction(self.server_func):
|
|
94
|
-
result =
|
|
116
|
+
result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
|
|
95
117
|
else:
|
|
96
118
|
result = self.server_func(*self.args, **self.kwargs)
|
|
97
119
|
if result is not None: # Server function completed
|
|
@@ -102,12 +124,12 @@ class Task:
|
|
|
102
124
|
|
|
103
125
|
elif isinstance(stop_condition, threading.Event):
|
|
104
126
|
# Event-based stopping for threads
|
|
105
|
-
logger.debug(
|
|
127
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - threading event {self.server_func}")
|
|
106
128
|
def run_with_check():
|
|
107
129
|
while not stop_condition.is_set():
|
|
108
130
|
try:
|
|
109
131
|
if asyncio.iscoroutinefunction(self.server_func):
|
|
110
|
-
result =
|
|
132
|
+
result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
|
|
111
133
|
else:
|
|
112
134
|
result = self.server_func(*self.args, **self.kwargs)
|
|
113
135
|
if result is not None:
|
|
@@ -118,26 +140,25 @@ class Task:
|
|
|
118
140
|
return run_with_check()
|
|
119
141
|
|
|
120
142
|
elif isinstance(stop_condition, threading.Condition):
|
|
121
|
-
logger.debug(
|
|
143
|
+
logger.debug(f"\tTask::run_sync( ): {id(self)} - threading condition {self.server_func}")
|
|
122
144
|
# Condition-based stopping - most flexible and efficient
|
|
123
145
|
def run_with_condition():
|
|
124
146
|
with stop_condition:
|
|
125
147
|
while True:
|
|
126
|
-
# Check if we should stop (condition should have a flag or predicate)
|
|
127
|
-
# The condition object should be used by external code to signal stopping
|
|
128
|
-
# We'll pass the condition to the server function so it can wait efficiently
|
|
129
148
|
try:
|
|
130
149
|
# Pass the condition to the server function if it accepts it
|
|
131
150
|
import inspect
|
|
132
151
|
sig = inspect.signature(self.server_func)
|
|
133
152
|
if 'stop_condition' in sig.parameters:
|
|
134
153
|
if asyncio.iscoroutinefunction(self.server_func):
|
|
135
|
-
result =
|
|
154
|
+
result = self._run_coroutine_sync(
|
|
155
|
+
self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
|
|
156
|
+
)
|
|
136
157
|
else:
|
|
137
158
|
result = self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
|
|
138
159
|
else:
|
|
139
160
|
if asyncio.iscoroutinefunction(self.server_func):
|
|
140
|
-
result =
|
|
161
|
+
result = self._run_coroutine_sync(self.server_func(*self.args, **self.kwargs))
|
|
141
162
|
else:
|
|
142
163
|
result = self.server_func(*self.args, **self.kwargs)
|
|
143
164
|
|
|
@@ -227,4 +248,3 @@ class Task:
|
|
|
227
248
|
# Sync function in async context - run in thread pool
|
|
228
249
|
loop = asyncio.get_event_loop()
|
|
229
250
|
return await loop.run_in_executor(None, self.run_sync)
|
|
230
|
-
|
|
@@ -35,7 +35,7 @@ from contextlib import asynccontextmanager
|
|
|
35
35
|
from bokeh.layouts import row, column
|
|
36
36
|
from bokeh.plotting import show
|
|
37
37
|
from bokeh.models import Button, CustomJS, TabPanel, Tabs, Spacer, Div
|
|
38
|
-
from cubevis.toolbox import CubeMask
|
|
38
|
+
from cubevis.toolbox import CubeMask
|
|
39
39
|
from cubevis.bokeh.utils import svg_icon
|
|
40
40
|
from bokeh.io import reset_output as reset_bokeh_output
|
|
41
41
|
from bokeh.io import output_notebook
|
|
@@ -43,7 +43,7 @@ from bokeh.models.dom import HTML
|
|
|
43
43
|
from bokeh.models.ui.tooltips import Tooltip
|
|
44
44
|
from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter
|
|
45
45
|
from cubevis.data import casaimage
|
|
46
|
-
from cubevis.bokeh.models import TipButton, Tip
|
|
46
|
+
from cubevis.bokeh.models import TipButton, Tip, BokehAppContext
|
|
47
47
|
from cubevis.utils import ContextMgrChain as CMC
|
|
48
48
|
|
|
49
49
|
class CreateMask:
|
|
@@ -149,12 +149,6 @@ class CreateMask:
|
|
|
149
149
|
if False a mask path which does not exist results in an exception
|
|
150
150
|
'''
|
|
151
151
|
|
|
152
|
-
###
|
|
153
|
-
### Create application context (which includes a temporary directory).
|
|
154
|
-
### This sets the title of the plot.
|
|
155
|
-
###
|
|
156
|
-
self._app_state = AppContext( 'Create Mask' )
|
|
157
|
-
|
|
158
152
|
###
|
|
159
153
|
### widgets shared across image tabs (masking multiple images)
|
|
160
154
|
###
|
|
@@ -243,23 +237,7 @@ class CreateMask:
|
|
|
243
237
|
### If debugging this, make only a small change before confirming that exit from the
|
|
244
238
|
### Python asyncio loop continues to work... seems to be fiddly
|
|
245
239
|
###
|
|
246
|
-
imdetails['gui']['cube'] = CubeMask( paths[0], mask=paths[1]
|
|
247
|
-
init_script = None if initialization_registered else \
|
|
248
|
-
CustomJS( args={ }, code='''
|
|
249
|
-
window.addEventListener( 'beforeunload',
|
|
250
|
-
(event) => {
|
|
251
|
-
function donePromise( ) {
|
|
252
|
-
|
|
253
|
-
return new Promise( (resolve,reject) => {
|
|
254
|
-
// call by name does not work here:
|
|
255
|
-
// document._cube_done(cb=resolve) ???
|
|
256
|
-
if ( document._cube_done ) document._cube_done(null,resolve)
|
|
257
|
-
} )
|
|
258
|
-
}
|
|
259
|
-
( async () => { await donePromise( ) } )( )
|
|
260
|
-
} )''' ) )
|
|
261
|
-
|
|
262
|
-
initialization_registered = True
|
|
240
|
+
imdetails['gui']['cube'] = CubeMask( paths[0], mask=paths[1] )
|
|
263
241
|
imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
|
|
264
242
|
|
|
265
243
|
|
|
@@ -276,6 +254,23 @@ class CreateMask:
|
|
|
276
254
|
self._fig['status'] = imdetails['gui']['status'] = imdetails['gui']['cube'].status_text( "<p>initialization</p>" , width=230, reuse=self._fig['status'] )
|
|
277
255
|
self._image_bitmask_controls = imdetails['gui']['cube'].bitmask_ctrl( reuse=self._image_bitmask_controls, button_type='light' )
|
|
278
256
|
|
|
257
|
+
|
|
258
|
+
,
|
|
259
|
+
imdetails['gui']['cube'].init_script = None if initialization_registered else \
|
|
260
|
+
CustomJS( args={ 'status': self._fig['status'] },
|
|
261
|
+
### app state is found based on one of the GUI's models
|
|
262
|
+
code='''const appstate = Bokeh.find.appState(status)
|
|
263
|
+
window.addEventListener( 'beforeunload',
|
|
264
|
+
(event) => {
|
|
265
|
+
function donePromise( ) {
|
|
266
|
+
return new Promise( (resolve,reject) => {
|
|
267
|
+
appstate?.cube_done?.(null,resolve)
|
|
268
|
+
} )
|
|
269
|
+
}
|
|
270
|
+
( async () => { await donePromise( ) } )( )
|
|
271
|
+
} )''' )
|
|
272
|
+
initialization_registered = True
|
|
273
|
+
|
|
279
274
|
###
|
|
280
275
|
### spectrum plot must be disabled during iteration due to "tap to change channel" functionality
|
|
281
276
|
###
|
|
@@ -319,7 +314,8 @@ class CreateMask:
|
|
|
319
314
|
|
|
320
315
|
self._image_control_tab_groups[imid] = result
|
|
321
316
|
result.js_on_change( 'active', CustomJS( args={ },
|
|
322
|
-
code='''
|
|
317
|
+
code='''const appstate = Bokeh.find.appState(cb_obj)
|
|
318
|
+
appstate.last_control_tab = cb_obj.active''' ) )
|
|
323
319
|
return result
|
|
324
320
|
|
|
325
321
|
def _create_image_panel( self, imagetuple ):
|
|
@@ -356,7 +352,8 @@ class CreateMask:
|
|
|
356
352
|
|
|
357
353
|
self._ctrl_state['stop'].js_on_click( CustomJS( args={ },
|
|
358
354
|
code='''if ( confirm( "Are you sure you want to end this mask creation session and close the GUI?" ) ) {
|
|
359
|
-
|
|
355
|
+
const appstate = Bokeh.find.appState(cb_obj)
|
|
356
|
+
appstate?.cube_done?.( )
|
|
360
357
|
}''' ) )
|
|
361
358
|
|
|
362
359
|
|
|
@@ -368,17 +365,21 @@ class CreateMask:
|
|
|
368
365
|
|
|
369
366
|
image_tabs = Tabs( tabs=tab_panels, tabs_location='below', height_policy='max', width_policy='max' )
|
|
370
367
|
|
|
371
|
-
self._fig['layout'] =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
self._fig['layout'] = BokehAppContext(
|
|
369
|
+
column( row( self._fig['help'],
|
|
370
|
+
Spacer( height=self._ctrl_state['stop'].height, sizing_mode="scale_width" ),
|
|
371
|
+
Div( text="<div><b>status:</b></div>" ),
|
|
372
|
+
self._fig['status'],
|
|
373
|
+
self._ctrl_state['stop'], sizing_mode="scale_width" ),
|
|
374
|
+
row( image_tabs, height_policy='max', width_policy='max' ),
|
|
375
|
+
height_policy='max', width_policy='max' ),
|
|
376
|
+
app_state={ ### while the state dictionary itself
|
|
377
|
+
'name': 'create mask', ### is used, these particular element
|
|
378
|
+
'initialized': True ### are not currently used for anything
|
|
379
|
+
}, title='Create Mask' )
|
|
379
380
|
|
|
380
381
|
###
|
|
381
|
-
### Keep track of which image is currently active in
|
|
382
|
+
### Keep track of which image is currently active in appstate.image_name (which is
|
|
382
383
|
### initialized in self._js['initialize']). Also, update the current control sub-tab
|
|
383
384
|
### when the field main-tab is changed. An attempt to manage this all within the
|
|
384
385
|
### control sub-tabs using a reference to self._image_control_tab_groups from
|
|
@@ -388,11 +389,12 @@ class CreateMask:
|
|
|
388
389
|
###
|
|
389
390
|
image_tabs.js_on_change( 'active', CustomJS( args=dict( names=[ t[0] for t in self._mask_state.items( ) ],
|
|
390
391
|
itergroups=self._image_control_tab_groups ),
|
|
391
|
-
code='''
|
|
392
|
-
|
|
392
|
+
code='''const appstate = Bokeh.find.appState(cb_obj)
|
|
393
|
+
if ( ! hasprop(appstate,'last_control_tab') ) {
|
|
394
|
+
appstate.last_control_tab = 0
|
|
393
395
|
}
|
|
394
|
-
|
|
395
|
-
itergroups[
|
|
396
|
+
appstate.image_name = names[cb_obj.active]
|
|
397
|
+
itergroups[appstate.image_name].active = appstate.last_control_tab''' ) )
|
|
396
398
|
|
|
397
399
|
# Change display type depending on runtime environment
|
|
398
400
|
if is_interactive_jupyter( ):
|
|
@@ -403,7 +405,7 @@ class CreateMask:
|
|
|
403
405
|
### output_file(self._imagename+'_webpage/index.html')
|
|
404
406
|
pass
|
|
405
407
|
|
|
406
|
-
|
|
408
|
+
self._fig['layout'].show( )
|
|
407
409
|
|
|
408
410
|
def _asyncio_loop( self ):
|
|
409
411
|
'''return the event loop which can be mixed in with an existing event loop
|