cubevis 1.0.14__py3-none-any.whl → 1.0.26__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/__init__.py +1 -0
- cubevis/bokeh/models/_bokeh_app_context.py +48 -1
- cubevis/bokeh/models/_showable.py +180 -89
- cubevis/bokeh/state/_initialize.py +7 -0
- 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 +41 -24
- cubevis/toolbox/_interactive_clean_ui.mustache +71 -44
- cubevis/toolbox/_interactive_clean_ui.py +71 -44
- cubevis/utils/__init__.py +2 -1
- cubevis/utils/_jupyter.py +35 -12
- cubevis/utils/_mutual_exclusion.py +117 -0
- {cubevis-1.0.14.dist-info → cubevis-1.0.26.dist-info}/METADATA +2 -2
- {cubevis-1.0.14.dist-info → cubevis-1.0.26.dist-info}/RECORD +26 -25
- {cubevis-1.0.14.dist-info → cubevis-1.0.26.dist-info}/WHEEL +0 -0
- {cubevis-1.0.14.dist-info → cubevis-1.0.26.dist-info}/licenses/LICENSE +0 -0
cubevis/bokeh/__init__.py
CHANGED
|
@@ -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,20 +1,30 @@
|
|
|
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
|
|
8
|
+
from ...utils import is_colab
|
|
7
9
|
|
|
8
10
|
logger = logging.getLogger(__name__)
|
|
9
11
|
|
|
10
12
|
class Showable(LayoutDOM,BokehInit):
|
|
11
13
|
"""Wrap a UIElement to make any Bokeh UI component showable with show()
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
This class works by acting as a simple container that delegates to its UI element.
|
|
14
16
|
For Jupyter notebook display, use show(showable) - automatic display via _repr_mimebundle_
|
|
15
17
|
is not reliably supported by Bokeh's architecture.
|
|
16
18
|
"""
|
|
17
19
|
|
|
20
|
+
### _usage_mode is needed to prevent mixing "bokeh.plotting.show(showable)" with
|
|
21
|
+
### "showable.show( )" or just evaluating "showable". This is required because the
|
|
22
|
+
### latter use "bokeh.embed.components" to create the HTML that is rendered while
|
|
23
|
+
### "bokeh.plotting.show(showable)" uses internal Bokeh rendering that is
|
|
24
|
+
### incompatable with "bokeh.embed.components" usage. For this reason, the user
|
|
25
|
+
### can use either one, but not both.
|
|
26
|
+
_usage_mode = None
|
|
27
|
+
|
|
18
28
|
@property
|
|
19
29
|
def document(self):
|
|
20
30
|
"""Get the document this model is attached to."""
|
|
@@ -26,58 +36,52 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
26
36
|
Intercept when Bokeh tries to attach us to a document.
|
|
27
37
|
This is called by bokeh.plotting.show() when it adds us to a document.
|
|
28
38
|
"""
|
|
29
|
-
from bokeh.io.state import curstate
|
|
30
|
-
import traceback
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
def get_caller_class_name(frame):
|
|
41
|
+
"""Attempt to find the name of the class the calling method belongs to."""
|
|
42
|
+
# Check if 'self' is in the caller's local variables (conventional for instance methods)
|
|
43
|
+
if 'self' in frame.f_locals:
|
|
44
|
+
return frame.f_locals['self'].__class__.__name__
|
|
45
|
+
# Check for 'cls' (conventional for class methods)
|
|
46
|
+
elif 'cls' in frame.f_locals:
|
|
47
|
+
return frame.f_locals['cls'].__name__
|
|
48
|
+
else:
|
|
49
|
+
# It might be a regular function or static method without explicit 'self'/'cls'
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Allow None (detaching from document) without any further checking
|
|
33
53
|
if doc is None:
|
|
34
54
|
self._document = None
|
|
35
55
|
return
|
|
36
56
|
|
|
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
|
-
)
|
|
57
|
+
from bokeh.io.state import curstate
|
|
58
|
+
state = curstate( )
|
|
54
59
|
|
|
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
|
-
)
|
|
60
|
+
# Validate environment (only one OUTPUT mode)
|
|
61
|
+
active_modes = []
|
|
62
|
+
if state.file: active_modes.append('file')
|
|
63
|
+
if state.notebook: active_modes.append('notebook')
|
|
66
64
|
|
|
67
|
-
#
|
|
68
|
-
if
|
|
65
|
+
# only allow a single GUI to be displayed since there is a backend
|
|
66
|
+
# this could be relaxed if the backend can manage events from two
|
|
67
|
+
# different GUIs
|
|
68
|
+
if len(active_modes) > 1:
|
|
69
69
|
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()"
|
|
70
|
+
f"{self.__class__.__name__} can only be displayed in a single Bokeh\n"
|
|
71
|
+
f"display mode. Either file or notebook, but not both."
|
|
74
72
|
)
|
|
75
73
|
|
|
76
|
-
#
|
|
77
|
-
|
|
74
|
+
# For notebook display, fixed sizing is required. This selects between the
|
|
75
|
+
# fixed, notebook dimensions and the default browser dimensions based on
|
|
76
|
+
# the Bokeh output that has been selected.
|
|
77
|
+
if 'notebook' in active_modes and self._display_config['notebook']['mode'] == 'fixed':
|
|
78
78
|
self.sizing_mode = None
|
|
79
|
-
self.width = self.
|
|
80
|
-
self.height = self.
|
|
79
|
+
self.width = self._display_config['notebook']['width']
|
|
80
|
+
self.height = self._display_config['notebook']['height']
|
|
81
|
+
else:
|
|
82
|
+
self.sizing_mode = self._display_config['browser']['mode']
|
|
83
|
+
self.width = self._display_config['browser']['width']
|
|
84
|
+
self.height = self._display_config['browser']['height']
|
|
81
85
|
|
|
82
86
|
# Now set the document
|
|
83
87
|
self._document = doc
|
|
@@ -87,12 +91,23 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
87
91
|
self._start_backend()
|
|
88
92
|
self._backend_started = True
|
|
89
93
|
|
|
94
|
+
def to_serializable(self, *args, **kwargs):
|
|
95
|
+
if self._display_context:
|
|
96
|
+
self._display_context.on_to_serializable( )
|
|
97
|
+
|
|
98
|
+
# Call parent's to_serializable
|
|
99
|
+
return super().to_serializable(*args, **kwargs)
|
|
100
|
+
|
|
90
101
|
def __init__( self, ui_element=None, backend_func=None,
|
|
91
102
|
result_retrieval=None,
|
|
92
103
|
notebook_width=1200, notebook_height=800,
|
|
93
|
-
notebook_sizing='fixed',
|
|
104
|
+
notebook_sizing='fixed',
|
|
105
|
+
display_context=None,
|
|
106
|
+
**kwargs ):
|
|
94
107
|
logger.debug(f"\tShowable::__init__(ui_element={type(ui_element).__name__ if ui_element else None}, {kwargs}): {id(self)}")
|
|
95
|
-
|
|
108
|
+
|
|
109
|
+
self._display_context = display_context
|
|
110
|
+
|
|
96
111
|
# Set default sizing if not provided
|
|
97
112
|
sizing_params = {'sizing_mode', 'width', 'height'}
|
|
98
113
|
provided_sizing_params = set(kwargs.keys()) & sizing_params
|
|
@@ -102,11 +117,20 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
102
117
|
# CRITICAL FIX: Don't call _ensure_in_document during __init__
|
|
103
118
|
# Let Bokeh handle document management through the normal flow
|
|
104
119
|
super().__init__(**kwargs)
|
|
105
|
-
|
|
120
|
+
|
|
106
121
|
# Set the UI element
|
|
107
122
|
if ui_element is not None:
|
|
108
123
|
self.ui = ui_element
|
|
109
124
|
|
|
125
|
+
# Keep track of defaults based on display mode
|
|
126
|
+
### self._notebook_width = notebook_width
|
|
127
|
+
### self._notebook_height = notebook_height
|
|
128
|
+
### self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
|
|
129
|
+
self._display_config = {
|
|
130
|
+
'notebook': { 'mode': notebook_sizing, 'width': notebook_width, 'height': notebook_height },
|
|
131
|
+
'browser': { 'mode': self.sizing_mode, 'width': self.width, 'height': self.height }
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
# Set the function to be called upon display
|
|
111
135
|
if backend_func is not None:
|
|
112
136
|
self._backend_startup_callback = backend_func
|
|
@@ -114,14 +138,35 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
114
138
|
# result (if one is/will be available)...
|
|
115
139
|
self._result_retrieval = result_retrieval
|
|
116
140
|
|
|
117
|
-
self._notebook_width = notebook_width
|
|
118
|
-
self._notebook_height = notebook_height
|
|
119
|
-
self._notebook_sizing = notebook_sizing # 'fixed' or 'stretch'
|
|
120
141
|
self._notebook_rendering = None
|
|
121
142
|
|
|
122
143
|
ui = Instance(UIElement, help="""
|
|
123
144
|
A UI element, which can be plots, layouts, widgets, or any other UIElement.
|
|
124
145
|
""")
|
|
146
|
+
###
|
|
147
|
+
### when 'disabled' is set to true, this message should be displayed over
|
|
148
|
+
### a grey-obscured GUI...
|
|
149
|
+
###
|
|
150
|
+
### May need to adjust message formatting to allow for more flexiblity,
|
|
151
|
+
### i.e. all of the message does not need to be in big green print. May
|
|
152
|
+
### need to allow something like:
|
|
153
|
+
###
|
|
154
|
+
### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
|
|
155
|
+
### Interaction Complete ✓
|
|
156
|
+
### </div>
|
|
157
|
+
### <div style="font-size: 14px; color: #666;">
|
|
158
|
+
### You can now close this GUI or continue working in your notebook
|
|
159
|
+
### </div>
|
|
160
|
+
###
|
|
161
|
+
### currently the message is displayed in showable.ts like:
|
|
162
|
+
###
|
|
163
|
+
### <div style="font-size: 24px; font-weight: bold; color: #4CAF50; margin-bottom: 10px;">
|
|
164
|
+
### ${this.model.disabled_message}
|
|
165
|
+
### </div>
|
|
166
|
+
###
|
|
167
|
+
disabled_message = String(default="Interaction Complete ✓", help="""
|
|
168
|
+
Message to show when disabled
|
|
169
|
+
""")
|
|
125
170
|
|
|
126
171
|
# FIXED: Remove the children property override
|
|
127
172
|
# Let LayoutDOM handle its own children management
|
|
@@ -138,7 +183,7 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
138
183
|
"""Ensure this Showable is in the current document"""
|
|
139
184
|
from bokeh.io import curdoc
|
|
140
185
|
current_doc = curdoc()
|
|
141
|
-
|
|
186
|
+
|
|
142
187
|
# FIXED: More careful document management
|
|
143
188
|
# Only add to document if we're not already in the right one
|
|
144
189
|
if self.document is None:
|
|
@@ -228,8 +273,11 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
228
273
|
Common logic for generating HTML in notebook environments.
|
|
229
274
|
Returns the HTML string to display, or None if not in a notebook.
|
|
230
275
|
"""
|
|
231
|
-
from bokeh.embed import components
|
|
276
|
+
from bokeh.embed import components, json_item
|
|
232
277
|
from bokeh.io.state import curstate
|
|
278
|
+
from bokeh.resources import CDN
|
|
279
|
+
import sys
|
|
280
|
+
import json as json_lib
|
|
233
281
|
|
|
234
282
|
state = curstate()
|
|
235
283
|
|
|
@@ -239,55 +287,93 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
239
287
|
if self.ui is None:
|
|
240
288
|
return '<div style="color: red; padding: 10px; border: 1px solid red;">Showable object with no UI set</div>'
|
|
241
289
|
|
|
290
|
+
if self._display_context:
|
|
291
|
+
self._display_context.on_show()
|
|
292
|
+
|
|
242
293
|
if self._notebook_rendering:
|
|
243
294
|
# Return a lightweight reference instead of re-rendering the full GUI
|
|
244
295
|
return f'''
|
|
245
296
|
<div style="padding: 10px; background: #f0f8f0; border-left: 4px solid #4CAF50; margin: 5px 0;">
|
|
246
|
-
<strong
|
|
297
|
+
<strong>→ iclean GUI active above</strong>
|
|
247
298
|
<small style="color: #666; display: block; margin-top: 5px;">
|
|
248
299
|
Showable ID: {self.id[-8:]} | Backend: Running
|
|
249
300
|
</small>
|
|
250
301
|
</div>
|
|
251
302
|
'''
|
|
252
303
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
if is_colab( ):
|
|
305
|
+
# Get all JS paths from the existing function
|
|
306
|
+
# This returns paths in the correct order:
|
|
307
|
+
# [casalib, bokeh-core, bokeh-widgets, bokeh-tables, cubevisjs]
|
|
308
|
+
from cubevis.bokeh import get_bokeh_js_paths
|
|
309
|
+
js_paths = get_bokeh_js_paths( )
|
|
310
|
+
|
|
311
|
+
# Build script tags for all libraries in order
|
|
312
|
+
all_scripts = '\n'.join([
|
|
313
|
+
f'<script type="text/javascript" src="{url}"></script>'
|
|
314
|
+
for url in js_paths
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
# Use json_item approach which is more reliable in iframes
|
|
318
|
+
item = json_item(self, target=f"bokeh-{self.id}")
|
|
319
|
+
item_json = json_lib.dumps(item)
|
|
320
|
+
|
|
321
|
+
# Build complete HTML with proper loading sequence
|
|
322
|
+
# get_bokeh_js_paths() already returns libs in the correct order:
|
|
323
|
+
# 1. casalib (third-party libs for CustomJS)
|
|
324
|
+
# 2. bokeh-core
|
|
325
|
+
# 3. bokeh-widgets
|
|
326
|
+
# 4. bokeh-tables
|
|
327
|
+
# 5. cubevisjs (custom Bokeh models)
|
|
328
|
+
html = f'''
|
|
329
|
+
{f'<link href="{CDN.css_files[0]}" rel="stylesheet" type="text/css">' if CDN.css_files else ""}
|
|
330
|
+
<div id="bokeh-{self.id}" class="bk-root"></div>
|
|
331
|
+
{all_scripts}
|
|
332
|
+
<script type="text/javascript">
|
|
333
|
+
(function() {{
|
|
334
|
+
var item = {item_json};
|
|
335
|
+
|
|
336
|
+
function embedWhenReady() {{
|
|
337
|
+
// Check if all required libraries are loaded
|
|
338
|
+
if (typeof Bokeh !== 'undefined' && Bokeh.embed) {{
|
|
339
|
+
var target = document.getElementById("bokeh-{self.id}");
|
|
340
|
+
if (target) {{
|
|
341
|
+
try {{
|
|
342
|
+
Bokeh.embed.embed_item(item);
|
|
343
|
+
console.log("Bokeh plot embedded successfully");
|
|
344
|
+
}} catch(e) {{
|
|
345
|
+
console.error("Error embedding Bokeh plot:", e);
|
|
346
|
+
}}
|
|
347
|
+
}} else {{
|
|
348
|
+
console.error("Target element not found");
|
|
349
|
+
setTimeout(embedWhenReady, 50);
|
|
350
|
+
}}
|
|
351
|
+
}} else {{
|
|
352
|
+
setTimeout(embedWhenReady, 50);
|
|
353
|
+
}}
|
|
354
|
+
}}
|
|
355
|
+
|
|
356
|
+
if (document.readyState === 'loading') {{
|
|
357
|
+
document.addEventListener('DOMContentLoaded', embedWhenReady);
|
|
358
|
+
}} else {{
|
|
359
|
+
embedWhenReady();
|
|
360
|
+
}}
|
|
361
|
+
}})();
|
|
362
|
+
</script>
|
|
363
|
+
'''
|
|
265
364
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
HTML representation for Jupyter display.
|
|
365
|
+
if start_backend:
|
|
366
|
+
self._start_backend()
|
|
269
367
|
|
|
270
|
-
|
|
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:
|
|
368
|
+
self._notebook_rendering = html
|
|
276
369
|
return html
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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")
|
|
370
|
+
else:
|
|
371
|
+
# In Jupyter Lab/Classic, use components() as before
|
|
372
|
+
script, div = components(self)
|
|
373
|
+
if start_backend:
|
|
374
|
+
self._start_backend()
|
|
375
|
+
self._notebook_rendering = f'{script}\n{div}'
|
|
376
|
+
return self._notebook_rendering
|
|
291
377
|
|
|
292
378
|
def _repr_mimebundle_(self, include=None, exclude=None):
|
|
293
379
|
"""
|
|
@@ -301,6 +387,8 @@ output_notebook()</pre>
|
|
|
301
387
|
- Bokeh show(showable)
|
|
302
388
|
- showable.show()
|
|
303
389
|
"""
|
|
390
|
+
if self.__class__._usage_mode is None:
|
|
391
|
+
self.__class__._usage_mode = "custom"
|
|
304
392
|
from bokeh.io.state import curstate
|
|
305
393
|
|
|
306
394
|
state = curstate()
|
|
@@ -311,13 +399,16 @@ output_notebook()</pre>
|
|
|
311
399
|
return {
|
|
312
400
|
'text/html': html
|
|
313
401
|
}
|
|
314
|
-
|
|
402
|
+
|
|
315
403
|
# Fall back to default Bokeh behavior for non-notebook environments
|
|
316
404
|
# Return None to let Bokeh handle it
|
|
317
405
|
return None
|
|
318
406
|
|
|
319
407
|
def show(self, start_backend=True):
|
|
320
408
|
"""Explicitly show this Showable using inline display in Jupyter"""
|
|
409
|
+
if self.__class__._usage_mode is None:
|
|
410
|
+
self.__class__._usage_mode = "custom"
|
|
411
|
+
|
|
321
412
|
from bokeh.io.state import curstate
|
|
322
413
|
|
|
323
414
|
self._ensure_in_document()
|
|
@@ -327,7 +418,7 @@ output_notebook()</pre>
|
|
|
327
418
|
if state.notebook:
|
|
328
419
|
# In Jupyter, display directly using IPython.display
|
|
329
420
|
from IPython.display import display, HTML
|
|
330
|
-
|
|
421
|
+
|
|
331
422
|
html = self._get_notebook_html(start_backend)
|
|
332
423
|
if html:
|
|
333
424
|
display(HTML(html))
|
|
@@ -321,6 +321,13 @@ def order_bokeh_js():
|
|
|
321
321
|
resources.Resources.js_files = property(js_files)
|
|
322
322
|
return
|
|
323
323
|
|
|
324
|
+
#def get_bokeh_js_paths( ):
|
|
325
|
+
# modes = ['cdn','inline','server','server-dev','relative','relative-dev','absolute','absolute-dev']
|
|
326
|
+
# return { 'new': { mode: resources.Resources(mode=mode).js_files for mode in modes },
|
|
327
|
+
# 'old': { mode: resources.Resources(mode=mode)._old_js_files for mode in modes } }
|
|
328
|
+
def get_bokeh_js_paths( ):
|
|
329
|
+
return resources.Resources(mode='cdn').js_files
|
|
330
|
+
|
|
324
331
|
def get_jupyter_state( ):
|
|
325
332
|
"""Get the package-level Jupyter state"""
|
|
326
333
|
return _JUPYTER_STATE
|
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
|
-
|