cubevis 1.0.19__tar.gz → 1.0.21__tar.gz
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-1.0.19 → cubevis-1.0.21}/PKG-INFO +1 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_bokeh_app_context.py +48 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_showable.py +15 -48
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_createmask.py +3 -9
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_createregion.py +3 -9
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_interactiveclean.mustache +2 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_interactiveclean.py +2 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_interactivecleannotebook.mustache +24 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_interactivecleannotebook.py +24 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/__init__.py +0 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_cube.py +16 -10
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_interactive_clean_ui.mustache +22 -7
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_interactive_clean_ui.py +22 -7
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/__init__.py +1 -0
- cubevis-1.0.21/cubevis/utils/_mutual_exclusion.py +117 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/pyproject.toml +1 -1
- {cubevis-1.0.19 → cubevis-1.0.21}/LICENSE +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/README.rst +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/LICENSE.rst +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/20px/fast-backward.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/20px/fast-forward.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/20px/step-backward.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/20px/step-forward.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/add-chan.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/add-chan.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/add-cube.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/add-cube.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/drag.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/drag.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/mask-selected.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/mask.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/mask.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/new-layer-sm-selected.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/new-layer-sm-selected.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/new-layer-sm.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/new-layer-sm.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/reset.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/reset.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/sub-chan.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/sub-chan.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/sub-cube.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/sub-cube.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/trash.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/trash.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/trash_full.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/trash_full.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/trash_full_raw.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/zoom-to-fit.png +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__icons__/zoom-to-fit.svg +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__js__/bokeh-3.6/cubevisjs.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__js__/bokeh-3.7/cubevisjs.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__js__/bokeh-3.8/cubevisjs.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/__js__/casalib.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/annotations/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/annotations/_ev_poly_annotation.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/components/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/format/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/format/_time_ticks.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/format/_wcs_ticks.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_edit_span.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_ev_text_input.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_shared_dict.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_tip.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/models/_tip_button.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/_data_pipe.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/_image_data_source.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/_image_pipe.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/_spectra_data_source.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/sources/_updatable_data_source.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/_current.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/_initialize.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/_javascript.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/_palette.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/_session.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/bokeh-2.4.1.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/state/js/casalib-v0.0.1.min.js +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/tools/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/tools/_cbreset_tool.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/tools/_drag_tool.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/utils/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/utils/_axes_labels.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/bokeh/utils/_svg_icon.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/data/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/data/casaimage/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/exe/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/exe/_context.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/exe/_mode.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/exe/_setting.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/exe/_task.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/_gclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_plotants.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/apps/_plotbandpass.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casashell/createmask.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casashell/iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casatasks/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casatasks/createmask.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casatasks/createregion.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casatasks/iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/private/casatasks/iclean_notebook.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/readme.rst +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/remote/__init__.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/remote/_gclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/remote/_local.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/remote/_remote_kernel.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_app_context.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_interactiveclean_wrappers.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/toolbox/_region_list.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_ResourceManager.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_browser.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_contextmgrchain.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_conversion.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_copydoc.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_docenum.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_git.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_import_protected_module.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_jupyter.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_logging.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_pkgs.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_regions.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_static.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/cubevis/utils/_tiles.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/alma-many-chan/alma-many-chan.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/basic-websockets-demo/client.html +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/basic-websockets-demo/client.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/basic-websockets-demo/server.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/createmask-demo/run-createmask.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/createregion-demo/run-createregion.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/cubemask-demo/image-slider-spectra-done-stats.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/cubemask-demo/image-slider-spectra-done.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/cubemask-demo/image-slider-spectra.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/cubemask-demo/image-slider.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/cubemask-demo/image.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/iclean-demo.ipynb +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/m100_interactive.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/mask0-iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/run-gclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/run-iclean-obj.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/run-iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-demo/vla-sim-jet-iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-first-look/run-fl-cont.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-first-look/run-fl-line.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-outlier/run-iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-outlier/test_outlier.txt +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/iclean-remote/iclean_remote_webserver.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/large-cube/run-largecube.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/svg-test.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/updatable-data-source/direct-plot.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/updatable-data-source/simple-update.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/updatable-data-source/updated-plot.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/uranus-demo/uranus-iclean.py +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/websocket-reconnect/client.html +0 -0
- {cubevis-1.0.19 → cubevis-1.0.21}/tests/manual/websocket-reconnect/server.py +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))
|
|
@@ -48,39 +48,11 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
48
48
|
# It might be a regular function or static method without explicit 'self'/'cls'
|
|
49
49
|
return None
|
|
50
50
|
|
|
51
|
-
# Detect Bokeh usage mode (i.e. self.__class__._usage_mode unset)
|
|
52
|
-
calling_mode = None
|
|
53
|
-
if self.__class__._usage_mode is None:
|
|
54
|
-
self.__class__._usage_mode = "bokeh"
|
|
55
|
-
calling_mode = "bokeh"
|
|
56
|
-
|
|
57
51
|
# Allow None (detaching from document) without any further checking
|
|
58
52
|
if doc is None:
|
|
59
53
|
self._document = None
|
|
60
54
|
return
|
|
61
55
|
|
|
62
|
-
if calling_mode is None:
|
|
63
|
-
import inspect
|
|
64
|
-
stack_frames = inspect.stack( )
|
|
65
|
-
try:
|
|
66
|
-
for frame in stack_frames[1:]:
|
|
67
|
-
if frame.function == '_repr_mimebundle_' or frame.function == 'show':
|
|
68
|
-
if get_caller_class_name(frame.frame) == self.__class__.__name__:
|
|
69
|
-
calling_mode = "custom"
|
|
70
|
-
break
|
|
71
|
-
finally:
|
|
72
|
-
# Essential to delete stack frames to avoid reference cycles
|
|
73
|
-
del stack_frames
|
|
74
|
-
|
|
75
|
-
if calling_mode != self.__class__._usage_mode:
|
|
76
|
-
### THIS CATCHES: using Bokeh show after Showable display methods
|
|
77
|
-
### using Showable.show after Bokeh show
|
|
78
|
-
raise RuntimeError(
|
|
79
|
-
f"\n{'='*70}\n" +
|
|
80
|
-
( (self._usage_error['custom'] % self.__class__.__name__) if calling_mode == 'custom' else
|
|
81
|
-
(self._usage_error['bokeh'] % 'bokeh.plotting.show') ) +
|
|
82
|
-
f"\n{'='*70}\n" )
|
|
83
|
-
|
|
84
56
|
from bokeh.io.state import curstate
|
|
85
57
|
state = curstate( )
|
|
86
58
|
|
|
@@ -118,12 +90,23 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
118
90
|
self._start_backend()
|
|
119
91
|
self._backend_started = True
|
|
120
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
|
+
|
|
121
100
|
def __init__( self, ui_element=None, backend_func=None,
|
|
122
101
|
result_retrieval=None,
|
|
123
102
|
notebook_width=1200, notebook_height=800,
|
|
124
|
-
notebook_sizing='fixed',
|
|
103
|
+
notebook_sizing='fixed',
|
|
104
|
+
display_context=None,
|
|
105
|
+
**kwargs ):
|
|
125
106
|
logger.debug(f"\tShowable::__init__(ui_element={type(ui_element).__name__ if ui_element else None}, {kwargs}): {id(self)}")
|
|
126
107
|
|
|
108
|
+
self._display_context = display_context
|
|
109
|
+
|
|
127
110
|
# Set default sizing if not provided
|
|
128
111
|
sizing_params = {'sizing_mode', 'width', 'height'}
|
|
129
112
|
provided_sizing_params = set(kwargs.keys()) & sizing_params
|
|
@@ -147,18 +130,6 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
147
130
|
'browser': { 'mode': self.sizing_mode, 'width': self.width, 'height': self.height }
|
|
148
131
|
}
|
|
149
132
|
|
|
150
|
-
# Error messages included in RuntimeErrors
|
|
151
|
-
self._usage_error = {
|
|
152
|
-
'custom': "❌ Cannot use %s display methods:\n\n" \
|
|
153
|
-
"Reason: bokeh.plotting.show() has already been used for display\n" \
|
|
154
|
-
" of this class. Mixing display methods within a single notebook\n" \
|
|
155
|
-
" corrupts Bokeh display within the notebook\n",
|
|
156
|
-
'bokeh': "❌ Cannot use %s display method:\n\n" \
|
|
157
|
-
"Reason: Showable display methods have already been used for display\n" \
|
|
158
|
-
" of this class. Mixing display methods within a single notebook\n" \
|
|
159
|
-
" corrupts Bokeh display within the notebook\n" }
|
|
160
|
-
|
|
161
|
-
|
|
162
133
|
# Set the function to be called upon display
|
|
163
134
|
if backend_func is not None:
|
|
164
135
|
self._backend_startup_callback = backend_func
|
|
@@ -304,13 +275,6 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
304
275
|
from bokeh.embed import components
|
|
305
276
|
from bokeh.io.state import curstate
|
|
306
277
|
|
|
307
|
-
if self.__class__._usage_mode != "custom":
|
|
308
|
-
### THIS CATCHES: Showable display via evaluation ( "ic" ) after Bokeh show
|
|
309
|
-
raise RuntimeError(
|
|
310
|
-
f"\n{'='*70}\n" +
|
|
311
|
-
(self._usage_error['custom'] % self.__class__.__name__) +
|
|
312
|
-
f"\n{'='*70}\n" )
|
|
313
|
-
|
|
314
278
|
state = curstate()
|
|
315
279
|
|
|
316
280
|
if not state.notebook:
|
|
@@ -319,6 +283,9 @@ class Showable(LayoutDOM,BokehInit):
|
|
|
319
283
|
if self.ui is None:
|
|
320
284
|
return '<div style="color: red; padding: 10px; border: 1px solid red;">Showable object with no UI set</div>'
|
|
321
285
|
|
|
286
|
+
if self._display_context:
|
|
287
|
+
self._display_context.on_show( )
|
|
288
|
+
|
|
322
289
|
if self._notebook_rendering:
|
|
323
290
|
# Return a lightweight reference instead of re-rendering the full GUI
|
|
324
291
|
return f'''
|
|
@@ -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
|
|
@@ -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
|
###
|
|
@@ -382,7 +376,7 @@ class CreateMask:
|
|
|
382
376
|
app_state={ ### while the state dictionary itself
|
|
383
377
|
'name': 'create mask', ### is used, these particular element
|
|
384
378
|
'initialized': True ### are not currently used for anything
|
|
385
|
-
} )
|
|
379
|
+
}, title='Create Mask' )
|
|
386
380
|
|
|
387
381
|
###
|
|
388
382
|
### Keep track of which image is currently active in appstate.image_name (which is
|
|
@@ -411,7 +405,7 @@ class CreateMask:
|
|
|
411
405
|
### output_file(self._imagename+'_webpage/index.html')
|
|
412
406
|
pass
|
|
413
407
|
|
|
414
|
-
|
|
408
|
+
self._fig['layout'].show( )
|
|
415
409
|
|
|
416
410
|
def _asyncio_loop( self ):
|
|
417
411
|
'''return the event loop which can be mixed in with an existing event loop
|
|
@@ -35,7 +35,7 @@ from contextlib import asynccontextmanager
|
|
|
35
35
|
from bokeh.layouts import row, column, grid
|
|
36
36
|
from bokeh.plotting import show
|
|
37
37
|
from bokeh.models import Button, CustomJS, TabPanel, Tabs, Spacer, Div, Dropdown
|
|
38
|
-
from cubevis.toolbox import CubeMask,
|
|
38
|
+
from cubevis.toolbox import CubeMask, RegionList
|
|
39
39
|
from cubevis.bokeh.utils import svg_icon
|
|
40
40
|
from bokeh.io import curdoc
|
|
41
41
|
from bokeh.io import reset_output as reset_bokeh_output
|
|
@@ -141,12 +141,6 @@ class CreateRegion:
|
|
|
141
141
|
path(s) to CASA image for which interactive regions will be drawn
|
|
142
142
|
'''
|
|
143
143
|
|
|
144
|
-
###
|
|
145
|
-
### Create application context (which includes a temporary directory).
|
|
146
|
-
### This sets the title of the plot.
|
|
147
|
-
###
|
|
148
|
-
self._app_state = AppContext( 'Create Region' )
|
|
149
|
-
|
|
150
144
|
###
|
|
151
145
|
### widgets shared across image tabs (masking multiple images)
|
|
152
146
|
###
|
|
@@ -435,7 +429,7 @@ class CreateRegion:
|
|
|
435
429
|
app_state={ ### while the state dictionary itself
|
|
436
430
|
'name': 'create region', ### is used, these particular element
|
|
437
431
|
'initialized': True ### are not currently used for anything
|
|
438
|
-
} )
|
|
432
|
+
}, title='Create Region' )
|
|
439
433
|
|
|
440
434
|
###
|
|
441
435
|
### Keep track of which image is currently active in appstate.image_name (which is
|
|
@@ -464,7 +458,7 @@ class CreateRegion:
|
|
|
464
458
|
### output_file(self._imagename+'_webpage/index.html')
|
|
465
459
|
pass
|
|
466
460
|
|
|
467
|
-
|
|
461
|
+
self._fig['layout'].show( )
|
|
468
462
|
|
|
469
463
|
def _asyncio_loop( self ):
|
|
470
464
|
'''return the event loop which can be mixed in with an existing event loop
|
|
@@ -87,7 +87,8 @@ class InteractiveClean:
|
|
|
87
87
|
interpolation='nearest', ... )( ) )
|
|
88
88
|
'''
|
|
89
89
|
self._id = uuid4( )
|
|
90
|
+
self._ui.exclusion_mgr.set_mode("tab")
|
|
90
91
|
context = exe.Context( exe.Mode.SYNC )
|
|
91
92
|
bokeh_ui, exec_task = self._ui( context, self._id )
|
|
92
|
-
show(
|
|
93
|
+
bokeh_ui.show( )
|
|
93
94
|
return context.execute( exec_task, self._id )
|
|
@@ -1849,7 +1849,8 @@ class InteractiveClean:
|
|
|
1849
1849
|
interpolation='nearest', ... )( ) )
|
|
1850
1850
|
'''
|
|
1851
1851
|
self._id = uuid4( )
|
|
1852
|
+
self._ui.exclusion_mgr.set_mode("tab")
|
|
1852
1853
|
context = exe.Context( exe.Mode.SYNC )
|
|
1853
1854
|
bokeh_ui, exec_task = self._ui( context, self._id )
|
|
1854
|
-
show(
|
|
1855
|
+
bokeh_ui.show( )
|
|
1855
1856
|
return context.execute( exec_task, self._id )
|
|
@@ -43,6 +43,22 @@ from cubevis.utils import find_pkg, load_pkg
|
|
|
43
43
|
from cubevis.toolbox import InteractiveCleanUI
|
|
44
44
|
from cubevis import exe
|
|
45
45
|
|
|
46
|
+
class DisplayContext:
|
|
47
|
+
def __init__(self, exclusion_manager):
|
|
48
|
+
self.exclusion_manager = exclusion_manager
|
|
49
|
+
self._custom_show_called = False
|
|
50
|
+
|
|
51
|
+
def on_show(self):
|
|
52
|
+
self._custom_show_called = True
|
|
53
|
+
self.exclusion_manager.set_mode('cell-custom-show')
|
|
54
|
+
|
|
55
|
+
def on_to_serializable(self):
|
|
56
|
+
# Only set mode if custom_show hasn't been called
|
|
57
|
+
if not self._custom_show_called:
|
|
58
|
+
self.exclusion_manager.set_mode('cell-bokeh-show')
|
|
59
|
+
# If custom_show was called, we're already in 'cell-custom-show' mode
|
|
60
|
+
# and don't need to do anything
|
|
61
|
+
|
|
46
62
|
class InteractiveCleanNotebook:
|
|
47
63
|
r'''InteractiveCleanNotebook(...) implements interactive clean using Bokeh
|
|
48
64
|
{{docstring}}
|
|
@@ -107,6 +123,13 @@ class InteractiveCleanNotebook:
|
|
|
107
123
|
def startup( ):
|
|
108
124
|
self._future = context.execute( exec_task, self._id )
|
|
109
125
|
|
|
126
|
+
display_ctx = DisplayContext(self._ui.exclusion_mgr)
|
|
110
127
|
### name is used in summary output of the Showable
|
|
111
|
-
showed = Showable(
|
|
128
|
+
showed = Showable(
|
|
129
|
+
bokeh_ui,
|
|
130
|
+
startup,
|
|
131
|
+
self.get_future,
|
|
132
|
+
name="iclean-jpy",
|
|
133
|
+
display_context=display_ctx
|
|
134
|
+
)
|
|
112
135
|
return showed
|
|
@@ -42,6 +42,22 @@ from cubevis.utils import find_pkg, load_pkg
|
|
|
42
42
|
from cubevis.toolbox import InteractiveCleanUI
|
|
43
43
|
from cubevis import exe
|
|
44
44
|
|
|
45
|
+
class DisplayContext:
|
|
46
|
+
def __init__(self, exclusion_manager):
|
|
47
|
+
self.exclusion_manager = exclusion_manager
|
|
48
|
+
self._custom_show_called = False
|
|
49
|
+
|
|
50
|
+
def on_show(self):
|
|
51
|
+
self._custom_show_called = True
|
|
52
|
+
self.exclusion_manager.set_mode('cell-custom-show')
|
|
53
|
+
|
|
54
|
+
def on_to_serializable(self):
|
|
55
|
+
# Only set mode if custom_show hasn't been called
|
|
56
|
+
if not self._custom_show_called:
|
|
57
|
+
self.exclusion_manager.set_mode('cell-bokeh-show')
|
|
58
|
+
# If custom_show was called, we're already in 'cell-custom-show' mode
|
|
59
|
+
# and don't need to do anything
|
|
60
|
+
|
|
45
61
|
class InteractiveCleanNotebook:
|
|
46
62
|
r'''InteractiveCleanNotebook(...) implements interactive clean using Bokeh
|
|
47
63
|
tclean ---- Radio Interferometric Image Reconstruction
|
|
@@ -1869,6 +1885,13 @@ class InteractiveCleanNotebook:
|
|
|
1869
1885
|
def startup( ):
|
|
1870
1886
|
self._future = context.execute( exec_task, self._id )
|
|
1871
1887
|
|
|
1888
|
+
display_ctx = DisplayContext(self._ui.exclusion_mgr)
|
|
1872
1889
|
### name is used in summary output of the Showable
|
|
1873
|
-
showed = Showable(
|
|
1890
|
+
showed = Showable(
|
|
1891
|
+
bokeh_ui,
|
|
1892
|
+
startup,
|
|
1893
|
+
self.get_future,
|
|
1894
|
+
name="iclean-jpy",
|
|
1895
|
+
display_context=display_ctx
|
|
1896
|
+
)
|
|
1874
1897
|
return showed
|
|
@@ -1513,18 +1513,24 @@ class CubeMask:
|
|
|
1513
1513
|
## """console.log("Running from script/terminal. Closing window.")
|
|
1514
1514
|
## window.close()"""
|
|
1515
1515
|
##) +
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1516
|
+
###
|
|
1517
|
+
### Previously the Python is_interactive_jupyter( ) function was used
|
|
1518
|
+
### to decide whether the window should be closed or not. However, to
|
|
1519
|
+
### support the use of the regular iclean (i.e. display in a separate
|
|
1520
|
+
### browser tab) the check is now whether a showable is available or
|
|
1521
|
+
### not since Showable is the coupling between GUIs and Jupyter
|
|
1522
|
+
### notebooks.
|
|
1523
|
+
###
|
|
1524
|
+
"""if ( showable ) {
|
|
1525
|
+
console.log("Running in jupyter notebook.\\nDisabling GUI via root Showable.")
|
|
1526
|
+
showable.disabled = true
|
|
1527
|
+
const data_pipes = Bokeh.activeDataPipes.getInstances( )
|
|
1528
|
+
console.log("Data pipes to be closed:", data_pipes)
|
|
1521
1529
|
} else {
|
|
1522
|
-
console.log("Running
|
|
1530
|
+
console.log("Running from script/terminal. Closing window.")
|
|
1531
|
+
window.close()
|
|
1523
1532
|
}
|
|
1524
|
-
"""
|
|
1525
|
-
"""console.log("Running from script/terminal. Closing window.")
|
|
1526
|
-
window.close()"""
|
|
1527
|
-
) +
|
|
1533
|
+
""" +
|
|
1528
1534
|
"""
|
|
1529
1535
|
}
|
|
1530
1536
|
}
|
|
@@ -53,6 +53,7 @@ from cubevis.bokeh.models import TipButton, Tip, EvTextInput, BokehAppContext
|
|
|
53
53
|
|
|
54
54
|
from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter, find_pkg, load_pkg
|
|
55
55
|
from cubevis.utils import ContextMgrChain as CMC
|
|
56
|
+
from cubevis.utils import MutualExclusionManager
|
|
56
57
|
|
|
57
58
|
# pylint: disable=no-name-in-module
|
|
58
59
|
from casatasks.private.imagerhelpers.imager_return_dict import ImagingDict
|
|
@@ -61,7 +62,7 @@ from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
|
|
|
61
62
|
# pylint: enable=no-name-in-module
|
|
62
63
|
|
|
63
64
|
from cubevis.utils import find_ws_address, convert_masks
|
|
64
|
-
from cubevis.toolbox import CubeMask
|
|
65
|
+
from cubevis.toolbox import CubeMask
|
|
65
66
|
from cubevis.bokeh.utils import svg_icon
|
|
66
67
|
from cubevis.bokeh.sources import DataPipe
|
|
67
68
|
from cubevis.utils import DocEnum
|
|
@@ -74,6 +75,25 @@ USE_MULTIPLE_GCLEAN_HACK=False
|
|
|
74
75
|
class InteractiveCleanUI:
|
|
75
76
|
'''InteractiveCleanUI(...) implements interactive clean using Bokeh
|
|
76
77
|
'''
|
|
78
|
+
|
|
79
|
+
exclusion_mgr = MutualExclusionManager(
|
|
80
|
+
name="interactive clean",
|
|
81
|
+
valid_modes={
|
|
82
|
+
"tab": "❌ Cannot use iclean task display:\n\n" \
|
|
83
|
+
"Reason: bokeh.plotting.show() or iclean show method has already been\n" \
|
|
84
|
+
" used for display of this class. Mixing display methods within\n" \
|
|
85
|
+
" a single notebook corrupts Bokeh display within the notebook\n",
|
|
86
|
+
"cell-bokeh-show": "❌ Cannot use bokeh.plotting.show() display method:\n\n" \
|
|
87
|
+
"Reason: iclean show or task method has already been used for display\n" \
|
|
88
|
+
" of this class. Mixing display methods within a single notebook\n" \
|
|
89
|
+
" corrupts Bokeh display within the notebook\n",
|
|
90
|
+
"cell-custom-show": "❌ Cannot use iclean show method:\n\n" \
|
|
91
|
+
"Reason: bokeh.plotting.show() or task has already been used for display\n" \
|
|
92
|
+
" of this class. Mixing display methods within a single notebook\n" \
|
|
93
|
+
" corrupts Bokeh display within the notebook\n",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
77
97
|
def __stop( self, _=None ):
|
|
78
98
|
self.__result_future.set_result(self.__retrieve_result( ))
|
|
79
99
|
|
|
@@ -164,11 +184,6 @@ class InteractiveCleanUI:
|
|
|
164
184
|
### the image display.
|
|
165
185
|
###
|
|
166
186
|
self._conv_spect_plot_width = 450
|
|
167
|
-
###
|
|
168
|
-
### Create application context (which includes a temporary directory).
|
|
169
|
-
### This sets the title of the plot.
|
|
170
|
-
###
|
|
171
|
-
self._app_state = AppContext( 'Interactive Clean' )
|
|
172
187
|
|
|
173
188
|
###
|
|
174
189
|
### Whether or not the Interactive Clean session is running remotely
|
|
@@ -948,7 +963,7 @@ class InteractiveCleanUI:
|
|
|
948
963
|
app_state={ ### while the state dictionary itself
|
|
949
964
|
'name': 'interactive clean', ### is used, these particular element
|
|
950
965
|
'initialized': True ### are not currently used for anything
|
|
951
|
-
} )
|
|
966
|
+
}, title='Interactive Clean' )
|
|
952
967
|
|
|
953
968
|
###
|
|
954
969
|
### Keep track of which image is currently active in appstate.image_name (which is
|
|
@@ -52,6 +52,7 @@ from cubevis.bokeh.models import TipButton, Tip, EvTextInput, BokehAppContext
|
|
|
52
52
|
|
|
53
53
|
from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter, find_pkg, load_pkg
|
|
54
54
|
from cubevis.utils import ContextMgrChain as CMC
|
|
55
|
+
from cubevis.utils import MutualExclusionManager
|
|
55
56
|
|
|
56
57
|
# pylint: disable=no-name-in-module
|
|
57
58
|
from casatasks.private.imagerhelpers.imager_return_dict import ImagingDict
|
|
@@ -60,7 +61,7 @@ from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
|
|
|
60
61
|
# pylint: enable=no-name-in-module
|
|
61
62
|
|
|
62
63
|
from cubevis.utils import find_ws_address, convert_masks
|
|
63
|
-
from cubevis.toolbox import CubeMask
|
|
64
|
+
from cubevis.toolbox import CubeMask
|
|
64
65
|
from cubevis.bokeh.utils import svg_icon
|
|
65
66
|
from cubevis.bokeh.sources import DataPipe
|
|
66
67
|
from cubevis.utils import DocEnum
|
|
@@ -73,6 +74,25 @@ USE_MULTIPLE_GCLEAN_HACK=False
|
|
|
73
74
|
class InteractiveCleanUI:
|
|
74
75
|
'''InteractiveCleanUI(...) implements interactive clean using Bokeh
|
|
75
76
|
'''
|
|
77
|
+
|
|
78
|
+
exclusion_mgr = MutualExclusionManager(
|
|
79
|
+
name="interactive clean",
|
|
80
|
+
valid_modes={
|
|
81
|
+
"tab": "❌ Cannot use iclean task display:\n\n" \
|
|
82
|
+
"Reason: bokeh.plotting.show() or iclean show method has already been\n" \
|
|
83
|
+
" used for display of this class. Mixing display methods within\n" \
|
|
84
|
+
" a single notebook corrupts Bokeh display within the notebook\n",
|
|
85
|
+
"cell-bokeh-show": "❌ Cannot use bokeh.plotting.show() display method:\n\n" \
|
|
86
|
+
"Reason: iclean show or task method has already been used for display\n" \
|
|
87
|
+
" of this class. Mixing display methods within a single notebook\n" \
|
|
88
|
+
" corrupts Bokeh display within the notebook\n",
|
|
89
|
+
"cell-custom-show": "❌ Cannot use iclean show method:\n\n" \
|
|
90
|
+
"Reason: bokeh.plotting.show() or task has already been used for display\n" \
|
|
91
|
+
" of this class. Mixing display methods within a single notebook\n" \
|
|
92
|
+
" corrupts Bokeh display within the notebook\n",
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
76
96
|
def __stop( self, _=None ):
|
|
77
97
|
self.__result_future.set_result(self.__retrieve_result( ))
|
|
78
98
|
|
|
@@ -163,11 +183,6 @@ class InteractiveCleanUI:
|
|
|
163
183
|
### the image display.
|
|
164
184
|
###
|
|
165
185
|
self._conv_spect_plot_width = 450
|
|
166
|
-
###
|
|
167
|
-
### Create application context (which includes a temporary directory).
|
|
168
|
-
### This sets the title of the plot.
|
|
169
|
-
###
|
|
170
|
-
self._app_state = AppContext( 'Interactive Clean' )
|
|
171
186
|
|
|
172
187
|
###
|
|
173
188
|
### Whether or not the Interactive Clean session is running remotely
|
|
@@ -947,7 +962,7 @@ class InteractiveCleanUI:
|
|
|
947
962
|
app_state={ ### while the state dictionary itself
|
|
948
963
|
'name': 'interactive clean', ### is used, these particular element
|
|
949
964
|
'initialized': True ### are not currently used for anything
|
|
950
|
-
} )
|
|
965
|
+
}, title='Interactive Clean' )
|
|
951
966
|
|
|
952
967
|
###
|
|
953
968
|
### Keep track of which image is currently active in appstate.image_name (which is
|
|
@@ -67,6 +67,7 @@ from ._static import static_vars, static_dir
|
|
|
67
67
|
from ._tiles import TMSTiles
|
|
68
68
|
from ._contextmgrchain import ContextMgrChain
|
|
69
69
|
from ._import_protected_module import ImportProtectedModule
|
|
70
|
+
from ._mutual_exclusion import MutualExclusionManager
|
|
70
71
|
|
|
71
72
|
@static_vars(mgr=None)
|
|
72
73
|
def resource_manager( ):
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MutualExclusionManager:
|
|
6
|
+
"""
|
|
7
|
+
Generic manager for enforcing mutual exclusion between different execution paths.
|
|
8
|
+
|
|
9
|
+
Use this when you want to ensure only one of several mutually exclusive paths
|
|
10
|
+
can be taken within a given context (thread).
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
mode_manager = MutualExclusionManager(
|
|
14
|
+
name="display_mode",
|
|
15
|
+
valid_modes={
|
|
16
|
+
'notebook': "Cannot use Showable after displaying with bokeh.plotting.show().",
|
|
17
|
+
'separate_tab': "Cannot use bokeh.plotting.show() after displaying with Showable."
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# In path A:
|
|
22
|
+
mode_manager.set_mode('notebook')
|
|
23
|
+
|
|
24
|
+
# In path B (will raise error if path A was already taken):
|
|
25
|
+
mode_manager.set_mode('separate_tab')
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str, valid_modes: Dict[str, str]):
|
|
29
|
+
"""
|
|
30
|
+
Args:
|
|
31
|
+
name: Descriptive name for this manager (used in error messages)
|
|
32
|
+
valid_modes: Dictionary mapping mode names to error messages to display
|
|
33
|
+
when that mode conflicts with an already-set mode.
|
|
34
|
+
"""
|
|
35
|
+
self.name = name
|
|
36
|
+
self.valid_modes = valid_modes
|
|
37
|
+
self._local = threading.local()
|
|
38
|
+
|
|
39
|
+
def set_mode(self, mode: str):
|
|
40
|
+
"""
|
|
41
|
+
Set the execution mode. Raises RuntimeError if a different mode was already set.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
mode: The mode identifier to set
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If mode is not in valid_modes
|
|
48
|
+
RuntimeError: If a different mode was already set in this context
|
|
49
|
+
"""
|
|
50
|
+
if mode not in self.valid_modes:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Invalid mode '{mode}' for {self.name}. "
|
|
53
|
+
f"Valid modes: {set(self.valid_modes.keys())}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
current_mode = self.get_mode()
|
|
57
|
+
if current_mode is not None and current_mode != mode:
|
|
58
|
+
# Use the error message associated with the mode being set
|
|
59
|
+
error_message = self.valid_modes[mode]
|
|
60
|
+
raise RuntimeError(error_message)
|
|
61
|
+
|
|
62
|
+
self._local.mode = mode
|
|
63
|
+
|
|
64
|
+
def get_mode(self) -> str | None:
|
|
65
|
+
"""
|
|
66
|
+
Get the current mode, or None if no mode has been set.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The current mode string, or None
|
|
70
|
+
"""
|
|
71
|
+
return getattr(self._local, 'mode', None)
|
|
72
|
+
|
|
73
|
+
def require_mode(self, mode: str):
|
|
74
|
+
"""
|
|
75
|
+
Check that we're in the specified mode, set it if unset, error if different.
|
|
76
|
+
|
|
77
|
+
This is a convenience method that combines get_mode checking with set_mode.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
mode: The required mode
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RuntimeError: If a different mode was already set
|
|
84
|
+
"""
|
|
85
|
+
self.set_mode(mode)
|
|
86
|
+
|
|
87
|
+
def forbid_mode(self, mode: str, message: str | None = None):
|
|
88
|
+
"""
|
|
89
|
+
Raise an error if the current mode matches the specified mode.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
mode: The mode to forbid
|
|
93
|
+
message: Optional custom error message. If None, uses the message from valid_modes.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
RuntimeError: If current mode matches the forbidden mode
|
|
97
|
+
"""
|
|
98
|
+
current_mode = self.get_mode()
|
|
99
|
+
if current_mode == mode:
|
|
100
|
+
if message is None:
|
|
101
|
+
message = self.valid_modes.get(
|
|
102
|
+
mode,
|
|
103
|
+
f"Cannot proceed: {self.name} is set to '{mode}', "
|
|
104
|
+
f"which is not allowed in this context."
|
|
105
|
+
)
|
|
106
|
+
raise RuntimeError(message)
|
|
107
|
+
|
|
108
|
+
def reset(self):
|
|
109
|
+
"""
|
|
110
|
+
Reset the mode for this context. Useful for testing or manual mode switching.
|
|
111
|
+
"""
|
|
112
|
+
if hasattr(self._local, 'mode'):
|
|
113
|
+
del self._local.mode
|
|
114
|
+
|
|
115
|
+
def __repr__(self):
|
|
116
|
+
mode = self.get_mode()
|
|
117
|
+
return f"MutualExclusionManager(name='{self.name}', current_mode={mode!r})"
|