Show More
@@ -0,0 +1,299 b'' | |||||
|
1 | """Contains eventful dict and list implementations.""" | |||
|
2 | ||||
|
3 | # void function used as a callback placeholder. | |||
|
4 | def _void(*p, **k): return None | |||
|
5 | ||||
|
6 | class EventfulDict(dict): | |||
|
7 | """Eventful dictionary. | |||
|
8 | ||||
|
9 | This class inherits from the Python intrinsic dictionary class, dict. It | |||
|
10 | adds events to the get, set, and del actions and optionally allows you to | |||
|
11 | intercept and cancel these actions. The eventfulness isn't recursive. In | |||
|
12 | other words, if you add a dict as a child, the events of that dict won't be | |||
|
13 | listened to. If you find you need something recursive, listen to the `add` | |||
|
14 | and `set` methods, and then cancel `dict` values from being set, and instead | |||
|
15 | set `EventfulDict`s that wrap those `dict`s. Then you can wire the events | |||
|
16 | to the same handlers if necessary. | |||
|
17 | ||||
|
18 | See the on_events, on_add, on_set, and on_del methods for registering | |||
|
19 | event handlers.""" | |||
|
20 | ||||
|
21 | def __init__(self, *args, **kwargs): | |||
|
22 | """Public constructor""" | |||
|
23 | self._add_callback = _void | |||
|
24 | self._del_callback = _void | |||
|
25 | self._set_callback = _void | |||
|
26 | dict.__init__(self, *args, **kwargs) | |||
|
27 | ||||
|
28 | def on_events(self, add_callback=None, set_callback=None, del_callback=None): | |||
|
29 | """Register callbacks for add, set, and del actions. | |||
|
30 | ||||
|
31 | See the doctstrings for on_(add/set/del) for details about each | |||
|
32 | callback. | |||
|
33 | ||||
|
34 | add_callback: [callback = None] | |||
|
35 | set_callback: [callback = None] | |||
|
36 | del_callback: [callback = None]""" | |||
|
37 | self.on_add(add_callback) | |||
|
38 | self.on_set(set_callback) | |||
|
39 | self.on_del(del_callback) | |||
|
40 | ||||
|
41 | def on_add(self, callback): | |||
|
42 | """Register a callback for when an item is added to the dict. | |||
|
43 | ||||
|
44 | Allows the listener to detect when items are added to the dictionary and | |||
|
45 | optionally cancel the addition. | |||
|
46 | ||||
|
47 | callback: callable or None | |||
|
48 | If you want to ignore the addition event, pass None as the callback. | |||
|
49 | The callback should have a signature of callback(key, value). The | |||
|
50 | callback should return a boolean True if the additon should be | |||
|
51 | canceled, False or None otherwise.""" | |||
|
52 | self._add_callback = callback if callable(callback) else _void | |||
|
53 | ||||
|
54 | def on_del(self, callback): | |||
|
55 | """Register a callback for when an item is deleted from the dict. | |||
|
56 | ||||
|
57 | Allows the listener to detect when items are deleted from the dictionary | |||
|
58 | and optionally cancel the deletion. | |||
|
59 | ||||
|
60 | callback: callable or None | |||
|
61 | If you want to ignore the deletion event, pass None as the callback. | |||
|
62 | The callback should have a signature of callback(key). The | |||
|
63 | callback should return a boolean True if the deletion should be | |||
|
64 | canceled, False or None otherwise.""" | |||
|
65 | self._del_callback = callback if callable(callback) else _void | |||
|
66 | ||||
|
67 | def on_set(self, callback): | |||
|
68 | """Register a callback for when an item is changed in the dict. | |||
|
69 | ||||
|
70 | Allows the listener to detect when items are changed in the dictionary | |||
|
71 | and optionally cancel the change. | |||
|
72 | ||||
|
73 | callback: callable or None | |||
|
74 | If you want to ignore the change event, pass None as the callback. | |||
|
75 | The callback should have a signature of callback(key, value). The | |||
|
76 | callback should return a boolean True if the change should be | |||
|
77 | canceled, False or None otherwise.""" | |||
|
78 | self._set_callback = callback if callable(callback) else _void | |||
|
79 | ||||
|
80 | def pop(self, key): | |||
|
81 | """Returns the value of an item in the dictionary and then deletes the | |||
|
82 | item from the dictionary.""" | |||
|
83 | if self._can_del(key): | |||
|
84 | return dict.pop(self, key) | |||
|
85 | else: | |||
|
86 | raise Exception('Cannot `pop`, deletion of key "{}" failed.'.format(key)) | |||
|
87 | ||||
|
88 | def popitem(self): | |||
|
89 | """Pop the next key/value pair from the dictionary.""" | |||
|
90 | key = next(iter(self)) | |||
|
91 | return key, self.pop(key) | |||
|
92 | ||||
|
93 | def update(self, other_dict): | |||
|
94 | """Copy the key/value pairs from another dictionary into this dictionary, | |||
|
95 | overwriting any conflicting keys in this dictionary.""" | |||
|
96 | for (key, value) in other_dict.items(): | |||
|
97 | self[key] = value | |||
|
98 | ||||
|
99 | def clear(self): | |||
|
100 | """Clear the dictionary.""" | |||
|
101 | for key in list(self.keys()): | |||
|
102 | del self[key] | |||
|
103 | ||||
|
104 | def __setitem__(self, key, value): | |||
|
105 | if (key in self and self._can_set(key, value)) or \ | |||
|
106 | (key not in self and self._can_add(key, value)): | |||
|
107 | return dict.__setitem__(self, key, value) | |||
|
108 | ||||
|
109 | def __delitem__(self, key): | |||
|
110 | if self._can_del(key): | |||
|
111 | return dict.__delitem__(self, key) | |||
|
112 | ||||
|
113 | def _can_add(self, key, value): | |||
|
114 | """Check if the item can be added to the dict.""" | |||
|
115 | return not bool(self._add_callback(key, value)) | |||
|
116 | ||||
|
117 | def _can_del(self, key): | |||
|
118 | """Check if the item can be deleted from the dict.""" | |||
|
119 | return not bool(self._del_callback(key)) | |||
|
120 | ||||
|
121 | def _can_set(self, key, value): | |||
|
122 | """Check if the item can be changed in the dict.""" | |||
|
123 | return not bool(self._set_callback(key, value)) | |||
|
124 | ||||
|
125 | ||||
|
126 | class EventfulList(list): | |||
|
127 | """Eventful list. | |||
|
128 | ||||
|
129 | This class inherits from the Python intrinsic `list` class. It adds events | |||
|
130 | that allow you to listen for actions that modify the list. You can | |||
|
131 | optionally cancel the actions. | |||
|
132 | ||||
|
133 | See the on_del, on_set, on_insert, on_sort, and on_reverse methods for | |||
|
134 | registering an event handler. | |||
|
135 | ||||
|
136 | Some of the method docstrings were taken from the Python documentation at | |||
|
137 | https://docs.python.org/2/tutorial/datastructures.html""" | |||
|
138 | ||||
|
139 | def __init__(self, *pargs, **kwargs): | |||
|
140 | """Public constructor""" | |||
|
141 | self._insert_callback = _void | |||
|
142 | self._set_callback = _void | |||
|
143 | self._del_callback = _void | |||
|
144 | self._sort_callback = _void | |||
|
145 | self._reverse_callback = _void | |||
|
146 | list.__init__(self, *pargs, **kwargs) | |||
|
147 | ||||
|
148 | def on_events(self, insert_callback=None, set_callback=None, | |||
|
149 | del_callback=None, reverse_callback=None, sort_callback=None): | |||
|
150 | """Register callbacks for add, set, and del actions. | |||
|
151 | ||||
|
152 | See the doctstrings for on_(insert/set/del/reverse/sort) for details | |||
|
153 | about each callback. | |||
|
154 | ||||
|
155 | insert_callback: [callback = None] | |||
|
156 | set_callback: [callback = None] | |||
|
157 | del_callback: [callback = None] | |||
|
158 | reverse_callback: [callback = None] | |||
|
159 | sort_callback: [callback = None]""" | |||
|
160 | self.on_insert(insert_callback) | |||
|
161 | self.on_set(set_callback) | |||
|
162 | self.on_del(del_callback) | |||
|
163 | self.on_reverse(reverse_callback) | |||
|
164 | self.on_sort(sort_callback) | |||
|
165 | ||||
|
166 | def on_insert(self, callback): | |||
|
167 | """Register a callback for when an item is inserted into the list. | |||
|
168 | ||||
|
169 | Allows the listener to detect when items are inserted into the list and | |||
|
170 | optionally cancel the insertion. | |||
|
171 | ||||
|
172 | callback: callable or None | |||
|
173 | If you want to ignore the insertion event, pass None as the callback. | |||
|
174 | The callback should have a signature of callback(index, value). The | |||
|
175 | callback should return a boolean True if the insertion should be | |||
|
176 | canceled, False or None otherwise.""" | |||
|
177 | self._insert_callback = callback if callable(callback) else _void | |||
|
178 | ||||
|
179 | def on_del(self, callback): | |||
|
180 | """Register a callback for item deletion. | |||
|
181 | ||||
|
182 | Allows the listener to detect when items are deleted from the list and | |||
|
183 | optionally cancel the deletion. | |||
|
184 | ||||
|
185 | callback: callable or None | |||
|
186 | If you want to ignore the deletion event, pass None as the callback. | |||
|
187 | The callback should have a signature of callback(index). The | |||
|
188 | callback should return a boolean True if the deletion should be | |||
|
189 | canceled, False or None otherwise.""" | |||
|
190 | self._del_callback = callback if callable(callback) else _void | |||
|
191 | ||||
|
192 | def on_set(self, callback): | |||
|
193 | """Register a callback for items are set. | |||
|
194 | ||||
|
195 | Allows the listener to detect when items are set and optionally cancel | |||
|
196 | the setting. Note, `set` is also called when one or more items are | |||
|
197 | added to the end of the list. | |||
|
198 | ||||
|
199 | callback: callable or None | |||
|
200 | If you want to ignore the set event, pass None as the callback. | |||
|
201 | The callback should have a signature of callback(index, value). The | |||
|
202 | callback should return a boolean True if the set should be | |||
|
203 | canceled, False or None otherwise.""" | |||
|
204 | self._set_callback = callback if callable(callback) else _void | |||
|
205 | ||||
|
206 | def on_reverse(self, callback): | |||
|
207 | """Register a callback for list reversal. | |||
|
208 | ||||
|
209 | callback: callable or None | |||
|
210 | If you want to ignore the reverse event, pass None as the callback. | |||
|
211 | The callback should have a signature of callback(). The | |||
|
212 | callback should return a boolean True if the reverse should be | |||
|
213 | canceled, False or None otherwise.""" | |||
|
214 | self._reverse_callback = callback if callable(callback) else _void | |||
|
215 | ||||
|
216 | def on_sort(self, callback): | |||
|
217 | """Register a callback for sortting of the list. | |||
|
218 | ||||
|
219 | callback: callable or None | |||
|
220 | If you want to ignore the sort event, pass None as the callback. | |||
|
221 | The callback signature should match that of Python list's `.sort` | |||
|
222 | method or `callback(*pargs, **kwargs)` as a catch all. The callback | |||
|
223 | should return a boolean True if the reverse should be canceled, | |||
|
224 | False or None otherwise.""" | |||
|
225 | self._sort_callback = callback if callable(callback) else _void | |||
|
226 | ||||
|
227 | def append(self, x): | |||
|
228 | """Add an item to the end of the list.""" | |||
|
229 | self[len(self):] = [x] | |||
|
230 | ||||
|
231 | def extend(self, L): | |||
|
232 | """Extend the list by appending all the items in the given list.""" | |||
|
233 | self[len(self):] = L | |||
|
234 | ||||
|
235 | def remove(self, x): | |||
|
236 | """Remove the first item from the list whose value is x. It is an error | |||
|
237 | if there is no such item.""" | |||
|
238 | del self[self.index(x)] | |||
|
239 | ||||
|
240 | def pop(self, i=None): | |||
|
241 | """Remove the item at the given position in the list, and return it. If | |||
|
242 | no index is specified, a.pop() removes and returns the last item in the | |||
|
243 | list.""" | |||
|
244 | if i is None: | |||
|
245 | i = len(self) - 1 | |||
|
246 | val = self[i] | |||
|
247 | del self[i] | |||
|
248 | return val | |||
|
249 | ||||
|
250 | def reverse(self): | |||
|
251 | """Reverse the elements of the list, in place.""" | |||
|
252 | if self._can_reverse(): | |||
|
253 | list.reverse(self) | |||
|
254 | ||||
|
255 | def insert(self, index, value): | |||
|
256 | """Insert an item at a given position. The first argument is the index | |||
|
257 | of the element before which to insert, so a.insert(0, x) inserts at the | |||
|
258 | front of the list, and a.insert(len(a), x) is equivalent to | |||
|
259 | a.append(x).""" | |||
|
260 | if self._can_insert(index, value): | |||
|
261 | list.insert(self, index, value) | |||
|
262 | ||||
|
263 | def sort(self, *pargs, **kwargs): | |||
|
264 | """Sort the items of the list in place (the arguments can be used for | |||
|
265 | sort customization, see Python's sorted() for their explanation).""" | |||
|
266 | if self._can_sort(*pargs, **kwargs): | |||
|
267 | list.sort(self, *pargs, **kwargs) | |||
|
268 | ||||
|
269 | def __delitem__(self, index): | |||
|
270 | if self._can_del(index): | |||
|
271 | list.__delitem__(self, index) | |||
|
272 | ||||
|
273 | def __setitem__(self, index, value): | |||
|
274 | if self._can_set(index, value): | |||
|
275 | list.__setitem__(self, index, value) | |||
|
276 | ||||
|
277 | def __setslice__(self, start, end, value): | |||
|
278 | if self._can_set(slice(start, end), value): | |||
|
279 | list.__setslice__(self, start, end, value) | |||
|
280 | ||||
|
281 | def _can_insert(self, index, value): | |||
|
282 | """Check if the item can be inserted.""" | |||
|
283 | return not bool(self._insert_callback(index, value)) | |||
|
284 | ||||
|
285 | def _can_del(self, index): | |||
|
286 | """Check if the item can be deleted.""" | |||
|
287 | return not bool(self._del_callback(index)) | |||
|
288 | ||||
|
289 | def _can_set(self, index, value): | |||
|
290 | """Check if the item can be set.""" | |||
|
291 | return not bool(self._set_callback(index, value)) | |||
|
292 | ||||
|
293 | def _can_reverse(self): | |||
|
294 | """Check if the list can be reversed.""" | |||
|
295 | return not bool(self._reverse_callback()) | |||
|
296 | ||||
|
297 | def _can_sort(self, *pargs, **kwargs): | |||
|
298 | """Check if the list can be sorted.""" | |||
|
299 | return not bool(self._sort_callback(*pargs, **kwargs)) |
@@ -911,7 +911,8 b' class InteractiveShell(SingletonConfigurable):' | |||||
911 | try: |
|
911 | try: | |
912 | main_mod = self._main_mod_cache[filename] |
|
912 | main_mod = self._main_mod_cache[filename] | |
913 | except KeyError: |
|
913 | except KeyError: | |
914 |
main_mod = self._main_mod_cache[filename] = types.ModuleType( |
|
914 | main_mod = self._main_mod_cache[filename] = types.ModuleType( | |
|
915 | py3compat.cast_bytes_py2(modname), | |||
915 | doc="Module created for script run in IPython") |
|
916 | doc="Module created for script run in IPython") | |
916 | else: |
|
917 | else: | |
917 | main_mod.__dict__.clear() |
|
918 | main_mod.__dict__.clear() |
@@ -472,6 +472,12 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
472 | with open(filename, 'r') as f: |
|
472 | with open(filename, 'r') as f: | |
473 | self.assertEqual(f.read(), 'blah') |
|
473 | self.assertEqual(f.read(), 'blah') | |
474 |
|
474 | |||
|
475 | def test_new_main_mod(self): | |||
|
476 | # Smoketest to check that this accepts a unicode module name | |||
|
477 | name = u'jiefmw' | |||
|
478 | mod = ip.new_main_mod(u'%s.py' % name, name) | |||
|
479 | self.assertEqual(mod.__name__, name) | |||
|
480 | ||||
475 | class TestSafeExecfileNonAsciiPath(unittest.TestCase): |
|
481 | class TestSafeExecfileNonAsciiPath(unittest.TestCase): | |
476 |
|
482 | |||
477 | @onlyif_unicode_paths |
|
483 | @onlyif_unicode_paths |
@@ -805,8 +805,10 b' class VerboseTB(TBTools):' | |||||
805 | elif token_type == tokenize.NEWLINE: |
|
805 | elif token_type == tokenize.NEWLINE: | |
806 | break |
|
806 | break | |
807 |
|
807 | |||
808 | except (IndexError, UnicodeDecodeError): |
|
808 | except (IndexError, UnicodeDecodeError, SyntaxError): | |
809 | # signals exit of tokenizer |
|
809 | # signals exit of tokenizer | |
|
810 | # SyntaxError can occur if the file is not actually Python | |||
|
811 | # - see gh-6300 | |||
810 | pass |
|
812 | pass | |
811 | except tokenize.TokenError as msg: |
|
813 | except tokenize.TokenError as msg: | |
812 | _m = ("An unexpected error occurred while tokenizing input\n" |
|
814 | _m = ("An unexpected error occurred while tokenizing input\n" |
@@ -235,12 +235,13 b' class IPythonHandler(AuthenticatedHandler):' | |||||
235 | raise web.HTTPError(400, u'Invalid JSON in body of request') |
|
235 | raise web.HTTPError(400, u'Invalid JSON in body of request') | |
236 | return model |
|
236 | return model | |
237 |
|
237 | |||
238 |
def |
|
238 | def write_error(self, status_code, **kwargs): | |
239 | """render custom error pages""" |
|
239 | """render custom error pages""" | |
240 |
exc |
|
240 | exc_info = kwargs.get('exc_info') | |
241 | message = '' |
|
241 | message = '' | |
242 | status_message = responses.get(status_code, 'Unknown HTTP Error') |
|
242 | status_message = responses.get(status_code, 'Unknown HTTP Error') | |
243 |
if exc |
|
243 | if exc_info: | |
|
244 | exception = exc_info[1] | |||
244 | # get the custom message, if defined |
|
245 | # get the custom message, if defined | |
245 | try: |
|
246 | try: | |
246 | message = exception.log_message % exception.args |
|
247 | message = exception.log_message % exception.args | |
@@ -260,13 +261,16 b' class IPythonHandler(AuthenticatedHandler):' | |||||
260 | exception=exception, |
|
261 | exception=exception, | |
261 | ) |
|
262 | ) | |
262 |
|
263 | |||
|
264 | self.set_header('Content-Type', 'text/html') | |||
263 | # render the template |
|
265 | # render the template | |
264 | try: |
|
266 | try: | |
265 | html = self.render_template('%s.html' % status_code, **ns) |
|
267 | html = self.render_template('%s.html' % status_code, **ns) | |
266 | except TemplateNotFound: |
|
268 | except TemplateNotFound: | |
267 | self.log.debug("No template for %d", status_code) |
|
269 | self.log.debug("No template for %d", status_code) | |
268 | html = self.render_template('error.html', **ns) |
|
270 | html = self.render_template('error.html', **ns) | |
269 | return html |
|
271 | ||
|
272 | self.write(html) | |||
|
273 | ||||
270 |
|
274 | |||
271 |
|
275 | |||
272 | class Template404(IPythonHandler): |
|
276 | class Template404(IPythonHandler): |
@@ -514,15 +514,20 b' define([' | |||||
514 | } |
|
514 | } | |
515 | }; |
|
515 | }; | |
516 |
|
516 | |||
|
517 | var ajax_error_msg = function (jqXHR) { | |||
|
518 | // Return a JSON error message if there is one, | |||
|
519 | // otherwise the basic HTTP status text. | |||
|
520 | if (jqXHR.responseJSON && jqXHR.responseJSON.message) { | |||
|
521 | return jqXHR.responseJSON.message; | |||
|
522 | } else { | |||
|
523 | return jqXHR.statusText; | |||
|
524 | } | |||
|
525 | } | |||
517 | var log_ajax_error = function (jqXHR, status, error) { |
|
526 | var log_ajax_error = function (jqXHR, status, error) { | |
518 | // log ajax failures with informative messages |
|
527 | // log ajax failures with informative messages | |
519 | var msg = "API request failed (" + jqXHR.status + "): "; |
|
528 | var msg = "API request failed (" + jqXHR.status + "): "; | |
520 | console.log(jqXHR); |
|
529 | console.log(jqXHR); | |
521 | if (jqXHR.responseJSON && jqXHR.responseJSON.message) { |
|
530 | msg += ajax_error_msg(jqXHR); | |
522 | msg += jqXHR.responseJSON.message; |
|
|||
523 | } else { |
|
|||
524 | msg += jqXHR.statusText; |
|
|||
525 | } |
|
|||
526 | console.log(msg); |
|
531 | console.log(msg); | |
527 | }; |
|
532 | }; | |
528 |
|
533 | |||
@@ -547,6 +552,7 b' define([' | |||||
547 | platform: platform, |
|
552 | platform: platform, | |
548 | is_or_has : is_or_has, |
|
553 | is_or_has : is_or_has, | |
549 | is_focused : is_focused, |
|
554 | is_focused : is_focused, | |
|
555 | ajax_error_msg : ajax_error_msg, | |||
550 | log_ajax_error : log_ajax_error, |
|
556 | log_ajax_error : log_ajax_error, | |
551 | }; |
|
557 | }; | |
552 |
|
558 |
@@ -2286,13 +2286,14 b' define([' | |||||
2286 | */ |
|
2286 | */ | |
2287 | Notebook.prototype.load_notebook_error = function (xhr, status, error) { |
|
2287 | Notebook.prototype.load_notebook_error = function (xhr, status, error) { | |
2288 | this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]); |
|
2288 | this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]); | |
|
2289 | utils.log_ajax_error(xhr, status, error); | |||
2289 | var msg; |
|
2290 | var msg; | |
2290 | if (xhr.status === 400) { |
|
2291 | if (xhr.status === 400) { | |
2291 | msg = error; |
|
2292 | msg = escape(utils.ajax_error_msg(xhr)); | |
2292 | } else if (xhr.status === 500) { |
|
2293 | } else if (xhr.status === 500) { | |
2293 | msg = "An unknown error occurred while loading this notebook. " + |
|
2294 | msg = "An unknown error occurred while loading this notebook. " + | |
2294 | "This version can load notebook formats " + |
|
2295 | "This version can load notebook formats " + | |
2295 | "v" + this.nbformat + " or earlier."; |
|
2296 | "v" + this.nbformat + " or earlier. See the server log for details."; | |
2296 | } |
|
2297 | } | |
2297 | dialog.modal({ |
|
2298 | dialog.modal({ | |
2298 | notebook: this, |
|
2299 | notebook: this, | |
@@ -2567,10 +2568,10 b' define([' | |||||
2567 | * @method delete_checkpoint_error |
|
2568 | * @method delete_checkpoint_error | |
2568 | * @param {jqXHR} xhr jQuery Ajax object |
|
2569 | * @param {jqXHR} xhr jQuery Ajax object | |
2569 | * @param {String} status Description of response status |
|
2570 | * @param {String} status Description of response status | |
2570 |
* @param {String} error |
|
2571 | * @param {String} error HTTP error message | |
2571 | */ |
|
2572 | */ | |
2572 |
Notebook.prototype.delete_checkpoint_error = function (xhr, status, error |
|
2573 | Notebook.prototype.delete_checkpoint_error = function (xhr, status, error) { | |
2573 | this.events.trigger('checkpoint_delete_failed.Notebook'); |
|
2574 | this.events.trigger('checkpoint_delete_failed.Notebook', [xhr, status, error]); | |
2574 | }; |
|
2575 | }; | |
2575 |
|
2576 | |||
2576 |
|
2577 |
@@ -62,22 +62,35 b' define([' | |||||
62 | // handle in the vertical slider is always |
|
62 | // handle in the vertical slider is always | |
63 | // consistent. |
|
63 | // consistent. | |
64 | var orientation = this.model.get('orientation'); |
|
64 | var orientation = this.model.get('orientation'); | |
65 |
var |
|
65 | var min = this.model.get('min'); | |
|
66 | var max = this.model.get('max'); | |||
66 | if (this.model.get('range')) { |
|
67 | if (this.model.get('range')) { | |
67 |
this.$slider.slider('option', 'values', [ |
|
68 | this.$slider.slider('option', 'values', [min, min]); | |
68 | } else { |
|
69 | } else { | |
69 |
this.$slider.slider('option', 'value', |
|
70 | this.$slider.slider('option', 'value', min); | |
70 | } |
|
71 | } | |
71 | this.$slider.slider('option', 'orientation', orientation); |
|
72 | this.$slider.slider('option', 'orientation', orientation); | |
72 | value = this.model.get('value'); |
|
73 | var value = this.model.get('value'); | |
73 | if (this.model.get('range')) { |
|
74 | if (this.model.get('range')) { | |
|
75 | // values for the range case are validated python-side in | |||
|
76 | // _Bounded{Int,Float}RangeWidget._validate | |||
74 | this.$slider.slider('option', 'values', value); |
|
77 | this.$slider.slider('option', 'values', value); | |
75 | this.$readout.text(value.join("-")); |
|
78 | this.$readout.text(value.join("-")); | |
76 | } else { |
|
79 | } else { | |
|
80 | if(value > max) { | |||
|
81 | value = max; | |||
|
82 | } | |||
|
83 | else if(value < min){ | |||
|
84 | value = min; | |||
|
85 | } | |||
77 | this.$slider.slider('option', 'value', value); |
|
86 | this.$slider.slider('option', 'value', value); | |
78 | this.$readout.text(value); |
|
87 | this.$readout.text(value); | |
79 | } |
|
88 | } | |
80 |
|
89 | |||
|
90 | if(this.model.get('value')!=value) { | |||
|
91 | this.model.set('value', value, {updated_view: this}); | |||
|
92 | this.touch(); | |||
|
93 | } | |||
81 |
|
94 | |||
82 | // Use the right CSS classes for vertical & horizontal sliders |
|
95 | // Use the right CSS classes for vertical & horizontal sliders | |
83 | if (orientation=='vertical') { |
|
96 | if (orientation=='vertical') { |
@@ -80,5 +80,5 b' default_handlers = [' | |||||
80 | (r"/tree%s" % notebook_path_regex, TreeHandler), |
|
80 | (r"/tree%s" % notebook_path_regex, TreeHandler), | |
81 | (r"/tree%s" % path_regex, TreeHandler), |
|
81 | (r"/tree%s" % path_regex, TreeHandler), | |
82 | (r"/tree", TreeHandler), |
|
82 | (r"/tree", TreeHandler), | |
83 | (r"", TreeRedirectHandler), |
|
83 | (r"/?", TreeRedirectHandler), | |
84 | ] |
|
84 | ] |
@@ -13,11 +13,12 b' in the IPython notebook front-end.' | |||||
13 | # Imports |
|
13 | # Imports | |
14 | #----------------------------------------------------------------------------- |
|
14 | #----------------------------------------------------------------------------- | |
15 | from contextlib import contextmanager |
|
15 | from contextlib import contextmanager | |
|
16 | import collections | |||
16 |
|
17 | |||
17 | from IPython.core.getipython import get_ipython |
|
18 | from IPython.core.getipython import get_ipython | |
18 | from IPython.kernel.comm import Comm |
|
19 | from IPython.kernel.comm import Comm | |
19 | from IPython.config import LoggingConfigurable |
|
20 | from IPython.config import LoggingConfigurable | |
20 | from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int |
|
21 | from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set | |
21 | from IPython.utils.py3compat import string_types |
|
22 | from IPython.utils.py3compat import string_types | |
22 |
|
23 | |||
23 | #----------------------------------------------------------------------------- |
|
24 | #----------------------------------------------------------------------------- | |
@@ -98,9 +99,9 b' class Widget(LoggingConfigurable):' | |||||
98 | #------------------------------------------------------------------------- |
|
99 | #------------------------------------------------------------------------- | |
99 | _model_name = Unicode('WidgetModel', help="""Name of the backbone model |
|
100 | _model_name = Unicode('WidgetModel', help="""Name of the backbone model | |
100 | registered in the front-end to create and sync this widget with.""") |
|
101 | registered in the front-end to create and sync this widget with.""") | |
101 | _view_name = Unicode(help="""Default view registered in the front-end |
|
102 | _view_name = Unicode('WidgetView', help="""Default view registered in the front-end | |
102 | to use to represent the widget.""", sync=True) |
|
103 | to use to represent the widget.""", sync=True) | |
103 |
|
|
104 | comm = Instance('IPython.kernel.comm.Comm') | |
104 |
|
105 | |||
105 | msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the |
|
106 | msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the | |
106 | front-end can send before receiving an idle msg from the back-end.""") |
|
107 | front-end can send before receiving an idle msg from the back-end.""") | |
@@ -110,7 +111,8 b' class Widget(LoggingConfigurable):' | |||||
110 | return [name for name in self.traits(sync=True)] |
|
111 | return [name for name in self.traits(sync=True)] | |
111 |
|
112 | |||
112 | _property_lock = Tuple((None, None)) |
|
113 | _property_lock = Tuple((None, None)) | |
113 |
|
114 | _send_state_lock = Int(0) | ||
|
115 | _states_to_send = Set(allow_none=False) | |||
114 | _display_callbacks = Instance(CallbackDispatcher, ()) |
|
116 | _display_callbacks = Instance(CallbackDispatcher, ()) | |
115 | _msg_callbacks = Instance(CallbackDispatcher, ()) |
|
117 | _msg_callbacks = Instance(CallbackDispatcher, ()) | |
116 |
|
118 | |||
@@ -119,10 +121,12 b' class Widget(LoggingConfigurable):' | |||||
119 | #------------------------------------------------------------------------- |
|
121 | #------------------------------------------------------------------------- | |
120 | def __init__(self, **kwargs): |
|
122 | def __init__(self, **kwargs): | |
121 | """Public constructor""" |
|
123 | """Public constructor""" | |
|
124 | self._model_id = kwargs.pop('model_id', None) | |||
122 | super(Widget, self).__init__(**kwargs) |
|
125 | super(Widget, self).__init__(**kwargs) | |
123 |
|
126 | |||
124 | self.on_trait_change(self._handle_property_changed, self.keys) |
|
127 | self.on_trait_change(self._handle_property_changed, self.keys) | |
125 | Widget._call_widget_constructed(self) |
|
128 | Widget._call_widget_constructed(self) | |
|
129 | self.open() | |||
126 |
|
130 | |||
127 | def __del__(self): |
|
131 | def __del__(self): | |
128 | """Object disposal""" |
|
132 | """Object disposal""" | |
@@ -132,20 +136,19 b' class Widget(LoggingConfigurable):' | |||||
132 | # Properties |
|
136 | # Properties | |
133 | #------------------------------------------------------------------------- |
|
137 | #------------------------------------------------------------------------- | |
134 |
|
138 | |||
135 | @property |
|
139 | def open(self): | |
136 | def comm(self): |
|
140 | """Open a comm to the frontend if one isn't already open.""" | |
137 | """Gets the Comm associated with this widget. |
|
141 | if self.comm is None: | |
138 |
|
142 | if self._model_id is None: | ||
139 | If a Comm doesn't exist yet, a Comm will be created automagically.""" |
|
143 | self.comm = Comm(target_name=self._model_name) | |
140 | if self._comm is None: |
|
144 | self._model_id = self.model_id | |
141 |
|
|
145 | else: | |
142 |
self. |
|
146 | self.comm = Comm(target_name=self._model_name, comm_id=self._model_id) | |
143 |
self. |
|
147 | self.comm.on_msg(self._handle_msg) | |
144 | Widget.widgets[self.model_id] = self |
|
148 | Widget.widgets[self.model_id] = self | |
145 |
|
149 | |||
146 | # first update |
|
150 | # first update | |
147 | self.send_state() |
|
151 | self.send_state() | |
148 | return self._comm |
|
|||
149 |
|
152 | |||
150 | @property |
|
153 | @property | |
151 | def model_id(self): |
|
154 | def model_id(self): | |
@@ -164,22 +167,22 b' class Widget(LoggingConfigurable):' | |||||
164 | Closes the underlying comm. |
|
167 | Closes the underlying comm. | |
165 | When the comm is closed, all of the widget views are automatically |
|
168 | When the comm is closed, all of the widget views are automatically | |
166 | removed from the front-end.""" |
|
169 | removed from the front-end.""" | |
167 |
if self. |
|
170 | if self.comm is not None: | |
168 | Widget.widgets.pop(self.model_id, None) |
|
171 | Widget.widgets.pop(self.model_id, None) | |
169 |
self. |
|
172 | self.comm.close() | |
170 |
self. |
|
173 | self.comm = None | |
171 |
|
174 | |||
172 | def send_state(self, key=None): |
|
175 | def send_state(self, key=None): | |
173 | """Sends the widget state, or a piece of it, to the front-end. |
|
176 | """Sends the widget state, or a piece of it, to the front-end. | |
174 |
|
177 | |||
175 | Parameters |
|
178 | Parameters | |
176 | ---------- |
|
179 | ---------- | |
177 | key : unicode (optional) |
|
180 | key : unicode, or iterable (optional) | |
178 | A single property's name to sync with the front-end. |
|
181 | A single property's name or iterable of property names to sync with the front-end. | |
179 | """ |
|
182 | """ | |
180 | self._send({ |
|
183 | self._send({ | |
181 | "method" : "update", |
|
184 | "method" : "update", | |
182 | "state" : self.get_state() |
|
185 | "state" : self.get_state(key=key) | |
183 | }) |
|
186 | }) | |
184 |
|
187 | |||
185 | def get_state(self, key=None): |
|
188 | def get_state(self, key=None): | |
@@ -187,10 +190,17 b' class Widget(LoggingConfigurable):' | |||||
187 |
|
190 | |||
188 | Parameters |
|
191 | Parameters | |
189 | ---------- |
|
192 | ---------- | |
190 | key : unicode (optional) |
|
193 | key : unicode or iterable (optional) | |
191 | A single property's name to get. |
|
194 | A single property's name or iterable of property names to get. | |
192 | """ |
|
195 | """ | |
193 |
|
|
196 | if key is None: | |
|
197 | keys = self.keys | |||
|
198 | elif isinstance(key, string_types): | |||
|
199 | keys = [key] | |||
|
200 | elif isinstance(key, collections.Iterable): | |||
|
201 | keys = key | |||
|
202 | else: | |||
|
203 | raise ValueError("key must be a string, an iterable of keys, or None") | |||
194 | state = {} |
|
204 | state = {} | |
195 | for k in keys: |
|
205 | for k in keys: | |
196 | f = self.trait_metadata(k, 'to_json') |
|
206 | f = self.trait_metadata(k, 'to_json') | |
@@ -255,10 +265,29 b' class Widget(LoggingConfigurable):' | |||||
255 | finally: |
|
265 | finally: | |
256 | self._property_lock = (None, None) |
|
266 | self._property_lock = (None, None) | |
257 |
|
267 | |||
|
268 | @contextmanager | |||
|
269 | def hold_sync(self): | |||
|
270 | """Hold syncing any state until the context manager is released""" | |||
|
271 | # We increment a value so that this can be nested. Syncing will happen when | |||
|
272 | # all levels have been released. | |||
|
273 | self._send_state_lock += 1 | |||
|
274 | try: | |||
|
275 | yield | |||
|
276 | finally: | |||
|
277 | self._send_state_lock -=1 | |||
|
278 | if self._send_state_lock == 0: | |||
|
279 | self.send_state(self._states_to_send) | |||
|
280 | self._states_to_send.clear() | |||
|
281 | ||||
258 | def _should_send_property(self, key, value): |
|
282 | def _should_send_property(self, key, value): | |
259 | """Check the property lock (property_lock)""" |
|
283 | """Check the property lock (property_lock)""" | |
260 |
|
|
284 | if (key == self._property_lock[0] and value == self._property_lock[1]): | |
261 | value != self._property_lock[1] |
|
285 | return False | |
|
286 | elif self._send_state_lock > 0: | |||
|
287 | self._states_to_send.add(key) | |||
|
288 | return False | |||
|
289 | else: | |||
|
290 | return True | |||
262 |
|
291 | |||
263 | # Event handlers |
|
292 | # Event handlers | |
264 | @_show_traceback |
|
293 | @_show_traceback | |
@@ -388,7 +417,10 b' class DOMWidget(Widget):' | |||||
388 | selector: unicode (optional, kwarg only) |
|
417 | selector: unicode (optional, kwarg only) | |
389 | JQuery selector to use to apply the CSS key/value. If no selector |
|
418 | JQuery selector to use to apply the CSS key/value. If no selector | |
390 | is provided, an empty selector is used. An empty selector makes the |
|
419 | is provided, an empty selector is used. An empty selector makes the | |
391 |
front-end try to apply the css to |
|
420 | front-end try to apply the css to a default element. The default | |
|
421 | element is an attribute unique to each view, which is a DOM element | |||
|
422 | of the view that should be styled with common CSS (see | |||
|
423 | `$el_to_style` in the Javascript code). | |||
392 | """ |
|
424 | """ | |
393 | if value is None: |
|
425 | if value is None: | |
394 | css_dict = dict_or_key |
|
426 | css_dict = dict_or_key |
@@ -23,7 +23,7 b' class Comm(LoggingConfigurable):' | |||||
23 | return self.shell.kernel.iopub_socket |
|
23 | return self.shell.kernel.iopub_socket | |
24 | session = Instance('IPython.kernel.zmq.session.Session') |
|
24 | session = Instance('IPython.kernel.zmq.session.Session') | |
25 | def _session_default(self): |
|
25 | def _session_default(self): | |
26 | if self.shell is None: |
|
26 | if self.shell is None or not hasattr(self.shell, 'kernel'): | |
27 | return |
|
27 | return | |
28 | return self.shell.kernel.session |
|
28 | return self.shell.kernel.session | |
29 |
|
29 | |||
@@ -56,6 +56,7 b' class Comm(LoggingConfigurable):' | |||||
56 |
|
56 | |||
57 | def _publish_msg(self, msg_type, data=None, metadata=None, **keys): |
|
57 | def _publish_msg(self, msg_type, data=None, metadata=None, **keys): | |
58 | """Helper for sending a comm message on IOPub""" |
|
58 | """Helper for sending a comm message on IOPub""" | |
|
59 | if self.session is not None: | |||
59 | data = {} if data is None else data |
|
60 | data = {} if data is None else data | |
60 | metadata = {} if metadata is None else metadata |
|
61 | metadata = {} if metadata is None else metadata | |
61 | content = json_clean(dict(data=data, comm_id=self.comm_id, **keys)) |
|
62 | content = json_clean(dict(data=data, comm_id=self.comm_id, **keys)) | |
@@ -77,7 +78,9 b' class Comm(LoggingConfigurable):' | |||||
77 | if data is None: |
|
78 | if data is None: | |
78 | data = self._open_data |
|
79 | data = self._open_data | |
79 | self._closed = False |
|
80 | self._closed = False | |
80 | get_ipython().comm_manager.register_comm(self) |
|
81 | ip = get_ipython() | |
|
82 | if hasattr(ip, 'comm_manager'): | |||
|
83 | ip.comm_manager.register_comm(self) | |||
81 | self._publish_msg('comm_open', data, metadata, target_name=self.target_name) |
|
84 | self._publish_msg('comm_open', data, metadata, target_name=self.target_name) | |
82 |
|
85 | |||
83 | def close(self, data=None, metadata=None): |
|
86 | def close(self, data=None, metadata=None): | |
@@ -88,7 +91,9 b' class Comm(LoggingConfigurable):' | |||||
88 | if data is None: |
|
91 | if data is None: | |
89 | data = self._close_data |
|
92 | data = self._close_data | |
90 | self._publish_msg('comm_close', data, metadata) |
|
93 | self._publish_msg('comm_close', data, metadata) | |
91 | get_ipython().comm_manager.unregister_comm(self) |
|
94 | ip = get_ipython() | |
|
95 | if hasattr(ip, 'comm_manager'): | |||
|
96 | ip.comm_manager.unregister_comm(self) | |||
92 | self._closed = True |
|
97 | self._closed = True | |
93 |
|
98 | |||
94 | def send(self, data=None, metadata=None): |
|
99 | def send(self, data=None, metadata=None): |
@@ -211,7 +211,9 b' class InputHookManager(object):' | |||||
211 | raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__) |
|
211 | raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__) | |
212 |
|
212 | |||
213 | from IPython.lib.inputhookwx import inputhook_wx |
|
213 | from IPython.lib.inputhookwx import inputhook_wx | |
|
214 | from IPython.external.appnope import nope | |||
214 | self.set_inputhook(inputhook_wx) |
|
215 | self.set_inputhook(inputhook_wx) | |
|
216 | nope() | |||
215 | self._current_gui = GUI_WX |
|
217 | self._current_gui = GUI_WX | |
216 | import wx |
|
218 | import wx | |
217 | if app is None: |
|
219 | if app is None: | |
@@ -227,9 +229,11 b' class InputHookManager(object):' | |||||
227 |
|
229 | |||
228 | This merely sets PyOS_InputHook to NULL. |
|
230 | This merely sets PyOS_InputHook to NULL. | |
229 | """ |
|
231 | """ | |
|
232 | from IPython.external.appnope import nap | |||
230 | if GUI_WX in self._apps: |
|
233 | if GUI_WX in self._apps: | |
231 | self._apps[GUI_WX]._in_event_loop = False |
|
234 | self._apps[GUI_WX]._in_event_loop = False | |
232 | self.clear_inputhook() |
|
235 | self.clear_inputhook() | |
|
236 | nap() | |||
233 |
|
237 | |||
234 | def enable_qt4(self, app=None): |
|
238 | def enable_qt4(self, app=None): | |
235 | """Enable event loop integration with PyQt4. |
|
239 | """Enable event loop integration with PyQt4. | |
@@ -254,8 +258,10 b' class InputHookManager(object):' | |||||
254 | app = QtGui.QApplication(sys.argv) |
|
258 | app = QtGui.QApplication(sys.argv) | |
255 | """ |
|
259 | """ | |
256 | from IPython.lib.inputhookqt4 import create_inputhook_qt4 |
|
260 | from IPython.lib.inputhookqt4 import create_inputhook_qt4 | |
|
261 | from IPython.external.appnope import nope | |||
257 | app, inputhook_qt4 = create_inputhook_qt4(self, app) |
|
262 | app, inputhook_qt4 = create_inputhook_qt4(self, app) | |
258 | self.set_inputhook(inputhook_qt4) |
|
263 | self.set_inputhook(inputhook_qt4) | |
|
264 | nope() | |||
259 |
|
265 | |||
260 | self._current_gui = GUI_QT4 |
|
266 | self._current_gui = GUI_QT4 | |
261 | app._in_event_loop = True |
|
267 | app._in_event_loop = True | |
@@ -267,9 +273,11 b' class InputHookManager(object):' | |||||
267 |
|
273 | |||
268 | This merely sets PyOS_InputHook to NULL. |
|
274 | This merely sets PyOS_InputHook to NULL. | |
269 | """ |
|
275 | """ | |
|
276 | from IPython.external.appnope import nap | |||
270 | if GUI_QT4 in self._apps: |
|
277 | if GUI_QT4 in self._apps: | |
271 | self._apps[GUI_QT4]._in_event_loop = False |
|
278 | self._apps[GUI_QT4]._in_event_loop = False | |
272 | self.clear_inputhook() |
|
279 | self.clear_inputhook() | |
|
280 | nap() | |||
273 |
|
281 | |||
274 | def enable_gtk(self, app=None): |
|
282 | def enable_gtk(self, app=None): | |
275 | """Enable event loop integration with PyGTK. |
|
283 | """Enable event loop integration with PyGTK. |
@@ -80,6 +80,8 b' def reads(s, **kwargs):' | |||||
80 | nb : NotebookNode |
|
80 | nb : NotebookNode | |
81 | The notebook that was read. |
|
81 | The notebook that was read. | |
82 | """ |
|
82 | """ | |
|
83 | from .current import NBFormatError | |||
|
84 | ||||
83 | nb_dict = parse_json(s, **kwargs) |
|
85 | nb_dict = parse_json(s, **kwargs) | |
84 | (major, minor) = get_version(nb_dict) |
|
86 | (major, minor) = get_version(nb_dict) | |
85 | if major in versions: |
|
87 | if major in versions: |
@@ -19,7 +19,8 b' from IPython.utils.traitlets import (' | |||||
19 | HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict, |
|
19 | HasTraits, MetaHasTraits, TraitType, Any, CBytes, Dict, | |
20 | Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError, |
|
20 | Int, Long, Integer, Float, Complex, Bytes, Unicode, TraitError, | |
21 | Undefined, Type, This, Instance, TCPAddress, List, Tuple, |
|
21 | Undefined, Type, This, Instance, TCPAddress, List, Tuple, | |
22 | ObjectName, DottedObjectName, CRegExp, link |
|
22 | ObjectName, DottedObjectName, CRegExp, link, directional_link, | |
|
23 | EventfulList, EventfulDict | |||
23 | ) |
|
24 | ) | |
24 | from IPython.utils import py3compat |
|
25 | from IPython.utils import py3compat | |
25 | from IPython.testing.decorators import skipif |
|
26 | from IPython.testing.decorators import skipif | |
@@ -1034,7 +1035,7 b' class TestCRegExp(TraitTestBase):' | |||||
1034 |
|
1035 | |||
1035 | _default_value = re.compile(r'') |
|
1036 | _default_value = re.compile(r'') | |
1036 | _good_values = [r'\d+', re.compile(r'\d+')] |
|
1037 | _good_values = [r'\d+', re.compile(r'\d+')] | |
1037 |
_bad_values = [ |
|
1038 | _bad_values = ['(', None, ()] | |
1038 |
|
1039 | |||
1039 | class DictTrait(HasTraits): |
|
1040 | class DictTrait(HasTraits): | |
1040 | value = Dict() |
|
1041 | value = Dict() | |
@@ -1147,6 +1148,71 b' class TestLink(TestCase):' | |||||
1147 | self.assertEqual(''.join(callback_count), 'ab') |
|
1148 | self.assertEqual(''.join(callback_count), 'ab') | |
1148 | del callback_count[:] |
|
1149 | del callback_count[:] | |
1149 |
|
1150 | |||
|
1151 | class TestDirectionalLink(TestCase): | |||
|
1152 | def test_connect_same(self): | |||
|
1153 | """Verify two traitlets of the same type can be linked together using directional_link.""" | |||
|
1154 | ||||
|
1155 | # Create two simple classes with Int traitlets. | |||
|
1156 | class A(HasTraits): | |||
|
1157 | value = Int() | |||
|
1158 | a = A(value=9) | |||
|
1159 | b = A(value=8) | |||
|
1160 | ||||
|
1161 | # Conenct the two classes. | |||
|
1162 | c = directional_link((a, 'value'), (b, 'value')) | |||
|
1163 | ||||
|
1164 | # Make sure the values are the same at the point of linking. | |||
|
1165 | self.assertEqual(a.value, b.value) | |||
|
1166 | ||||
|
1167 | # Change one the value of the source and check that it synchronizes the target. | |||
|
1168 | a.value = 5 | |||
|
1169 | self.assertEqual(b.value, 5) | |||
|
1170 | # Change one the value of the target and check that it has no impact on the source | |||
|
1171 | b.value = 6 | |||
|
1172 | self.assertEqual(a.value, 5) | |||
|
1173 | ||||
|
1174 | def test_link_different(self): | |||
|
1175 | """Verify two traitlets of different types can be linked together using link.""" | |||
|
1176 | ||||
|
1177 | # Create two simple classes with Int traitlets. | |||
|
1178 | class A(HasTraits): | |||
|
1179 | value = Int() | |||
|
1180 | class B(HasTraits): | |||
|
1181 | count = Int() | |||
|
1182 | a = A(value=9) | |||
|
1183 | b = B(count=8) | |||
|
1184 | ||||
|
1185 | # Conenct the two classes. | |||
|
1186 | c = directional_link((a, 'value'), (b, 'count')) | |||
|
1187 | ||||
|
1188 | # Make sure the values are the same at the point of linking. | |||
|
1189 | self.assertEqual(a.value, b.count) | |||
|
1190 | ||||
|
1191 | # Change one the value of the source and check that it synchronizes the target. | |||
|
1192 | a.value = 5 | |||
|
1193 | self.assertEqual(b.count, 5) | |||
|
1194 | # Change one the value of the target and check that it has no impact on the source | |||
|
1195 | b.value = 6 | |||
|
1196 | self.assertEqual(a.value, 5) | |||
|
1197 | ||||
|
1198 | def test_unlink(self): | |||
|
1199 | """Verify two linked traitlets can be unlinked.""" | |||
|
1200 | ||||
|
1201 | # Create two simple classes with Int traitlets. | |||
|
1202 | class A(HasTraits): | |||
|
1203 | value = Int() | |||
|
1204 | a = A(value=9) | |||
|
1205 | b = A(value=8) | |||
|
1206 | ||||
|
1207 | # Connect the two classes. | |||
|
1208 | c = directional_link((a, 'value'), (b, 'value')) | |||
|
1209 | a.value = 4 | |||
|
1210 | c.unlink() | |||
|
1211 | ||||
|
1212 | # Change one of the values to make sure they don't stay in sync. | |||
|
1213 | a.value = 5 | |||
|
1214 | self.assertNotEqual(a.value, b.value) | |||
|
1215 | ||||
1150 | class Pickleable(HasTraits): |
|
1216 | class Pickleable(HasTraits): | |
1151 | i = Int() |
|
1217 | i = Int() | |
1152 | j = Int() |
|
1218 | j = Int() | |
@@ -1172,3 +1238,64 b' def test_pickle_hastraits():' | |||||
1172 | nt.assert_equal(c2.i, c.i) |
|
1238 | nt.assert_equal(c2.i, c.i) | |
1173 | nt.assert_equal(c2.j, c.j) |
|
1239 | nt.assert_equal(c2.j, c.j) | |
1174 |
|
1240 | |||
|
1241 | class TestEventful(TestCase): | |||
|
1242 | ||||
|
1243 | def test_list(self): | |||
|
1244 | """Does the EventfulList work?""" | |||
|
1245 | event_cache = [] | |||
|
1246 | ||||
|
1247 | class A(HasTraits): | |||
|
1248 | x = EventfulList([c for c in 'abc']) | |||
|
1249 | a = A() | |||
|
1250 | a.x.on_events(lambda i, x: event_cache.append('insert'), \ | |||
|
1251 | lambda i, x: event_cache.append('set'), \ | |||
|
1252 | lambda i: event_cache.append('del'), \ | |||
|
1253 | lambda: event_cache.append('reverse'), \ | |||
|
1254 | lambda *p, **k: event_cache.append('sort')) | |||
|
1255 | ||||
|
1256 | a.x.remove('c') | |||
|
1257 | # ab | |||
|
1258 | a.x.insert(0, 'z') | |||
|
1259 | # zab | |||
|
1260 | del a.x[1] | |||
|
1261 | # zb | |||
|
1262 | a.x.reverse() | |||
|
1263 | # bz | |||
|
1264 | a.x[1] = 'o' | |||
|
1265 | # bo | |||
|
1266 | a.x.append('a') | |||
|
1267 | # boa | |||
|
1268 | a.x.sort() | |||
|
1269 | # abo | |||
|
1270 | ||||
|
1271 | # Were the correct events captured? | |||
|
1272 | self.assertEqual(event_cache, ['del', 'insert', 'del', 'reverse', 'set', 'set', 'sort']) | |||
|
1273 | ||||
|
1274 | # Is the output correct? | |||
|
1275 | self.assertEqual(a.x, [c for c in 'abo']) | |||
|
1276 | ||||
|
1277 | def test_dict(self): | |||
|
1278 | """Does the EventfulDict work?""" | |||
|
1279 | event_cache = [] | |||
|
1280 | ||||
|
1281 | class A(HasTraits): | |||
|
1282 | x = EventfulDict({c: c for c in 'abc'}) | |||
|
1283 | a = A() | |||
|
1284 | a.x.on_events(lambda k, v: event_cache.append('add'), \ | |||
|
1285 | lambda k, v: event_cache.append('set'), \ | |||
|
1286 | lambda k: event_cache.append('del')) | |||
|
1287 | ||||
|
1288 | del a.x['c'] | |||
|
1289 | # ab | |||
|
1290 | a.x['z'] = 1 | |||
|
1291 | # abz | |||
|
1292 | a.x['z'] = 'z' | |||
|
1293 | # abz | |||
|
1294 | a.x.pop('a') | |||
|
1295 | # bz | |||
|
1296 | ||||
|
1297 | # Were the correct events captured? | |||
|
1298 | self.assertEqual(event_cache, ['del', 'add', 'set', 'del']) | |||
|
1299 | ||||
|
1300 | # Is the output correct? | |||
|
1301 | self.assertEqual(a.x, {c: c for c in 'bz'}) |
@@ -54,6 +54,7 b' except:' | |||||
54 |
|
54 | |||
55 | from .importstring import import_item |
|
55 | from .importstring import import_item | |
56 | from IPython.utils import py3compat |
|
56 | from IPython.utils import py3compat | |
|
57 | from IPython.utils import eventful | |||
57 | from IPython.utils.py3compat import iteritems |
|
58 | from IPython.utils.py3compat import iteritems | |
58 | from IPython.testing.skipdoctest import skip_doctest |
|
59 | from IPython.testing.skipdoctest import skip_doctest | |
59 |
|
60 | |||
@@ -226,6 +227,61 b' class link(object):' | |||||
226 | (obj,attr) = key |
|
227 | (obj,attr) = key | |
227 | obj.on_trait_change(callback, attr, remove=True) |
|
228 | obj.on_trait_change(callback, attr, remove=True) | |
228 |
|
229 | |||
|
230 | @skip_doctest | |||
|
231 | class directional_link(object): | |||
|
232 | """Link the trait of a source object with traits of target objects. | |||
|
233 | ||||
|
234 | Parameters | |||
|
235 | ---------- | |||
|
236 | source : pair of object, name | |||
|
237 | targets : pairs of objects/attributes | |||
|
238 | ||||
|
239 | Examples | |||
|
240 | -------- | |||
|
241 | ||||
|
242 | >>> c = directional_link((src, 'value'), (tgt1, 'value'), (tgt2, 'value')) | |||
|
243 | >>> src.value = 5 # updates target objects | |||
|
244 | >>> tgt1.value = 6 # does not update other objects | |||
|
245 | """ | |||
|
246 | updating = False | |||
|
247 | ||||
|
248 | def __init__(self, source, *targets): | |||
|
249 | self.source = source | |||
|
250 | self.targets = targets | |||
|
251 | ||||
|
252 | # Update current value | |||
|
253 | src_attr_value = getattr(source[0], source[1]) | |||
|
254 | for obj, attr in targets: | |||
|
255 | if getattr(obj, attr) != src_attr_value: | |||
|
256 | setattr(obj, attr, src_attr_value) | |||
|
257 | ||||
|
258 | # Wire | |||
|
259 | self.source[0].on_trait_change(self._update, self.source[1]) | |||
|
260 | ||||
|
261 | @contextlib.contextmanager | |||
|
262 | def _busy_updating(self): | |||
|
263 | self.updating = True | |||
|
264 | try: | |||
|
265 | yield | |||
|
266 | finally: | |||
|
267 | self.updating = False | |||
|
268 | ||||
|
269 | def _update(self, name, old, new): | |||
|
270 | if self.updating: | |||
|
271 | return | |||
|
272 | with self._busy_updating(): | |||
|
273 | for obj, attr in self.targets: | |||
|
274 | setattr(obj, attr, new) | |||
|
275 | ||||
|
276 | def unlink(self): | |||
|
277 | self.source[0].on_trait_change(self._update, self.source[1], remove=True) | |||
|
278 | self.source = None | |||
|
279 | self.targets = [] | |||
|
280 | ||||
|
281 | def dlink(source, *targets): | |||
|
282 | """Shorter helper function returning a directional_link object""" | |||
|
283 | return directional_link(source, *targets) | |||
|
284 | ||||
229 | #----------------------------------------------------------------------------- |
|
285 | #----------------------------------------------------------------------------- | |
230 | # Base TraitType for all traits |
|
286 | # Base TraitType for all traits | |
231 | #----------------------------------------------------------------------------- |
|
287 | #----------------------------------------------------------------------------- | |
@@ -1490,6 +1546,49 b' class Dict(Instance):' | |||||
1490 | super(Dict,self).__init__(klass=dict, args=args, |
|
1546 | super(Dict,self).__init__(klass=dict, args=args, | |
1491 | allow_none=allow_none, **metadata) |
|
1547 | allow_none=allow_none, **metadata) | |
1492 |
|
1548 | |||
|
1549 | ||||
|
1550 | class EventfulDict(Instance): | |||
|
1551 | """An instance of an EventfulDict.""" | |||
|
1552 | ||||
|
1553 | def __init__(self, default_value=None, allow_none=True, **metadata): | |||
|
1554 | """Create a EventfulDict trait type from a dict. | |||
|
1555 | ||||
|
1556 | The default value is created by doing | |||
|
1557 | ``eventful.EvenfulDict(default_value)``, which creates a copy of the | |||
|
1558 | ``default_value``. | |||
|
1559 | """ | |||
|
1560 | if default_value is None: | |||
|
1561 | args = ((),) | |||
|
1562 | elif isinstance(default_value, dict): | |||
|
1563 | args = (default_value,) | |||
|
1564 | elif isinstance(default_value, SequenceTypes): | |||
|
1565 | args = (default_value,) | |||
|
1566 | else: | |||
|
1567 | raise TypeError('default value of EventfulDict was %s' % default_value) | |||
|
1568 | ||||
|
1569 | super(EventfulDict, self).__init__(klass=eventful.EventfulDict, args=args, | |||
|
1570 | allow_none=allow_none, **metadata) | |||
|
1571 | ||||
|
1572 | ||||
|
1573 | class EventfulList(Instance): | |||
|
1574 | """An instance of an EventfulList.""" | |||
|
1575 | ||||
|
1576 | def __init__(self, default_value=None, allow_none=True, **metadata): | |||
|
1577 | """Create a EventfulList trait type from a dict. | |||
|
1578 | ||||
|
1579 | The default value is created by doing | |||
|
1580 | ``eventful.EvenfulList(default_value)``, which creates a copy of the | |||
|
1581 | ``default_value``. | |||
|
1582 | """ | |||
|
1583 | if default_value is None: | |||
|
1584 | args = ((),) | |||
|
1585 | else: | |||
|
1586 | args = (default_value,) | |||
|
1587 | ||||
|
1588 | super(EventfulList, self).__init__(klass=eventful.EventfulList, args=args, | |||
|
1589 | allow_none=allow_none, **metadata) | |||
|
1590 | ||||
|
1591 | ||||
1493 | class TCPAddress(TraitType): |
|
1592 | class TCPAddress(TraitType): | |
1494 | """A trait for an (ip, port) tuple. |
|
1593 | """A trait for an (ip, port) tuple. | |
1495 |
|
1594 |
@@ -7,6 +7,6 b' rmagic' | |||||
7 | .. note:: |
|
7 | .. note:: | |
8 |
|
8 | |||
9 | The rmagic extension has been moved to `rpy2 <http://rpy.sourceforge.net/rpy2.html>`_ |
|
9 | The rmagic extension has been moved to `rpy2 <http://rpy.sourceforge.net/rpy2.html>`_ | |
10 |
as :mod:`rpy2.i |
|
10 | as :mod:`rpy2.ipython`. | |
11 |
|
11 | |||
12 | .. automodule:: IPython.extensions.rmagic |
|
12 | .. automodule:: IPython.extensions.rmagic |
@@ -73,7 +73,7 b' Example' | |||||
73 | language_version = '0.1' |
|
73 | language_version = '0.1' | |
74 | banner = "Echo kernel - as useful as a parrot" |
|
74 | banner = "Echo kernel - as useful as a parrot" | |
75 |
|
75 | |||
76 |
def do_execute(self, code, silent, store_history=True, user_exp |
|
76 | def do_execute(self, code, silent, store_history=True, user_expressions=None, | |
77 | allow_stdin=False): |
|
77 | allow_stdin=False): | |
78 | if not silent: |
|
78 | if not silent: | |
79 | stream_content = {'name': 'stdout', 'data':code} |
|
79 | stream_content = {'name': 'stdout', 'data':code} |
@@ -1,4 +1,4 b'' | |||||
1 | #!/bin/sh |
|
1 | #!/bin/bash | |
2 |
|
2 | |||
3 | git submodule init |
|
3 | git submodule init | |
4 | git submodule update |
|
4 | git submodule update |
@@ -3,6 +3,7 b'' | |||||
3 | """ |
|
3 | """ | |
4 |
|
4 | |||
5 | import os |
|
5 | import os | |
|
6 | from shutil import rmtree | |||
6 |
|
7 | |||
7 | from toollib import * |
|
8 | from toollib import * | |
8 |
|
9 | |||
@@ -20,7 +21,7 b' compile_tree()' | |||||
20 | for d in ['build', 'dist', pjoin('docs', 'build'), pjoin('docs', 'dist'), |
|
21 | for d in ['build', 'dist', pjoin('docs', 'build'), pjoin('docs', 'dist'), | |
21 | pjoin('docs', 'source', 'api', 'generated')]: |
|
22 | pjoin('docs', 'source', 'api', 'generated')]: | |
22 | if os.path.isdir(d): |
|
23 | if os.path.isdir(d): | |
23 |
r |
|
24 | rmtree(d) | |
24 |
|
25 | |||
25 | # Build source and binary distros |
|
26 | # Build source and binary distros | |
26 | sh(sdists) |
|
27 | sh(sdists) |
@@ -56,7 +56,7 b" sh('./setup.py register')" | |||||
56 | # Upload all files |
|
56 | # Upload all files | |
57 | sh(sdists + ' upload') |
|
57 | sh(sdists + ' upload') | |
58 | for py in ('2.7', '3.4'): |
|
58 | for py in ('2.7', '3.4'): | |
59 | sh('python%s setupegg.py bdist_wheel' % py) |
|
59 | sh('python%s setupegg.py bdist_wheel upload' % py) | |
60 |
|
60 | |||
61 | cd(distdir) |
|
61 | cd(distdir) | |
62 | print( 'Uploading distribution files...') |
|
62 | print( 'Uploading distribution files...') |
General Comments 0
You need to be logged in to leave comments.
Login now