-
-
Notifications
You must be signed in to change notification settings - Fork 394
/
comms.py
215 lines (170 loc) · 5.75 KB
/
comms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import json
import uuid
import sys
import os
import traceback
try:
from StringIO import StringIO
except:
from io import StringIO
from ipykernel.comm import Comm as IPyComm
from IPython import get_ipython
class StandardOutput(list):
"""
Context manager to capture standard output for any code it
is wrapping and make it available as a list, e.g.:
>>> with StandardOutput() as stdout:
... print('This gets captured')
>>> print(stdout[0])
This gets captured
"""
def __enter__(self):
self._stdout = sys.stdout
sys.stdout = self._stringio = StringIO()
return self
def __exit__(self, *args):
self.extend(self._stringio.getvalue().splitlines())
sys.stdout = self._stdout
class Comm(object):
"""
Comm encompasses any uni- or bi-directional connection between
a python process and a frontend allowing passing of messages
between the two. A Comms class must implement methods
send data and handle received message events.
If the Comm has to be set up on the frontend a template to
handle the creation of the comms channel along with a message
handler to process incoming messages must be supplied.
The template must accept three arguments:
* comms_target - A unique id to register to register as the
comms target.
* msg_handler - JS code which has the msg variable in scope and
performs appropriate action for the supplied message.
* init_frame - The initial frame to render on the frontend.
"""
template = ''
def __init__(self, plot, target=None, on_msg=None):
"""
Initializes a Comms object
"""
self.target = target if target else uuid.uuid4().hex
self._plot = plot
self._on_msg = on_msg
self._comm = None
def init(self, on_msg=None):
"""
Initializes comms channel.
"""
def send(self, data):
"""
Sends data to the frontend
"""
@classmethod
def decode(cls, msg):
"""
Decode incoming message, e.g. by parsing json.
"""
return msg
@property
def comm(self):
if not self._comm:
raise ValueError('Comm has not been initialized')
return self._comm
def _handle_msg(self, msg):
"""
Decode received message before passing it to on_msg callback
if it has been defined.
"""
try:
stdout = []
msg = self.decode(msg)
if self._on_msg:
# Comm swallows standard output so we need to capture
# it and then send it to the frontend
with StandardOutput() as stdout:
self._on_msg(msg)
except Exception as e:
frame =traceback.extract_tb(sys.exc_info()[2])[-2]
fname,lineno,fn,text = frame
error_kwargs = dict(type=type(e).__name__, fn=fn, fname=fname,
line=lineno, error=str(e))
error = '{fname} {fn} L{line}\n\t{type}: {error}'.format(**error_kwargs)
if stdout:
stdout = '\n\t'+'\n\t'.join(stdout)
error = '\n'.join([stdout, error])
msg = {'msg_type': "Error", 'traceback': error}
else:
stdout = '\n\t'+'\n\t'.join(stdout) if stdout else ''
msg = {'msg_type': "Ready", 'content': stdout}
self.comm.send(json.dumps(msg))
class JupyterComm(Comm):
"""
JupyterComm provides a Comm for the notebook which is initialized
the first time data is pushed to the frontend.
"""
template = """
<script>
function msg_handler(msg) {{
var msg = msg.content.data;
{msg_handler}
}}
if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel !== undefined)) {{
comm_manager = Jupyter.notebook.kernel.comm_manager;
comm_manager.register_target("{comms_target}", function(comm) {{ comm.on_msg(msg_handler);}});
}}
</script>
<div id="fig_{comms_target}">
{init_frame}
</div>
"""
def init(self):
if self._comm:
return
self._comm = IPyComm(target_name=self.target, data={})
self._comm.on_msg(self._handle_msg)
@classmethod
def decode(cls, msg):
return msg['content']['data']
def send(self, data):
"""
Pushes data across comm socket.
"""
if not self._comm:
self.init()
self.comm.send(data)
class JupyterCommJS(JupyterComm):
"""
JupyterCommJS provides a comms channel for the Jupyter notebook,
which is initialized on the frontend. This allows sending events
initiated on the frontend to python.
"""
template = """
<script>
function msg_handler(msg) {{
var msg = msg.content.data;
{msg_handler}
}}
if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel !== undefined)) {{
var comm_manager = Jupyter.notebook.kernel.comm_manager;
comm = comm_manager.new_comm("{comms_target}", {{}}, {{}}, {{}}, "{comms_target}");
comm.on_msg(msg_handler);
}}
</script>
<div id="fig_{comms_target}">
{init_frame}
</div>
"""
def __init__(self, plot, target=None, on_msg=None):
"""
Initializes a Comms object
"""
super(JupyterCommJS, self).__init__(plot, target, on_msg)
self.manager = get_ipython().kernel.comm_manager
self.manager.register_target(self.target, self._handle_open)
def _handle_open(self, comm, msg):
self._comm = comm
self._comm.on_msg(self._handle_msg)
def send(self, data):
"""
Pushes data across comm socket.
"""
self.comm.send(data)