##// END OF EJS Templates
Merge pull request #4943 from takluyver/docs-shotgun-4...
Min RK -
r14931:5feaa035 merge
parent child Browse files
Show More
@@ -1,419 +1,423 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16
16
17 from IPython.kernel.comm import Comm
17 from IPython.kernel.comm import Comm
18 from IPython.config import LoggingConfigurable
18 from IPython.config import LoggingConfigurable
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
20 from IPython.utils.py3compat import string_types
20 from IPython.utils.py3compat import string_types
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Classes
23 # Classes
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 class CallbackDispatcher(LoggingConfigurable):
25 class CallbackDispatcher(LoggingConfigurable):
26 """A structure for registering and running callbacks"""
26 """A structure for registering and running callbacks"""
27 callbacks = List()
27 callbacks = List()
28
28
29 def __call__(self, *args, **kwargs):
29 def __call__(self, *args, **kwargs):
30 """Call all of the registered callbacks."""
30 """Call all of the registered callbacks."""
31 value = None
31 value = None
32 for callback in self.callbacks:
32 for callback in self.callbacks:
33 try:
33 try:
34 local_value = callback(*args, **kwargs)
34 local_value = callback(*args, **kwargs)
35 except Exception as e:
35 except Exception as e:
36 self.log.warn("Exception in callback %s: %s", callback, e)
36 self.log.warn("Exception in callback %s: %s", callback, e)
37 else:
37 else:
38 value = local_value if local_value is not None else value
38 value = local_value if local_value is not None else value
39 return value
39 return value
40
40
41 def register_callback(self, callback, remove=False):
41 def register_callback(self, callback, remove=False):
42 """(Un)Register a callback
42 """(Un)Register a callback
43
43
44 Parameters
44 Parameters
45 ----------
45 ----------
46 callback: method handle
46 callback: method handle
47 Method to be registered or unregistered.
47 Method to be registered or unregistered.
48 remove=False: bool
48 remove=False: bool
49 Whether to unregister the callback."""
49 Whether to unregister the callback."""
50
50
51 # (Un)Register the callback.
51 # (Un)Register the callback.
52 if remove and callback in self.callbacks:
52 if remove and callback in self.callbacks:
53 self.callbacks.remove(callback)
53 self.callbacks.remove(callback)
54 elif not remove and callback not in self.callbacks:
54 elif not remove and callback not in self.callbacks:
55 self.callbacks.append(callback)
55 self.callbacks.append(callback)
56
56
57
57
58 class Widget(LoggingConfigurable):
58 class Widget(LoggingConfigurable):
59 #-------------------------------------------------------------------------
59 #-------------------------------------------------------------------------
60 # Class attributes
60 # Class attributes
61 #-------------------------------------------------------------------------
61 #-------------------------------------------------------------------------
62 _widget_construction_callback = None
62 _widget_construction_callback = None
63 widgets = {}
63 widgets = {}
64
64
65 @staticmethod
65 @staticmethod
66 def on_widget_constructed(callback):
66 def on_widget_constructed(callback):
67 """Registers a callback to be called when a widget is constructed.
67 """Registers a callback to be called when a widget is constructed.
68
68
69 The callback must have the following signature:
69 The callback must have the following signature:
70 callback(widget)"""
70 callback(widget)"""
71 Widget._widget_construction_callback = callback
71 Widget._widget_construction_callback = callback
72
72
73 @staticmethod
73 @staticmethod
74 def _call_widget_constructed(widget):
74 def _call_widget_constructed(widget):
75 """Static method, called when a widget is constructed."""
75 """Static method, called when a widget is constructed."""
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
77 Widget._widget_construction_callback(widget)
77 Widget._widget_construction_callback(widget)
78
78
79 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
80 # Traits
80 # Traits
81 #-------------------------------------------------------------------------
81 #-------------------------------------------------------------------------
82 model_name = Unicode('WidgetModel', help="""Name of the backbone model
82 model_name = Unicode('WidgetModel', help="""Name of the backbone model
83 registered in the front-end to create and sync this widget with.""")
83 registered in the front-end to create and sync this widget with.""")
84 _view_name = Unicode(help="""Default view registered in the front-end
84 _view_name = Unicode(help="""Default view registered in the front-end
85 to use to represent the widget.""", sync=True)
85 to use to represent the widget.""", sync=True)
86 _comm = Instance('IPython.kernel.comm.Comm')
86 _comm = Instance('IPython.kernel.comm.Comm')
87
87
88 closed = Bool(False)
88 closed = Bool(False)
89
89
90 keys = List()
90 keys = List()
91 def _keys_default(self):
91 def _keys_default(self):
92 return [name for name in self.traits(sync=True)]
92 return [name for name in self.traits(sync=True)]
93
93
94 _property_lock = Tuple((None, None))
94 _property_lock = Tuple((None, None))
95
95
96 _display_callbacks = Instance(CallbackDispatcher, ())
96 _display_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
98
98
99 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
100 # (Con/de)structor
100 # (Con/de)structor
101 #-------------------------------------------------------------------------
101 #-------------------------------------------------------------------------
102 def __init__(self, **kwargs):
102 def __init__(self, **kwargs):
103 """Public constructor"""
103 """Public constructor"""
104 super(Widget, self).__init__(**kwargs)
104 super(Widget, self).__init__(**kwargs)
105
105
106 self.on_trait_change(self._handle_property_changed, self.keys)
106 self.on_trait_change(self._handle_property_changed, self.keys)
107 Widget._call_widget_constructed(self)
107 Widget._call_widget_constructed(self)
108
108
109 def __del__(self):
109 def __del__(self):
110 """Object disposal"""
110 """Object disposal"""
111 self.close()
111 self.close()
112
112
113 #-------------------------------------------------------------------------
113 #-------------------------------------------------------------------------
114 # Properties
114 # Properties
115 #-------------------------------------------------------------------------
115 #-------------------------------------------------------------------------
116
116
117 @property
117 @property
118 def comm(self):
118 def comm(self):
119 """Gets the Comm associated with this widget.
119 """Gets the Comm associated with this widget.
120
120
121 If a Comm doesn't exist yet, a Comm will be created automagically."""
121 If a Comm doesn't exist yet, a Comm will be created automagically."""
122 if self._comm is None:
122 if self._comm is None:
123 # Create a comm.
123 # Create a comm.
124 self._comm = Comm(target_name=self.model_name)
124 self._comm = Comm(target_name=self.model_name)
125 self._comm.on_msg(self._handle_msg)
125 self._comm.on_msg(self._handle_msg)
126 self._comm.on_close(self._close)
126 self._comm.on_close(self._close)
127 Widget.widgets[self.model_id] = self
127 Widget.widgets[self.model_id] = self
128
128
129 # first update
129 # first update
130 self.send_state()
130 self.send_state()
131 return self._comm
131 return self._comm
132
132
133 @property
133 @property
134 def model_id(self):
134 def model_id(self):
135 """Gets the model id of this widget.
135 """Gets the model id of this widget.
136
136
137 If a Comm doesn't exist yet, a Comm will be created automagically."""
137 If a Comm doesn't exist yet, a Comm will be created automagically."""
138 return self.comm.comm_id
138 return self.comm.comm_id
139
139
140 #-------------------------------------------------------------------------
140 #-------------------------------------------------------------------------
141 # Methods
141 # Methods
142 #-------------------------------------------------------------------------
142 #-------------------------------------------------------------------------
143 def _close(self):
143 def _close(self):
144 """Private close - cleanup objects, registry entries"""
144 """Private close - cleanup objects, registry entries"""
145 del Widget.widgets[self.model_id]
145 del Widget.widgets[self.model_id]
146 self._comm = None
146 self._comm = None
147 self.closed = True
147 self.closed = True
148
148
149 def close(self):
149 def close(self):
150 """Close method.
150 """Close method.
151
151
152 Closes the widget which closes the underlying comm.
152 Closes the widget which closes the underlying comm.
153 When the comm is closed, all of the widget views are automatically
153 When the comm is closed, all of the widget views are automatically
154 removed from the front-end."""
154 removed from the front-end."""
155 if not self.closed:
155 if not self.closed:
156 self._comm.close()
156 self._comm.close()
157 self._close()
157 self._close()
158
158
159 def send_state(self, key=None):
159 def send_state(self, key=None):
160 """Sends the widget state, or a piece of it, to the front-end.
160 """Sends the widget state, or a piece of it, to the front-end.
161
161
162 Parameters
162 Parameters
163 ----------
163 ----------
164 key : unicode (optional)
164 key : unicode (optional)
165 A single property's name to sync with the front-end.
165 A single property's name to sync with the front-end.
166 """
166 """
167 self._send({
167 self._send({
168 "method" : "update",
168 "method" : "update",
169 "state" : self.get_state()
169 "state" : self.get_state()
170 })
170 })
171
171
172 def get_state(self, key=None):
172 def get_state(self, key=None):
173 """Gets the widget state, or a piece of it.
173 """Gets the widget state, or a piece of it.
174
174
175 Parameters
175 Parameters
176 ----------
176 ----------
177 key : unicode (optional)
177 key : unicode (optional)
178 A single property's name to get.
178 A single property's name to get.
179 """
179 """
180 keys = self.keys if key is None else [key]
180 keys = self.keys if key is None else [key]
181 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
181 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
182
182
183 def send(self, content):
183 def send(self, content):
184 """Sends a custom msg to the widget model in the front-end.
184 """Sends a custom msg to the widget model in the front-end.
185
185
186 Parameters
186 Parameters
187 ----------
187 ----------
188 content : dict
188 content : dict
189 Content of the message to send.
189 Content of the message to send.
190 """
190 """
191 self._send({"method": "custom", "content": content})
191 self._send({"method": "custom", "content": content})
192
192
193 def on_msg(self, callback, remove=False):
193 def on_msg(self, callback, remove=False):
194 """(Un)Register a custom msg receive callback.
194 """(Un)Register a custom msg receive callback.
195
195
196 Parameters
196 Parameters
197 ----------
197 ----------
198 callback: callable
198 callback: callable
199 callback will be passed two arguments when a message arrives:
199 callback will be passed two arguments when a message arrives::
200
200 callback(widget, content)
201 callback(widget, content)
202
201 remove: bool
203 remove: bool
202 True if the callback should be unregistered."""
204 True if the callback should be unregistered."""
203 self._msg_callbacks.register_callback(callback, remove=remove)
205 self._msg_callbacks.register_callback(callback, remove=remove)
204
206
205 def on_displayed(self, callback, remove=False):
207 def on_displayed(self, callback, remove=False):
206 """(Un)Register a widget displayed callback.
208 """(Un)Register a widget displayed callback.
207
209
208 Parameters
210 Parameters
209 ----------
211 ----------
210 callback: method handler
212 callback: method handler
211 Must have a signature of:
213 Must have a signature of::
214
212 callback(widget, **kwargs)
215 callback(widget, **kwargs)
216
213 kwargs from display are passed through without modification.
217 kwargs from display are passed through without modification.
214 remove: bool
218 remove: bool
215 True if the callback should be unregistered."""
219 True if the callback should be unregistered."""
216 self._display_callbacks.register_callback(callback, remove=remove)
220 self._display_callbacks.register_callback(callback, remove=remove)
217
221
218 #-------------------------------------------------------------------------
222 #-------------------------------------------------------------------------
219 # Support methods
223 # Support methods
220 #-------------------------------------------------------------------------
224 #-------------------------------------------------------------------------
221 @contextmanager
225 @contextmanager
222 def _lock_property(self, key, value):
226 def _lock_property(self, key, value):
223 """Lock a property-value pair.
227 """Lock a property-value pair.
224
228
225 NOTE: This, in addition to the single lock for all state changes, is
229 NOTE: This, in addition to the single lock for all state changes, is
226 flawed. In the future we may want to look into buffering state changes
230 flawed. In the future we may want to look into buffering state changes
227 back to the front-end."""
231 back to the front-end."""
228 self._property_lock = (key, value)
232 self._property_lock = (key, value)
229 try:
233 try:
230 yield
234 yield
231 finally:
235 finally:
232 self._property_lock = (None, None)
236 self._property_lock = (None, None)
233
237
234 def _should_send_property(self, key, value):
238 def _should_send_property(self, key, value):
235 """Check the property lock (property_lock)"""
239 """Check the property lock (property_lock)"""
236 return key != self._property_lock[0] or \
240 return key != self._property_lock[0] or \
237 value != self._property_lock[1]
241 value != self._property_lock[1]
238
242
239 # Event handlers
243 # Event handlers
240 def _handle_msg(self, msg):
244 def _handle_msg(self, msg):
241 """Called when a msg is received from the front-end"""
245 """Called when a msg is received from the front-end"""
242 data = msg['content']['data']
246 data = msg['content']['data']
243 method = data['method']
247 method = data['method']
244 if not method in ['backbone', 'custom']:
248 if not method in ['backbone', 'custom']:
245 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
249 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
246
250
247 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
251 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
248 if method == 'backbone' and 'sync_data' in data:
252 if method == 'backbone' and 'sync_data' in data:
249 sync_data = data['sync_data']
253 sync_data = data['sync_data']
250 self._handle_receive_state(sync_data) # handles all methods
254 self._handle_receive_state(sync_data) # handles all methods
251
255
252 # Handle a custom msg from the front-end
256 # Handle a custom msg from the front-end
253 elif method == 'custom':
257 elif method == 'custom':
254 if 'content' in data:
258 if 'content' in data:
255 self._handle_custom_msg(data['content'])
259 self._handle_custom_msg(data['content'])
256
260
257 def _handle_receive_state(self, sync_data):
261 def _handle_receive_state(self, sync_data):
258 """Called when a state is received from the front-end."""
262 """Called when a state is received from the front-end."""
259 for name in self.keys:
263 for name in self.keys:
260 if name in sync_data:
264 if name in sync_data:
261 value = self._unpack_widgets(sync_data[name])
265 value = self._unpack_widgets(sync_data[name])
262 with self._lock_property(name, value):
266 with self._lock_property(name, value):
263 setattr(self, name, value)
267 setattr(self, name, value)
264
268
265 def _handle_custom_msg(self, content):
269 def _handle_custom_msg(self, content):
266 """Called when a custom msg is received."""
270 """Called when a custom msg is received."""
267 self._msg_callbacks(self, content)
271 self._msg_callbacks(self, content)
268
272
269 def _handle_property_changed(self, name, old, new):
273 def _handle_property_changed(self, name, old, new):
270 """Called when a property has been changed."""
274 """Called when a property has been changed."""
271 # Make sure this isn't information that the front-end just sent us.
275 # Make sure this isn't information that the front-end just sent us.
272 if self._should_send_property(name, new):
276 if self._should_send_property(name, new):
273 # Send new state to front-end
277 # Send new state to front-end
274 self.send_state(key=name)
278 self.send_state(key=name)
275
279
276 def _handle_displayed(self, **kwargs):
280 def _handle_displayed(self, **kwargs):
277 """Called when a view has been displayed for this widget instance"""
281 """Called when a view has been displayed for this widget instance"""
278 self._display_callbacks(self, **kwargs)
282 self._display_callbacks(self, **kwargs)
279
283
280 def _pack_widgets(self, x):
284 def _pack_widgets(self, x):
281 """Recursively converts all widget instances to model id strings.
285 """Recursively converts all widget instances to model id strings.
282
286
283 Children widgets will be stored and transmitted to the front-end by
287 Children widgets will be stored and transmitted to the front-end by
284 their model ids. Return value must be JSON-able."""
288 their model ids. Return value must be JSON-able."""
285 if isinstance(x, dict):
289 if isinstance(x, dict):
286 return {k: self._pack_widgets(v) for k, v in x.items()}
290 return {k: self._pack_widgets(v) for k, v in x.items()}
287 elif isinstance(x, list):
291 elif isinstance(x, list):
288 return [self._pack_widgets(v) for v in x]
292 return [self._pack_widgets(v) for v in x]
289 elif isinstance(x, Widget):
293 elif isinstance(x, Widget):
290 return x.model_id
294 return x.model_id
291 else:
295 else:
292 return x # Value must be JSON-able
296 return x # Value must be JSON-able
293
297
294 def _unpack_widgets(self, x):
298 def _unpack_widgets(self, x):
295 """Recursively converts all model id strings to widget instances.
299 """Recursively converts all model id strings to widget instances.
296
300
297 Children widgets will be stored and transmitted to the front-end by
301 Children widgets will be stored and transmitted to the front-end by
298 their model ids."""
302 their model ids."""
299 if isinstance(x, dict):
303 if isinstance(x, dict):
300 return {k: self._unpack_widgets(v) for k, v in x.items()}
304 return {k: self._unpack_widgets(v) for k, v in x.items()}
301 elif isinstance(x, list):
305 elif isinstance(x, list):
302 return [self._unpack_widgets(v) for v in x]
306 return [self._unpack_widgets(v) for v in x]
303 elif isinstance(x, string_types):
307 elif isinstance(x, string_types):
304 return x if x not in Widget.widgets else Widget.widgets[x]
308 return x if x not in Widget.widgets else Widget.widgets[x]
305 else:
309 else:
306 return x
310 return x
307
311
308 def _ipython_display_(self, **kwargs):
312 def _ipython_display_(self, **kwargs):
309 """Called when `IPython.display.display` is called on the widget."""
313 """Called when `IPython.display.display` is called on the widget."""
310 # Show view. By sending a display message, the comm is opened and the
314 # Show view. By sending a display message, the comm is opened and the
311 # initial state is sent.
315 # initial state is sent.
312 self._send({"method": "display"})
316 self._send({"method": "display"})
313 self._handle_displayed(**kwargs)
317 self._handle_displayed(**kwargs)
314
318
315 def _send(self, msg):
319 def _send(self, msg):
316 """Sends a message to the model in the front-end."""
320 """Sends a message to the model in the front-end."""
317 self.comm.send(msg)
321 self.comm.send(msg)
318
322
319
323
320 class DOMWidget(Widget):
324 class DOMWidget(Widget):
321 visible = Bool(True, help="Whether the widget is visible.", sync=True)
325 visible = Bool(True, help="Whether the widget is visible.", sync=True)
322 _css = Dict(sync=True) # Internal CSS property dict
326 _css = Dict(sync=True) # Internal CSS property dict
323
327
324 def get_css(self, key, selector=""):
328 def get_css(self, key, selector=""):
325 """Get a CSS property of the widget.
329 """Get a CSS property of the widget.
326
330
327 Note: This function does not actually request the CSS from the
331 Note: This function does not actually request the CSS from the
328 front-end; Only properties that have been set with set_css can be read.
332 front-end; Only properties that have been set with set_css can be read.
329
333
330 Parameters
334 Parameters
331 ----------
335 ----------
332 key: unicode
336 key: unicode
333 CSS key
337 CSS key
334 selector: unicode (optional)
338 selector: unicode (optional)
335 JQuery selector used when the CSS key/value was set.
339 JQuery selector used when the CSS key/value was set.
336 """
340 """
337 if selector in self._css and key in self._css[selector]:
341 if selector in self._css and key in self._css[selector]:
338 return self._css[selector][key]
342 return self._css[selector][key]
339 else:
343 else:
340 return None
344 return None
341
345
342 def set_css(self, dict_or_key, value=None, selector=''):
346 def set_css(self, dict_or_key, value=None, selector=''):
343 """Set one or more CSS properties of the widget.
347 """Set one or more CSS properties of the widget.
344
348
345 This function has two signatures:
349 This function has two signatures:
346 - set_css(css_dict, selector='')
350 - set_css(css_dict, selector='')
347 - set_css(key, value, selector='')
351 - set_css(key, value, selector='')
348
352
349 Parameters
353 Parameters
350 ----------
354 ----------
351 css_dict : dict
355 css_dict : dict
352 CSS key/value pairs to apply
356 CSS key/value pairs to apply
353 key: unicode
357 key: unicode
354 CSS key
358 CSS key
355 value:
359 value:
356 CSS value
360 CSS value
357 selector: unicode (optional, kwarg only)
361 selector: unicode (optional, kwarg only)
358 JQuery selector to use to apply the CSS key/value. If no selector
362 JQuery selector to use to apply the CSS key/value. If no selector
359 is provided, an empty selector is used. An empty selector makes the
363 is provided, an empty selector is used. An empty selector makes the
360 front-end try to apply the css to a default element. The default
364 front-end try to apply the css to a default element. The default
361 element is an attribute unique to each view, which is a DOM element
365 element is an attribute unique to each view, which is a DOM element
362 of the view that should be styled with common CSS (see
366 of the view that should be styled with common CSS (see
363 `$el_to_style` in the Javascript code).
367 `$el_to_style` in the Javascript code).
364 """
368 """
365 if not selector in self._css:
369 if not selector in self._css:
366 self._css[selector] = {}
370 self._css[selector] = {}
367 my_css = self._css[selector]
371 my_css = self._css[selector]
368
372
369 if value is None:
373 if value is None:
370 css_dict = dict_or_key
374 css_dict = dict_or_key
371 else:
375 else:
372 css_dict = {dict_or_key: value}
376 css_dict = {dict_or_key: value}
373
377
374 for (key, value) in css_dict.items():
378 for (key, value) in css_dict.items():
375 if not (key in my_css and value == my_css[key]):
379 if not (key in my_css and value == my_css[key]):
376 my_css[key] = value
380 my_css[key] = value
377 self.send_state('_css')
381 self.send_state('_css')
378
382
379 def add_class(self, class_names, selector=""):
383 def add_class(self, class_names, selector=""):
380 """Add class[es] to a DOM element.
384 """Add class[es] to a DOM element.
381
385
382 Parameters
386 Parameters
383 ----------
387 ----------
384 class_names: unicode or list
388 class_names: unicode or list
385 Class name(s) to add to the DOM element(s).
389 Class name(s) to add to the DOM element(s).
386 selector: unicode (optional)
390 selector: unicode (optional)
387 JQuery selector to select the DOM element(s) that the class(es) will
391 JQuery selector to select the DOM element(s) that the class(es) will
388 be added to.
392 be added to.
389 """
393 """
390 class_list = class_names
394 class_list = class_names
391 if isinstance(class_list, list):
395 if isinstance(class_list, list):
392 class_list = ' '.join(class_list)
396 class_list = ' '.join(class_list)
393
397
394 self.send({
398 self.send({
395 "msg_type" : "add_class",
399 "msg_type" : "add_class",
396 "class_list" : class_list,
400 "class_list" : class_list,
397 "selector" : selector
401 "selector" : selector
398 })
402 })
399
403
400 def remove_class(self, class_names, selector=""):
404 def remove_class(self, class_names, selector=""):
401 """Remove class[es] from a DOM element.
405 """Remove class[es] from a DOM element.
402
406
403 Parameters
407 Parameters
404 ----------
408 ----------
405 class_names: unicode or list
409 class_names: unicode or list
406 Class name(s) to remove from the DOM element(s).
410 Class name(s) to remove from the DOM element(s).
407 selector: unicode (optional)
411 selector: unicode (optional)
408 JQuery selector to select the DOM element(s) that the class(es) will
412 JQuery selector to select the DOM element(s) that the class(es) will
409 be removed from.
413 be removed from.
410 """
414 """
411 class_list = class_names
415 class_list = class_names
412 if isinstance(class_list, list):
416 if isinstance(class_list, list):
413 class_list = ' '.join(class_list)
417 class_list = ' '.join(class_list)
414
418
415 self.send({
419 self.send({
416 "msg_type" : "remove_class",
420 "msg_type" : "remove_class",
417 "class_list" : class_list,
421 "class_list" : class_list,
418 "selector" : selector,
422 "selector" : selector,
419 })
423 })
@@ -1,128 +1,130 b''
1 """Utility for calling pandoc"""
1 """Utility for calling pandoc"""
2 #-----------------------------------------------------------------------------
2 #-----------------------------------------------------------------------------
3 # Copyright (c) 2014 the IPython Development Team.
3 # Copyright (c) 2014 the IPython Development Team.
4 #
4 #
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6 #
6 #
7 # The full license is in the file COPYING.txt, distributed with this software.
7 # The full license is in the file COPYING.txt, distributed with this software.
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 from __future__ import print_function
13 from __future__ import print_function
14
14
15 # Stdlib imports
15 # Stdlib imports
16 import subprocess
16 import subprocess
17 import re
17 import re
18 import warnings
18 import warnings
19 from io import TextIOWrapper, BytesIO
19 from io import TextIOWrapper, BytesIO
20
20
21 # IPython imports
21 # IPython imports
22 from IPython.utils.py3compat import cast_bytes
22 from IPython.utils.py3compat import cast_bytes
23 from IPython.utils.version import check_version
23 from IPython.utils.version import check_version
24 from IPython.utils.process import is_cmd_found, FindCmdError
24 from IPython.utils.process import is_cmd_found, FindCmdError
25
25
26 from .exceptions import ConversionException
26 from .exceptions import ConversionException
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Classes and functions
29 # Classes and functions
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 _minimal_version = "1.12.1"
31 _minimal_version = "1.12.1"
32
32
33 def pandoc(source, fmt, to, extra_args=None, encoding='utf-8'):
33 def pandoc(source, fmt, to, extra_args=None, encoding='utf-8'):
34 """Convert an input string in format `from` to format `to` via pandoc.
34 """Convert an input string in format `from` to format `to` via pandoc.
35
35
36 Parameters
36 Parameters
37 ----------
37 ----------
38 source : string
38 source : string
39 Input string, assumed to be valid format `from`.
39 Input string, assumed to be valid format `from`.
40 fmt : string
40 fmt : string
41 The name of the input format (markdown, etc.)
41 The name of the input format (markdown, etc.)
42 to : string
42 to : string
43 The name of the output format (html, etc.)
43 The name of the output format (html, etc.)
44
44
45 Returns
45 Returns
46 -------
46 -------
47 out : unicode
47 out : unicode
48 Output as returned by pandoc.
48 Output as returned by pandoc.
49
49
50 Exceptions
50 Raises
51 ----------
51 ------
52 This function will raise PandocMissing if pandoc is not installed.
52 PandocMissing
53 If pandoc is not installed.
54
53 Any error messages generated by pandoc are printed to stderr.
55 Any error messages generated by pandoc are printed to stderr.
54
56
55 """
57 """
56 cmd = ['pandoc', '-f', fmt, '-t', to]
58 cmd = ['pandoc', '-f', fmt, '-t', to]
57 if extra_args:
59 if extra_args:
58 cmd.extend(extra_args)
60 cmd.extend(extra_args)
59
61
60 # this will raise an exception that will pop us out of here
62 # this will raise an exception that will pop us out of here
61 check_pandoc_version()
63 check_pandoc_version()
62
64
63 # we can safely continue
65 # we can safely continue
64 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
66 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
65 out, _ = p.communicate(cast_bytes(source, encoding))
67 out, _ = p.communicate(cast_bytes(source, encoding))
66 out = TextIOWrapper(BytesIO(out), encoding, 'replace').read()
68 out = TextIOWrapper(BytesIO(out), encoding, 'replace').read()
67 return out.rstrip('\n')
69 return out.rstrip('\n')
68
70
69
71
70 def get_pandoc_version():
72 def get_pandoc_version():
71 """Gets the Pandoc version if Pandoc is installed.
73 """Gets the Pandoc version if Pandoc is installed.
72
74
73 Return
74 ------
75 If the minimal version is not met, it will probe Pandoc for its version, cache it and return that value.
75 If the minimal version is not met, it will probe Pandoc for its version, cache it and return that value.
76 If the minimal version is met, it will return the cached version and stop probing Pandoc
76 If the minimal version is met, it will return the cached version and stop probing Pandoc
77 (unless `clean_cache()` is called).
77 (unless :func:`clean_cache()` is called).
78
78
79 Exceptions
79 Raises
80 ----------
80 ------
81 PandocMissing will be raised if pandoc is unavailable.
81 PandocMissing
82 If pandoc is unavailable.
82 """
83 """
83 global __version
84 global __version
84
85
85 if __version is None:
86 if __version is None:
86 if not is_cmd_found('pandoc'):
87 if not is_cmd_found('pandoc'):
87 raise PandocMissing()
88 raise PandocMissing()
88
89
89 out = subprocess.check_output( ['pandoc', '-v'], universal_newlines=True)
90 out = subprocess.check_output( ['pandoc', '-v'], universal_newlines=True)
90 pv_re = re.compile(r'(\d{0,3}\.\d{0,3}\.\d{0,3})')
91 pv_re = re.compile(r'(\d{0,3}\.\d{0,3}\.\d{0,3})')
91 __version = pv_re.search(out).group(0)
92 __version = pv_re.search(out).group(0)
92 return __version
93 return __version
93
94
94
95
95 def check_pandoc_version():
96 def check_pandoc_version():
96 """Returns True if minimal pandoc version is met.
97 """Returns True if minimal pandoc version is met.
97
98
98 Exceptions
99 Raises
99 ----------
100 ------
100 PandocMissing will be raised if pandoc is unavailable.
101 PandocMissing
102 If pandoc is unavailable.
101 """
103 """
102 v = get_pandoc_version()
104 v = get_pandoc_version()
103 ok = check_version(v , _minimal_version )
105 ok = check_version(v , _minimal_version )
104 if not ok:
106 if not ok:
105 warnings.warn( "You are using an old version of pandoc (%s)\n" % v +
107 warnings.warn( "You are using an old version of pandoc (%s)\n" % v +
106 "Recommended version is %s.\nTry updating." % _minimal_version +
108 "Recommended version is %s.\nTry updating." % _minimal_version +
107 "http://johnmacfarlane.net/pandoc/installing.html.\nContinuing with doubts...",
109 "http://johnmacfarlane.net/pandoc/installing.html.\nContinuing with doubts...",
108 RuntimeWarning, stacklevel=2)
110 RuntimeWarning, stacklevel=2)
109 return ok
111 return ok
110
112
111 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
112 # Exception handling
114 # Exception handling
113 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
114 class PandocMissing(ConversionException):
116 class PandocMissing(ConversionException):
115 """Exception raised when Pandoc is missing. """
117 """Exception raised when Pandoc is missing. """
116 def __init__(self, *args, **kwargs):
118 def __init__(self, *args, **kwargs):
117 super(PandocMissing, self).__init__( "Pandoc wasn't found.\n" +
119 super(PandocMissing, self).__init__( "Pandoc wasn't found.\n" +
118 "Please check that pandoc is installed:\n" +
120 "Please check that pandoc is installed:\n" +
119 "http://johnmacfarlane.net/pandoc/installing.html" )
121 "http://johnmacfarlane.net/pandoc/installing.html" )
120
122
121 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
122 # Internal state management
124 # Internal state management
123 #-----------------------------------------------------------------------------
125 #-----------------------------------------------------------------------------
124 def clean_cache():
126 def clean_cache():
125 global __version
127 global __version
126 __version = None
128 __version = None
127
129
128 __version = None
130 __version = None
General Comments 0
You need to be logged in to leave comments. Login now