##// END OF EJS Templates
update completion_ and objection_info_request...
MinRK -
Show More
@@ -1,526 +1,535 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2012 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
7 3
8 4 //============================================================================
9 5 // Utilities
10 6 //============================================================================
7
11 8 IPython.namespace('IPython.utils');
12 9
13 10 IPython.utils = (function (IPython) {
14 11 "use strict";
15 12
16 13 IPython.load_extensions = function () {
17 14 // load one or more IPython notebook extensions with requirejs
18 15
19 16 var extensions = [];
20 17 var extension_names = arguments;
21 18 for (var i = 0; i < extension_names.length; i++) {
22 19 extensions.push("nbextensions/" + arguments[i]);
23 20 }
24 21
25 22 require(extensions,
26 23 function () {
27 24 for (var i = 0; i < arguments.length; i++) {
28 25 var ext = arguments[i];
29 26 var ext_name = extension_names[i];
30 27 // success callback
31 28 console.log("Loaded extension: " + ext_name);
32 29 if (ext && ext.load_ipython_extension !== undefined) {
33 30 ext.load_ipython_extension();
34 31 }
35 32 }
36 33 },
37 34 function (err) {
38 35 // failure callback
39 36 console.log("Failed to load extension(s):", err.requireModules, err);
40 37 }
41 38 );
42 39 };
43 40
44 41 //============================================================================
45 42 // Cross-browser RegEx Split
46 43 //============================================================================
47 44
48 45 // This code has been MODIFIED from the code licensed below to not replace the
49 46 // default browser split. The license is reproduced here.
50 47
51 48 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
52 49 /*!
53 50 * Cross-Browser Split 1.1.1
54 51 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
55 52 * Available under the MIT License
56 53 * ECMAScript compliant, uniform cross-browser split method
57 54 */
58 55
59 56 /**
60 57 * Splits a string into an array of strings using a regex or string
61 58 * separator. Matches of the separator are not included in the result array.
62 59 * However, if `separator` is a regex that contains capturing groups,
63 60 * backreferences are spliced into the result each time `separator` is
64 61 * matched. Fixes browser bugs compared to the native
65 62 * `String.prototype.split` and can be used reliably cross-browser.
66 63 * @param {String} str String to split.
67 64 * @param {RegExp|String} separator Regex or string to use for separating
68 65 * the string.
69 66 * @param {Number} [limit] Maximum number of items to include in the result
70 67 * array.
71 68 * @returns {Array} Array of substrings.
72 69 * @example
73 70 *
74 71 * // Basic use
75 72 * regex_split('a b c d', ' ');
76 73 * // -> ['a', 'b', 'c', 'd']
77 74 *
78 75 * // With limit
79 76 * regex_split('a b c d', ' ', 2);
80 77 * // -> ['a', 'b']
81 78 *
82 79 * // Backreferences in result array
83 80 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
84 81 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
85 82 */
86 83 var regex_split = function (str, separator, limit) {
87 84 // If `separator` is not a regex, use `split`
88 85 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
89 86 return split.call(str, separator, limit);
90 87 }
91 88 var output = [],
92 89 flags = (separator.ignoreCase ? "i" : "") +
93 90 (separator.multiline ? "m" : "") +
94 91 (separator.extended ? "x" : "") + // Proposed for ES6
95 92 (separator.sticky ? "y" : ""), // Firefox 3+
96 93 lastLastIndex = 0,
97 94 // Make `global` and avoid `lastIndex` issues by working with a copy
98 95 separator = new RegExp(separator.source, flags + "g"),
99 96 separator2, match, lastIndex, lastLength;
100 97 str += ""; // Type-convert
101 98
102 99 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
103 100 if (!compliantExecNpcg) {
104 101 // Doesn't need flags gy, but they don't hurt
105 102 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
106 103 }
107 104 /* Values for `limit`, per the spec:
108 105 * If undefined: 4294967295 // Math.pow(2, 32) - 1
109 106 * If 0, Infinity, or NaN: 0
110 107 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
111 108 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
112 109 * If other: Type-convert, then use the above rules
113 110 */
114 111 limit = typeof(limit) === "undefined" ?
115 112 -1 >>> 0 : // Math.pow(2, 32) - 1
116 113 limit >>> 0; // ToUint32(limit)
117 114 while (match = separator.exec(str)) {
118 115 // `separator.lastIndex` is not reliable cross-browser
119 116 lastIndex = match.index + match[0].length;
120 117 if (lastIndex > lastLastIndex) {
121 118 output.push(str.slice(lastLastIndex, match.index));
122 119 // Fix browsers whose `exec` methods don't consistently return `undefined` for
123 120 // nonparticipating capturing groups
124 121 if (!compliantExecNpcg && match.length > 1) {
125 122 match[0].replace(separator2, function () {
126 123 for (var i = 1; i < arguments.length - 2; i++) {
127 124 if (typeof(arguments[i]) === "undefined") {
128 125 match[i] = undefined;
129 126 }
130 127 }
131 128 });
132 129 }
133 130 if (match.length > 1 && match.index < str.length) {
134 131 Array.prototype.push.apply(output, match.slice(1));
135 132 }
136 133 lastLength = match[0].length;
137 134 lastLastIndex = lastIndex;
138 135 if (output.length >= limit) {
139 136 break;
140 137 }
141 138 }
142 139 if (separator.lastIndex === match.index) {
143 140 separator.lastIndex++; // Avoid an infinite loop
144 141 }
145 142 }
146 143 if (lastLastIndex === str.length) {
147 144 if (lastLength || !separator.test("")) {
148 145 output.push("");
149 146 }
150 147 } else {
151 148 output.push(str.slice(lastLastIndex));
152 149 }
153 150 return output.length > limit ? output.slice(0, limit) : output;
154 151 };
155 152
156 153 //============================================================================
157 154 // End contributed Cross-browser RegEx Split
158 155 //============================================================================
159 156
160 157
161 158 var uuid = function () {
162 159 // http://www.ietf.org/rfc/rfc4122.txt
163 160 var s = [];
164 161 var hexDigits = "0123456789ABCDEF";
165 162 for (var i = 0; i < 32; i++) {
166 163 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
167 164 }
168 165 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
169 166 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
170 167
171 168 var uuid = s.join("");
172 169 return uuid;
173 170 };
174 171
175 172
176 173 //Fix raw text to parse correctly in crazy XML
177 174 function xmlencode(string) {
178 175 return string.replace(/\&/g,'&'+'amp;')
179 176 .replace(/</g,'&'+'lt;')
180 177 .replace(/>/g,'&'+'gt;')
181 178 .replace(/\'/g,'&'+'apos;')
182 179 .replace(/\"/g,'&'+'quot;')
183 180 .replace(/`/g,'&'+'#96;');
184 181 }
185 182
186 183
187 184 //Map from terminal commands to CSS classes
188 185 var ansi_colormap = {
189 186 "01":"ansibold",
190 187
191 188 "30":"ansiblack",
192 189 "31":"ansired",
193 190 "32":"ansigreen",
194 191 "33":"ansiyellow",
195 192 "34":"ansiblue",
196 193 "35":"ansipurple",
197 194 "36":"ansicyan",
198 195 "37":"ansigray",
199 196
200 197 "40":"ansibgblack",
201 198 "41":"ansibgred",
202 199 "42":"ansibggreen",
203 200 "43":"ansibgyellow",
204 201 "44":"ansibgblue",
205 202 "45":"ansibgpurple",
206 203 "46":"ansibgcyan",
207 204 "47":"ansibggray"
208 205 };
209 206
210 207 function _process_numbers(attrs, numbers) {
211 208 // process ansi escapes
212 209 var n = numbers.shift();
213 210 if (ansi_colormap[n]) {
214 211 if ( ! attrs["class"] ) {
215 212 attrs["class"] = ansi_colormap[n];
216 213 } else {
217 214 attrs["class"] += " " + ansi_colormap[n];
218 215 }
219 216 } else if (n == "38" || n == "48") {
220 217 // VT100 256 color or 24 bit RGB
221 218 if (numbers.length < 2) {
222 219 console.log("Not enough fields for VT100 color", numbers);
223 220 return;
224 221 }
225 222
226 223 var index_or_rgb = numbers.shift();
227 224 var r,g,b;
228 225 if (index_or_rgb == "5") {
229 226 // 256 color
230 227 var idx = parseInt(numbers.shift());
231 228 if (idx < 16) {
232 229 // indexed ANSI
233 230 // ignore bright / non-bright distinction
234 231 idx = idx % 8;
235 232 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
236 233 if ( ! attrs["class"] ) {
237 234 attrs["class"] = ansiclass;
238 235 } else {
239 236 attrs["class"] += " " + ansiclass;
240 237 }
241 238 return;
242 239 } else if (idx < 232) {
243 240 // 216 color 6x6x6 RGB
244 241 idx = idx - 16;
245 242 b = idx % 6;
246 243 g = Math.floor(idx / 6) % 6;
247 244 r = Math.floor(idx / 36) % 6;
248 245 // convert to rgb
249 246 r = (r * 51);
250 247 g = (g * 51);
251 248 b = (b * 51);
252 249 } else {
253 250 // grayscale
254 251 idx = idx - 231;
255 252 // it's 1-24 and should *not* include black or white,
256 253 // so a 26 point scale
257 254 r = g = b = Math.floor(idx * 256 / 26);
258 255 }
259 256 } else if (index_or_rgb == "2") {
260 257 // Simple 24 bit RGB
261 258 if (numbers.length > 3) {
262 259 console.log("Not enough fields for RGB", numbers);
263 260 return;
264 261 }
265 262 r = numbers.shift();
266 263 g = numbers.shift();
267 264 b = numbers.shift();
268 265 } else {
269 266 console.log("unrecognized control", numbers);
270 267 return;
271 268 }
272 269 if (r !== undefined) {
273 270 // apply the rgb color
274 271 var line;
275 272 if (n == "38") {
276 273 line = "color: ";
277 274 } else {
278 275 line = "background-color: ";
279 276 }
280 277 line = line + "rgb(" + r + "," + g + "," + b + ");"
281 278 if ( !attrs["style"] ) {
282 279 attrs["style"] = line;
283 280 } else {
284 281 attrs["style"] += " " + line;
285 282 }
286 283 }
287 284 }
288 285 }
289 286
290 287 function ansispan(str) {
291 288 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
292 289 // regular ansi escapes (using the table above)
293 290 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
294 291 if (!pattern) {
295 292 // [(01|22|39|)m close spans
296 293 return "</span>";
297 294 }
298 295 // consume sequence of color escapes
299 296 var numbers = pattern.match(/\d+/g);
300 297 var attrs = {};
301 298 while (numbers.length > 0) {
302 299 _process_numbers(attrs, numbers);
303 300 }
304 301
305 302 var span = "<span ";
306 303 for (var attr in attrs) {
307 304 var value = attrs[attr];
308 305 span = span + " " + attr + '="' + attrs[attr] + '"';
309 306 }
310 307 return span + ">";
311 308 });
312 309 };
313 310
314 311 // Transform ANSI color escape codes into HTML <span> tags with css
315 312 // classes listed in the above ansi_colormap object. The actual color used
316 313 // are set in the css file.
317 314 function fixConsole(txt) {
318 315 txt = xmlencode(txt);
319 316 var re = /\033\[([\dA-Fa-f;]*?)m/;
320 317 var opened = false;
321 318 var cmds = [];
322 319 var opener = "";
323 320 var closer = "";
324 321
325 322 // Strip all ANSI codes that are not color related. Matches
326 323 // all ANSI codes that do not end with "m".
327 324 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
328 325 txt = txt.replace(ignored_re, "");
329 326
330 327 // color ansi codes
331 328 txt = ansispan(txt);
332 329 return txt;
333 330 }
334 331
335 332 // Remove chunks that should be overridden by the effect of
336 333 // carriage return characters
337 334 function fixCarriageReturn(txt) {
338 335 var tmp = txt;
339 336 do {
340 337 txt = tmp;
341 338 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
342 339 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
343 340 } while (tmp.length < txt.length);
344 341 return txt;
345 342 }
346 343
347 344 // Locate any URLs and convert them to a anchor tag
348 345 function autoLinkUrls(txt) {
349 346 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
350 347 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
351 348 }
352 349
353 350 var points_to_pixels = function (points) {
354 351 // A reasonably good way of converting between points and pixels.
355 352 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
356 353 $(body).append(test);
357 354 var pixel_per_point = test.width()/10000;
358 355 test.remove();
359 356 return Math.floor(points*pixel_per_point);
360 357 };
361 358
362 359 var always_new = function (constructor) {
363 360 // wrapper around contructor to avoid requiring `var a = new constructor()`
364 361 // useful for passing constructors as callbacks,
365 362 // not for programmer laziness.
366 363 // from http://programmers.stackexchange.com/questions/118798
367 364 return function () {
368 365 var obj = Object.create(constructor.prototype);
369 366 constructor.apply(obj, arguments);
370 367 return obj;
371 368 };
372 369 };
373 370
374 371 var url_path_join = function () {
375 372 // join a sequence of url components with '/'
376 373 var url = '';
377 374 for (var i = 0; i < arguments.length; i++) {
378 375 if (arguments[i] === '') {
379 376 continue;
380 377 }
381 378 if (url.length > 0 && url[url.length-1] != '/') {
382 379 url = url + '/' + arguments[i];
383 380 } else {
384 381 url = url + arguments[i];
385 382 }
386 383 }
387 384 url = url.replace(/\/\/+/, '/');
388 385 return url;
389 386 };
390 387
391 388 var parse_url = function (url) {
392 389 // an `a` element with an href allows attr-access to the parsed segments of a URL
393 390 // a = parse_url("http://localhost:8888/path/name#hash")
394 391 // a.protocol = "http:"
395 392 // a.host = "localhost:8888"
396 393 // a.hostname = "localhost"
397 394 // a.port = 8888
398 395 // a.pathname = "/path/name"
399 396 // a.hash = "#hash"
400 397 var a = document.createElement("a");
401 398 a.href = url;
402 399 return a;
403 400 };
404 401
405 402 var encode_uri_components = function (uri) {
406 403 // encode just the components of a multi-segment uri,
407 404 // leaving '/' separators
408 405 return uri.split('/').map(encodeURIComponent).join('/');
409 406 };
410 407
411 408 var url_join_encode = function () {
412 409 // join a sequence of url components with '/',
413 410 // encoding each component with encodeURIComponent
414 411 return encode_uri_components(url_path_join.apply(null, arguments));
415 412 };
416 413
417 414
418 415 var splitext = function (filename) {
419 416 // mimic Python os.path.splitext
420 417 // Returns ['base', '.ext']
421 418 var idx = filename.lastIndexOf('.');
422 419 if (idx > 0) {
423 420 return [filename.slice(0, idx), filename.slice(idx)];
424 421 } else {
425 422 return [filename, ''];
426 423 }
427 424 };
428 425
429 426
430 427 var escape_html = function (text) {
431 428 // escape text to HTML
432 429 return $("<div/>").text(text).html();
433 }
430 };
434 431
435 432
436 433 var get_body_data = function(key) {
437 434 // get a url-encoded item from body.data and decode it
438 435 // we should never have any encoded URLs anywhere else in code
439 436 // until we are building an actual request
440 437 return decodeURIComponent($('body').data(key));
441 438 };
442
443
439
440 var absolute_cursor_pos = function (cm, cursor) {
441 // get the absolute cursor position from CodeMirror's col, ch
442 if (!cursor) {
443 cursor = cm.getCursor();
444 }
445 var cursor_pos = cursor.ch;
446 for (var i = 0; i < cursor.line; i++) {
447 cursor_pos += cm.getLine(i).length + 1;
448 }
449 return cursor_pos;
450 };
451
444 452 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
445 453 var browser = (function() {
446 454 if (typeof navigator === 'undefined') {
447 455 // navigator undefined in node
448 456 return 'None';
449 457 }
450 458 var N= navigator.appName, ua= navigator.userAgent, tem;
451 459 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
452 460 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
453 461 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
454 462 return M;
455 463 })();
456 464
457 465 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
458 466 var platform = (function () {
459 467 if (typeof navigator === 'undefined') {
460 468 // navigator undefined in node
461 469 return 'None';
462 470 }
463 471 var OSName="None";
464 472 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
465 473 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
466 474 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
467 475 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
468 return OSName
476 return OSName;
469 477 })();
470 478
471 479 var is_or_has = function (a, b) {
472 480 // Is b a child of a or a itself?
473 481 return a.has(b).length !==0 || a.is(b);
474 }
482 };
475 483
476 484 var is_focused = function (e) {
477 485 // Is element e, or one of its children focused?
478 486 e = $(e);
479 487 var target = $(document.activeElement);
480 488 if (target.length > 0) {
481 489 if (is_or_has(e, target)) {
482 490 return true;
483 491 } else {
484 492 return false;
485 493 }
486 494 } else {
487 495 return false;
488 496 }
489 }
497 };
490 498
491 499 var log_ajax_error = function (jqXHR, status, error) {
492 500 // log ajax failures with informative messages
493 501 var msg = "API request failed (" + jqXHR.status + "): ";
494 502 console.log(jqXHR);
495 503 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
496 504 msg += jqXHR.responseJSON.message;
497 505 } else {
498 506 msg += jqXHR.statusText;
499 507 }
500 508 console.log(msg);
501 509 };
502
510
503 511 return {
504 512 regex_split : regex_split,
505 513 uuid : uuid,
506 514 fixConsole : fixConsole,
507 515 fixCarriageReturn : fixCarriageReturn,
508 516 autoLinkUrls : autoLinkUrls,
509 517 points_to_pixels : points_to_pixels,
510 518 get_body_data : get_body_data,
511 519 parse_url : parse_url,
512 520 url_path_join : url_path_join,
513 521 url_join_encode : url_join_encode,
514 522 encode_uri_components : encode_uri_components,
515 523 splitext : splitext,
516 524 escape_html : escape_html,
517 525 always_new : always_new,
526 absolute_cursor_pos : absolute_cursor_pos,
518 527 browser : browser,
519 528 platform: platform,
520 529 is_or_has : is_or_has,
521 530 is_focused : is_focused,
522 531 log_ajax_error : log_ajax_error,
523 532 };
524 533
525 534 }(IPython));
526 535
@@ -1,370 +1,377 b''
1 // function completer.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 // Completer
2 5 //
3 // completer should be a class that takes an cell instance
6 // Completer is be a class that takes a cell instance.
7
4 8 var IPython = (function (IPython) {
5 9 // that will prevent us from misspelling
6 10 "use strict";
7 11
8 12 // easier key mapping
9 13 var keycodes = IPython.keyboard.keycodes;
10 14
11 15 function prepend_n_prc(str, n) {
12 16 for( var i =0 ; i< n ; i++){
13 17 str = '%'+str ;
14 18 }
15 19 return str;
16 20 };
17 21
18 22 function _existing_completion(item, completion_array){
19 23 for( var i=0; i < completion_array.length; i++) {
20 24 if (completion_array[i].trim().substr(-item.length) == item) {
21 25 return true;
22 26 }
23 27 }
24 28 return false;
25 29 };
26 30
27 31 // what is the common start of all completions
28 32 function shared_start(B, drop_prct) {
29 33 if (B.length == 1) {
30 34 return B[0];
31 35 }
32 36 var A = [];
33 37 var common;
34 38 var min_lead_prct = 10;
35 39 for (var i = 0; i < B.length; i++) {
36 40 var str = B[i].str;
37 41 var localmin = 0;
38 42 if(drop_prct === true){
39 43 while ( str.substr(0, 1) == '%') {
40 44 localmin = localmin+1;
41 45 str = str.substring(1);
42 46 }
43 47 }
44 48 min_lead_prct = Math.min(min_lead_prct, localmin);
45 49 A.push(str);
46 50 }
47 51
48 52 if (A.length > 1) {
49 53 var tem1, tem2, s;
50 54 A = A.slice(0).sort();
51 55 tem1 = A[0];
52 56 s = tem1.length;
53 57 tem2 = A.pop();
54 58 while (s && tem2.indexOf(tem1) == -1) {
55 59 tem1 = tem1.substring(0, --s);
56 60 }
57 61 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
58 62 return {
59 63 str:prepend_n_prc('', min_lead_prct),
60 64 type: "computed",
61 65 from: B[0].from,
62 66 to: B[0].to
63 67 };
64 68 }
65 69 return {
66 70 str: prepend_n_prc(tem1, min_lead_prct),
67 71 type: "computed",
68 72 from: B[0].from,
69 73 to: B[0].to
70 74 };
71 75 }
72 76 return null;
73 77 }
74 78
75 79
76 80 var Completer = function (cell) {
77 81 this.cell = cell;
78 82 this.editor = cell.code_mirror;
79 83 var that = this;
80 84 $([IPython.events]).on('status_busy.Kernel', function () {
81 85 that.skip_kernel_completion = true;
82 86 });
83 87 $([IPython.events]).on('status_idle.Kernel', function () {
84 88 that.skip_kernel_completion = false;
85 89 });
86 90 };
87 91
88 92 Completer.prototype.startCompletion = function () {
89 93 // call for a 'first' completion, that will set the editor and do some
90 94 // special behavior like autopicking if only one completion available.
91 95 if (this.editor.somethingSelected()) return;
92 96 this.done = false;
93 97 // use to get focus back on opera
94 98 this.carry_on_completion(true);
95 99 };
96 100
97 101
98 102 // easy access for julia to monkeypatch
99 103 //
100 104 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
101 105
102 106 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
103 107 return Completer.reinvoke_re.test(pre_cursor);
104 108 };
105 109
106 110 /**
107 111 *
108 112 * pass true as parameter if this is the first invocation of the completer
109 113 * this will prevent the completer to dissmiss itself if it is not on a
110 114 * word boundary like pressing tab after a space, and make it autopick the
111 115 * only choice if there is only one which prevent from popping the UI. as
112 116 * well as fast-forwarding the typing if all completion have a common
113 117 * shared start
114 118 **/
115 119 Completer.prototype.carry_on_completion = function (first_invocation) {
116 120 // Pass true as parameter if you want the completer to autopick when
117 121 // only one completion. This function is automatically reinvoked at
118 122 // each keystroke with first_invocation = false
119 123 var cur = this.editor.getCursor();
120 124 var line = this.editor.getLine(cur.line);
121 125 var pre_cursor = this.editor.getRange({
122 126 line: cur.line,
123 127 ch: cur.ch - 1
124 128 }, cur);
125 129
126 130 // we need to check that we are still on a word boundary
127 131 // because while typing the completer is still reinvoking itself
128 132 // so dismiss if we are on a "bad" caracter
129 133 if (!this.reinvoke(pre_cursor) && !first_invocation) {
130 134 this.close();
131 135 return;
132 136 }
133 137
134 138 this.autopick = false;
135 139 if (first_invocation) {
136 140 this.autopick = true;
137 141 }
138 142
139 143 // We want a single cursor position.
140 144 if (this.editor.somethingSelected()) {
141 145 return;
142 146 }
143 147
144 148 // one kernel completion came back, finish_completing will be called with the results
145 149 // we fork here and directly call finish completing if kernel is busy
146 150 if (this.skip_kernel_completion) {
147 151 this.finish_completing({
148 'matches': [],
152 matches: [],
149 153 matched_text: ""
150 154 });
151 155 } else {
152 this.cell.kernel.complete(line, cur.ch, $.proxy(this.finish_completing, this));
156 var cursor_pos = IPython.utils.absolute_cursor_pos(this.editor, cur);
157 this.cell.kernel.complete(this.editor.getValue(), cursor_pos,
158 $.proxy(this.finish_completing, this)
159 );
153 160 }
154 161 };
155 162
156 163 Completer.prototype.finish_completing = function (msg) {
157 164 // let's build a function that wrap all that stuff into what is needed
158 165 // for the new completer:
159 166 var content = msg.content;
160 167 var matched_text = content.matched_text;
161 168 var matches = content.matches;
162 169
163 170 var cur = this.editor.getCursor();
164 171 var results = CodeMirror.contextHint(this.editor);
165 172 var filtered_results = [];
166 173 //remove results from context completion
167 174 //that are already in kernel completion
168 175 for (var i=0; i < results.length; i++) {
169 176 if (!_existing_completion(results[i].str, matches)) {
170 177 filtered_results.push(results[i]);
171 178 }
172 179 }
173 180
174 181 // append the introspection result, in order, at at the beginning of
175 182 // the table and compute the replacement range from current cursor
176 183 // positon and matched_text length.
177 184 for (var i = matches.length - 1; i >= 0; --i) {
178 185 filtered_results.unshift({
179 186 str: matches[i],
180 187 type: "introspection",
181 188 from: {
182 189 line: cur.line,
183 190 ch: cur.ch - matched_text.length
184 191 },
185 192 to: {
186 193 line: cur.line,
187 194 ch: cur.ch
188 195 }
189 196 });
190 197 }
191 198
192 199 // one the 2 sources results have been merge, deal with it
193 200 this.raw_result = filtered_results;
194 201
195 202 // if empty result return
196 203 if (!this.raw_result || !this.raw_result.length) return;
197 204
198 205 // When there is only one completion, use it directly.
199 206 if (this.autopick && this.raw_result.length == 1) {
200 207 this.insert(this.raw_result[0]);
201 208 return;
202 209 }
203 210
204 211 if (this.raw_result.length == 1) {
205 212 // test if first and only completion totally matches
206 213 // what is typed, in this case dismiss
207 214 var str = this.raw_result[0].str;
208 215 var pre_cursor = this.editor.getRange({
209 216 line: cur.line,
210 217 ch: cur.ch - str.length
211 218 }, cur);
212 219 if (pre_cursor == str) {
213 220 this.close();
214 221 return;
215 222 }
216 223 }
217 224
218 225 if (!this.visible) {
219 226 this.complete = $('<div/>').addClass('completions');
220 227 this.complete.attr('id', 'complete');
221 228
222 229 // Currently webkit doesn't use the size attr correctly. See:
223 230 // https://code.google.com/p/chromium/issues/detail?id=4579
224 231 this.sel = $('<select/>')
225 232 .attr('tabindex', -1)
226 233 .attr('multiple', 'true');
227 234 this.complete.append(this.sel);
228 235 this.visible = true;
229 236 $('body').append(this.complete);
230 237
231 238 //build the container
232 239 var that = this;
233 240 this.sel.dblclick(function () {
234 241 that.pick();
235 242 });
236 243 this.sel.focus(function () {
237 244 that.editor.focus();
238 245 });
239 246 this._handle_keydown = function (cm, event) {
240 247 that.keydown(event);
241 248 };
242 249 this.editor.on('keydown', this._handle_keydown);
243 250 this._handle_keypress = function (cm, event) {
244 251 that.keypress(event);
245 252 };
246 253 this.editor.on('keypress', this._handle_keypress);
247 254 }
248 255 this.sel.attr('size', Math.min(10, this.raw_result.length));
249 256
250 257 // After everything is on the page, compute the postion.
251 258 // We put it above the code if it is too close to the bottom of the page.
252 259 cur.ch = cur.ch-matched_text.length;
253 260 var pos = this.editor.cursorCoords(cur);
254 261 var left = pos.left-3;
255 262 var top;
256 263 var cheight = this.complete.height();
257 264 var wheight = $(window).height();
258 265 if (pos.bottom+cheight+5 > wheight) {
259 266 top = pos.top-cheight-4;
260 267 } else {
261 268 top = pos.bottom+1;
262 269 }
263 270 this.complete.css('left', left + 'px');
264 271 this.complete.css('top', top + 'px');
265 272
266 273 // Clear and fill the list.
267 274 this.sel.text('');
268 275 this.build_gui_list(this.raw_result);
269 276 return true;
270 277 };
271 278
272 279 Completer.prototype.insert = function (completion) {
273 280 this.editor.replaceRange(completion.str, completion.from, completion.to);
274 281 };
275 282
276 283 Completer.prototype.build_gui_list = function (completions) {
277 284 for (var i = 0; i < completions.length; ++i) {
278 285 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
279 286 this.sel.append(opt);
280 287 }
281 288 this.sel.children().first().attr('selected', 'true');
282 289 this.sel.scrollTop(0);
283 290 };
284 291
285 292 Completer.prototype.close = function () {
286 293 this.done = true;
287 294 $('#complete').remove();
288 295 this.editor.off('keydown', this._handle_keydown);
289 296 this.editor.off('keypress', this._handle_keypress);
290 297 this.visible = false;
291 298 };
292 299
293 300 Completer.prototype.pick = function () {
294 301 this.insert(this.raw_result[this.sel[0].selectedIndex]);
295 302 this.close();
296 303 };
297 304
298 305 Completer.prototype.keydown = function (event) {
299 306 var code = event.keyCode;
300 307 var that = this;
301 308
302 309 // Enter
303 310 if (code == keycodes.enter) {
304 311 CodeMirror.e_stop(event);
305 312 this.pick();
306 313 // Escape or backspace
307 314 } else if (code == keycodes.esc || code == keycodes.backspace) {
308 315 CodeMirror.e_stop(event);
309 316 this.close();
310 317 } else if (code == keycodes.tab) {
311 318 //all the fastforwarding operation,
312 319 //Check that shared start is not null which can append with prefixed completion
313 320 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
314 321 // to erase py
315 322 var sh = shared_start(this.raw_result, true);
316 323 if (sh) {
317 324 this.insert(sh);
318 325 }
319 326 this.close();
320 327 //reinvoke self
321 328 setTimeout(function () {
322 329 that.carry_on_completion();
323 330 }, 50);
324 331 } else if (code == keycodes.up || code == keycodes.down) {
325 332 // need to do that to be able to move the arrow
326 333 // when on the first or last line ofo a code cell
327 334 CodeMirror.e_stop(event);
328 335
329 336 var options = this.sel.find('option');
330 337 var index = this.sel[0].selectedIndex;
331 338 if (code == keycodes.up) {
332 339 index--;
333 340 }
334 341 if (code == keycodes.down) {
335 342 index++;
336 343 }
337 344 index = Math.min(Math.max(index, 0), options.length-1);
338 345 this.sel[0].selectedIndex = index;
339 346 } else if (code == keycodes.left || code == keycodes.right) {
340 347 this.close();
341 348 }
342 349 };
343 350
344 351 Completer.prototype.keypress = function (event) {
345 352 // FIXME: This is a band-aid.
346 353 // on keypress, trigger insertion of a single character.
347 354 // This simulates the old behavior of completion as you type,
348 355 // before events were disconnected and CodeMirror stopped
349 356 // receiving events while the completer is focused.
350 357
351 358 var that = this;
352 359 var code = event.keyCode;
353 360
354 361 // don't handle keypress if it's not a character (arrows on FF)
355 362 // or ENTER/TAB
356 363 if (event.charCode === 0 ||
357 364 code == keycodes.tab ||
358 365 code == keycodes.enter
359 366 ) return;
360 367
361 368 this.close();
362 369 this.editor.focus();
363 370 setTimeout(function () {
364 371 that.carry_on_completion();
365 372 }, 50);
366 373 };
367 374 IPython.Completer = Completer;
368 375
369 376 return IPython;
370 377 }(IPython));
@@ -1,387 +1,350 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
7 4 //============================================================================
8 5 // Tooltip
9 6 //============================================================================
10 7 //
11 8 // you can set the autocall time by setting `IPython.tooltip.time_before_tooltip` in ms
12 9 //
13 10 // you can configure the differents action of pressing shift-tab several times in a row by
14 11 // setting/appending different fonction in the array
15 12 // IPython.tooltip.tabs_functions
16 13 //
17 14 // eg :
18 15 // IPython.tooltip.tabs_functions[4] = function (){console.log('this is the action of the 4th tab pressing')}
19 16 //
20 17 var IPython = (function (IPython) {
21 18 "use strict";
22 19
23 20 var utils = IPython.utils;
24 21
25 22 // tooltip constructor
26 23 var Tooltip = function () {
27 24 var that = this;
28 25 this.time_before_tooltip = 1200;
29 26
30 27 // handle to html
31 28 this.tooltip = $('#tooltip');
32 29 this._hidden = true;
33 30
34 31 // variable for consecutive call
35 32 this._old_cell = null;
36 33 this._old_request = null;
37 34 this._consecutive_counter = 0;
38 35
39 36 // 'sticky ?'
40 37 this._sticky = false;
41 38
42 39 // display tooltip if the docstring is empty?
43 40 this._hide_if_no_docstring = false;
44 41
45 42 // contain the button in the upper right corner
46 43 this.buttons = $('<div/>').addClass('tooltipbuttons');
47 44
48 45 // will contain the docstring
49 46 this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
50 47
51 48 // build the buttons menu on the upper right
52 49 // expand the tooltip to see more
53 50 var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
54 51 .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () {
55 52 that.expand();
56 53 }).append(
57 54 $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
58 55
59 56 // open in pager
60 57 var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press shift-tab 4 times)');
61 58 var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
62 59 morelink.append(morespan);
63 60 morelink.click(function () {
64 61 that.showInPager(that._old_cell);
65 62 });
66 63
67 64 // close the tooltip
68 65 var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
69 66 var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
70 67 closelink.append(closespan);
71 68 closelink.click(function () {
72 69 that.remove_and_cancel_tooltip(true);
73 70 });
74 71
75 72 this._clocklink = $('<a/>').attr('href', "#");
76 73 this._clocklink.attr('role', "button");
77 74 this._clocklink.addClass('ui-button');
78 75 this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds');
79 76 var clockspan = $('<span/>').text('Close');
80 77 clockspan.addClass('ui-icon');
81 78 clockspan.addClass('ui-icon-clock');
82 79 this._clocklink.append(clockspan);
83 80 this._clocklink.click(function () {
84 81 that.cancel_stick();
85 82 });
86 83
87 84
88 85
89 86
90 87 //construct the tooltip
91 88 // add in the reverse order you want them to appear
92 89 this.buttons.append(closelink);
93 90 this.buttons.append(expandlink);
94 91 this.buttons.append(morelink);
95 92 this.buttons.append(this._clocklink);
96 93 this._clocklink.hide();
97 94
98 95
99 96 // we need a phony element to make the small arrow
100 97 // of the tooltip in css
101 98 // we will move the arrow later
102 99 this.arrow = $('<div/>').addClass('pretooltiparrow');
103 100 this.tooltip.append(this.buttons);
104 101 this.tooltip.append(this.arrow);
105 102 this.tooltip.append(this.text);
106 103
107 104 // function that will be called if you press tab 1, 2, 3... times in a row
108 this.tabs_functions = [function (cell, text) {
109 that._request_tooltip(cell, text);
105 this.tabs_functions = [function (cell, text, cursor) {
106 that._request_tooltip(cell, text, cursor);
110 107 }, function () {
111 108 that.expand();
112 109 }, function () {
113 110 that.stick();
114 111 }, function (cell) {
115 112 that.cancel_stick();
116 113 that.showInPager(cell);
117 114 }];
118 115 // call after all the tabs function above have bee call to clean their effects
119 116 // if necessary
120 117 this.reset_tabs_function = function (cell, text) {
121 118 this._old_cell = (cell) ? cell : null;
122 119 this._old_request = (text) ? text : null;
123 120 this._consecutive_counter = 0;
124 121 };
125 122 };
126 123
127 124 Tooltip.prototype.is_visible = function () {
128 125 return !this._hidden;
129 126 };
130 127
131 128 Tooltip.prototype.showInPager = function (cell) {
132 129 // reexecute last call in pager by appending ? to show back in pager
133 130 var that = this;
134 var callbacks = {'shell' : {
135 'payload' : {
136 'page' : $.proxy(cell._open_with_pager, cell)
137 }
138 }
139 };
140 cell.kernel.execute(that.name + '?', callbacks, {'silent': false, 'store_history': true});
131 var payload = {};
132 payload.text = that._reply.content.data['text/plain'];
133
134 $([IPython.events]).trigger('open_with_text.Pager', payload);
141 135 this.remove_and_cancel_tooltip();
142 136 };
143 137
144 138 // grow the tooltip verticaly
145 139 Tooltip.prototype.expand = function () {
146 140 this.text.removeClass('smalltooltip');
147 141 this.text.addClass('bigtooltip');
148 142 $('#expanbutton').hide('slow');
149 143 };
150 144
151 145 // deal with all the logic of hiding the tooltip
152 146 // and reset it's status
153 147 Tooltip.prototype._hide = function () {
154 148 this._hidden = true;
155 149 this.tooltip.fadeOut('fast');
156 150 $('#expanbutton').show('slow');
157 151 this.text.removeClass('bigtooltip');
158 152 this.text.addClass('smalltooltip');
159 153 // keep scroll top to be sure to always see the first line
160 154 this.text.scrollTop(0);
161 155 this.code_mirror = null;
162 156 };
163 157
164 158 // return true on successfully removing a visible tooltip; otherwise return
165 159 // false.
166 160 Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
167 161 // note that we don't handle closing directly inside the calltip
168 162 // as in the completer, because it is not focusable, so won't
169 163 // get the event.
170 164 this.cancel_pending();
171 165 if (!this._hidden) {
172 166 if (force || !this._sticky) {
173 167 this.cancel_stick();
174 168 this._hide();
175 169 }
176 170 this.reset_tabs_function();
177 171 return true;
178 172 } else {
179 173 return false;
180 174 }
181 175 };
182 176
183 177 // cancel autocall done after '(' for example.
184 178 Tooltip.prototype.cancel_pending = function () {
185 179 if (this._tooltip_timeout !== null) {
186 180 clearTimeout(this._tooltip_timeout);
187 181 this._tooltip_timeout = null;
188 182 }
189 183 };
190 184
191 185 // will trigger tooltip after timeout
192 186 Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
193 187 var that = this;
194 188 this._tooltip_timeout = setTimeout(function () {
195 189 that.request(cell, hide_if_no_docstring);
196 190 }, that.time_before_tooltip);
197 191 };
198 192
199 193 // easy access for julia monkey patching.
200 194 Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
201 195
202 196 Tooltip.prototype.extract_oir_token = function(line){
203 197 // use internally just to make the request to the kernel
204 198 // Feel free to shorten this logic if you are better
205 199 // than me in regEx
206 200 // basicaly you shoul be able to get xxx.xxx.xxx from
207 201 // something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
208 202 // remove everything between matchin bracket (need to iterate)
209 203 var matchBracket = /\([^\(\)]+\)/g;
210 204 var endBracket = /\([^\(]*$/g;
211 205 var oldline = line;
212 206
213 207 line = line.replace(matchBracket, "");
214 208 while (oldline != line) {
215 209 oldline = line;
216 210 line = line.replace(matchBracket, "");
217 211 }
218 212 // remove everything after last open bracket
219 213 line = line.replace(endBracket, "");
220 214 // reset the regex object
221 215 Tooltip.last_token_re.lastIndex = 0;
222 216 return Tooltip.last_token_re.exec(line);
223 217 };
224 218
225 Tooltip.prototype._request_tooltip = function (cell, line) {
219 Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) {
226 220 var callbacks = $.proxy(this._show, this);
227 var oir_token = this.extract_oir_token(line);
228 var msg_id = cell.kernel.object_info(oir_token, callbacks);
221 var msg_id = cell.kernel.object_info(text, cursor_pos, callbacks);
229 222 };
230 223
231 224 // make an imediate completion request
232 225 Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
233 226 // request(codecell)
234 227 // Deal with extracting the text from the cell and counting
235 228 // call in a row
236 229 this.cancel_pending();
237 230 var editor = cell.code_mirror;
238 231 var cursor = editor.getCursor();
239 var text = editor.getRange({
240 line: cursor.line,
241 ch: 0
242 }, cursor).trim();
232 var cursor_pos = IPython.utils.absolute_cursor_pos(editor, cursor);
233 var text = cell.get_text();
243 234
244 235 this._hide_if_no_docstring = hide_if_no_docstring;
245 236
246 237 if(editor.somethingSelected()){
247 238 text = editor.getSelection();
248 239 }
249 240
250 241 // need a permanent handel to code_mirror for future auto recall
251 242 this.code_mirror = editor;
252 243
253 244 // now we treat the different number of keypress
254 245 // first if same cell, same text, increment counter by 1
255 246 if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
256 247 this._consecutive_counter++;
257 248 } else {
258 249 // else reset
259 250 this.cancel_stick();
260 251 this.reset_tabs_function (cell, text);
261 252 }
262 253
263 // don't do anything if line beggin with '(' or is empty
264 if (text === "" || text === "(") {
265 return;
266 }
254 // don't do anything if line begins with '(' or is empty
255 // if (text === "" || text === "(") {
256 // return;
257 // }
267 258
268 this.tabs_functions[this._consecutive_counter](cell, text);
259 this.tabs_functions[this._consecutive_counter](cell, text, cursor_pos);
269 260
270 261 // then if we are at the end of list function, reset
271 262 if (this._consecutive_counter == this.tabs_functions.length) {
272 this.reset_tabs_function (cell, text);
273 }
263 this.reset_tabs_function (cell, text, cursor);
264 }
274 265
275 266 return;
276 267 };
277 268
278 269 // cancel the option of having the tooltip to stick
279 270 Tooltip.prototype.cancel_stick = function () {
280 271 clearTimeout(this._stick_timeout);
281 272 this._stick_timeout = null;
282 273 this._clocklink.hide('slow');
283 274 this._sticky = false;
284 275 };
285 276
286 277 // put the tooltip in a sicky state for 10 seconds
287 278 // it won't be removed by remove_and_cancell() unless you called with
288 279 // the first parameter set to true.
289 280 // remove_and_cancell_tooltip(true)
290 281 Tooltip.prototype.stick = function (time) {
291 282 time = (time !== undefined) ? time : 10;
292 283 var that = this;
293 284 this._sticky = true;
294 285 this._clocklink.show('slow');
295 286 this._stick_timeout = setTimeout(function () {
296 287 that._sticky = false;
297 288 that._clocklink.hide('slow');
298 289 }, time * 1000);
299 290 };
300 291
301 292 // should be called with the kernel reply to actually show the tooltip
302 293 Tooltip.prototype._show = function (reply) {
303 294 // move the bubble if it is not hidden
304 295 // otherwise fade it
296 this._reply = reply;
305 297 var content = reply.content;
306 298 if (!content.found) {
307 299 // object not found, nothing to show
308 300 return;
309 301 }
310 302 this.name = content.name;
311 303
312 304 // do some math to have the tooltip arrow on more or less on left or right
313 305 // width of the editor
314 306 var w = $(this.code_mirror.getScrollerElement()).width();
315 307 // ofset of the editor
316 308 var o = $(this.code_mirror.getScrollerElement()).offset();
317 309
318 310 // whatever anchor/head order but arrow at mid x selection
319 311 var anchor = this.code_mirror.cursorCoords(false);
320 312 var head = this.code_mirror.cursorCoords(true);
321 313 var xinit = (head.left+anchor.left)/2;
322 314 var xinter = o.left + (xinit - o.left) / w * (w - 450);
323 315 var posarrowleft = xinit - xinter;
324 316
325 317 if (this._hidden === false) {
326 318 this.tooltip.animate({
327 319 'left': xinter - 30 + 'px',
328 320 'top': (head.bottom + 10) + 'px'
329 321 });
330 322 } else {
331 323 this.tooltip.css({
332 324 'left': xinter - 30 + 'px'
333 325 });
334 326 this.tooltip.css({
335 327 'top': (head.bottom + 10) + 'px'
336 328 });
337 329 }
338 330 this.arrow.animate({
339 331 'left': posarrowleft + 'px'
340 332 });
341
342 // build docstring
343 var defstring = content.call_def;
344 if (!defstring) {
345 defstring = content.init_definition;
346 }
347 if (!defstring) {
348 defstring = content.definition;
349 }
350
351 var docstring = content.call_docstring;
352 if (!docstring) {
353 docstring = content.init_docstring;
354 }
355 if (!docstring) {
356 docstring = content.docstring;
357 }
358
359 if (!docstring) {
360 // For reals this time, no docstring
361 if (this._hide_if_no_docstring) {
362 return;
363 } else {
364 docstring = "<empty docstring>";
365 }
366 }
367
333
368 334 this._hidden = false;
369 335 this.tooltip.fadeIn('fast');
370 336 this.text.children().remove();
371
337
338 // This should support rich data types, but only text/plain for now
372 339 // Any HTML within the docstring is escaped by the fixConsole() method.
373 var pre = $('<pre/>').html(utils.fixConsole(docstring));
374 if (defstring) {
375 var defstring_html = $('<pre/>').html(utils.fixConsole(defstring));
376 this.text.append(defstring_html);
377 }
340 var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain']));
378 341 this.text.append(pre);
379 342 // keep scroll top to be sure to always see the first line
380 343 this.text.scrollTop(0);
381 344 };
382 345
383 346 IPython.Tooltip = Tooltip;
384 347
385 348 return IPython;
386 349
387 350 }(IPython));
@@ -1,621 +1,618 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 //============================================================================
5 5 // Kernel
6 6 //============================================================================
7 7
8 8 /**
9 9 * @module IPython
10 10 * @namespace IPython
11 11 * @submodule Kernel
12 12 */
13 13
14 14 var IPython = (function (IPython) {
15 15 "use strict";
16 16
17 17 var utils = IPython.utils;
18 18
19 19 // Initialization and connection.
20 20 /**
21 21 * A Kernel Class to communicate with the Python kernel
22 22 * @Class Kernel
23 23 */
24 24 var Kernel = function (kernel_service_url) {
25 25 this.kernel_id = null;
26 26 this.shell_channel = null;
27 27 this.iopub_channel = null;
28 28 this.stdin_channel = null;
29 29 this.kernel_service_url = kernel_service_url;
30 30 this.running = false;
31 31 this.username = "username";
32 32 this.session_id = utils.uuid();
33 33 this._msg_callbacks = {};
34 34 this.post = $.post;
35 35
36 36 if (typeof(WebSocket) !== 'undefined') {
37 37 this.WebSocket = WebSocket;
38 38 } else if (typeof(MozWebSocket) !== 'undefined') {
39 39 this.WebSocket = MozWebSocket;
40 40 } else {
41 41 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
42 42 }
43 43
44 44 this.bind_events();
45 45 this.init_iopub_handlers();
46 46 this.comm_manager = new IPython.CommManager(this);
47 47 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
48 48
49 49 this.last_msg_id = null;
50 50 this.last_msg_callbacks = {};
51 51 };
52 52
53 53
54 54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
55 55 var msg = {
56 56 header : {
57 57 msg_id : utils.uuid(),
58 58 username : this.username,
59 59 session : this.session_id,
60 60 msg_type : msg_type
61 61 },
62 62 metadata : metadata || {},
63 63 content : content,
64 64 parent_header : {}
65 65 };
66 66 return msg;
67 67 };
68 68
69 69 Kernel.prototype.bind_events = function () {
70 70 var that = this;
71 71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
72 72 that.send_input_reply(data);
73 73 });
74 74 };
75 75
76 76 // Initialize the iopub handlers
77 77
78 78 Kernel.prototype.init_iopub_handlers = function () {
79 79 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
80 80 this._iopub_handlers = {};
81 81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
82 82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
83 83
84 84 for (var i=0; i < output_msg_types.length; i++) {
85 85 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
86 86 }
87 87 };
88 88
89 89 /**
90 90 * Start the Python kernel
91 91 * @method start
92 92 */
93 93 Kernel.prototype.start = function (params) {
94 94 params = params || {};
95 95 if (!this.running) {
96 96 var qs = $.param(params);
97 97 this.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
98 98 $.proxy(this._kernel_started, this),
99 99 'json'
100 100 );
101 101 }
102 102 };
103 103
104 104 /**
105 105 * Restart the python kernel.
106 106 *
107 107 * Emit a 'status_restarting.Kernel' event with
108 108 * the current object as parameter
109 109 *
110 110 * @method restart
111 111 */
112 112 Kernel.prototype.restart = function () {
113 113 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
114 114 if (this.running) {
115 115 this.stop_channels();
116 116 this.post(utils.url_join_encode(this.kernel_url, "restart"),
117 117 $.proxy(this._kernel_started, this),
118 118 'json'
119 119 );
120 120 }
121 121 };
122 122
123 123
124 124 Kernel.prototype._kernel_started = function (json) {
125 125 console.log("Kernel started: ", json.id);
126 126 this.running = true;
127 127 this.kernel_id = json.id;
128 128 // trailing 's' in https will become wss for secure web sockets
129 129 this.ws_host = location.protocol.replace('http', 'ws') + "//" + location.host;
130 130 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
131 131 this.start_channels();
132 132 };
133 133
134 134
135 135 Kernel.prototype._websocket_closed = function(ws_url, early) {
136 136 this.stop_channels();
137 137 $([IPython.events]).trigger('websocket_closed.Kernel',
138 138 {ws_url: ws_url, kernel: this, early: early}
139 139 );
140 140 };
141 141
142 142 /**
143 143 * Start the `shell`and `iopub` channels.
144 144 * Will stop and restart them if they already exist.
145 145 *
146 146 * @method start_channels
147 147 */
148 148 Kernel.prototype.start_channels = function () {
149 149 var that = this;
150 150 this.stop_channels();
151 151 var ws_host_url = this.ws_host + this.kernel_url;
152 152 console.log("Starting WebSockets:", ws_host_url);
153 153 this.shell_channel = new this.WebSocket(
154 154 this.ws_host + utils.url_join_encode(this.kernel_url, "shell")
155 155 );
156 156 this.stdin_channel = new this.WebSocket(
157 157 this.ws_host + utils.url_join_encode(this.kernel_url, "stdin")
158 158 );
159 159 this.iopub_channel = new this.WebSocket(
160 160 this.ws_host + utils.url_join_encode(this.kernel_url, "iopub")
161 161 );
162 162
163 163 var already_called_onclose = false; // only alert once
164 164 var ws_closed_early = function(evt){
165 165 if (already_called_onclose){
166 166 return;
167 167 }
168 168 already_called_onclose = true;
169 169 if ( ! evt.wasClean ){
170 170 that._websocket_closed(ws_host_url, true);
171 171 }
172 172 };
173 173 var ws_closed_late = function(evt){
174 174 if (already_called_onclose){
175 175 return;
176 176 }
177 177 already_called_onclose = true;
178 178 if ( ! evt.wasClean ){
179 179 that._websocket_closed(ws_host_url, false);
180 180 }
181 181 };
182 182 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
183 183 for (var i=0; i < channels.length; i++) {
184 184 channels[i].onopen = $.proxy(this._ws_opened, this);
185 185 channels[i].onclose = ws_closed_early;
186 186 }
187 187 // switch from early-close to late-close message after 1s
188 188 setTimeout(function() {
189 189 for (var i=0; i < channels.length; i++) {
190 190 if (channels[i] !== null) {
191 191 channels[i].onclose = ws_closed_late;
192 192 }
193 193 }
194 194 }, 1000);
195 195 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
196 196 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
197 197 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
198 198 };
199 199
200 200 /**
201 201 * Handle a websocket entering the open state
202 202 * sends session and cookie authentication info as first message.
203 203 * Once all sockets are open, signal the Kernel.status_started event.
204 204 * @method _ws_opened
205 205 */
206 206 Kernel.prototype._ws_opened = function (evt) {
207 207 // send the session id so the Session object Python-side
208 208 // has the same identity
209 209 evt.target.send(this.session_id + ':' + document.cookie);
210 210
211 211 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
212 212 for (var i=0; i < channels.length; i++) {
213 213 // if any channel is not ready, don't trigger event.
214 214 if ( !channels[i].readyState ) return;
215 215 }
216 216 // all events ready, trigger started event.
217 217 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
218 218 };
219 219
220 220 /**
221 221 * Stop the websocket channels.
222 222 * @method stop_channels
223 223 */
224 224 Kernel.prototype.stop_channels = function () {
225 225 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
226 226 for (var i=0; i < channels.length; i++) {
227 227 if ( channels[i] !== null ) {
228 228 channels[i].onclose = null;
229 229 channels[i].close();
230 230 }
231 231 }
232 232 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
233 233 };
234 234
235 235 // Main public methods.
236 236
237 237 // send a message on the Kernel's shell channel
238 238 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
239 239 var msg = this._get_msg(msg_type, content, metadata);
240 240 this.shell_channel.send(JSON.stringify(msg));
241 241 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
242 242 return msg.header.msg_id;
243 243 };
244 244
245 245 /**
246 246 * Get kernel info
247 247 *
248 248 * @param callback {function}
249 249 * @method object_info
250 250 *
251 251 * When calling this method, pass a callback function that expects one argument.
252 252 * The callback will be passed the complete `kernel_info_reply` message documented
253 253 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
254 254 */
255 255 Kernel.prototype.kernel_info = function (callback) {
256 256 var callbacks;
257 257 if (callback) {
258 258 callbacks = { shell : { reply : callback } };
259 259 }
260 260 return this.send_shell_message("kernel_info_request", {}, callbacks);
261 261 };
262 262
263 263 /**
264 264 * Get info on an object
265 265 *
266 * @param objname {string}
266 * @param code {string}
267 * @param cursor_pos {integer}
267 268 * @param callback {function}
268 269 * @method object_info
269 270 *
270 271 * When calling this method, pass a callback function that expects one argument.
271 272 * The callback will be passed the complete `object_info_reply` message documented
272 273 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
273 274 */
274 Kernel.prototype.object_info = function (objname, callback) {
275 Kernel.prototype.object_info = function (code, cursor_pos, callback) {
275 276 var callbacks;
276 277 if (callback) {
277 278 callbacks = { shell : { reply : callback } };
278 279 }
279 280
280 if (typeof(objname) !== null && objname !== null) {
281 var content = {
282 oname : objname.toString(),
283 detail_level : 0,
284 };
285 return this.send_shell_message("object_info_request", content, callbacks);
286 }
287 return;
281 var content = {
282 code : code,
283 cursor_pos : cursor_pos,
284 detail_level : 0,
285 };
286 return this.send_shell_message("object_info_request", content, callbacks);
288 287 };
289 288
290 289 /**
291 290 * Execute given code into kernel, and pass result to callback.
292 291 *
293 292 * @async
294 293 * @method execute
295 294 * @param {string} code
296 295 * @param [callbacks] {Object} With the following keys (all optional)
297 296 * @param callbacks.shell.reply {function}
298 297 * @param callbacks.shell.payload.[payload_name] {function}
299 298 * @param callbacks.iopub.output {function}
300 299 * @param callbacks.iopub.clear_output {function}
301 300 * @param callbacks.input {function}
302 301 * @param {object} [options]
303 302 * @param [options.silent=false] {Boolean}
304 303 * @param [options.user_expressions=empty_dict] {Dict}
305 304 * @param [options.allow_stdin=false] {Boolean} true|false
306 305 *
307 306 * @example
308 307 *
309 308 * The options object should contain the options for the execute call. Its default
310 309 * values are:
311 310 *
312 311 * options = {
313 312 * silent : true,
314 313 * user_expressions : {},
315 314 * allow_stdin : false
316 315 * }
317 316 *
318 317 * When calling this method pass a callbacks structure of the form:
319 318 *
320 319 * callbacks = {
321 320 * shell : {
322 321 * reply : execute_reply_callback,
323 322 * payload : {
324 323 * set_next_input : set_next_input_callback,
325 324 * }
326 325 * },
327 326 * iopub : {
328 327 * output : output_callback,
329 328 * clear_output : clear_output_callback,
330 329 * },
331 330 * input : raw_input_callback
332 331 * }
333 332 *
334 333 * Each callback will be passed the entire message as a single arugment.
335 334 * Payload handlers will be passed the corresponding payload and the execute_reply message.
336 335 */
337 336 Kernel.prototype.execute = function (code, callbacks, options) {
338 337
339 338 var content = {
340 339 code : code,
341 340 silent : true,
342 341 store_history : false,
343 342 user_expressions : {},
344 343 allow_stdin : false
345 344 };
346 345 callbacks = callbacks || {};
347 346 if (callbacks.input !== undefined) {
348 347 content.allow_stdin = true;
349 348 }
350 349 $.extend(true, content, options);
351 350 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
352 351 return this.send_shell_message("execute_request", content, callbacks);
353 352 };
354 353
355 354 /**
356 355 * When calling this method, pass a function to be called with the `complete_reply` message
357 356 * as its only argument when it arrives.
358 357 *
359 358 * `complete_reply` is documented
360 359 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
361 360 *
362 361 * @method complete
363 * @param line {integer}
362 * @param code {string}
364 363 * @param cursor_pos {integer}
365 364 * @param callback {function}
366 365 *
367 366 */
368 Kernel.prototype.complete = function (line, cursor_pos, callback) {
367 Kernel.prototype.complete = function (code, cursor_pos, callback) {
369 368 var callbacks;
370 369 if (callback) {
371 370 callbacks = { shell : { reply : callback } };
372 371 }
373 372 var content = {
374 text : '',
375 line : line,
376 block : null,
377 cursor_pos : cursor_pos
373 code : code,
374 cursor_pos : cursor_pos,
378 375 };
379 376 return this.send_shell_message("complete_request", content, callbacks);
380 377 };
381 378
382 379
383 380 Kernel.prototype.interrupt = function () {
384 381 if (this.running) {
385 382 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
386 383 this.post(utils.url_join_encode(this.kernel_url, "interrupt"));
387 384 }
388 385 };
389 386
390 387
391 388 Kernel.prototype.kill = function () {
392 389 if (this.running) {
393 390 this.running = false;
394 391 var settings = {
395 392 cache : false,
396 393 type : "DELETE",
397 394 error : utils.log_ajax_error,
398 395 };
399 396 $.ajax(utils.url_join_encode(this.kernel_url), settings);
400 397 }
401 398 };
402 399
403 400 Kernel.prototype.send_input_reply = function (input) {
404 401 var content = {
405 402 value : input,
406 403 };
407 404 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
408 405 var msg = this._get_msg("input_reply", content);
409 406 this.stdin_channel.send(JSON.stringify(msg));
410 407 return msg.header.msg_id;
411 408 };
412 409
413 410
414 411 // Reply handlers
415 412
416 413 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
417 414 this._iopub_handlers[msg_type] = callback;
418 415 };
419 416
420 417 Kernel.prototype.get_iopub_handler = function (msg_type) {
421 418 // get iopub handler for a specific message type
422 419 return this._iopub_handlers[msg_type];
423 420 };
424 421
425 422
426 423 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
427 424 // get callbacks for a specific message
428 425 if (msg_id == this.last_msg_id) {
429 426 return this.last_msg_callbacks;
430 427 } else {
431 428 return this._msg_callbacks[msg_id];
432 429 }
433 430 };
434 431
435 432
436 433 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
437 434 if (this._msg_callbacks[msg_id] !== undefined ) {
438 435 delete this._msg_callbacks[msg_id];
439 436 }
440 437 };
441 438
442 439 Kernel.prototype._finish_shell = function (msg_id) {
443 440 var callbacks = this._msg_callbacks[msg_id];
444 441 if (callbacks !== undefined) {
445 442 callbacks.shell_done = true;
446 443 if (callbacks.iopub_done) {
447 444 this.clear_callbacks_for_msg(msg_id);
448 445 }
449 446 }
450 447 };
451 448
452 449 Kernel.prototype._finish_iopub = function (msg_id) {
453 450 var callbacks = this._msg_callbacks[msg_id];
454 451 if (callbacks !== undefined) {
455 452 callbacks.iopub_done = true;
456 453 if (!callbacks.shell_done) {
457 454 this.clear_callbacks_for_msg(msg_id);
458 455 }
459 456 }
460 457 };
461 458
462 459 /* Set callbacks for a particular message.
463 460 * Callbacks should be a struct of the following form:
464 461 * shell : {
465 462 *
466 463 * }
467 464
468 465 */
469 466 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
470 467 this.last_msg_id = msg_id;
471 468 if (callbacks) {
472 469 // shallow-copy mapping, because we will modify it at the top level
473 470 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
474 471 cbcopy.shell = callbacks.shell;
475 472 cbcopy.iopub = callbacks.iopub;
476 473 cbcopy.input = callbacks.input;
477 474 cbcopy.shell_done = (!callbacks.shell);
478 475 cbcopy.iopub_done = (!callbacks.iopub);
479 476 } else {
480 477 this.last_msg_callbacks = {};
481 478 }
482 479 };
483 480
484 481
485 482 Kernel.prototype._handle_shell_reply = function (e) {
486 483 var reply = $.parseJSON(e.data);
487 484 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
488 485 var content = reply.content;
489 486 var metadata = reply.metadata;
490 487 var parent_id = reply.parent_header.msg_id;
491 488 var callbacks = this.get_callbacks_for_msg(parent_id);
492 489 if (!callbacks || !callbacks.shell) {
493 490 return;
494 491 }
495 492 var shell_callbacks = callbacks.shell;
496 493
497 494 // signal that shell callbacks are done
498 495 this._finish_shell(parent_id);
499 496
500 497 if (shell_callbacks.reply !== undefined) {
501 498 shell_callbacks.reply(reply);
502 499 }
503 500 if (content.payload && shell_callbacks.payload) {
504 501 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
505 502 }
506 503 };
507 504
508 505
509 506 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
510 507 var l = payloads.length;
511 508 // Payloads are handled by triggering events because we don't want the Kernel
512 509 // to depend on the Notebook or Pager classes.
513 510 for (var i=0; i<l; i++) {
514 511 var payload = payloads[i];
515 512 var callback = payload_callbacks[payload.source];
516 513 if (callback) {
517 514 callback(payload, msg);
518 515 }
519 516 }
520 517 };
521 518
522 519 Kernel.prototype._handle_status_message = function (msg) {
523 520 var execution_state = msg.content.execution_state;
524 521 var parent_id = msg.parent_header.msg_id;
525 522
526 523 // dispatch status msg callbacks, if any
527 524 var callbacks = this.get_callbacks_for_msg(parent_id);
528 525 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
529 526 try {
530 527 callbacks.iopub.status(msg);
531 528 } catch (e) {
532 529 console.log("Exception in status msg handler", e, e.stack);
533 530 }
534 531 }
535 532
536 533 if (execution_state === 'busy') {
537 534 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
538 535 } else if (execution_state === 'idle') {
539 536 // signal that iopub callbacks are (probably) done
540 537 // async output may still arrive,
541 538 // but only for the most recent request
542 539 this._finish_iopub(parent_id);
543 540
544 541 // trigger status_idle event
545 542 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
546 543 } else if (execution_state === 'restarting') {
547 544 // autorestarting is distinct from restarting,
548 545 // in that it means the kernel died and the server is restarting it.
549 546 // status_restarting sets the notification widget,
550 547 // autorestart shows the more prominent dialog.
551 548 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
552 549 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
553 550 } else if (execution_state === 'dead') {
554 551 this.stop_channels();
555 552 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
556 553 }
557 554 };
558 555
559 556
560 557 // handle clear_output message
561 558 Kernel.prototype._handle_clear_output = function (msg) {
562 559 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
563 560 if (!callbacks || !callbacks.iopub) {
564 561 return;
565 562 }
566 563 var callback = callbacks.iopub.clear_output;
567 564 if (callback) {
568 565 callback(msg);
569 566 }
570 567 };
571 568
572 569
573 570 // handle an output message (execute_result, display_data, etc.)
574 571 Kernel.prototype._handle_output_message = function (msg) {
575 572 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
576 573 if (!callbacks || !callbacks.iopub) {
577 574 return;
578 575 }
579 576 var callback = callbacks.iopub.output;
580 577 if (callback) {
581 578 callback(msg);
582 579 }
583 580 };
584 581
585 582 // dispatch IOPub messages to respective handlers.
586 583 // each message type should have a handler.
587 584 Kernel.prototype._handle_iopub_message = function (e) {
588 585 var msg = $.parseJSON(e.data);
589 586
590 587 var handler = this.get_iopub_handler(msg.header.msg_type);
591 588 if (handler !== undefined) {
592 589 handler(msg);
593 590 }
594 591 };
595 592
596 593
597 594 Kernel.prototype._handle_input_request = function (e) {
598 595 var request = $.parseJSON(e.data);
599 596 var header = request.header;
600 597 var content = request.content;
601 598 var metadata = request.metadata;
602 599 var msg_type = header.msg_type;
603 600 if (msg_type !== 'input_request') {
604 601 console.log("Invalid input request!", request);
605 602 return;
606 603 }
607 604 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
608 605 if (callbacks) {
609 606 if (callbacks.input) {
610 607 callbacks.input(request);
611 608 }
612 609 }
613 610 };
614 611
615 612
616 613 IPython.Kernel = Kernel;
617 614
618 615 return IPython;
619 616
620 617 }(IPython));
621 618
@@ -1,618 +1,620 b''
1 1 """Base classes to manage a Client's interaction with a running kernel"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import atexit
9 9 import errno
10 10 from threading import Thread
11 11 import time
12 12
13 13 import zmq
14 14 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
15 15 # during garbage collection of threads at exit:
16 16 from zmq import ZMQError
17 17 from zmq.eventloop import ioloop, zmqstream
18 18
19 19 # Local imports
20 20 from .channelsabc import (
21 21 ShellChannelABC, IOPubChannelABC,
22 22 HBChannelABC, StdInChannelABC,
23 23 )
24 24 from IPython.utils.py3compat import string_types, iteritems
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Constants and exceptions
28 28 #-----------------------------------------------------------------------------
29 29
30 30 class InvalidPortNumber(Exception):
31 31 pass
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Utility functions
35 35 #-----------------------------------------------------------------------------
36 36
37 37 # some utilities to validate message structure, these might get moved elsewhere
38 38 # if they prove to have more generic utility
39 39
40 40 def validate_string_list(lst):
41 41 """Validate that the input is a list of strings.
42 42
43 43 Raises ValueError if not."""
44 44 if not isinstance(lst, list):
45 45 raise ValueError('input %r must be a list' % lst)
46 46 for x in lst:
47 47 if not isinstance(x, string_types):
48 48 raise ValueError('element %r in list must be a string' % x)
49 49
50 50
51 51 def validate_string_dict(dct):
52 52 """Validate that the input is a dict with string keys and values.
53 53
54 54 Raises ValueError if not."""
55 55 for k,v in iteritems(dct):
56 56 if not isinstance(k, string_types):
57 57 raise ValueError('key %r in dict must be a string' % k)
58 58 if not isinstance(v, string_types):
59 59 raise ValueError('value %r in dict must be a string' % v)
60 60
61 61
62 62 #-----------------------------------------------------------------------------
63 63 # ZMQ Socket Channel classes
64 64 #-----------------------------------------------------------------------------
65 65
66 66 class ZMQSocketChannel(Thread):
67 67 """The base class for the channels that use ZMQ sockets."""
68 68 context = None
69 69 session = None
70 70 socket = None
71 71 ioloop = None
72 72 stream = None
73 73 _address = None
74 74 _exiting = False
75 75 proxy_methods = []
76 76
77 77 def __init__(self, context, session, address):
78 78 """Create a channel.
79 79
80 80 Parameters
81 81 ----------
82 82 context : :class:`zmq.Context`
83 83 The ZMQ context to use.
84 84 session : :class:`session.Session`
85 85 The session to use.
86 86 address : zmq url
87 87 Standard (ip, port) tuple that the kernel is listening on.
88 88 """
89 89 super(ZMQSocketChannel, self).__init__()
90 90 self.daemon = True
91 91
92 92 self.context = context
93 93 self.session = session
94 94 if isinstance(address, tuple):
95 95 if address[1] == 0:
96 96 message = 'The port number for a channel cannot be 0.'
97 97 raise InvalidPortNumber(message)
98 98 address = "tcp://%s:%i" % address
99 99 self._address = address
100 100 atexit.register(self._notice_exit)
101 101
102 102 def _notice_exit(self):
103 103 self._exiting = True
104 104
105 105 def _run_loop(self):
106 106 """Run my loop, ignoring EINTR events in the poller"""
107 107 while True:
108 108 try:
109 109 self.ioloop.start()
110 110 except ZMQError as e:
111 111 if e.errno == errno.EINTR:
112 112 continue
113 113 else:
114 114 raise
115 115 except Exception:
116 116 if self._exiting:
117 117 break
118 118 else:
119 119 raise
120 120 else:
121 121 break
122 122
123 123 def stop(self):
124 124 """Stop the channel's event loop and join its thread.
125 125
126 126 This calls :meth:`~threading.Thread.join` and returns when the thread
127 127 terminates. :class:`RuntimeError` will be raised if
128 128 :meth:`~threading.Thread.start` is called again.
129 129 """
130 130 if self.ioloop is not None:
131 131 self.ioloop.stop()
132 132 self.join()
133 133 self.close()
134 134
135 135 def close(self):
136 136 if self.ioloop is not None:
137 137 try:
138 138 self.ioloop.close(all_fds=True)
139 139 except Exception:
140 140 pass
141 141 if self.socket is not None:
142 142 try:
143 143 self.socket.close(linger=0)
144 144 except Exception:
145 145 pass
146 146 self.socket = None
147 147
148 148 @property
149 149 def address(self):
150 150 """Get the channel's address as a zmq url string.
151 151
152 152 These URLS have the form: 'tcp://127.0.0.1:5555'.
153 153 """
154 154 return self._address
155 155
156 156 def _queue_send(self, msg):
157 157 """Queue a message to be sent from the IOLoop's thread.
158 158
159 159 Parameters
160 160 ----------
161 161 msg : message to send
162 162
163 163 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
164 164 thread control of the action.
165 165 """
166 166 def thread_send():
167 167 self.session.send(self.stream, msg)
168 168 self.ioloop.add_callback(thread_send)
169 169
170 170 def _handle_recv(self, msg):
171 171 """Callback for stream.on_recv.
172 172
173 173 Unpacks message, and calls handlers with it.
174 174 """
175 175 ident,smsg = self.session.feed_identities(msg)
176 176 self.call_handlers(self.session.unserialize(smsg))
177 177
178 178
179 179
180 180 class ShellChannel(ZMQSocketChannel):
181 181 """The shell channel for issuing request/replies to the kernel."""
182 182
183 183 command_queue = None
184 184 # flag for whether execute requests should be allowed to call raw_input:
185 185 allow_stdin = True
186 186 proxy_methods = [
187 187 'execute',
188 188 'complete',
189 189 'object_info',
190 190 'history',
191 191 'kernel_info',
192 192 'shutdown',
193 193 ]
194 194
195 195 def __init__(self, context, session, address):
196 196 super(ShellChannel, self).__init__(context, session, address)
197 197 self.ioloop = ioloop.IOLoop()
198 198
199 199 def run(self):
200 200 """The thread's main activity. Call start() instead."""
201 201 self.socket = self.context.socket(zmq.DEALER)
202 202 self.socket.linger = 1000
203 203 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
204 204 self.socket.connect(self.address)
205 205 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
206 206 self.stream.on_recv(self._handle_recv)
207 207 self._run_loop()
208 208
209 209 def call_handlers(self, msg):
210 210 """This method is called in the ioloop thread when a message arrives.
211 211
212 212 Subclasses should override this method to handle incoming messages.
213 213 It is important to remember that this method is called in the thread
214 214 so that some logic must be done to ensure that the application level
215 215 handlers are called in the application thread.
216 216 """
217 217 raise NotImplementedError('call_handlers must be defined in a subclass.')
218 218
219 219 def execute(self, code, silent=False, store_history=True,
220 220 user_expressions=None, allow_stdin=None):
221 221 """Execute code in the kernel.
222 222
223 223 Parameters
224 224 ----------
225 225 code : str
226 226 A string of Python code.
227 227
228 228 silent : bool, optional (default False)
229 229 If set, the kernel will execute the code as quietly possible, and
230 230 will force store_history to be False.
231 231
232 232 store_history : bool, optional (default True)
233 233 If set, the kernel will store command history. This is forced
234 234 to be False if silent is True.
235 235
236 236 user_expressions : dict, optional
237 237 A dict mapping names to expressions to be evaluated in the user's
238 238 dict. The expression values are returned as strings formatted using
239 239 :func:`repr`.
240 240
241 241 allow_stdin : bool, optional (default self.allow_stdin)
242 242 Flag for whether the kernel can send stdin requests to frontends.
243 243
244 244 Some frontends (e.g. the Notebook) do not support stdin requests.
245 245 If raw_input is called from code executed from such a frontend, a
246 246 StdinNotImplementedError will be raised.
247 247
248 248 Returns
249 249 -------
250 250 The msg_id of the message sent.
251 251 """
252 252 if user_expressions is None:
253 253 user_expressions = {}
254 254 if allow_stdin is None:
255 255 allow_stdin = self.allow_stdin
256 256
257 257
258 258 # Don't waste network traffic if inputs are invalid
259 259 if not isinstance(code, string_types):
260 260 raise ValueError('code %r must be a string' % code)
261 261 validate_string_dict(user_expressions)
262 262
263 263 # Create class for content/msg creation. Related to, but possibly
264 264 # not in Session.
265 265 content = dict(code=code, silent=silent, store_history=store_history,
266 266 user_expressions=user_expressions,
267 267 allow_stdin=allow_stdin,
268 268 )
269 269 msg = self.session.msg('execute_request', content)
270 270 self._queue_send(msg)
271 271 return msg['header']['msg_id']
272 272
273 def complete(self, text, line, cursor_pos, block=None):
273 def complete(self, code, cursor_pos=0, block=None):
274 274 """Tab complete text in the kernel's namespace.
275 275
276 276 Parameters
277 277 ----------
278 text : str
279 The text to complete.
280 line : str
281 The full line of text that is the surrounding context for the
282 text to complete.
283 cursor_pos : int
284 The position of the cursor in the line where the completion was
285 requested.
286 block : str, optional
287 The full block of code in which the completion is being requested.
278 code : str
279 The context in which completion is requested.
280 Can be anything between a variable name and an entire cell.
281 cursor_pos : int, optional
282 The position of the cursor in the block of code where the completion was requested.
288 283
289 284 Returns
290 285 -------
291 286 The msg_id of the message sent.
292 287 """
293 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
288 content = dict(code=code, cursor_pos=cursor_pos)
294 289 msg = self.session.msg('complete_request', content)
295 290 self._queue_send(msg)
296 291 return msg['header']['msg_id']
297 292
298 def object_info(self, oname, detail_level=0):
293 def object_info(self, code, cursor_pos=0, detail_level=0):
299 294 """Get metadata information about an object in the kernel's namespace.
300 295
296 It is up to the kernel to determine the appropriate object to inspect.
297
301 298 Parameters
302 299 ----------
303 oname : str
304 A string specifying the object name.
300 code : str
301 The context in which info is requested.
302 Can be anything between a variable name and an entire cell.
303 cursor_pos : int, optional
304 The position of the cursor in the block of code where the info was requested.
305 305 detail_level : int, optional
306 306 The level of detail for the introspection (0-2)
307 307
308 308 Returns
309 309 -------
310 310 The msg_id of the message sent.
311 311 """
312 content = dict(oname=oname, detail_level=detail_level)
312 content = dict(code=code, cursor_pos=cursor_pos,
313 detail_level=detail_level,
314 )
313 315 msg = self.session.msg('object_info_request', content)
314 316 self._queue_send(msg)
315 317 return msg['header']['msg_id']
316 318
317 319 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
318 320 """Get entries from the kernel's history list.
319 321
320 322 Parameters
321 323 ----------
322 324 raw : bool
323 325 If True, return the raw input.
324 326 output : bool
325 327 If True, then return the output as well.
326 328 hist_access_type : str
327 329 'range' (fill in session, start and stop params), 'tail' (fill in n)
328 330 or 'search' (fill in pattern param).
329 331
330 332 session : int
331 333 For a range request, the session from which to get lines. Session
332 334 numbers are positive integers; negative ones count back from the
333 335 current session.
334 336 start : int
335 337 The first line number of a history range.
336 338 stop : int
337 339 The final (excluded) line number of a history range.
338 340
339 341 n : int
340 342 The number of lines of history to get for a tail request.
341 343
342 344 pattern : str
343 345 The glob-syntax pattern for a search request.
344 346
345 347 Returns
346 348 -------
347 349 The msg_id of the message sent.
348 350 """
349 351 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
350 352 **kwargs)
351 353 msg = self.session.msg('history_request', content)
352 354 self._queue_send(msg)
353 355 return msg['header']['msg_id']
354 356
355 357 def kernel_info(self):
356 358 """Request kernel info."""
357 359 msg = self.session.msg('kernel_info_request')
358 360 self._queue_send(msg)
359 361 return msg['header']['msg_id']
360 362
361 363 def shutdown(self, restart=False):
362 364 """Request an immediate kernel shutdown.
363 365
364 366 Upon receipt of the (empty) reply, client code can safely assume that
365 367 the kernel has shut down and it's safe to forcefully terminate it if
366 368 it's still alive.
367 369
368 370 The kernel will send the reply via a function registered with Python's
369 371 atexit module, ensuring it's truly done as the kernel is done with all
370 372 normal operation.
371 373 """
372 374 # Send quit message to kernel. Once we implement kernel-side setattr,
373 375 # this should probably be done that way, but for now this will do.
374 376 msg = self.session.msg('shutdown_request', {'restart':restart})
375 377 self._queue_send(msg)
376 378 return msg['header']['msg_id']
377 379
378 380
379 381
380 382 class IOPubChannel(ZMQSocketChannel):
381 383 """The iopub channel which listens for messages that the kernel publishes.
382 384
383 385 This channel is where all output is published to frontends.
384 386 """
385 387
386 388 def __init__(self, context, session, address):
387 389 super(IOPubChannel, self).__init__(context, session, address)
388 390 self.ioloop = ioloop.IOLoop()
389 391
390 392 def run(self):
391 393 """The thread's main activity. Call start() instead."""
392 394 self.socket = self.context.socket(zmq.SUB)
393 395 self.socket.linger = 1000
394 396 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
395 397 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
396 398 self.socket.connect(self.address)
397 399 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
398 400 self.stream.on_recv(self._handle_recv)
399 401 self._run_loop()
400 402
401 403 def call_handlers(self, msg):
402 404 """This method is called in the ioloop thread when a message arrives.
403 405
404 406 Subclasses should override this method to handle incoming messages.
405 407 It is important to remember that this method is called in the thread
406 408 so that some logic must be done to ensure that the application leve
407 409 handlers are called in the application thread.
408 410 """
409 411 raise NotImplementedError('call_handlers must be defined in a subclass.')
410 412
411 413 def flush(self, timeout=1.0):
412 414 """Immediately processes all pending messages on the iopub channel.
413 415
414 416 Callers should use this method to ensure that :meth:`call_handlers`
415 417 has been called for all messages that have been received on the
416 418 0MQ SUB socket of this channel.
417 419
418 420 This method is thread safe.
419 421
420 422 Parameters
421 423 ----------
422 424 timeout : float, optional
423 425 The maximum amount of time to spend flushing, in seconds. The
424 426 default is one second.
425 427 """
426 428 # We do the IOLoop callback process twice to ensure that the IOLoop
427 429 # gets to perform at least one full poll.
428 430 stop_time = time.time() + timeout
429 431 for i in range(2):
430 432 self._flushed = False
431 433 self.ioloop.add_callback(self._flush)
432 434 while not self._flushed and time.time() < stop_time:
433 435 time.sleep(0.01)
434 436
435 437 def _flush(self):
436 438 """Callback for :method:`self.flush`."""
437 439 self.stream.flush()
438 440 self._flushed = True
439 441
440 442
441 443 class StdInChannel(ZMQSocketChannel):
442 444 """The stdin channel to handle raw_input requests that the kernel makes."""
443 445
444 446 msg_queue = None
445 447 proxy_methods = ['input']
446 448
447 449 def __init__(self, context, session, address):
448 450 super(StdInChannel, self).__init__(context, session, address)
449 451 self.ioloop = ioloop.IOLoop()
450 452
451 453 def run(self):
452 454 """The thread's main activity. Call start() instead."""
453 455 self.socket = self.context.socket(zmq.DEALER)
454 456 self.socket.linger = 1000
455 457 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
456 458 self.socket.connect(self.address)
457 459 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
458 460 self.stream.on_recv(self._handle_recv)
459 461 self._run_loop()
460 462
461 463 def call_handlers(self, msg):
462 464 """This method is called in the ioloop thread when a message arrives.
463 465
464 466 Subclasses should override this method to handle incoming messages.
465 467 It is important to remember that this method is called in the thread
466 468 so that some logic must be done to ensure that the application leve
467 469 handlers are called in the application thread.
468 470 """
469 471 raise NotImplementedError('call_handlers must be defined in a subclass.')
470 472
471 473 def input(self, string):
472 474 """Send a string of raw input to the kernel."""
473 475 content = dict(value=string)
474 476 msg = self.session.msg('input_reply', content)
475 477 self._queue_send(msg)
476 478
477 479
478 480 class HBChannel(ZMQSocketChannel):
479 481 """The heartbeat channel which monitors the kernel heartbeat.
480 482
481 483 Note that the heartbeat channel is paused by default. As long as you start
482 484 this channel, the kernel manager will ensure that it is paused and un-paused
483 485 as appropriate.
484 486 """
485 487
486 488 time_to_dead = 3.0
487 489 socket = None
488 490 poller = None
489 491 _running = None
490 492 _pause = None
491 493 _beating = None
492 494
493 495 def __init__(self, context, session, address):
494 496 super(HBChannel, self).__init__(context, session, address)
495 497 self._running = False
496 498 self._pause =True
497 499 self.poller = zmq.Poller()
498 500
499 501 def _create_socket(self):
500 502 if self.socket is not None:
501 503 # close previous socket, before opening a new one
502 504 self.poller.unregister(self.socket)
503 505 self.socket.close()
504 506 self.socket = self.context.socket(zmq.REQ)
505 507 self.socket.linger = 1000
506 508 self.socket.connect(self.address)
507 509
508 510 self.poller.register(self.socket, zmq.POLLIN)
509 511
510 512 def _poll(self, start_time):
511 513 """poll for heartbeat replies until we reach self.time_to_dead.
512 514
513 515 Ignores interrupts, and returns the result of poll(), which
514 516 will be an empty list if no messages arrived before the timeout,
515 517 or the event tuple if there is a message to receive.
516 518 """
517 519
518 520 until_dead = self.time_to_dead - (time.time() - start_time)
519 521 # ensure poll at least once
520 522 until_dead = max(until_dead, 1e-3)
521 523 events = []
522 524 while True:
523 525 try:
524 526 events = self.poller.poll(1000 * until_dead)
525 527 except ZMQError as e:
526 528 if e.errno == errno.EINTR:
527 529 # ignore interrupts during heartbeat
528 530 # this may never actually happen
529 531 until_dead = self.time_to_dead - (time.time() - start_time)
530 532 until_dead = max(until_dead, 1e-3)
531 533 pass
532 534 else:
533 535 raise
534 536 except Exception:
535 537 if self._exiting:
536 538 break
537 539 else:
538 540 raise
539 541 else:
540 542 break
541 543 return events
542 544
543 545 def run(self):
544 546 """The thread's main activity. Call start() instead."""
545 547 self._create_socket()
546 548 self._running = True
547 549 self._beating = True
548 550
549 551 while self._running:
550 552 if self._pause:
551 553 # just sleep, and skip the rest of the loop
552 554 time.sleep(self.time_to_dead)
553 555 continue
554 556
555 557 since_last_heartbeat = 0.0
556 558 # io.rprint('Ping from HB channel') # dbg
557 559 # no need to catch EFSM here, because the previous event was
558 560 # either a recv or connect, which cannot be followed by EFSM
559 561 self.socket.send(b'ping')
560 562 request_time = time.time()
561 563 ready = self._poll(request_time)
562 564 if ready:
563 565 self._beating = True
564 566 # the poll above guarantees we have something to recv
565 567 self.socket.recv()
566 568 # sleep the remainder of the cycle
567 569 remainder = self.time_to_dead - (time.time() - request_time)
568 570 if remainder > 0:
569 571 time.sleep(remainder)
570 572 continue
571 573 else:
572 574 # nothing was received within the time limit, signal heart failure
573 575 self._beating = False
574 576 since_last_heartbeat = time.time() - request_time
575 577 self.call_handlers(since_last_heartbeat)
576 578 # and close/reopen the socket, because the REQ/REP cycle has been broken
577 579 self._create_socket()
578 580 continue
579 581
580 582 def pause(self):
581 583 """Pause the heartbeat."""
582 584 self._pause = True
583 585
584 586 def unpause(self):
585 587 """Unpause the heartbeat."""
586 588 self._pause = False
587 589
588 590 def is_beating(self):
589 591 """Is the heartbeat running and responsive (and not paused)."""
590 592 if self.is_alive() and not self._pause and self._beating:
591 593 return True
592 594 else:
593 595 return False
594 596
595 597 def stop(self):
596 598 """Stop the channel's event loop and join its thread."""
597 599 self._running = False
598 600 super(HBChannel, self).stop()
599 601
600 602 def call_handlers(self, since_last_heartbeat):
601 603 """This method is called in the ioloop thread when a message arrives.
602 604
603 605 Subclasses should override this method to handle incoming messages.
604 606 It is important to remember that this method is called in the thread
605 607 so that some logic must be done to ensure that the application level
606 608 handlers are called in the application thread.
607 609 """
608 610 raise NotImplementedError('call_handlers must be defined in a subclass.')
609 611
610 612
611 613 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
612 614 # ABC Registration
613 615 #-----------------------------------------------------------------------------
614 616
615 617 ShellChannelABC.register(ShellChannel)
616 618 IOPubChannelABC.register(IOPubChannel)
617 619 HBChannelABC.register(HBChannel)
618 620 StdInChannelABC.register(StdInChannel)
@@ -1,190 +1,192 b''
1 1 """A kernel client for in-process kernels."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from IPython.kernel.channelsabc import (
7 7 ShellChannelABC, IOPubChannelABC,
8 8 HBChannelABC, StdInChannelABC,
9 9 )
10 10
11 11 from .socket import DummySocket
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Channel classes
15 15 #-----------------------------------------------------------------------------
16 16
17 17 class InProcessChannel(object):
18 18 """Base class for in-process channels."""
19 19 proxy_methods = []
20 20
21 21 def __init__(self, client=None):
22 22 super(InProcessChannel, self).__init__()
23 23 self.client = client
24 24 self._is_alive = False
25 25
26 26 #--------------------------------------------------------------------------
27 27 # Channel interface
28 28 #--------------------------------------------------------------------------
29 29
30 30 def is_alive(self):
31 31 return self._is_alive
32 32
33 33 def start(self):
34 34 self._is_alive = True
35 35
36 36 def stop(self):
37 37 self._is_alive = False
38 38
39 39 def call_handlers(self, msg):
40 40 """ This method is called in the main thread when a message arrives.
41 41
42 42 Subclasses should override this method to handle incoming messages.
43 43 """
44 44 raise NotImplementedError('call_handlers must be defined in a subclass.')
45 45
46 46 #--------------------------------------------------------------------------
47 47 # InProcessChannel interface
48 48 #--------------------------------------------------------------------------
49 49
50 50 def call_handlers_later(self, *args, **kwds):
51 51 """ Call the message handlers later.
52 52
53 53 The default implementation just calls the handlers immediately, but this
54 54 method exists so that GUI toolkits can defer calling the handlers until
55 55 after the event loop has run, as expected by GUI frontends.
56 56 """
57 57 self.call_handlers(*args, **kwds)
58 58
59 59 def process_events(self):
60 60 """ Process any pending GUI events.
61 61
62 62 This method will be never be called from a frontend without an event
63 63 loop (e.g., a terminal frontend).
64 64 """
65 65 raise NotImplementedError
66 66
67 67
68 68 class InProcessShellChannel(InProcessChannel):
69 69 """See `IPython.kernel.channels.ShellChannel` for docstrings."""
70 70
71 71 # flag for whether execute requests should be allowed to call raw_input
72 72 allow_stdin = True
73 73 proxy_methods = [
74 74 'execute',
75 75 'complete',
76 76 'object_info',
77 77 'history',
78 78 'shutdown',
79 79 'kernel_info',
80 80 ]
81 81
82 82 #--------------------------------------------------------------------------
83 83 # ShellChannel interface
84 84 #--------------------------------------------------------------------------
85 85
86 86 def execute(self, code, silent=False, store_history=True,
87 87 user_expressions={}, allow_stdin=None):
88 88 if allow_stdin is None:
89 89 allow_stdin = self.allow_stdin
90 90 content = dict(code=code, silent=silent, store_history=store_history,
91 91 user_expressions=user_expressions,
92 92 allow_stdin=allow_stdin)
93 93 msg = self.client.session.msg('execute_request', content)
94 94 self._dispatch_to_kernel(msg)
95 95 return msg['header']['msg_id']
96 96
97 def complete(self, text, line, cursor_pos, block=None):
98 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
97 def complete(self, code, cursor_pos=0):
98 content = dict(code=code, cursor_pos=cursor_pos)
99 99 msg = self.client.session.msg('complete_request', content)
100 100 self._dispatch_to_kernel(msg)
101 101 return msg['header']['msg_id']
102 102
103 def object_info(self, oname, detail_level=0):
104 content = dict(oname=oname, detail_level=detail_level)
103 def object_info(self, code, cursor_pos=0, detail_level=0):
104 content = dict(code=code, cursor_pos=cursor_pos,
105 detail_level=detail_level,
106 )
105 107 msg = self.client.session.msg('object_info_request', content)
106 108 self._dispatch_to_kernel(msg)
107 109 return msg['header']['msg_id']
108 110
109 111 def history(self, raw=True, output=False, hist_access_type='range', **kwds):
110 112 content = dict(raw=raw, output=output,
111 113 hist_access_type=hist_access_type, **kwds)
112 114 msg = self.client.session.msg('history_request', content)
113 115 self._dispatch_to_kernel(msg)
114 116 return msg['header']['msg_id']
115 117
116 118 def shutdown(self, restart=False):
117 119 # FIXME: What to do here?
118 120 raise NotImplementedError('Cannot shutdown in-process kernel')
119 121
120 122 def kernel_info(self):
121 123 """Request kernel info."""
122 124 msg = self.client.session.msg('kernel_info_request')
123 125 self._dispatch_to_kernel(msg)
124 126 return msg['header']['msg_id']
125 127
126 128 #--------------------------------------------------------------------------
127 129 # Protected interface
128 130 #--------------------------------------------------------------------------
129 131
130 132 def _dispatch_to_kernel(self, msg):
131 133 """ Send a message to the kernel and handle a reply.
132 134 """
133 135 kernel = self.client.kernel
134 136 if kernel is None:
135 137 raise RuntimeError('Cannot send request. No kernel exists.')
136 138
137 139 stream = DummySocket()
138 140 self.client.session.send(stream, msg)
139 141 msg_parts = stream.recv_multipart()
140 142 kernel.dispatch_shell(stream, msg_parts)
141 143
142 144 idents, reply_msg = self.client.session.recv(stream, copy=False)
143 145 self.call_handlers_later(reply_msg)
144 146
145 147
146 148 class InProcessIOPubChannel(InProcessChannel):
147 149 """See `IPython.kernel.channels.IOPubChannel` for docstrings."""
148 150
149 151 def flush(self, timeout=1.0):
150 152 pass
151 153
152 154
153 155 class InProcessStdInChannel(InProcessChannel):
154 156 """See `IPython.kernel.channels.StdInChannel` for docstrings."""
155 157
156 158 proxy_methods = ['input']
157 159
158 160 def input(self, string):
159 161 kernel = self.client.kernel
160 162 if kernel is None:
161 163 raise RuntimeError('Cannot send input reply. No kernel exists.')
162 164 kernel.raw_input_str = string
163 165
164 166
165 167 class InProcessHBChannel(InProcessChannel):
166 168 """See `IPython.kernel.channels.HBChannel` for docstrings."""
167 169
168 170 time_to_dead = 3.0
169 171
170 172 def __init__(self, *args, **kwds):
171 173 super(InProcessHBChannel, self).__init__(*args, **kwds)
172 174 self._pause = True
173 175
174 176 def pause(self):
175 177 self._pause = True
176 178
177 179 def unpause(self):
178 180 self._pause = False
179 181
180 182 def is_beating(self):
181 183 return not self._pause
182 184
183 185 #-----------------------------------------------------------------------------
184 186 # ABC Registration
185 187 #-----------------------------------------------------------------------------
186 188
187 189 ShellChannelABC.register(InProcessShellChannel)
188 190 IOPubChannelABC.register(InProcessIOPubChannel)
189 191 HBChannelABC.register(InProcessHBChannel)
190 192 StdInChannelABC.register(InProcessStdInChannel)
@@ -1,111 +1,105 b''
1 #-------------------------------------------------------------------------------
2 # Copyright (C) 2012 The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING, distributed as part of this software.
6 #-------------------------------------------------------------------------------
1 # Copyright (c) IPython Development Team.
2 # Distributed under the terms of the Modified BSD License.
7 3
8 #-----------------------------------------------------------------------------
9 # Imports
10 #-----------------------------------------------------------------------------
11 4 from __future__ import print_function
12 5
13 # Standard library imports
14 6 import unittest
15 7
16 # Local imports
17 8 from IPython.kernel.inprocess.blocking import BlockingInProcessKernelClient
18 9 from IPython.kernel.inprocess.manager import InProcessKernelManager
19 10
20 11 #-----------------------------------------------------------------------------
21 12 # Test case
22 13 #-----------------------------------------------------------------------------
23 14
24 15 class InProcessKernelManagerTestCase(unittest.TestCase):
25 16
26 17 def test_interface(self):
27 18 """ Does the in-process kernel manager implement the basic KM interface?
28 19 """
29 20 km = InProcessKernelManager()
30 21 self.assert_(not km.has_kernel)
31 22
32 23 km.start_kernel()
33 24 self.assert_(km.has_kernel)
34 25 self.assert_(km.kernel is not None)
35 26
36 27 kc = BlockingInProcessKernelClient(kernel=km.kernel)
37 28 self.assert_(not kc.channels_running)
38 29
39 30 kc.start_channels()
40 31 self.assert_(kc.channels_running)
41 32
42 33 old_kernel = km.kernel
43 34 km.restart_kernel()
44 35 self.assert_(km.kernel is not None)
45 36 self.assertNotEquals(km.kernel, old_kernel)
46 37
47 38 km.shutdown_kernel()
48 39 self.assert_(not km.has_kernel)
49 40
50 41 self.assertRaises(NotImplementedError, km.interrupt_kernel)
51 42 self.assertRaises(NotImplementedError, km.signal_kernel, 9)
52 43
53 44 kc.stop_channels()
54 45 self.assert_(not kc.channels_running)
55 46
56 47 def test_execute(self):
57 48 """ Does executing code in an in-process kernel work?
58 49 """
59 50 km = InProcessKernelManager()
60 51 km.start_kernel()
61 52 kc = BlockingInProcessKernelClient(kernel=km.kernel)
62 53 kc.start_channels()
63 54 kc.execute('foo = 1')
64 55 self.assertEquals(km.kernel.shell.user_ns['foo'], 1)
65 56
66 57 def test_complete(self):
67 58 """ Does requesting completion from an in-process kernel work?
68 59 """
69 60 km = InProcessKernelManager()
70 61 km.start_kernel()
71 62 kc = BlockingInProcessKernelClient(kernel=km.kernel)
72 63 kc.start_channels()
73 64 km.kernel.shell.push({'my_bar': 0, 'my_baz': 1})
74 kc.complete('my_ba', 'my_ba', 5)
65 kc.complete('my_ba', 5)
75 66 msg = kc.get_shell_msg()
76 67 self.assertEqual(msg['header']['msg_type'], 'complete_reply')
77 68 self.assertEqual(sorted(msg['content']['matches']),
78 69 ['my_bar', 'my_baz'])
79 70
80 71 def test_object_info(self):
81 72 """ Does requesting object information from an in-process kernel work?
82 73 """
83 74 km = InProcessKernelManager()
84 75 km.start_kernel()
85 76 kc = BlockingInProcessKernelClient(kernel=km.kernel)
86 77 kc.start_channels()
87 78 km.kernel.shell.user_ns['foo'] = 1
88 79 kc.object_info('foo')
89 80 msg = kc.get_shell_msg()
90 self.assertEquals(msg['header']['msg_type'], 'object_info_reply')
91 self.assertEquals(msg['content']['name'], 'foo')
92 self.assertEquals(msg['content']['type_name'], 'int')
81 self.assertEqual(msg['header']['msg_type'], 'object_info_reply')
82 content = msg['content']
83 assert content['found']
84 self.assertEqual(content['name'], 'foo')
85 text = content['data']['text/plain']
86 self.assertIn('int', text)
93 87
94 88 def test_history(self):
95 89 """ Does requesting history from an in-process kernel work?
96 90 """
97 91 km = InProcessKernelManager()
98 92 km.start_kernel()
99 93 kc = BlockingInProcessKernelClient(kernel=km.kernel)
100 94 kc.start_channels()
101 95 kc.execute('%who')
102 96 kc.history(hist_access_type='tail', n=1)
103 97 msg = kc.shell_channel.get_msgs()[-1]
104 98 self.assertEquals(msg['header']['msg_type'], 'history_reply')
105 99 history = msg['content']['history']
106 100 self.assertEquals(len(history), 1)
107 101 self.assertEquals(history[0][2], '%who')
108 102
109 103
110 104 if __name__ == '__main__':
111 105 unittest.main()
@@ -1,420 +1,399 b''
1 1 """Test suite for our zeromq-based message specification."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import re
7 7 from distutils.version import LooseVersion as V
8 8 from subprocess import PIPE
9 9 try:
10 10 from queue import Empty # Py 3
11 11 except ImportError:
12 12 from Queue import Empty # Py 2
13 13
14 14 import nose.tools as nt
15 15
16 16 from IPython.kernel import KernelManager
17 17
18 18 from IPython.utils.traitlets import (
19 19 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
20 20 )
21 21 from IPython.utils.py3compat import string_types, iteritems
22 22
23 23 from .utils import TIMEOUT, start_global_kernel, flush_channels, execute
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Globals
27 27 #-----------------------------------------------------------------------------
28 28 KC = None
29 29
30 30 def setup():
31 31 global KC
32 32 KC = start_global_kernel()
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Message Spec References
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class Reference(HasTraits):
39 39
40 40 """
41 41 Base class for message spec specification testing.
42 42
43 43 This class is the core of the message specification test. The
44 44 idea is that child classes implement trait attributes for each
45 45 message keys, so that message keys can be tested against these
46 46 traits using :meth:`check` method.
47 47
48 48 """
49 49
50 50 def check(self, d):
51 51 """validate a dict against our traits"""
52 52 for key in self.trait_names():
53 53 nt.assert_in(key, d)
54 54 # FIXME: always allow None, probably not a good idea
55 55 if d[key] is None:
56 56 continue
57 57 try:
58 58 setattr(self, key, d[key])
59 59 except TraitError as e:
60 60 assert False, str(e)
61 61
62 62 class Version(Unicode):
63 63 def validate(self, obj, value):
64 64 min_version = self.default_value
65 65 if V(value) < V(min_version):
66 66 raise TraitError("bad version: %s < %s" % (value, min_version))
67 67
68 68 class RMessage(Reference):
69 69 msg_id = Unicode()
70 70 msg_type = Unicode()
71 71 header = Dict()
72 72 parent_header = Dict()
73 73 content = Dict()
74 74
75 75 def check(self, d):
76 76 super(RMessage, self).check(d)
77 77 RHeader().check(self.header)
78 78 if self.parent_header:
79 79 RHeader().check(self.parent_header)
80 80
81 81 class RHeader(Reference):
82 82 msg_id = Unicode()
83 83 msg_type = Unicode()
84 84 session = Unicode()
85 85 username = Unicode()
86 86 version = Version('5.0')
87 87
88 mime_pat = re.compile(r'\w+/\w+')
89
90 class MimeBundle(Reference):
91 metadata = Dict()
92 data = Dict()
93 def _data_changed(self, name, old, new):
94 for k,v in iteritems(new):
95 assert mime_pat.match(k)
96 nt.assert_is_instance(v, string_types)
97
98 # shell replies
88 99
89 100 class ExecuteReply(Reference):
90 101 execution_count = Integer()
91 102 status = Enum((u'ok', u'error'))
92 103
93 104 def check(self, d):
94 105 Reference.check(self, d)
95 106 if d['status'] == 'ok':
96 107 ExecuteReplyOkay().check(d)
97 108 elif d['status'] == 'error':
98 109 ExecuteReplyError().check(d)
99 110
100 111
101 112 class ExecuteReplyOkay(Reference):
102 113 payload = List(Dict)
103 114 user_expressions = Dict()
104 115
105 116
106 117 class ExecuteReplyError(Reference):
107 118 ename = Unicode()
108 119 evalue = Unicode()
109 120 traceback = List(Unicode)
110 121
111 122
112 class OInfoReply(Reference):
123 class OInfoReply(MimeBundle):
113 124 name = Unicode()
114 125 found = Bool()
115 ismagic = Bool()
116 isalias = Bool()
117 namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive'))
118 type_name = Unicode()
119 string_form = Unicode()
120 base_class = Unicode()
121 length = Integer()
122 file = Unicode()
123 definition = Unicode()
124 argspec = Dict()
125 init_definition = Unicode()
126 docstring = Unicode()
127 init_docstring = Unicode()
128 class_docstring = Unicode()
129 call_def = Unicode()
130 call_docstring = Unicode()
131 source = Unicode()
132
133 def check(self, d):
134 super(OInfoReply, self).check(d)
135 if d['argspec'] is not None:
136 ArgSpec().check(d['argspec'])
137 126
138 127
139 128 class ArgSpec(Reference):
140 129 args = List(Unicode)
141 130 varargs = Unicode()
142 131 varkw = Unicode()
143 132 defaults = List()
144 133
145 134
146 135 class Status(Reference):
147 136 execution_state = Enum((u'busy', u'idle', u'starting'))
148 137
149 138
150 139 class CompleteReply(Reference):
151 140 matches = List(Unicode)
152 141
153 142
154 143 class KernelInfoReply(Reference):
155 144 protocol_version = Version('5.0')
156 145 ipython_version = Version('2.0')
157 146 language_version = Version('2.7')
158 147 language = Unicode()
159 148
160 149
161 150 # IOPub messages
162 151
163 152 class ExecuteInput(Reference):
164 153 code = Unicode()
165 154 execution_count = Integer()
166 155
167 156
168 157 Error = ExecuteReplyError
169 158
170 159
171 160 class Stream(Reference):
172 161 name = Enum((u'stdout', u'stderr'))
173 162 data = Unicode()
174 163
175 164
176 mime_pat = re.compile(r'\w+/\w+')
177
178 class DisplayData(Reference):
165 class DisplayData(MimeBundle):
179 166 source = Unicode()
180 metadata = Dict()
181 data = Dict()
182 def _data_changed(self, name, old, new):
183 for k,v in iteritems(new):
184 assert mime_pat.match(k)
185 nt.assert_is_instance(v, string_types)
186 167
187 168
188 class ExecuteResult(Reference):
169 class ExecuteResult(MimeBundle):
189 170 execution_count = Integer()
190 data = Dict()
191 def _data_changed(self, name, old, new):
192 for k,v in iteritems(new):
193 assert mime_pat.match(k)
194 nt.assert_is_instance(v, string_types)
195 171
196 172
197 173 references = {
198 174 'execute_reply' : ExecuteReply(),
199 175 'object_info_reply' : OInfoReply(),
200 176 'status' : Status(),
201 177 'complete_reply' : CompleteReply(),
202 178 'kernel_info_reply': KernelInfoReply(),
203 179 'execute_input' : ExecuteInput(),
204 180 'execute_result' : ExecuteResult(),
205 181 'error' : Error(),
206 182 'stream' : Stream(),
207 183 'display_data' : DisplayData(),
208 184 'header' : RHeader(),
209 185 }
210 186 """
211 187 Specifications of `content` part of the reply messages.
212 188 """
213 189
214 190
215 191 def validate_message(msg, msg_type=None, parent=None):
216 192 """validate a message
217 193
218 194 This is a generator, and must be iterated through to actually
219 195 trigger each test.
220 196
221 197 If msg_type and/or parent are given, the msg_type and/or parent msg_id
222 198 are compared with the given values.
223 199 """
224 200 RMessage().check(msg)
225 201 if msg_type:
226 202 nt.assert_equal(msg['msg_type'], msg_type)
227 203 if parent:
228 204 nt.assert_equal(msg['parent_header']['msg_id'], parent)
229 205 content = msg['content']
230 206 ref = references[msg['msg_type']]
231 207 ref.check(content)
232 208
233 209
234 210 #-----------------------------------------------------------------------------
235 211 # Tests
236 212 #-----------------------------------------------------------------------------
237 213
238 214 # Shell channel
239 215
240 216 def test_execute():
241 217 flush_channels()
242 218
243 219 msg_id = KC.execute(code='x=1')
244 220 reply = KC.get_shell_msg(timeout=TIMEOUT)
245 221 validate_message(reply, 'execute_reply', msg_id)
246 222
247 223
248 224 def test_execute_silent():
249 225 flush_channels()
250 226 msg_id, reply = execute(code='x=1', silent=True)
251 227
252 228 # flush status=idle
253 229 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
254 230 validate_message(status, 'status', msg_id)
255 231 nt.assert_equal(status['content']['execution_state'], 'idle')
256 232
257 233 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
258 234 count = reply['execution_count']
259 235
260 236 msg_id, reply = execute(code='x=2', silent=True)
261 237
262 238 # flush status=idle
263 239 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
264 240 validate_message(status, 'status', msg_id)
265 241 nt.assert_equal(status['content']['execution_state'], 'idle')
266 242
267 243 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
268 244 count_2 = reply['execution_count']
269 245 nt.assert_equal(count_2, count)
270 246
271 247
272 248 def test_execute_error():
273 249 flush_channels()
274 250
275 251 msg_id, reply = execute(code='1/0')
276 252 nt.assert_equal(reply['status'], 'error')
277 253 nt.assert_equal(reply['ename'], 'ZeroDivisionError')
278 254
279 255 error = KC.iopub_channel.get_msg(timeout=TIMEOUT)
280 256 validate_message(error, 'error', msg_id)
281 257
282 258
283 259 def test_execute_inc():
284 260 """execute request should increment execution_count"""
285 261 flush_channels()
286 262
287 263 msg_id, reply = execute(code='x=1')
288 264 count = reply['execution_count']
289 265
290 266 flush_channels()
291 267
292 268 msg_id, reply = execute(code='x=2')
293 269 count_2 = reply['execution_count']
294 270 nt.assert_equal(count_2, count+1)
295 271
296 272
297 273 def test_user_expressions():
298 274 flush_channels()
299 275
300 276 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
301 277 user_expressions = reply['user_expressions']
302 278 nt.assert_equal(user_expressions, {u'foo': {
303 279 u'status': u'ok',
304 280 u'data': {u'text/plain': u'2'},
305 281 u'metadata': {},
306 282 }})
307 283
308 284
309 285 def test_user_expressions_fail():
310 286 flush_channels()
311 287
312 288 msg_id, reply = execute(code='x=0', user_expressions=dict(foo='nosuchname'))
313 289 user_expressions = reply['user_expressions']
314 290 foo = user_expressions['foo']
315 291 nt.assert_equal(foo['status'], 'error')
316 292 nt.assert_equal(foo['ename'], 'NameError')
317 293
318 294
319 295 def test_oinfo():
320 296 flush_channels()
321 297
322 298 msg_id = KC.object_info('a')
323 299 reply = KC.get_shell_msg(timeout=TIMEOUT)
324 300 validate_message(reply, 'object_info_reply', msg_id)
325 301
326 302
327 303 def test_oinfo_found():
328 304 flush_channels()
329 305
330 306 msg_id, reply = execute(code='a=5')
331 307
332 308 msg_id = KC.object_info('a')
333 309 reply = KC.get_shell_msg(timeout=TIMEOUT)
334 310 validate_message(reply, 'object_info_reply', msg_id)
335 311 content = reply['content']
336 312 assert content['found']
337 argspec = content['argspec']
338 nt.assert_is(argspec, None)
313 nt.assert_equal(content['name'], 'a')
314 text = content['data']['text/plain']
315 nt.assert_in('Type:', text)
316 nt.assert_in('Docstring:', text)
339 317
340 318
341 319 def test_oinfo_detail():
342 320 flush_channels()
343 321
344 322 msg_id, reply = execute(code='ip=get_ipython()')
345 323
346 msg_id = KC.object_info('ip.object_inspect', detail_level=2)
324 msg_id = KC.object_info('ip.object_inspect', cursor_pos=10, detail_level=1)
347 325 reply = KC.get_shell_msg(timeout=TIMEOUT)
348 326 validate_message(reply, 'object_info_reply', msg_id)
349 327 content = reply['content']
350 328 assert content['found']
351 argspec = content['argspec']
352 nt.assert_is_instance(argspec, dict, "expected non-empty argspec dict, got %r" % argspec)
353 nt.assert_equal(argspec['defaults'], [0])
329 nt.assert_equal(content['name'], 'ip.object_inspect')
330 text = content['data']['text/plain']
331 nt.assert_in('Definition:', text)
332 nt.assert_in('Source:', text)
354 333
355 334
356 335 def test_oinfo_not_found():
357 336 flush_channels()
358 337
359 338 msg_id = KC.object_info('dne')
360 339 reply = KC.get_shell_msg(timeout=TIMEOUT)
361 340 validate_message(reply, 'object_info_reply', msg_id)
362 341 content = reply['content']
363 342 nt.assert_false(content['found'])
364 343
365 344
366 345 def test_complete():
367 346 flush_channels()
368 347
369 348 msg_id, reply = execute(code="alpha = albert = 5")
370 349
371 msg_id = KC.complete('al', 'al', 2)
350 msg_id = KC.complete('al', 2)
372 351 reply = KC.get_shell_msg(timeout=TIMEOUT)
373 352 validate_message(reply, 'complete_reply', msg_id)
374 353 matches = reply['content']['matches']
375 354 for name in ('alpha', 'albert'):
376 355 nt.assert_in(name, matches)
377 356
378 357
379 358 def test_kernel_info_request():
380 359 flush_channels()
381 360
382 361 msg_id = KC.kernel_info()
383 362 reply = KC.get_shell_msg(timeout=TIMEOUT)
384 363 validate_message(reply, 'kernel_info_reply', msg_id)
385 364
386 365
387 366 def test_single_payload():
388 367 flush_channels()
389 368 msg_id, reply = execute(code="for i in range(3):\n"+
390 369 " x=range?\n")
391 370 payload = reply['payload']
392 371 next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"]
393 372 nt.assert_equal(len(next_input_pls), 1)
394 373
395 374
396 375 # IOPub channel
397 376
398 377
399 378 def test_stream():
400 379 flush_channels()
401 380
402 381 msg_id, reply = execute("print('hi')")
403 382
404 383 stdout = KC.iopub_channel.get_msg(timeout=TIMEOUT)
405 384 validate_message(stdout, 'stream', msg_id)
406 385 content = stdout['content']
407 386 nt.assert_equal(content['name'], u'stdout')
408 387 nt.assert_equal(content['data'], u'hi\n')
409 388
410 389
411 390 def test_display_data():
412 391 flush_channels()
413 392
414 393 msg_id, reply = execute("from IPython.core.display import display; display(1)")
415 394
416 395 display = KC.iopub_channel.get_msg(timeout=TIMEOUT)
417 396 validate_message(display, 'display_data', parent=msg_id)
418 397 data = display['content']['data']
419 398 nt.assert_equal(data['text/plain'], u'1')
420 399
@@ -1,846 +1,850 b''
1 #!/usr/bin/env python
2 1 """An interactive kernel that talks to frontends over 0MQ."""
3 2
4 3 # Copyright (c) IPython Development Team.
5 4 # Distributed under the terms of the Modified BSD License.
6 5
7 6 from __future__ import print_function
8 7
9 8 import getpass
10 9 import sys
11 10 import time
12 11 import traceback
13 12 import logging
14 13 import uuid
15 14
16 15 from datetime import datetime
17 16 from signal import (
18 17 signal, default_int_handler, SIGINT
19 18 )
20 19
21 20 import zmq
22 21 from zmq.eventloop import ioloop
23 22 from zmq.eventloop.zmqstream import ZMQStream
24 23
25 24 from IPython.config.configurable import Configurable
26 25 from IPython.core.error import StdinNotImplementedError
27 26 from IPython.core import release
28 27 from IPython.utils import py3compat
29 28 from IPython.utils.py3compat import builtin_mod, unicode_type, string_types
30 29 from IPython.utils.jsonutil import json_clean
30 from IPython.utils.tokenutil import token_at_cursor
31 31 from IPython.utils.traitlets import (
32 32 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
33 33 Type, Bool,
34 34 )
35 35
36 36 from .serialize import serialize_object, unpack_apply_message
37 37 from .session import Session
38 38 from .zmqshell import ZMQInteractiveShell
39 39
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Main kernel class
43 43 #-----------------------------------------------------------------------------
44 44
45 45 protocol_version = release.kernel_protocol_version
46 46 ipython_version = release.version
47 47 language_version = sys.version.split()[0]
48 48
49 49
50 50 class Kernel(Configurable):
51 51
52 52 #---------------------------------------------------------------------------
53 53 # Kernel interface
54 54 #---------------------------------------------------------------------------
55 55
56 56 # attribute to override with a GUI
57 57 eventloop = Any(None)
58 58 def _eventloop_changed(self, name, old, new):
59 59 """schedule call to eventloop from IOLoop"""
60 60 loop = ioloop.IOLoop.instance()
61 61 loop.add_callback(self.enter_eventloop)
62 62
63 63 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
64 64 shell_class = Type(ZMQInteractiveShell)
65 65
66 66 session = Instance(Session)
67 67 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
68 68 shell_streams = List()
69 69 control_stream = Instance(ZMQStream)
70 70 iopub_socket = Instance(zmq.Socket)
71 71 stdin_socket = Instance(zmq.Socket)
72 72 log = Instance(logging.Logger)
73 73
74 74 user_module = Any()
75 75 def _user_module_changed(self, name, old, new):
76 76 if self.shell is not None:
77 77 self.shell.user_module = new
78 78
79 79 user_ns = Instance(dict, args=None, allow_none=True)
80 80 def _user_ns_changed(self, name, old, new):
81 81 if self.shell is not None:
82 82 self.shell.user_ns = new
83 83 self.shell.init_user_ns()
84 84
85 85 # identities:
86 86 int_id = Integer(-1)
87 87 ident = Unicode()
88 88
89 89 def _ident_default(self):
90 90 return unicode_type(uuid.uuid4())
91 91
92 92 # Private interface
93 93
94 94 _darwin_app_nap = Bool(True, config=True,
95 95 help="""Whether to use appnope for compatiblity with OS X App Nap.
96 96
97 97 Only affects OS X >= 10.9.
98 98 """
99 99 )
100 100
101 101 # track associations with current request
102 102 _allow_stdin = Bool(False)
103 103 _parent_header = Dict()
104 104 _parent_ident = Any(b'')
105 105 # Time to sleep after flushing the stdout/err buffers in each execute
106 106 # cycle. While this introduces a hard limit on the minimal latency of the
107 107 # execute cycle, it helps prevent output synchronization problems for
108 108 # clients.
109 109 # Units are in seconds. The minimum zmq latency on local host is probably
110 110 # ~150 microseconds, set this to 500us for now. We may need to increase it
111 111 # a little if it's not enough after more interactive testing.
112 112 _execute_sleep = Float(0.0005, config=True)
113 113
114 114 # Frequency of the kernel's event loop.
115 115 # Units are in seconds, kernel subclasses for GUI toolkits may need to
116 116 # adapt to milliseconds.
117 117 _poll_interval = Float(0.05, config=True)
118 118
119 119 # If the shutdown was requested over the network, we leave here the
120 120 # necessary reply message so it can be sent by our registered atexit
121 121 # handler. This ensures that the reply is only sent to clients truly at
122 122 # the end of our shutdown process (which happens after the underlying
123 123 # IPython shell's own shutdown).
124 124 _shutdown_message = None
125 125
126 126 # This is a dict of port number that the kernel is listening on. It is set
127 127 # by record_ports and used by connect_request.
128 128 _recorded_ports = Dict()
129 129
130 130 # A reference to the Python builtin 'raw_input' function.
131 131 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
132 132 _sys_raw_input = Any()
133 133 _sys_eval_input = Any()
134 134
135 135 # set of aborted msg_ids
136 136 aborted = Set()
137 137
138 138
139 139 def __init__(self, **kwargs):
140 140 super(Kernel, self).__init__(**kwargs)
141 141
142 142 # Initialize the InteractiveShell subclass
143 143 self.shell = self.shell_class.instance(parent=self,
144 144 profile_dir = self.profile_dir,
145 145 user_module = self.user_module,
146 146 user_ns = self.user_ns,
147 147 kernel = self,
148 148 )
149 149 self.shell.displayhook.session = self.session
150 150 self.shell.displayhook.pub_socket = self.iopub_socket
151 151 self.shell.displayhook.topic = self._topic('execute_result')
152 152 self.shell.display_pub.session = self.session
153 153 self.shell.display_pub.pub_socket = self.iopub_socket
154 154 self.shell.data_pub.session = self.session
155 155 self.shell.data_pub.pub_socket = self.iopub_socket
156 156
157 157 # TMP - hack while developing
158 158 self.shell._reply_content = None
159 159
160 160 # Build dict of handlers for message types
161 161 msg_types = [ 'execute_request', 'complete_request',
162 162 'object_info_request', 'history_request',
163 163 'kernel_info_request',
164 164 'connect_request', 'shutdown_request',
165 165 'apply_request',
166 166 ]
167 167 self.shell_handlers = {}
168 168 for msg_type in msg_types:
169 169 self.shell_handlers[msg_type] = getattr(self, msg_type)
170 170
171 171 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
172 172 comm_manager = self.shell.comm_manager
173 173 for msg_type in comm_msg_types:
174 174 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
175 175
176 176 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
177 177 self.control_handlers = {}
178 178 for msg_type in control_msg_types:
179 179 self.control_handlers[msg_type] = getattr(self, msg_type)
180 180
181 181
182 182 def dispatch_control(self, msg):
183 183 """dispatch control requests"""
184 184 idents,msg = self.session.feed_identities(msg, copy=False)
185 185 try:
186 186 msg = self.session.unserialize(msg, content=True, copy=False)
187 187 except:
188 188 self.log.error("Invalid Control Message", exc_info=True)
189 189 return
190 190
191 191 self.log.debug("Control received: %s", msg)
192 192
193 193 header = msg['header']
194 194 msg_id = header['msg_id']
195 195 msg_type = header['msg_type']
196 196
197 197 handler = self.control_handlers.get(msg_type, None)
198 198 if handler is None:
199 199 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
200 200 else:
201 201 try:
202 202 handler(self.control_stream, idents, msg)
203 203 except Exception:
204 204 self.log.error("Exception in control handler:", exc_info=True)
205 205
206 206 def dispatch_shell(self, stream, msg):
207 207 """dispatch shell requests"""
208 208 # flush control requests first
209 209 if self.control_stream:
210 210 self.control_stream.flush()
211 211
212 212 idents,msg = self.session.feed_identities(msg, copy=False)
213 213 try:
214 214 msg = self.session.unserialize(msg, content=True, copy=False)
215 215 except:
216 216 self.log.error("Invalid Message", exc_info=True)
217 217 return
218 218
219 219 header = msg['header']
220 220 msg_id = header['msg_id']
221 221 msg_type = msg['header']['msg_type']
222 222
223 223 # Print some info about this message and leave a '--->' marker, so it's
224 224 # easier to trace visually the message chain when debugging. Each
225 225 # handler prints its message at the end.
226 226 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
227 227 self.log.debug(' Content: %s\n --->\n ', msg['content'])
228 228
229 229 if msg_id in self.aborted:
230 230 self.aborted.remove(msg_id)
231 231 # is it safe to assume a msg_id will not be resubmitted?
232 232 reply_type = msg_type.split('_')[0] + '_reply'
233 233 status = {'status' : 'aborted'}
234 234 md = {'engine' : self.ident}
235 235 md.update(status)
236 236 reply_msg = self.session.send(stream, reply_type, metadata=md,
237 237 content=status, parent=msg, ident=idents)
238 238 return
239 239
240 240 handler = self.shell_handlers.get(msg_type, None)
241 241 if handler is None:
242 242 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
243 243 else:
244 244 # ensure default_int_handler during handler call
245 245 sig = signal(SIGINT, default_int_handler)
246 self.log.debug("%s: %s", msg_type, msg)
246 247 try:
247 248 handler(stream, idents, msg)
248 249 except Exception:
249 250 self.log.error("Exception in message handler:", exc_info=True)
250 251 finally:
251 252 signal(SIGINT, sig)
252 253
253 254 def enter_eventloop(self):
254 255 """enter eventloop"""
255 256 self.log.info("entering eventloop %s", self.eventloop)
256 257 for stream in self.shell_streams:
257 258 # flush any pending replies,
258 259 # which may be skipped by entering the eventloop
259 260 stream.flush(zmq.POLLOUT)
260 261 # restore default_int_handler
261 262 signal(SIGINT, default_int_handler)
262 263 while self.eventloop is not None:
263 264 try:
264 265 self.eventloop(self)
265 266 except KeyboardInterrupt:
266 267 # Ctrl-C shouldn't crash the kernel
267 268 self.log.error("KeyboardInterrupt caught in kernel")
268 269 continue
269 270 else:
270 271 # eventloop exited cleanly, this means we should stop (right?)
271 272 self.eventloop = None
272 273 break
273 274 self.log.info("exiting eventloop")
274 275
275 276 def start(self):
276 277 """register dispatchers for streams"""
277 278 self.shell.exit_now = False
278 279 if self.control_stream:
279 280 self.control_stream.on_recv(self.dispatch_control, copy=False)
280 281
281 282 def make_dispatcher(stream):
282 283 def dispatcher(msg):
283 284 return self.dispatch_shell(stream, msg)
284 285 return dispatcher
285 286
286 287 for s in self.shell_streams:
287 288 s.on_recv(make_dispatcher(s), copy=False)
288 289
289 290 # publish idle status
290 291 self._publish_status('starting')
291 292
292 293 def do_one_iteration(self):
293 294 """step eventloop just once"""
294 295 if self.control_stream:
295 296 self.control_stream.flush()
296 297 for stream in self.shell_streams:
297 298 # handle at most one request per iteration
298 299 stream.flush(zmq.POLLIN, 1)
299 300 stream.flush(zmq.POLLOUT)
300 301
301 302
302 303 def record_ports(self, ports):
303 304 """Record the ports that this kernel is using.
304 305
305 306 The creator of the Kernel instance must call this methods if they
306 307 want the :meth:`connect_request` method to return the port numbers.
307 308 """
308 309 self._recorded_ports = ports
309 310
310 311 #---------------------------------------------------------------------------
311 312 # Kernel request handlers
312 313 #---------------------------------------------------------------------------
313 314
314 315 def _make_metadata(self, other=None):
315 316 """init metadata dict, for execute/apply_reply"""
316 317 new_md = {
317 318 'dependencies_met' : True,
318 319 'engine' : self.ident,
319 320 'started': datetime.now(),
320 321 }
321 322 if other:
322 323 new_md.update(other)
323 324 return new_md
324 325
325 326 def _publish_execute_input(self, code, parent, execution_count):
326 327 """Publish the code request on the iopub stream."""
327 328
328 329 self.session.send(self.iopub_socket, u'execute_input',
329 330 {u'code':code, u'execution_count': execution_count},
330 331 parent=parent, ident=self._topic('execute_input')
331 332 )
332 333
333 334 def _publish_status(self, status, parent=None):
334 335 """send status (busy/idle) on IOPub"""
335 336 self.session.send(self.iopub_socket,
336 337 u'status',
337 338 {u'execution_state': status},
338 339 parent=parent,
339 340 ident=self._topic('status'),
340 341 )
341 342
342 343 def _forward_input(self, allow_stdin=False):
343 344 """Forward raw_input and getpass to the current frontend.
344 345
345 346 via input_request
346 347 """
347 348 self._allow_stdin = allow_stdin
348 349
349 350 if py3compat.PY3:
350 351 self._sys_raw_input = builtin_mod.input
351 352 builtin_mod.input = self.raw_input
352 353 else:
353 354 self._sys_raw_input = builtin_mod.raw_input
354 355 self._sys_eval_input = builtin_mod.input
355 356 builtin_mod.raw_input = self.raw_input
356 357 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
357 358 self._save_getpass = getpass.getpass
358 359 getpass.getpass = self.getpass
359 360
360 361 def _restore_input(self):
361 362 """Restore raw_input, getpass"""
362 363 if py3compat.PY3:
363 364 builtin_mod.input = self._sys_raw_input
364 365 else:
365 366 builtin_mod.raw_input = self._sys_raw_input
366 367 builtin_mod.input = self._sys_eval_input
367 368
368 369 getpass.getpass = self._save_getpass
369 370
370 371 def set_parent(self, ident, parent):
371 372 """Record the parent state
372 373
373 374 For associating side effects with their requests.
374 375 """
375 376 self._parent_ident = ident
376 377 self._parent_header = parent
377 378 self.shell.set_parent(parent)
378 379
379 380 def execute_request(self, stream, ident, parent):
380 381 """handle an execute_request"""
381 382
382 383 self._publish_status(u'busy', parent)
383 384
384 385 try:
385 386 content = parent[u'content']
386 387 code = py3compat.cast_unicode_py2(content[u'code'])
387 388 silent = content[u'silent']
388 389 store_history = content.get(u'store_history', not silent)
389 390 except:
390 391 self.log.error("Got bad msg: ")
391 392 self.log.error("%s", parent)
392 393 return
393 394
394 395 md = self._make_metadata(parent['metadata'])
395 396
396 397 shell = self.shell # we'll need this a lot here
397 398
398 399 self._forward_input(content.get('allow_stdin', False))
399 400 # Set the parent message of the display hook and out streams.
400 401 self.set_parent(ident, parent)
401 402
402 403 # Re-broadcast our input for the benefit of listening clients, and
403 404 # start computing output
404 405 if not silent:
405 406 self._publish_execute_input(code, parent, shell.execution_count)
406 407
407 408 reply_content = {}
408 409 # FIXME: the shell calls the exception handler itself.
409 410 shell._reply_content = None
410 411 try:
411 412 shell.run_cell(code, store_history=store_history, silent=silent)
412 413 except:
413 414 status = u'error'
414 415 # FIXME: this code right now isn't being used yet by default,
415 416 # because the run_cell() call above directly fires off exception
416 417 # reporting. This code, therefore, is only active in the scenario
417 418 # where runlines itself has an unhandled exception. We need to
418 419 # uniformize this, for all exception construction to come from a
419 420 # single location in the codbase.
420 421 etype, evalue, tb = sys.exc_info()
421 422 tb_list = traceback.format_exception(etype, evalue, tb)
422 423 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
423 424 else:
424 425 status = u'ok'
425 426 finally:
426 427 self._restore_input()
427 428
428 429 reply_content[u'status'] = status
429 430
430 431 # Return the execution counter so clients can display prompts
431 432 reply_content['execution_count'] = shell.execution_count - 1
432 433
433 434 # FIXME - fish exception info out of shell, possibly left there by
434 435 # runlines. We'll need to clean up this logic later.
435 436 if shell._reply_content is not None:
436 437 reply_content.update(shell._reply_content)
437 438 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
438 439 reply_content['engine_info'] = e_info
439 440 # reset after use
440 441 shell._reply_content = None
441 442
442 443 if 'traceback' in reply_content:
443 444 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
444 445
445 446
446 447 # At this point, we can tell whether the main code execution succeeded
447 448 # or not. If it did, we proceed to evaluate user_expressions
448 449 if reply_content['status'] == 'ok':
449 450 reply_content[u'user_expressions'] = \
450 451 shell.user_expressions(content.get(u'user_expressions', {}))
451 452 else:
452 453 # If there was an error, don't even try to compute expressions
453 454 reply_content[u'user_expressions'] = {}
454 455
455 456 # Payloads should be retrieved regardless of outcome, so we can both
456 457 # recover partial output (that could have been generated early in a
457 458 # block, before an error) and clear the payload system always.
458 459 reply_content[u'payload'] = shell.payload_manager.read_payload()
459 460 # Be agressive about clearing the payload because we don't want
460 461 # it to sit in memory until the next execute_request comes in.
461 462 shell.payload_manager.clear_payload()
462 463
463 464 # Flush output before sending the reply.
464 465 sys.stdout.flush()
465 466 sys.stderr.flush()
466 467 # FIXME: on rare occasions, the flush doesn't seem to make it to the
467 468 # clients... This seems to mitigate the problem, but we definitely need
468 469 # to better understand what's going on.
469 470 if self._execute_sleep:
470 471 time.sleep(self._execute_sleep)
471 472
472 473 # Send the reply.
473 474 reply_content = json_clean(reply_content)
474 475
475 476 md['status'] = reply_content['status']
476 477 if reply_content['status'] == 'error' and \
477 478 reply_content['ename'] == 'UnmetDependency':
478 479 md['dependencies_met'] = False
479 480
480 481 reply_msg = self.session.send(stream, u'execute_reply',
481 482 reply_content, parent, metadata=md,
482 483 ident=ident)
483 484
484 485 self.log.debug("%s", reply_msg)
485 486
486 487 if not silent and reply_msg['content']['status'] == u'error':
487 488 self._abort_queues()
488 489
489 490 self._publish_status(u'idle', parent)
490 491
491 492 def complete_request(self, stream, ident, parent):
492 txt, matches = self._complete(parent)
493 content = parent['content']
494 code = content['code']
495 cursor_pos = content['cursor_pos']
496
497 txt, matches = self.shell.complete('', code, cursor_pos)
493 498 matches = {'matches' : matches,
494 499 'matched_text' : txt,
495 500 'status' : 'ok'}
496 501 matches = json_clean(matches)
497 502 completion_msg = self.session.send(stream, 'complete_reply',
498 503 matches, parent, ident)
499 504 self.log.debug("%s", completion_msg)
500 505
501 506 def object_info_request(self, stream, ident, parent):
502 507 content = parent['content']
503 object_info = self.shell.object_inspect(content['oname'],
504 detail_level = content.get('detail_level', 0)
505 )
508
509 name = token_at_cursor(content['code'], content['cursor_pos'])
510 info = self.shell.object_inspect(name)
511
512 reply_content = {'status' : 'ok'}
513 reply_content['data'] = data = {}
514 reply_content['metadata'] = {}
515 reply_content['name'] = name
516 reply_content['found'] = info['found']
517 if info['found']:
518 info_text = self.shell.object_inspect_text(
519 name,
520 detail_level=content.get('detail_level', 0),
521 )
522 reply_content['data']['text/plain'] = info_text
506 523 # Before we send this object over, we scrub it for JSON usage
507 oinfo = json_clean(object_info)
524 reply_content = json_clean(reply_content)
508 525 msg = self.session.send(stream, 'object_info_reply',
509 oinfo, parent, ident)
526 reply_content, parent, ident)
510 527 self.log.debug("%s", msg)
511 528
512 529 def history_request(self, stream, ident, parent):
513 530 # We need to pull these out, as passing **kwargs doesn't work with
514 531 # unicode keys before Python 2.6.5.
515 532 hist_access_type = parent['content']['hist_access_type']
516 533 raw = parent['content']['raw']
517 534 output = parent['content']['output']
518 535 if hist_access_type == 'tail':
519 536 n = parent['content']['n']
520 537 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
521 538 include_latest=True)
522 539
523 540 elif hist_access_type == 'range':
524 541 session = parent['content']['session']
525 542 start = parent['content']['start']
526 543 stop = parent['content']['stop']
527 544 hist = self.shell.history_manager.get_range(session, start, stop,
528 545 raw=raw, output=output)
529 546
530 547 elif hist_access_type == 'search':
531 548 n = parent['content'].get('n')
532 549 unique = parent['content'].get('unique', False)
533 550 pattern = parent['content']['pattern']
534 551 hist = self.shell.history_manager.search(
535 552 pattern, raw=raw, output=output, n=n, unique=unique)
536 553
537 554 else:
538 555 hist = []
539 556 hist = list(hist)
540 557 content = {'history' : hist}
541 558 content = json_clean(content)
542 559 msg = self.session.send(stream, 'history_reply',
543 560 content, parent, ident)
544 561 self.log.debug("Sending history reply with %i entries", len(hist))
545 562
546 563 def connect_request(self, stream, ident, parent):
547 564 if self._recorded_ports is not None:
548 565 content = self._recorded_ports.copy()
549 566 else:
550 567 content = {}
551 568 msg = self.session.send(stream, 'connect_reply',
552 569 content, parent, ident)
553 570 self.log.debug("%s", msg)
554 571
555 572 def kernel_info_request(self, stream, ident, parent):
556 573 vinfo = {
557 574 'protocol_version': protocol_version,
558 575 'ipython_version': ipython_version,
559 576 'language_version': language_version,
560 577 'language': 'python',
561 578 }
562 579 msg = self.session.send(stream, 'kernel_info_reply',
563 580 vinfo, parent, ident)
564 581 self.log.debug("%s", msg)
565 582
566 583 def shutdown_request(self, stream, ident, parent):
567 584 self.shell.exit_now = True
568 585 content = dict(status='ok')
569 586 content.update(parent['content'])
570 587 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
571 588 # same content, but different msg_id for broadcasting on IOPub
572 589 self._shutdown_message = self.session.msg(u'shutdown_reply',
573 590 content, parent
574 591 )
575 592
576 593 self._at_shutdown()
577 594 # call sys.exit after a short delay
578 595 loop = ioloop.IOLoop.instance()
579 596 loop.add_timeout(time.time()+0.1, loop.stop)
580 597
581 598 #---------------------------------------------------------------------------
582 599 # Engine methods
583 600 #---------------------------------------------------------------------------
584 601
585 602 def apply_request(self, stream, ident, parent):
586 603 try:
587 604 content = parent[u'content']
588 605 bufs = parent[u'buffers']
589 606 msg_id = parent['header']['msg_id']
590 607 except:
591 608 self.log.error("Got bad msg: %s", parent, exc_info=True)
592 609 return
593 610
594 611 self._publish_status(u'busy', parent)
595 612
596 613 # Set the parent message of the display hook and out streams.
597 614 shell = self.shell
598 615 shell.set_parent(parent)
599 616
600 617 # execute_input_msg = self.session.msg(u'execute_input',{u'code':code}, parent=parent)
601 618 # self.iopub_socket.send(execute_input_msg)
602 619 # self.session.send(self.iopub_socket, u'execute_input', {u'code':code},parent=parent)
603 620 md = self._make_metadata(parent['metadata'])
604 621 try:
605 622 working = shell.user_ns
606 623
607 624 prefix = "_"+str(msg_id).replace("-","")+"_"
608 625
609 626 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
610 627
611 628 fname = getattr(f, '__name__', 'f')
612 629
613 630 fname = prefix+"f"
614 631 argname = prefix+"args"
615 632 kwargname = prefix+"kwargs"
616 633 resultname = prefix+"result"
617 634
618 635 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
619 636 # print ns
620 637 working.update(ns)
621 638 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
622 639 try:
623 640 exec(code, shell.user_global_ns, shell.user_ns)
624 641 result = working.get(resultname)
625 642 finally:
626 643 for key in ns:
627 644 working.pop(key)
628 645
629 646 result_buf = serialize_object(result,
630 647 buffer_threshold=self.session.buffer_threshold,
631 648 item_threshold=self.session.item_threshold,
632 649 )
633 650
634 651 except:
635 652 # invoke IPython traceback formatting
636 653 shell.showtraceback()
637 654 # FIXME - fish exception info out of shell, possibly left there by
638 655 # run_code. We'll need to clean up this logic later.
639 656 reply_content = {}
640 657 if shell._reply_content is not None:
641 658 reply_content.update(shell._reply_content)
642 659 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
643 660 reply_content['engine_info'] = e_info
644 661 # reset after use
645 662 shell._reply_content = None
646 663
647 664 self.session.send(self.iopub_socket, u'error', reply_content, parent=parent,
648 665 ident=self._topic('error'))
649 666 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
650 667 result_buf = []
651 668
652 669 if reply_content['ename'] == 'UnmetDependency':
653 670 md['dependencies_met'] = False
654 671 else:
655 672 reply_content = {'status' : 'ok'}
656 673
657 674 # put 'ok'/'error' status in header, for scheduler introspection:
658 675 md['status'] = reply_content['status']
659 676
660 677 # flush i/o
661 678 sys.stdout.flush()
662 679 sys.stderr.flush()
663 680
664 681 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
665 682 parent=parent, ident=ident,buffers=result_buf, metadata=md)
666 683
667 684 self._publish_status(u'idle', parent)
668 685
669 686 #---------------------------------------------------------------------------
670 687 # Control messages
671 688 #---------------------------------------------------------------------------
672 689
673 690 def abort_request(self, stream, ident, parent):
674 691 """abort a specifig msg by id"""
675 692 msg_ids = parent['content'].get('msg_ids', None)
676 693 if isinstance(msg_ids, string_types):
677 694 msg_ids = [msg_ids]
678 695 if not msg_ids:
679 696 self.abort_queues()
680 697 for mid in msg_ids:
681 698 self.aborted.add(str(mid))
682 699
683 700 content = dict(status='ok')
684 701 reply_msg = self.session.send(stream, 'abort_reply', content=content,
685 702 parent=parent, ident=ident)
686 703 self.log.debug("%s", reply_msg)
687 704
688 705 def clear_request(self, stream, idents, parent):
689 706 """Clear our namespace."""
690 707 self.shell.reset(False)
691 708 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
692 709 content = dict(status='ok'))
693 710
694 711
695 712 #---------------------------------------------------------------------------
696 713 # Protected interface
697 714 #---------------------------------------------------------------------------
698 715
699 716 def _wrap_exception(self, method=None):
700 717 # import here, because _wrap_exception is only used in parallel,
701 718 # and parallel has higher min pyzmq version
702 719 from IPython.parallel.error import wrap_exception
703 720 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
704 721 content = wrap_exception(e_info)
705 722 return content
706 723
707 724 def _topic(self, topic):
708 725 """prefixed topic for IOPub messages"""
709 726 if self.int_id >= 0:
710 727 base = "engine.%i" % self.int_id
711 728 else:
712 729 base = "kernel.%s" % self.ident
713 730
714 731 return py3compat.cast_bytes("%s.%s" % (base, topic))
715 732
716 733 def _abort_queues(self):
717 734 for stream in self.shell_streams:
718 735 if stream:
719 736 self._abort_queue(stream)
720 737
721 738 def _abort_queue(self, stream):
722 739 poller = zmq.Poller()
723 740 poller.register(stream.socket, zmq.POLLIN)
724 741 while True:
725 742 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
726 743 if msg is None:
727 744 return
728 745
729 746 self.log.info("Aborting:")
730 747 self.log.info("%s", msg)
731 748 msg_type = msg['header']['msg_type']
732 749 reply_type = msg_type.split('_')[0] + '_reply'
733 750
734 751 status = {'status' : 'aborted'}
735 752 md = {'engine' : self.ident}
736 753 md.update(status)
737 754 reply_msg = self.session.send(stream, reply_type, metadata=md,
738 755 content=status, parent=msg, ident=idents)
739 756 self.log.debug("%s", reply_msg)
740 757 # We need to wait a bit for requests to come in. This can probably
741 758 # be set shorter for true asynchronous clients.
742 759 poller.poll(50)
743 760
744 761
745 762 def _no_raw_input(self):
746 763 """Raise StdinNotImplentedError if active frontend doesn't support
747 764 stdin."""
748 765 raise StdinNotImplementedError("raw_input was called, but this "
749 766 "frontend does not support stdin.")
750 767
751 768 def getpass(self, prompt=''):
752 769 """Forward getpass to frontends
753 770
754 771 Raises
755 772 ------
756 773 StdinNotImplentedError if active frontend doesn't support stdin.
757 774 """
758 775 if not self._allow_stdin:
759 776 raise StdinNotImplementedError(
760 777 "getpass was called, but this frontend does not support input requests."
761 778 )
762 779 return self._input_request(prompt,
763 780 self._parent_ident,
764 781 self._parent_header,
765 782 password=True,
766 783 )
767 784
768 785 def raw_input(self, prompt=''):
769 786 """Forward raw_input to frontends
770 787
771 788 Raises
772 789 ------
773 790 StdinNotImplentedError if active frontend doesn't support stdin.
774 791 """
775 792 if not self._allow_stdin:
776 793 raise StdinNotImplementedError(
777 794 "raw_input was called, but this frontend does not support input requests."
778 795 )
779 796 return self._input_request(prompt,
780 797 self._parent_ident,
781 798 self._parent_header,
782 799 password=False,
783 800 )
784 801
785 802 def _input_request(self, prompt, ident, parent, password=False):
786 803 # Flush output before making the request.
787 804 sys.stderr.flush()
788 805 sys.stdout.flush()
789 806 # flush the stdin socket, to purge stale replies
790 807 while True:
791 808 try:
792 809 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
793 810 except zmq.ZMQError as e:
794 811 if e.errno == zmq.EAGAIN:
795 812 break
796 813 else:
797 814 raise
798 815
799 816 # Send the input request.
800 817 content = json_clean(dict(prompt=prompt, password=password))
801 818 self.session.send(self.stdin_socket, u'input_request', content, parent,
802 819 ident=ident)
803 820
804 821 # Await a response.
805 822 while True:
806 823 try:
807 824 ident, reply = self.session.recv(self.stdin_socket, 0)
808 825 except Exception:
809 826 self.log.warn("Invalid Message:", exc_info=True)
810 827 except KeyboardInterrupt:
811 828 # re-raise KeyboardInterrupt, to truncate traceback
812 829 raise KeyboardInterrupt
813 830 else:
814 831 break
815 832 try:
816 833 value = py3compat.unicode_to_str(reply['content']['value'])
817 834 except:
818 835 self.log.error("Bad input_reply: %s", parent)
819 836 value = ''
820 837 if value == '\x04':
821 838 # EOF
822 839 raise EOFError
823 840 return value
824 841
825 def _complete(self, msg):
826 c = msg['content']
827 try:
828 cpos = int(c['cursor_pos'])
829 except:
830 # If we don't get something that we can convert to an integer, at
831 # least attempt the completion guessing the cursor is at the end of
832 # the text, if there's any, and otherwise of the line
833 cpos = len(c['text'])
834 if cpos==0:
835 cpos = len(c['line'])
836 return self.shell.complete(c['text'], c['line'], cpos)
837
838 842 def _at_shutdown(self):
839 843 """Actions taken at shutdown by the kernel, called by python's atexit.
840 844 """
841 845 # io.rprint("Kernel at_shutdown") # dbg
842 846 if self._shutdown_message is not None:
843 847 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
844 848 self.log.debug("%s", self._shutdown_message)
845 849 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
846 850
@@ -1,194 +1,198 b''
1 1 """test IPython.embed_kernel()"""
2 2
3 3 #-------------------------------------------------------------------------------
4 4 # Copyright (C) 2012 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-------------------------------------------------------------------------------
9 9
10 10 #-------------------------------------------------------------------------------
11 11 # Imports
12 12 #-------------------------------------------------------------------------------
13 13
14 14 import os
15 15 import shutil
16 16 import sys
17 17 import tempfile
18 18 import time
19 19
20 20 from contextlib import contextmanager
21 21 from subprocess import Popen, PIPE
22 22
23 23 import nose.tools as nt
24 24
25 25 from IPython.kernel import BlockingKernelClient
26 26 from IPython.utils import path, py3compat
27 27 from IPython.utils.py3compat import unicode_type
28 28
29 29 #-------------------------------------------------------------------------------
30 30 # Tests
31 31 #-------------------------------------------------------------------------------
32 32
33 33 SETUP_TIMEOUT = 60
34 34 TIMEOUT = 15
35 35
36 36 def setup():
37 37 """setup temporary IPYTHONDIR for tests"""
38 38 global IPYTHONDIR
39 39 global env
40 40 global save_get_ipython_dir
41 41
42 42 IPYTHONDIR = tempfile.mkdtemp()
43 43
44 44 env = os.environ.copy()
45 45 env["IPYTHONDIR"] = IPYTHONDIR
46 46
47 47 save_get_ipython_dir = path.get_ipython_dir
48 48 path.get_ipython_dir = lambda : IPYTHONDIR
49 49
50 50
51 51 def teardown():
52 52 path.get_ipython_dir = save_get_ipython_dir
53 53
54 54 try:
55 55 shutil.rmtree(IPYTHONDIR)
56 56 except (OSError, IOError):
57 57 # no such file
58 58 pass
59 59
60 60
61 61 @contextmanager
62 62 def setup_kernel(cmd):
63 63 """start an embedded kernel in a subprocess, and wait for it to be ready
64 64
65 65 Returns
66 66 -------
67 67 kernel_manager: connected KernelManager instance
68 68 """
69 69 kernel = Popen([sys.executable, '-c', cmd], stdout=PIPE, stderr=PIPE, env=env)
70 70 connection_file = os.path.join(IPYTHONDIR,
71 71 'profile_default',
72 72 'security',
73 73 'kernel-%i.json' % kernel.pid
74 74 )
75 75 # wait for connection file to exist, timeout after 5s
76 76 tic = time.time()
77 77 while not os.path.exists(connection_file) \
78 78 and kernel.poll() is None \
79 79 and time.time() < tic + SETUP_TIMEOUT:
80 80 time.sleep(0.1)
81 81
82 82 if kernel.poll() is not None:
83 83 o,e = kernel.communicate()
84 84 e = py3compat.cast_unicode(e)
85 85 raise IOError("Kernel failed to start:\n%s" % e)
86 86
87 87 if not os.path.exists(connection_file):
88 88 if kernel.poll() is None:
89 89 kernel.terminate()
90 90 raise IOError("Connection file %r never arrived" % connection_file)
91 91
92 92 client = BlockingKernelClient(connection_file=connection_file)
93 93 client.load_connection_file()
94 94 client.start_channels()
95 95
96 96 try:
97 97 yield client
98 98 finally:
99 99 client.stop_channels()
100 100 kernel.terminate()
101 101
102 102 def test_embed_kernel_basic():
103 103 """IPython.embed_kernel() is basically functional"""
104 104 cmd = '\n'.join([
105 105 'from IPython import embed_kernel',
106 106 'def go():',
107 107 ' a=5',
108 108 ' b="hi there"',
109 109 ' embed_kernel()',
110 110 'go()',
111 111 '',
112 112 ])
113 113
114 114 with setup_kernel(cmd) as client:
115 115 # oinfo a (int)
116 116 msg_id = client.object_info('a')
117 117 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
118 118 content = msg['content']
119 119 nt.assert_true(content['found'])
120 120
121 121 msg_id = client.execute("c=a*2")
122 122 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
123 123 content = msg['content']
124 124 nt.assert_equal(content['status'], u'ok')
125 125
126 126 # oinfo c (should be 10)
127 127 msg_id = client.object_info('c')
128 128 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
129 129 content = msg['content']
130 130 nt.assert_true(content['found'])
131 nt.assert_equal(content['string_form'], u'10')
131 text = content['data']['text/plain']
132 nt.assert_in('10', text)
132 133
133 134 def test_embed_kernel_namespace():
134 135 """IPython.embed_kernel() inherits calling namespace"""
135 136 cmd = '\n'.join([
136 137 'from IPython import embed_kernel',
137 138 'def go():',
138 139 ' a=5',
139 140 ' b="hi there"',
140 141 ' embed_kernel()',
141 142 'go()',
142 143 '',
143 144 ])
144 145
145 146 with setup_kernel(cmd) as client:
146 147 # oinfo a (int)
147 148 msg_id = client.object_info('a')
148 149 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
149 150 content = msg['content']
150 151 nt.assert_true(content['found'])
151 nt.assert_equal(content['string_form'], u'5')
152 text = content['data']['text/plain']
153 nt.assert_in(u'5', text)
152 154
153 155 # oinfo b (str)
154 156 msg_id = client.object_info('b')
155 157 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
156 158 content = msg['content']
157 159 nt.assert_true(content['found'])
158 nt.assert_equal(content['string_form'], u'hi there')
160 text = content['data']['text/plain']
161 nt.assert_in(u'hi there', text)
159 162
160 163 # oinfo c (undefined)
161 164 msg_id = client.object_info('c')
162 165 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
163 166 content = msg['content']
164 167 nt.assert_false(content['found'])
165 168
166 169 def test_embed_kernel_reentrant():
167 170 """IPython.embed_kernel() can be called multiple times"""
168 171 cmd = '\n'.join([
169 172 'from IPython import embed_kernel',
170 173 'count = 0',
171 174 'def go():',
172 175 ' global count',
173 176 ' embed_kernel()',
174 177 ' count = count + 1',
175 178 '',
176 179 'while True:'
177 180 ' go()',
178 181 '',
179 182 ])
180 183
181 184 with setup_kernel(cmd) as client:
182 185 for i in range(5):
183 186 msg_id = client.object_info('count')
184 187 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
185 188 content = msg['content']
186 189 nt.assert_true(content['found'])
187 nt.assert_equal(content['string_form'], unicode_type(i))
190 text = content['data']['text/plain']
191 nt.assert_in(unicode_type(i), text)
188 192
189 193 # exit from embed_kernel
190 194 client.execute("get_ipython().exit_now = True")
191 195 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
192 196 time.sleep(0.2)
193 197
194 198
@@ -1,45 +1,48 b''
1 1 import nose.tools as nt
2 2
3 3 from .test_embed_kernel import setup, teardown, setup_kernel
4 4
5 5 TIMEOUT = 15
6 6
7 7 def test_ipython_start_kernel_userns():
8 8 cmd = ('from IPython import start_kernel\n'
9 9 'ns = {"tre": 123}\n'
10 10 'start_kernel(user_ns=ns)')
11 11
12 12 with setup_kernel(cmd) as client:
13 13 msg_id = client.object_info('tre')
14 14 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
15 15 content = msg['content']
16 16 assert content['found']
17 nt.assert_equal(content['string_form'], u'123')
17 text = content['data']['text/plain']
18 nt.assert_in(u'123', text)
18 19
19 20 # user_module should be an instance of DummyMod
20 21 msg_id = client.execute("usermod = get_ipython().user_module")
21 22 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
22 23 content = msg['content']
23 24 nt.assert_equal(content['status'], u'ok')
24 25 msg_id = client.object_info('usermod')
25 26 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
26 27 content = msg['content']
27 28 assert content['found']
28 nt.assert_in('DummyMod', content['string_form'])
29 text = content['data']['text/plain']
30 nt.assert_in(u'DummyMod', text)
29 31
30 32 def test_ipython_start_kernel_no_userns():
31 33 # Issue #4188 - user_ns should be passed to shell as None, not {}
32 34 cmd = ('from IPython import start_kernel\n'
33 35 'start_kernel()')
34 36
35 37 with setup_kernel(cmd) as client:
36 38 # user_module should not be an instance of DummyMod
37 39 msg_id = client.execute("usermod = get_ipython().user_module")
38 40 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
39 41 content = msg['content']
40 42 nt.assert_equal(content['status'], u'ok')
41 43 msg_id = client.object_info('usermod')
42 44 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
43 45 content = msg['content']
44 46 assert content['found']
45 nt.assert_not_in('DummyMod', content['string_form'])
47 text = content['data']['text/plain']
48 nt.assert_not_in(u'DummyMod', text)
@@ -1,2124 +1,2133 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os.path
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12 import time
13 13 from unicodedata import category
14 14 import webbrowser
15 15
16 16 # System library imports
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 # Local imports
20 20 from IPython.config.configurable import LoggingConfigurable
21 21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 22 from IPython.qt.rich_text import HtmlExporter
23 23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 24 from IPython.utils.text import columnize
25 25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 26 from .ansi_code_processor import QtAnsiCodeProcessor
27 27 from .completion_widget import CompletionWidget
28 28 from .completion_html import CompletionHtml
29 29 from .completion_plain import CompletionPlain
30 30 from .kill_ring import QtKillRing
31 31
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Functions
35 35 #-----------------------------------------------------------------------------
36 36
37 37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39 39
40 40 def commonprefix(items):
41 41 """Get common prefix for completions
42 42
43 43 Return the longest common prefix of a list of strings, but with special
44 44 treatment of escape characters that might precede commands in IPython,
45 45 such as %magic functions. Used in tab completion.
46 46
47 47 For a more general function, see os.path.commonprefix
48 48 """
49 49 # the last item will always have the least leading % symbol
50 50 # min / max are first/last in alphabetical order
51 51 first_match = ESCAPE_RE.match(min(items))
52 52 last_match = ESCAPE_RE.match(max(items))
53 53 # common suffix is (common prefix of reversed items) reversed
54 54 if first_match and last_match:
55 55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 56 else:
57 57 prefix = ''
58 58
59 59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 60 return prefix+os.path.commonprefix(items)
61 61
62 62 def is_letter_or_number(char):
63 63 """ Returns whether the specified unicode character is a letter or a number.
64 64 """
65 65 cat = category(char)
66 66 return cat.startswith('L') or cat.startswith('N')
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Classes
70 70 #-----------------------------------------------------------------------------
71 71
72 72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
73 73 """ An abstract base class for console-type widgets. This class has
74 74 functionality for:
75 75
76 76 * Maintaining a prompt and editing region
77 77 * Providing the traditional Unix-style console keyboard shortcuts
78 78 * Performing tab completion
79 79 * Paging text
80 80 * Handling ANSI escape codes
81 81
82 82 ConsoleWidget also provides a number of utility methods that will be
83 83 convenient to implementors of a console-style widget.
84 84 """
85 85
86 86 #------ Configuration ------------------------------------------------------
87 87
88 88 ansi_codes = Bool(True, config=True,
89 89 help="Whether to process ANSI escape codes."
90 90 )
91 91 buffer_size = Integer(500, config=True,
92 92 help="""
93 93 The maximum number of lines of text before truncation. Specifying a
94 94 non-positive number disables text truncation (not recommended).
95 95 """
96 96 )
97 97 execute_on_complete_input = Bool(True, config=True,
98 98 help="""Whether to automatically execute on syntactically complete input.
99 99
100 100 If False, Shift-Enter is required to submit each execution.
101 101 Disabling this is mainly useful for non-Python kernels,
102 102 where the completion check would be wrong.
103 103 """
104 104 )
105 105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
106 106 default_value = 'ncurses',
107 107 help="""
108 108 The type of completer to use. Valid values are:
109 109
110 110 'plain' : Show the available completion as a text list
111 111 Below the editing area.
112 112 'droplist': Show the completion in a drop down list navigable
113 113 by the arrow keys, and from which you can select
114 114 completion by pressing Return.
115 115 'ncurses' : Show the completion as a text list which is navigable by
116 116 `tab` and arrow keys.
117 117 """
118 118 )
119 119 # NOTE: this value can only be specified during initialization.
120 120 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
121 121 help="""
122 122 The type of underlying text widget to use. Valid values are 'plain',
123 123 which specifies a QPlainTextEdit, and 'rich', which specifies a
124 124 QTextEdit.
125 125 """
126 126 )
127 127 # NOTE: this value can only be specified during initialization.
128 128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
129 129 default_value='inside', config=True,
130 130 help="""
131 131 The type of paging to use. Valid values are:
132 132
133 133 'inside'
134 134 The widget pages like a traditional terminal.
135 135 'hsplit'
136 136 When paging is requested, the widget is split horizontally. The top
137 137 pane contains the console, and the bottom pane contains the paged text.
138 138 'vsplit'
139 139 Similar to 'hsplit', except that a vertical splitter is used.
140 140 'custom'
141 141 No action is taken by the widget beyond emitting a
142 142 'custom_page_requested(str)' signal.
143 143 'none'
144 144 The text is written directly to the console.
145 145 """)
146 146
147 147 font_family = Unicode(config=True,
148 148 help="""The font family to use for the console.
149 149 On OSX this defaults to Monaco, on Windows the default is
150 150 Consolas with fallback of Courier, and on other platforms
151 151 the default is Monospace.
152 152 """)
153 153 def _font_family_default(self):
154 154 if sys.platform == 'win32':
155 155 # Consolas ships with Vista/Win7, fallback to Courier if needed
156 156 return 'Consolas'
157 157 elif sys.platform == 'darwin':
158 158 # OSX always has Monaco, no need for a fallback
159 159 return 'Monaco'
160 160 else:
161 161 # Monospace should always exist, no need for a fallback
162 162 return 'Monospace'
163 163
164 164 font_size = Integer(config=True,
165 165 help="""The font size. If unconfigured, Qt will be entrusted
166 166 with the size of the font.
167 167 """)
168 168
169 169 width = Integer(81, config=True,
170 170 help="""The width of the console at start time in number
171 171 of characters (will double with `hsplit` paging)
172 172 """)
173 173
174 174 height = Integer(25, config=True,
175 175 help="""The height of the console at start time in number
176 176 of characters (will double with `vsplit` paging)
177 177 """)
178 178
179 179 # Whether to override ShortcutEvents for the keybindings defined by this
180 180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
181 181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
182 182 override_shortcuts = Bool(False)
183 183
184 184 # ------ Custom Qt Widgets -------------------------------------------------
185 185
186 186 # For other projects to easily override the Qt widgets used by the console
187 187 # (e.g. Spyder)
188 188 custom_control = None
189 189 custom_page_control = None
190 190
191 191 #------ Signals ------------------------------------------------------------
192 192
193 193 # Signals that indicate ConsoleWidget state.
194 194 copy_available = QtCore.Signal(bool)
195 195 redo_available = QtCore.Signal(bool)
196 196 undo_available = QtCore.Signal(bool)
197 197
198 198 # Signal emitted when paging is needed and the paging style has been
199 199 # specified as 'custom'.
200 200 custom_page_requested = QtCore.Signal(object)
201 201
202 202 # Signal emitted when the font is changed.
203 203 font_changed = QtCore.Signal(QtGui.QFont)
204 204
205 205 #------ Protected class variables ------------------------------------------
206 206
207 207 # control handles
208 208 _control = None
209 209 _page_control = None
210 210 _splitter = None
211 211
212 212 # When the control key is down, these keys are mapped.
213 213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
214 214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
215 215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
216 216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
217 217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
218 218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
219 219 if not sys.platform == 'darwin':
220 220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
221 221 # cursor to the bottom of the buffer.
222 222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
223 223
224 224 # The shortcuts defined by this widget. We need to keep track of these to
225 225 # support 'override_shortcuts' above.
226 226 _shortcuts = set(_ctrl_down_remap.keys()) | \
227 227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
228 228 QtCore.Qt.Key_V }
229 229
230 230 _temp_buffer_filled = False
231 231
232 232 #---------------------------------------------------------------------------
233 233 # 'QObject' interface
234 234 #---------------------------------------------------------------------------
235 235
236 236 def __init__(self, parent=None, **kw):
237 237 """ Create a ConsoleWidget.
238 238
239 239 Parameters
240 240 ----------
241 241 parent : QWidget, optional [default None]
242 242 The parent for this widget.
243 243 """
244 244 QtGui.QWidget.__init__(self, parent)
245 245 LoggingConfigurable.__init__(self, **kw)
246 246
247 247 # While scrolling the pager on Mac OS X, it tears badly. The
248 248 # NativeGesture is platform and perhaps build-specific hence
249 249 # we take adequate precautions here.
250 250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
251 251 if hasattr(QtCore.QEvent, 'NativeGesture'):
252 252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
253 253
254 254 # Create the layout and underlying text widget.
255 255 layout = QtGui.QStackedLayout(self)
256 256 layout.setContentsMargins(0, 0, 0, 0)
257 257 self._control = self._create_control()
258 258 if self.paging in ('hsplit', 'vsplit'):
259 259 self._splitter = QtGui.QSplitter()
260 260 if self.paging == 'hsplit':
261 261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
262 262 else:
263 263 self._splitter.setOrientation(QtCore.Qt.Vertical)
264 264 self._splitter.addWidget(self._control)
265 265 layout.addWidget(self._splitter)
266 266 else:
267 267 layout.addWidget(self._control)
268 268
269 269 # Create the paging widget, if necessary.
270 270 if self.paging in ('inside', 'hsplit', 'vsplit'):
271 271 self._page_control = self._create_page_control()
272 272 if self._splitter:
273 273 self._page_control.hide()
274 274 self._splitter.addWidget(self._page_control)
275 275 else:
276 276 layout.addWidget(self._page_control)
277 277
278 278 # Initialize protected variables. Some variables contain useful state
279 279 # information for subclasses; they should be considered read-only.
280 280 self._append_before_prompt_pos = 0
281 281 self._ansi_processor = QtAnsiCodeProcessor()
282 282 if self.gui_completion == 'ncurses':
283 283 self._completion_widget = CompletionHtml(self)
284 284 elif self.gui_completion == 'droplist':
285 285 self._completion_widget = CompletionWidget(self)
286 286 elif self.gui_completion == 'plain':
287 287 self._completion_widget = CompletionPlain(self)
288 288
289 289 self._continuation_prompt = '> '
290 290 self._continuation_prompt_html = None
291 291 self._executing = False
292 292 self._filter_resize = False
293 293 self._html_exporter = HtmlExporter(self._control)
294 294 self._input_buffer_executing = ''
295 295 self._input_buffer_pending = ''
296 296 self._kill_ring = QtKillRing(self._control)
297 297 self._prompt = ''
298 298 self._prompt_html = None
299 299 self._prompt_pos = 0
300 300 self._prompt_sep = ''
301 301 self._reading = False
302 302 self._reading_callback = None
303 303 self._tab_width = 8
304 304
305 305 # List of strings pending to be appended as plain text in the widget.
306 306 # The text is not immediately inserted when available to not
307 307 # choke the Qt event loop with paint events for the widget in
308 308 # case of lots of output from kernel.
309 309 self._pending_insert_text = []
310 310
311 311 # Timer to flush the pending stream messages. The interval is adjusted
312 312 # later based on actual time taken for flushing a screen (buffer_size)
313 313 # of output text.
314 314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
315 315 self._pending_text_flush_interval.setInterval(100)
316 316 self._pending_text_flush_interval.setSingleShot(True)
317 317 self._pending_text_flush_interval.timeout.connect(
318 318 self._on_flush_pending_stream_timer)
319 319
320 320 # Set a monospaced font.
321 321 self.reset_font()
322 322
323 323 # Configure actions.
324 324 action = QtGui.QAction('Print', None)
325 325 action.setEnabled(True)
326 326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
327 327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
328 328 # Only override the default if there is a collision.
329 329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
330 330 printkey = "Ctrl+Shift+P"
331 331 action.setShortcut(printkey)
332 332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
333 333 action.triggered.connect(self.print_)
334 334 self.addAction(action)
335 335 self.print_action = action
336 336
337 337 action = QtGui.QAction('Save as HTML/XML', None)
338 338 action.setShortcut(QtGui.QKeySequence.Save)
339 339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
340 340 action.triggered.connect(self.export_html)
341 341 self.addAction(action)
342 342 self.export_action = action
343 343
344 344 action = QtGui.QAction('Select All', None)
345 345 action.setEnabled(True)
346 346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
347 347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
348 348 # Only override the default if there is a collision.
349 349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
350 350 selectall = "Ctrl+Shift+A"
351 351 action.setShortcut(selectall)
352 352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
353 353 action.triggered.connect(self.select_all)
354 354 self.addAction(action)
355 355 self.select_all_action = action
356 356
357 357 self.increase_font_size = QtGui.QAction("Bigger Font",
358 358 self,
359 359 shortcut=QtGui.QKeySequence.ZoomIn,
360 360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
361 361 statusTip="Increase the font size by one point",
362 362 triggered=self._increase_font_size)
363 363 self.addAction(self.increase_font_size)
364 364
365 365 self.decrease_font_size = QtGui.QAction("Smaller Font",
366 366 self,
367 367 shortcut=QtGui.QKeySequence.ZoomOut,
368 368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
369 369 statusTip="Decrease the font size by one point",
370 370 triggered=self._decrease_font_size)
371 371 self.addAction(self.decrease_font_size)
372 372
373 373 self.reset_font_size = QtGui.QAction("Normal Font",
374 374 self,
375 375 shortcut="Ctrl+0",
376 376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
377 377 statusTip="Restore the Normal font size",
378 378 triggered=self.reset_font)
379 379 self.addAction(self.reset_font_size)
380 380
381 381 # Accept drag and drop events here. Drops were already turned off
382 382 # in self._control when that widget was created.
383 383 self.setAcceptDrops(True)
384 384
385 385 #---------------------------------------------------------------------------
386 386 # Drag and drop support
387 387 #---------------------------------------------------------------------------
388 388
389 389 def dragEnterEvent(self, e):
390 390 if e.mimeData().hasUrls():
391 391 # The link action should indicate to that the drop will insert
392 392 # the file anme.
393 393 e.setDropAction(QtCore.Qt.LinkAction)
394 394 e.accept()
395 395 elif e.mimeData().hasText():
396 396 # By changing the action to copy we don't need to worry about
397 397 # the user accidentally moving text around in the widget.
398 398 e.setDropAction(QtCore.Qt.CopyAction)
399 399 e.accept()
400 400
401 401 def dragMoveEvent(self, e):
402 402 if e.mimeData().hasUrls():
403 403 pass
404 404 elif e.mimeData().hasText():
405 405 cursor = self._control.cursorForPosition(e.pos())
406 406 if self._in_buffer(cursor.position()):
407 407 e.setDropAction(QtCore.Qt.CopyAction)
408 408 self._control.setTextCursor(cursor)
409 409 else:
410 410 e.setDropAction(QtCore.Qt.IgnoreAction)
411 411 e.accept()
412 412
413 413 def dropEvent(self, e):
414 414 if e.mimeData().hasUrls():
415 415 self._keep_cursor_in_buffer()
416 416 cursor = self._control.textCursor()
417 417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
418 418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
419 419 for f in filenames)
420 420 self._insert_plain_text_into_buffer(cursor, text)
421 421 elif e.mimeData().hasText():
422 422 cursor = self._control.cursorForPosition(e.pos())
423 423 if self._in_buffer(cursor.position()):
424 424 text = e.mimeData().text()
425 425 self._insert_plain_text_into_buffer(cursor, text)
426 426
427 427 def eventFilter(self, obj, event):
428 428 """ Reimplemented to ensure a console-like behavior in the underlying
429 429 text widgets.
430 430 """
431 431 etype = event.type()
432 432 if etype == QtCore.QEvent.KeyPress:
433 433
434 434 # Re-map keys for all filtered widgets.
435 435 key = event.key()
436 436 if self._control_key_down(event.modifiers()) and \
437 437 key in self._ctrl_down_remap:
438 438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
439 439 self._ctrl_down_remap[key],
440 440 QtCore.Qt.NoModifier)
441 441 QtGui.qApp.sendEvent(obj, new_event)
442 442 return True
443 443
444 444 elif obj == self._control:
445 445 return self._event_filter_console_keypress(event)
446 446
447 447 elif obj == self._page_control:
448 448 return self._event_filter_page_keypress(event)
449 449
450 450 # Make middle-click paste safe.
451 451 elif etype == QtCore.QEvent.MouseButtonRelease and \
452 452 event.button() == QtCore.Qt.MidButton and \
453 453 obj == self._control.viewport():
454 454 cursor = self._control.cursorForPosition(event.pos())
455 455 self._control.setTextCursor(cursor)
456 456 self.paste(QtGui.QClipboard.Selection)
457 457 return True
458 458
459 459 # Manually adjust the scrollbars *after* a resize event is dispatched.
460 460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
461 461 self._filter_resize = True
462 462 QtGui.qApp.sendEvent(obj, event)
463 463 self._adjust_scrollbars()
464 464 self._filter_resize = False
465 465 return True
466 466
467 467 # Override shortcuts for all filtered widgets.
468 468 elif etype == QtCore.QEvent.ShortcutOverride and \
469 469 self.override_shortcuts and \
470 470 self._control_key_down(event.modifiers()) and \
471 471 event.key() in self._shortcuts:
472 472 event.accept()
473 473
474 474 # Handle scrolling of the vsplit pager. This hack attempts to solve
475 475 # problems with tearing of the help text inside the pager window. This
476 476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
477 477 # perfect but makes the pager more usable.
478 478 elif etype in self._pager_scroll_events and \
479 479 obj == self._page_control:
480 480 self._page_control.repaint()
481 481 return True
482 482
483 483 elif etype == QtCore.QEvent.MouseMove:
484 484 anchor = self._control.anchorAt(event.pos())
485 485 QtGui.QToolTip.showText(event.globalPos(), anchor)
486 486
487 487 return super(ConsoleWidget, self).eventFilter(obj, event)
488 488
489 489 #---------------------------------------------------------------------------
490 490 # 'QWidget' interface
491 491 #---------------------------------------------------------------------------
492 492
493 493 def sizeHint(self):
494 494 """ Reimplemented to suggest a size that is 80 characters wide and
495 495 25 lines high.
496 496 """
497 497 font_metrics = QtGui.QFontMetrics(self.font)
498 498 margin = (self._control.frameWidth() +
499 499 self._control.document().documentMargin()) * 2
500 500 style = self.style()
501 501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
502 502
503 503 # Note 1: Despite my best efforts to take the various margins into
504 504 # account, the width is still coming out a bit too small, so we include
505 505 # a fudge factor of one character here.
506 506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
507 507 # to a Qt bug on certain Mac OS systems where it returns 0.
508 508 width = font_metrics.width(' ') * self.width + margin
509 509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
510 510 if self.paging == 'hsplit':
511 511 width = width * 2 + splitwidth
512 512
513 513 height = font_metrics.height() * self.height + margin
514 514 if self.paging == 'vsplit':
515 515 height = height * 2 + splitwidth
516 516
517 517 return QtCore.QSize(width, height)
518 518
519 519 #---------------------------------------------------------------------------
520 520 # 'ConsoleWidget' public interface
521 521 #---------------------------------------------------------------------------
522 522
523 523 def can_copy(self):
524 524 """ Returns whether text can be copied to the clipboard.
525 525 """
526 526 return self._control.textCursor().hasSelection()
527 527
528 528 def can_cut(self):
529 529 """ Returns whether text can be cut to the clipboard.
530 530 """
531 531 cursor = self._control.textCursor()
532 532 return (cursor.hasSelection() and
533 533 self._in_buffer(cursor.anchor()) and
534 534 self._in_buffer(cursor.position()))
535 535
536 536 def can_paste(self):
537 537 """ Returns whether text can be pasted from the clipboard.
538 538 """
539 539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
540 540 return bool(QtGui.QApplication.clipboard().text())
541 541 return False
542 542
543 543 def clear(self, keep_input=True):
544 544 """ Clear the console.
545 545
546 546 Parameters
547 547 ----------
548 548 keep_input : bool, optional (default True)
549 549 If set, restores the old input buffer if a new prompt is written.
550 550 """
551 551 if self._executing:
552 552 self._control.clear()
553 553 else:
554 554 if keep_input:
555 555 input_buffer = self.input_buffer
556 556 self._control.clear()
557 557 self._show_prompt()
558 558 if keep_input:
559 559 self.input_buffer = input_buffer
560 560
561 561 def copy(self):
562 562 """ Copy the currently selected text to the clipboard.
563 563 """
564 564 self.layout().currentWidget().copy()
565 565
566 566 def copy_anchor(self, anchor):
567 567 """ Copy anchor text to the clipboard
568 568 """
569 569 QtGui.QApplication.clipboard().setText(anchor)
570 570
571 571 def cut(self):
572 572 """ Copy the currently selected text to the clipboard and delete it
573 573 if it's inside the input buffer.
574 574 """
575 575 self.copy()
576 576 if self.can_cut():
577 577 self._control.textCursor().removeSelectedText()
578 578
579 579 def execute(self, source=None, hidden=False, interactive=False):
580 580 """ Executes source or the input buffer, possibly prompting for more
581 581 input.
582 582
583 583 Parameters
584 584 ----------
585 585 source : str, optional
586 586
587 587 The source to execute. If not specified, the input buffer will be
588 588 used. If specified and 'hidden' is False, the input buffer will be
589 589 replaced with the source before execution.
590 590
591 591 hidden : bool, optional (default False)
592 592
593 593 If set, no output will be shown and the prompt will not be modified.
594 594 In other words, it will be completely invisible to the user that
595 595 an execution has occurred.
596 596
597 597 interactive : bool, optional (default False)
598 598
599 599 Whether the console is to treat the source as having been manually
600 600 entered by the user. The effect of this parameter depends on the
601 601 subclass implementation.
602 602
603 603 Raises
604 604 ------
605 605 RuntimeError
606 606 If incomplete input is given and 'hidden' is True. In this case,
607 607 it is not possible to prompt for more input.
608 608
609 609 Returns
610 610 -------
611 611 A boolean indicating whether the source was executed.
612 612 """
613 613 # WARNING: The order in which things happen here is very particular, in
614 614 # large part because our syntax highlighting is fragile. If you change
615 615 # something, test carefully!
616 616
617 617 # Decide what to execute.
618 618 if source is None:
619 619 source = self.input_buffer
620 620 if not hidden:
621 621 # A newline is appended later, but it should be considered part
622 622 # of the input buffer.
623 623 source += '\n'
624 624 elif not hidden:
625 625 self.input_buffer = source
626 626
627 627 # Execute the source or show a continuation prompt if it is incomplete.
628 628 if self.execute_on_complete_input:
629 629 complete = self._is_complete(source, interactive)
630 630 else:
631 631 complete = not interactive
632 632 if hidden:
633 633 if complete or not self.execute_on_complete_input:
634 634 self._execute(source, hidden)
635 635 else:
636 636 error = 'Incomplete noninteractive input: "%s"'
637 637 raise RuntimeError(error % source)
638 638 else:
639 639 if complete:
640 640 self._append_plain_text('\n')
641 641 self._input_buffer_executing = self.input_buffer
642 642 self._executing = True
643 643 self._prompt_finished()
644 644
645 645 # The maximum block count is only in effect during execution.
646 646 # This ensures that _prompt_pos does not become invalid due to
647 647 # text truncation.
648 648 self._control.document().setMaximumBlockCount(self.buffer_size)
649 649
650 650 # Setting a positive maximum block count will automatically
651 651 # disable the undo/redo history, but just to be safe:
652 652 self._control.setUndoRedoEnabled(False)
653 653
654 654 # Perform actual execution.
655 655 self._execute(source, hidden)
656 656
657 657 else:
658 658 # Do this inside an edit block so continuation prompts are
659 659 # removed seamlessly via undo/redo.
660 660 cursor = self._get_end_cursor()
661 661 cursor.beginEditBlock()
662 662 cursor.insertText('\n')
663 663 self._insert_continuation_prompt(cursor)
664 664 cursor.endEditBlock()
665 665
666 666 # Do not do this inside the edit block. It works as expected
667 667 # when using a QPlainTextEdit control, but does not have an
668 668 # effect when using a QTextEdit. I believe this is a Qt bug.
669 669 self._control.moveCursor(QtGui.QTextCursor.End)
670 670
671 671 return complete
672 672
673 673 def export_html(self):
674 674 """ Shows a dialog to export HTML/XML in various formats.
675 675 """
676 676 self._html_exporter.export()
677 677
678 678 def _get_input_buffer(self, force=False):
679 679 """ The text that the user has entered entered at the current prompt.
680 680
681 681 If the console is currently executing, the text that is executing will
682 682 always be returned.
683 683 """
684 684 # If we're executing, the input buffer may not even exist anymore due to
685 685 # the limit imposed by 'buffer_size'. Therefore, we store it.
686 686 if self._executing and not force:
687 687 return self._input_buffer_executing
688 688
689 689 cursor = self._get_end_cursor()
690 690 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
691 691 input_buffer = cursor.selection().toPlainText()
692 692
693 693 # Strip out continuation prompts.
694 694 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
695 695
696 696 def _set_input_buffer(self, string):
697 697 """ Sets the text in the input buffer.
698 698
699 699 If the console is currently executing, this call has no *immediate*
700 700 effect. When the execution is finished, the input buffer will be updated
701 701 appropriately.
702 702 """
703 703 # If we're executing, store the text for later.
704 704 if self._executing:
705 705 self._input_buffer_pending = string
706 706 return
707 707
708 708 # Remove old text.
709 709 cursor = self._get_end_cursor()
710 710 cursor.beginEditBlock()
711 711 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
712 712 cursor.removeSelectedText()
713 713
714 714 # Insert new text with continuation prompts.
715 715 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
716 716 cursor.endEditBlock()
717 717 self._control.moveCursor(QtGui.QTextCursor.End)
718 718
719 719 input_buffer = property(_get_input_buffer, _set_input_buffer)
720 720
721 721 def _get_font(self):
722 722 """ The base font being used by the ConsoleWidget.
723 723 """
724 724 return self._control.document().defaultFont()
725 725
726 726 def _set_font(self, font):
727 727 """ Sets the base font for the ConsoleWidget to the specified QFont.
728 728 """
729 729 font_metrics = QtGui.QFontMetrics(font)
730 730 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
731 731
732 732 self._completion_widget.setFont(font)
733 733 self._control.document().setDefaultFont(font)
734 734 if self._page_control:
735 735 self._page_control.document().setDefaultFont(font)
736 736
737 737 self.font_changed.emit(font)
738 738
739 739 font = property(_get_font, _set_font)
740 740
741 741 def open_anchor(self, anchor):
742 742 """ Open selected anchor in the default webbrowser
743 743 """
744 744 webbrowser.open( anchor )
745 745
746 746 def paste(self, mode=QtGui.QClipboard.Clipboard):
747 747 """ Paste the contents of the clipboard into the input region.
748 748
749 749 Parameters
750 750 ----------
751 751 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
752 752
753 753 Controls which part of the system clipboard is used. This can be
754 754 used to access the selection clipboard in X11 and the Find buffer
755 755 in Mac OS. By default, the regular clipboard is used.
756 756 """
757 757 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
758 758 # Make sure the paste is safe.
759 759 self._keep_cursor_in_buffer()
760 760 cursor = self._control.textCursor()
761 761
762 762 # Remove any trailing newline, which confuses the GUI and forces the
763 763 # user to backspace.
764 764 text = QtGui.QApplication.clipboard().text(mode).rstrip()
765 765 self._insert_plain_text_into_buffer(cursor, dedent(text))
766 766
767 767 def print_(self, printer = None):
768 768 """ Print the contents of the ConsoleWidget to the specified QPrinter.
769 769 """
770 770 if (not printer):
771 771 printer = QtGui.QPrinter()
772 772 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
773 773 return
774 774 self._control.print_(printer)
775 775
776 776 def prompt_to_top(self):
777 777 """ Moves the prompt to the top of the viewport.
778 778 """
779 779 if not self._executing:
780 780 prompt_cursor = self._get_prompt_cursor()
781 781 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
782 782 self._set_cursor(prompt_cursor)
783 783 self._set_top_cursor(prompt_cursor)
784 784
785 785 def redo(self):
786 786 """ Redo the last operation. If there is no operation to redo, nothing
787 787 happens.
788 788 """
789 789 self._control.redo()
790 790
791 791 def reset_font(self):
792 792 """ Sets the font to the default fixed-width font for this platform.
793 793 """
794 794 if sys.platform == 'win32':
795 795 # Consolas ships with Vista/Win7, fallback to Courier if needed
796 796 fallback = 'Courier'
797 797 elif sys.platform == 'darwin':
798 798 # OSX always has Monaco
799 799 fallback = 'Monaco'
800 800 else:
801 801 # Monospace should always exist
802 802 fallback = 'Monospace'
803 803 font = get_font(self.font_family, fallback)
804 804 if self.font_size:
805 805 font.setPointSize(self.font_size)
806 806 else:
807 807 font.setPointSize(QtGui.qApp.font().pointSize())
808 808 font.setStyleHint(QtGui.QFont.TypeWriter)
809 809 self._set_font(font)
810 810
811 811 def change_font_size(self, delta):
812 812 """Change the font size by the specified amount (in points).
813 813 """
814 814 font = self.font
815 815 size = max(font.pointSize() + delta, 1) # minimum 1 point
816 816 font.setPointSize(size)
817 817 self._set_font(font)
818 818
819 819 def _increase_font_size(self):
820 820 self.change_font_size(1)
821 821
822 822 def _decrease_font_size(self):
823 823 self.change_font_size(-1)
824 824
825 825 def select_all(self):
826 826 """ Selects all the text in the buffer.
827 827 """
828 828 self._control.selectAll()
829 829
830 830 def _get_tab_width(self):
831 831 """ The width (in terms of space characters) for tab characters.
832 832 """
833 833 return self._tab_width
834 834
835 835 def _set_tab_width(self, tab_width):
836 836 """ Sets the width (in terms of space characters) for tab characters.
837 837 """
838 838 font_metrics = QtGui.QFontMetrics(self.font)
839 839 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
840 840
841 841 self._tab_width = tab_width
842 842
843 843 tab_width = property(_get_tab_width, _set_tab_width)
844 844
845 845 def undo(self):
846 846 """ Undo the last operation. If there is no operation to undo, nothing
847 847 happens.
848 848 """
849 849 self._control.undo()
850 850
851 851 #---------------------------------------------------------------------------
852 852 # 'ConsoleWidget' abstract interface
853 853 #---------------------------------------------------------------------------
854 854
855 855 def _is_complete(self, source, interactive):
856 856 """ Returns whether 'source' can be executed. When triggered by an
857 857 Enter/Return key press, 'interactive' is True; otherwise, it is
858 858 False.
859 859 """
860 860 raise NotImplementedError
861 861
862 862 def _execute(self, source, hidden):
863 863 """ Execute 'source'. If 'hidden', do not show any output.
864 864 """
865 865 raise NotImplementedError
866 866
867 867 def _prompt_started_hook(self):
868 868 """ Called immediately after a new prompt is displayed.
869 869 """
870 870 pass
871 871
872 872 def _prompt_finished_hook(self):
873 873 """ Called immediately after a prompt is finished, i.e. when some input
874 874 will be processed and a new prompt displayed.
875 875 """
876 876 pass
877 877
878 878 def _up_pressed(self, shift_modifier):
879 879 """ Called when the up key is pressed. Returns whether to continue
880 880 processing the event.
881 881 """
882 882 return True
883 883
884 884 def _down_pressed(self, shift_modifier):
885 885 """ Called when the down key is pressed. Returns whether to continue
886 886 processing the event.
887 887 """
888 888 return True
889 889
890 890 def _tab_pressed(self):
891 891 """ Called when the tab key is pressed. Returns whether to continue
892 892 processing the event.
893 893 """
894 894 return False
895 895
896 896 #--------------------------------------------------------------------------
897 897 # 'ConsoleWidget' protected interface
898 898 #--------------------------------------------------------------------------
899 899
900 900 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
901 901 """ A low-level method for appending content to the end of the buffer.
902 902
903 903 If 'before_prompt' is enabled, the content will be inserted before the
904 904 current prompt, if there is one.
905 905 """
906 906 # Determine where to insert the content.
907 907 cursor = self._control.textCursor()
908 908 if before_prompt and (self._reading or not self._executing):
909 909 self._flush_pending_stream()
910 910 cursor.setPosition(self._append_before_prompt_pos)
911 911 else:
912 912 if insert != self._insert_plain_text:
913 913 self._flush_pending_stream()
914 914 cursor.movePosition(QtGui.QTextCursor.End)
915 915 start_pos = cursor.position()
916 916
917 917 # Perform the insertion.
918 918 result = insert(cursor, input, *args, **kwargs)
919 919
920 920 # Adjust the prompt position if we have inserted before it. This is safe
921 921 # because buffer truncation is disabled when not executing.
922 922 if before_prompt and not self._executing:
923 923 diff = cursor.position() - start_pos
924 924 self._append_before_prompt_pos += diff
925 925 self._prompt_pos += diff
926 926
927 927 return result
928 928
929 929 def _append_block(self, block_format=None, before_prompt=False):
930 930 """ Appends an new QTextBlock to the end of the console buffer.
931 931 """
932 932 self._append_custom(self._insert_block, block_format, before_prompt)
933 933
934 934 def _append_html(self, html, before_prompt=False):
935 935 """ Appends HTML at the end of the console buffer.
936 936 """
937 937 self._append_custom(self._insert_html, html, before_prompt)
938 938
939 939 def _append_html_fetching_plain_text(self, html, before_prompt=False):
940 940 """ Appends HTML, then returns the plain text version of it.
941 941 """
942 942 return self._append_custom(self._insert_html_fetching_plain_text,
943 943 html, before_prompt)
944 944
945 945 def _append_plain_text(self, text, before_prompt=False):
946 946 """ Appends plain text, processing ANSI codes if enabled.
947 947 """
948 948 self._append_custom(self._insert_plain_text, text, before_prompt)
949 949
950 950 def _cancel_completion(self):
951 951 """ If text completion is progress, cancel it.
952 952 """
953 953 self._completion_widget.cancel_completion()
954 954
955 955 def _clear_temporary_buffer(self):
956 956 """ Clears the "temporary text" buffer, i.e. all the text following
957 957 the prompt region.
958 958 """
959 959 # Select and remove all text below the input buffer.
960 960 cursor = self._get_prompt_cursor()
961 961 prompt = self._continuation_prompt.lstrip()
962 962 if(self._temp_buffer_filled):
963 963 self._temp_buffer_filled = False
964 964 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
965 965 temp_cursor = QtGui.QTextCursor(cursor)
966 966 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
967 967 text = temp_cursor.selection().toPlainText().lstrip()
968 968 if not text.startswith(prompt):
969 969 break
970 970 else:
971 971 # We've reached the end of the input buffer and no text follows.
972 972 return
973 973 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
974 974 cursor.movePosition(QtGui.QTextCursor.End,
975 975 QtGui.QTextCursor.KeepAnchor)
976 976 cursor.removeSelectedText()
977 977
978 978 # After doing this, we have no choice but to clear the undo/redo
979 979 # history. Otherwise, the text is not "temporary" at all, because it
980 980 # can be recalled with undo/redo. Unfortunately, Qt does not expose
981 981 # fine-grained control to the undo/redo system.
982 982 if self._control.isUndoRedoEnabled():
983 983 self._control.setUndoRedoEnabled(False)
984 984 self._control.setUndoRedoEnabled(True)
985 985
986 986 def _complete_with_items(self, cursor, items):
987 987 """ Performs completion with 'items' at the specified cursor location.
988 988 """
989 989 self._cancel_completion()
990 990
991 991 if len(items) == 1:
992 992 cursor.setPosition(self._control.textCursor().position(),
993 993 QtGui.QTextCursor.KeepAnchor)
994 994 cursor.insertText(items[0])
995 995
996 996 elif len(items) > 1:
997 997 current_pos = self._control.textCursor().position()
998 998 prefix = commonprefix(items)
999 999 if prefix:
1000 1000 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1001 1001 cursor.insertText(prefix)
1002 1002 current_pos = cursor.position()
1003 1003
1004 1004 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1005 1005 self._completion_widget.show_items(cursor, items)
1006 1006
1007 1007
1008 1008 def _fill_temporary_buffer(self, cursor, text, html=False):
1009 1009 """fill the area below the active editting zone with text"""
1010 1010
1011 1011 current_pos = self._control.textCursor().position()
1012 1012
1013 1013 cursor.beginEditBlock()
1014 1014 self._append_plain_text('\n')
1015 1015 self._page(text, html=html)
1016 1016 cursor.endEditBlock()
1017 1017
1018 1018 cursor.setPosition(current_pos)
1019 1019 self._control.moveCursor(QtGui.QTextCursor.End)
1020 1020 self._control.setTextCursor(cursor)
1021 1021
1022 1022 self._temp_buffer_filled = True
1023 1023
1024 1024
1025 1025 def _context_menu_make(self, pos):
1026 1026 """ Creates a context menu for the given QPoint (in widget coordinates).
1027 1027 """
1028 1028 menu = QtGui.QMenu(self)
1029 1029
1030 1030 self.cut_action = menu.addAction('Cut', self.cut)
1031 1031 self.cut_action.setEnabled(self.can_cut())
1032 1032 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1033 1033
1034 1034 self.copy_action = menu.addAction('Copy', self.copy)
1035 1035 self.copy_action.setEnabled(self.can_copy())
1036 1036 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1037 1037
1038 1038 self.paste_action = menu.addAction('Paste', self.paste)
1039 1039 self.paste_action.setEnabled(self.can_paste())
1040 1040 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1041 1041
1042 1042 anchor = self._control.anchorAt(pos)
1043 1043 if anchor:
1044 1044 menu.addSeparator()
1045 1045 self.copy_link_action = menu.addAction(
1046 1046 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1047 1047 self.open_link_action = menu.addAction(
1048 1048 'Open Link', lambda: self.open_anchor(anchor=anchor))
1049 1049
1050 1050 menu.addSeparator()
1051 1051 menu.addAction(self.select_all_action)
1052 1052
1053 1053 menu.addSeparator()
1054 1054 menu.addAction(self.export_action)
1055 1055 menu.addAction(self.print_action)
1056 1056
1057 1057 return menu
1058 1058
1059 1059 def _control_key_down(self, modifiers, include_command=False):
1060 1060 """ Given a KeyboardModifiers flags object, return whether the Control
1061 1061 key is down.
1062 1062
1063 1063 Parameters
1064 1064 ----------
1065 1065 include_command : bool, optional (default True)
1066 1066 Whether to treat the Command key as a (mutually exclusive) synonym
1067 1067 for Control when in Mac OS.
1068 1068 """
1069 1069 # Note that on Mac OS, ControlModifier corresponds to the Command key
1070 1070 # while MetaModifier corresponds to the Control key.
1071 1071 if sys.platform == 'darwin':
1072 1072 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1073 1073 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1074 1074 else:
1075 1075 return bool(modifiers & QtCore.Qt.ControlModifier)
1076 1076
1077 1077 def _create_control(self):
1078 1078 """ Creates and connects the underlying text widget.
1079 1079 """
1080 1080 # Create the underlying control.
1081 1081 if self.custom_control:
1082 1082 control = self.custom_control()
1083 1083 elif self.kind == 'plain':
1084 1084 control = QtGui.QPlainTextEdit()
1085 1085 elif self.kind == 'rich':
1086 1086 control = QtGui.QTextEdit()
1087 1087 control.setAcceptRichText(False)
1088 1088 control.setMouseTracking(True)
1089 1089
1090 1090 # Prevent the widget from handling drops, as we already provide
1091 1091 # the logic in this class.
1092 1092 control.setAcceptDrops(False)
1093 1093
1094 1094 # Install event filters. The filter on the viewport is needed for
1095 1095 # mouse events.
1096 1096 control.installEventFilter(self)
1097 1097 control.viewport().installEventFilter(self)
1098 1098
1099 1099 # Connect signals.
1100 1100 control.customContextMenuRequested.connect(
1101 1101 self._custom_context_menu_requested)
1102 1102 control.copyAvailable.connect(self.copy_available)
1103 1103 control.redoAvailable.connect(self.redo_available)
1104 1104 control.undoAvailable.connect(self.undo_available)
1105 1105
1106 1106 # Hijack the document size change signal to prevent Qt from adjusting
1107 1107 # the viewport's scrollbar. We are relying on an implementation detail
1108 1108 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1109 1109 # this functionality we cannot create a nice terminal interface.
1110 1110 layout = control.document().documentLayout()
1111 1111 layout.documentSizeChanged.disconnect()
1112 1112 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1113 1113
1114 1114 # Configure the control.
1115 1115 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1116 1116 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1117 1117 control.setReadOnly(True)
1118 1118 control.setUndoRedoEnabled(False)
1119 1119 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1120 1120 return control
1121 1121
1122 1122 def _create_page_control(self):
1123 1123 """ Creates and connects the underlying paging widget.
1124 1124 """
1125 1125 if self.custom_page_control:
1126 1126 control = self.custom_page_control()
1127 1127 elif self.kind == 'plain':
1128 1128 control = QtGui.QPlainTextEdit()
1129 1129 elif self.kind == 'rich':
1130 1130 control = QtGui.QTextEdit()
1131 1131 control.installEventFilter(self)
1132 1132 viewport = control.viewport()
1133 1133 viewport.installEventFilter(self)
1134 1134 control.setReadOnly(True)
1135 1135 control.setUndoRedoEnabled(False)
1136 1136 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1137 1137 return control
1138 1138
1139 1139 def _event_filter_console_keypress(self, event):
1140 1140 """ Filter key events for the underlying text widget to create a
1141 1141 console-like interface.
1142 1142 """
1143 1143 intercepted = False
1144 1144 cursor = self._control.textCursor()
1145 1145 position = cursor.position()
1146 1146 key = event.key()
1147 1147 ctrl_down = self._control_key_down(event.modifiers())
1148 1148 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1149 1149 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1150 1150
1151 1151 #------ Special sequences ----------------------------------------------
1152 1152
1153 1153 if event.matches(QtGui.QKeySequence.Copy):
1154 1154 self.copy()
1155 1155 intercepted = True
1156 1156
1157 1157 elif event.matches(QtGui.QKeySequence.Cut):
1158 1158 self.cut()
1159 1159 intercepted = True
1160 1160
1161 1161 elif event.matches(QtGui.QKeySequence.Paste):
1162 1162 self.paste()
1163 1163 intercepted = True
1164 1164
1165 1165 #------ Special modifier logic -----------------------------------------
1166 1166
1167 1167 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1168 1168 intercepted = True
1169 1169
1170 1170 # Special handling when tab completing in text mode.
1171 1171 self._cancel_completion()
1172 1172
1173 1173 if self._in_buffer(position):
1174 1174 # Special handling when a reading a line of raw input.
1175 1175 if self._reading:
1176 1176 self._append_plain_text('\n')
1177 1177 self._reading = False
1178 1178 if self._reading_callback:
1179 1179 self._reading_callback()
1180 1180
1181 1181 # If the input buffer is a single line or there is only
1182 1182 # whitespace after the cursor, execute. Otherwise, split the
1183 1183 # line with a continuation prompt.
1184 1184 elif not self._executing:
1185 1185 cursor.movePosition(QtGui.QTextCursor.End,
1186 1186 QtGui.QTextCursor.KeepAnchor)
1187 1187 at_end = len(cursor.selectedText().strip()) == 0
1188 1188 single_line = (self._get_end_cursor().blockNumber() ==
1189 1189 self._get_prompt_cursor().blockNumber())
1190 1190 if (at_end or shift_down or single_line) and not ctrl_down:
1191 1191 self.execute(interactive = not shift_down)
1192 1192 else:
1193 1193 # Do this inside an edit block for clean undo/redo.
1194 1194 cursor.beginEditBlock()
1195 1195 cursor.setPosition(position)
1196 1196 cursor.insertText('\n')
1197 1197 self._insert_continuation_prompt(cursor)
1198 1198 cursor.endEditBlock()
1199 1199
1200 1200 # Ensure that the whole input buffer is visible.
1201 1201 # FIXME: This will not be usable if the input buffer is
1202 1202 # taller than the console widget.
1203 1203 self._control.moveCursor(QtGui.QTextCursor.End)
1204 1204 self._control.setTextCursor(cursor)
1205 1205
1206 1206 #------ Control/Cmd modifier -------------------------------------------
1207 1207
1208 1208 elif ctrl_down:
1209 1209 if key == QtCore.Qt.Key_G:
1210 1210 self._keyboard_quit()
1211 1211 intercepted = True
1212 1212
1213 1213 elif key == QtCore.Qt.Key_K:
1214 1214 if self._in_buffer(position):
1215 1215 cursor.clearSelection()
1216 1216 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1217 1217 QtGui.QTextCursor.KeepAnchor)
1218 1218 if not cursor.hasSelection():
1219 1219 # Line deletion (remove continuation prompt)
1220 1220 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1221 1221 QtGui.QTextCursor.KeepAnchor)
1222 1222 cursor.movePosition(QtGui.QTextCursor.Right,
1223 1223 QtGui.QTextCursor.KeepAnchor,
1224 1224 len(self._continuation_prompt))
1225 1225 self._kill_ring.kill_cursor(cursor)
1226 1226 self._set_cursor(cursor)
1227 1227 intercepted = True
1228 1228
1229 1229 elif key == QtCore.Qt.Key_L:
1230 1230 self.prompt_to_top()
1231 1231 intercepted = True
1232 1232
1233 1233 elif key == QtCore.Qt.Key_O:
1234 1234 if self._page_control and self._page_control.isVisible():
1235 1235 self._page_control.setFocus()
1236 1236 intercepted = True
1237 1237
1238 1238 elif key == QtCore.Qt.Key_U:
1239 1239 if self._in_buffer(position):
1240 1240 cursor.clearSelection()
1241 1241 start_line = cursor.blockNumber()
1242 1242 if start_line == self._get_prompt_cursor().blockNumber():
1243 1243 offset = len(self._prompt)
1244 1244 else:
1245 1245 offset = len(self._continuation_prompt)
1246 1246 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1247 1247 QtGui.QTextCursor.KeepAnchor)
1248 1248 cursor.movePosition(QtGui.QTextCursor.Right,
1249 1249 QtGui.QTextCursor.KeepAnchor, offset)
1250 1250 self._kill_ring.kill_cursor(cursor)
1251 1251 self._set_cursor(cursor)
1252 1252 intercepted = True
1253 1253
1254 1254 elif key == QtCore.Qt.Key_Y:
1255 1255 self._keep_cursor_in_buffer()
1256 1256 self._kill_ring.yank()
1257 1257 intercepted = True
1258 1258
1259 1259 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1260 1260 if key == QtCore.Qt.Key_Backspace:
1261 1261 cursor = self._get_word_start_cursor(position)
1262 1262 else: # key == QtCore.Qt.Key_Delete
1263 1263 cursor = self._get_word_end_cursor(position)
1264 1264 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1265 1265 self._kill_ring.kill_cursor(cursor)
1266 1266 intercepted = True
1267 1267
1268 1268 elif key == QtCore.Qt.Key_D:
1269 1269 if len(self.input_buffer) == 0:
1270 1270 self.exit_requested.emit(self)
1271 1271 else:
1272 1272 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1273 1273 QtCore.Qt.Key_Delete,
1274 1274 QtCore.Qt.NoModifier)
1275 1275 QtGui.qApp.sendEvent(self._control, new_event)
1276 1276 intercepted = True
1277 1277
1278 1278 #------ Alt modifier ---------------------------------------------------
1279 1279
1280 1280 elif alt_down:
1281 1281 if key == QtCore.Qt.Key_B:
1282 1282 self._set_cursor(self._get_word_start_cursor(position))
1283 1283 intercepted = True
1284 1284
1285 1285 elif key == QtCore.Qt.Key_F:
1286 1286 self._set_cursor(self._get_word_end_cursor(position))
1287 1287 intercepted = True
1288 1288
1289 1289 elif key == QtCore.Qt.Key_Y:
1290 1290 self._kill_ring.rotate()
1291 1291 intercepted = True
1292 1292
1293 1293 elif key == QtCore.Qt.Key_Backspace:
1294 1294 cursor = self._get_word_start_cursor(position)
1295 1295 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1296 1296 self._kill_ring.kill_cursor(cursor)
1297 1297 intercepted = True
1298 1298
1299 1299 elif key == QtCore.Qt.Key_D:
1300 1300 cursor = self._get_word_end_cursor(position)
1301 1301 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1302 1302 self._kill_ring.kill_cursor(cursor)
1303 1303 intercepted = True
1304 1304
1305 1305 elif key == QtCore.Qt.Key_Delete:
1306 1306 intercepted = True
1307 1307
1308 1308 elif key == QtCore.Qt.Key_Greater:
1309 1309 self._control.moveCursor(QtGui.QTextCursor.End)
1310 1310 intercepted = True
1311 1311
1312 1312 elif key == QtCore.Qt.Key_Less:
1313 1313 self._control.setTextCursor(self._get_prompt_cursor())
1314 1314 intercepted = True
1315 1315
1316 1316 #------ No modifiers ---------------------------------------------------
1317 1317
1318 1318 else:
1319 1319 if shift_down:
1320 1320 anchormode = QtGui.QTextCursor.KeepAnchor
1321 1321 else:
1322 1322 anchormode = QtGui.QTextCursor.MoveAnchor
1323 1323
1324 1324 if key == QtCore.Qt.Key_Escape:
1325 1325 self._keyboard_quit()
1326 1326 intercepted = True
1327 1327
1328 1328 elif key == QtCore.Qt.Key_Up:
1329 1329 if self._reading or not self._up_pressed(shift_down):
1330 1330 intercepted = True
1331 1331 else:
1332 1332 prompt_line = self._get_prompt_cursor().blockNumber()
1333 1333 intercepted = cursor.blockNumber() <= prompt_line
1334 1334
1335 1335 elif key == QtCore.Qt.Key_Down:
1336 1336 if self._reading or not self._down_pressed(shift_down):
1337 1337 intercepted = True
1338 1338 else:
1339 1339 end_line = self._get_end_cursor().blockNumber()
1340 1340 intercepted = cursor.blockNumber() == end_line
1341 1341
1342 1342 elif key == QtCore.Qt.Key_Tab:
1343 1343 if not self._reading:
1344 1344 if self._tab_pressed():
1345 1345 # real tab-key, insert four spaces
1346 1346 cursor.insertText(' '*4)
1347 1347 intercepted = True
1348 1348
1349 1349 elif key == QtCore.Qt.Key_Left:
1350 1350
1351 1351 # Move to the previous line
1352 1352 line, col = cursor.blockNumber(), cursor.columnNumber()
1353 1353 if line > self._get_prompt_cursor().blockNumber() and \
1354 1354 col == len(self._continuation_prompt):
1355 1355 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1356 1356 mode=anchormode)
1357 1357 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1358 1358 mode=anchormode)
1359 1359 intercepted = True
1360 1360
1361 1361 # Regular left movement
1362 1362 else:
1363 1363 intercepted = not self._in_buffer(position - 1)
1364 1364
1365 1365 elif key == QtCore.Qt.Key_Right:
1366 1366 original_block_number = cursor.blockNumber()
1367 1367 cursor.movePosition(QtGui.QTextCursor.Right,
1368 1368 mode=anchormode)
1369 1369 if cursor.blockNumber() != original_block_number:
1370 1370 cursor.movePosition(QtGui.QTextCursor.Right,
1371 1371 n=len(self._continuation_prompt),
1372 1372 mode=anchormode)
1373 1373 self._set_cursor(cursor)
1374 1374 intercepted = True
1375 1375
1376 1376 elif key == QtCore.Qt.Key_Home:
1377 1377 start_line = cursor.blockNumber()
1378 1378 if start_line == self._get_prompt_cursor().blockNumber():
1379 1379 start_pos = self._prompt_pos
1380 1380 else:
1381 1381 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1382 1382 QtGui.QTextCursor.KeepAnchor)
1383 1383 start_pos = cursor.position()
1384 1384 start_pos += len(self._continuation_prompt)
1385 1385 cursor.setPosition(position)
1386 1386 if shift_down and self._in_buffer(position):
1387 1387 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1388 1388 else:
1389 1389 cursor.setPosition(start_pos)
1390 1390 self._set_cursor(cursor)
1391 1391 intercepted = True
1392 1392
1393 1393 elif key == QtCore.Qt.Key_Backspace:
1394 1394
1395 1395 # Line deletion (remove continuation prompt)
1396 1396 line, col = cursor.blockNumber(), cursor.columnNumber()
1397 1397 if not self._reading and \
1398 1398 col == len(self._continuation_prompt) and \
1399 1399 line > self._get_prompt_cursor().blockNumber():
1400 1400 cursor.beginEditBlock()
1401 1401 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1402 1402 QtGui.QTextCursor.KeepAnchor)
1403 1403 cursor.removeSelectedText()
1404 1404 cursor.deletePreviousChar()
1405 1405 cursor.endEditBlock()
1406 1406 intercepted = True
1407 1407
1408 1408 # Regular backwards deletion
1409 1409 else:
1410 1410 anchor = cursor.anchor()
1411 1411 if anchor == position:
1412 1412 intercepted = not self._in_buffer(position - 1)
1413 1413 else:
1414 1414 intercepted = not self._in_buffer(min(anchor, position))
1415 1415
1416 1416 elif key == QtCore.Qt.Key_Delete:
1417 1417
1418 1418 # Line deletion (remove continuation prompt)
1419 1419 if not self._reading and self._in_buffer(position) and \
1420 1420 cursor.atBlockEnd() and not cursor.hasSelection():
1421 1421 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1422 1422 QtGui.QTextCursor.KeepAnchor)
1423 1423 cursor.movePosition(QtGui.QTextCursor.Right,
1424 1424 QtGui.QTextCursor.KeepAnchor,
1425 1425 len(self._continuation_prompt))
1426 1426 cursor.removeSelectedText()
1427 1427 intercepted = True
1428 1428
1429 1429 # Regular forwards deletion:
1430 1430 else:
1431 1431 anchor = cursor.anchor()
1432 1432 intercepted = (not self._in_buffer(anchor) or
1433 1433 not self._in_buffer(position))
1434 1434
1435 1435 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1436 1436 # using the keyboard in any part of the buffer. Also, permit scrolling
1437 1437 # with Page Up/Down keys. Finally, if we're executing, don't move the
1438 1438 # cursor (if even this made sense, we can't guarantee that the prompt
1439 1439 # position is still valid due to text truncation).
1440 1440 if not (self._control_key_down(event.modifiers(), include_command=True)
1441 1441 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1442 1442 or (self._executing and not self._reading)):
1443 1443 self._keep_cursor_in_buffer()
1444 1444
1445 1445 return intercepted
1446 1446
1447 1447 def _event_filter_page_keypress(self, event):
1448 1448 """ Filter key events for the paging widget to create console-like
1449 1449 interface.
1450 1450 """
1451 1451 key = event.key()
1452 1452 ctrl_down = self._control_key_down(event.modifiers())
1453 1453 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1454 1454
1455 1455 if ctrl_down:
1456 1456 if key == QtCore.Qt.Key_O:
1457 1457 self._control.setFocus()
1458 1458 intercept = True
1459 1459
1460 1460 elif alt_down:
1461 1461 if key == QtCore.Qt.Key_Greater:
1462 1462 self._page_control.moveCursor(QtGui.QTextCursor.End)
1463 1463 intercepted = True
1464 1464
1465 1465 elif key == QtCore.Qt.Key_Less:
1466 1466 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1467 1467 intercepted = True
1468 1468
1469 1469 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1470 1470 if self._splitter:
1471 1471 self._page_control.hide()
1472 1472 self._control.setFocus()
1473 1473 else:
1474 1474 self.layout().setCurrentWidget(self._control)
1475 1475 return True
1476 1476
1477 1477 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1478 1478 QtCore.Qt.Key_Tab):
1479 1479 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1480 1480 QtCore.Qt.Key_PageDown,
1481 1481 QtCore.Qt.NoModifier)
1482 1482 QtGui.qApp.sendEvent(self._page_control, new_event)
1483 1483 return True
1484 1484
1485 1485 elif key == QtCore.Qt.Key_Backspace:
1486 1486 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1487 1487 QtCore.Qt.Key_PageUp,
1488 1488 QtCore.Qt.NoModifier)
1489 1489 QtGui.qApp.sendEvent(self._page_control, new_event)
1490 1490 return True
1491 1491
1492 1492 return False
1493 1493
1494 1494 def _on_flush_pending_stream_timer(self):
1495 1495 """ Flush the pending stream output and change the
1496 1496 prompt position appropriately.
1497 1497 """
1498 1498 cursor = self._control.textCursor()
1499 1499 cursor.movePosition(QtGui.QTextCursor.End)
1500 1500 pos = cursor.position()
1501 1501 self._flush_pending_stream()
1502 1502 cursor.movePosition(QtGui.QTextCursor.End)
1503 1503 diff = cursor.position() - pos
1504 1504 if diff > 0:
1505 1505 self._prompt_pos += diff
1506 1506 self._append_before_prompt_pos += diff
1507 1507
1508 1508 def _flush_pending_stream(self):
1509 1509 """ Flush out pending text into the widget. """
1510 1510 text = self._pending_insert_text
1511 1511 self._pending_insert_text = []
1512 1512 buffer_size = self._control.document().maximumBlockCount()
1513 1513 if buffer_size > 0:
1514 1514 text = self._get_last_lines_from_list(text, buffer_size)
1515 1515 text = ''.join(text)
1516 1516 t = time.time()
1517 1517 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1518 1518 # Set the flush interval to equal the maximum time to update text.
1519 1519 self._pending_text_flush_interval.setInterval(max(100,
1520 1520 (time.time()-t)*1000))
1521 1521
1522 1522 def _format_as_columns(self, items, separator=' '):
1523 1523 """ Transform a list of strings into a single string with columns.
1524 1524
1525 1525 Parameters
1526 1526 ----------
1527 1527 items : sequence of strings
1528 1528 The strings to process.
1529 1529
1530 1530 separator : str, optional [default is two spaces]
1531 1531 The string that separates columns.
1532 1532
1533 1533 Returns
1534 1534 -------
1535 1535 The formatted string.
1536 1536 """
1537 1537 # Calculate the number of characters available.
1538 1538 width = self._control.viewport().width()
1539 1539 char_width = QtGui.QFontMetrics(self.font).width(' ')
1540 1540 displaywidth = max(10, (width / char_width) - 1)
1541 1541
1542 1542 return columnize(items, separator, displaywidth)
1543 1543
1544 1544 def _get_block_plain_text(self, block):
1545 1545 """ Given a QTextBlock, return its unformatted text.
1546 1546 """
1547 1547 cursor = QtGui.QTextCursor(block)
1548 1548 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1549 1549 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1550 1550 QtGui.QTextCursor.KeepAnchor)
1551 1551 return cursor.selection().toPlainText()
1552 1552
1553 1553 def _get_cursor(self):
1554 1554 """ Convenience method that returns a cursor for the current position.
1555 1555 """
1556 1556 return self._control.textCursor()
1557 1557
1558 1558 def _get_end_cursor(self):
1559 1559 """ Convenience method that returns a cursor for the last character.
1560 1560 """
1561 1561 cursor = self._control.textCursor()
1562 1562 cursor.movePosition(QtGui.QTextCursor.End)
1563 1563 return cursor
1564 1564
1565 1565 def _get_input_buffer_cursor_column(self):
1566 1566 """ Returns the column of the cursor in the input buffer, excluding the
1567 1567 contribution by the prompt, or -1 if there is no such column.
1568 1568 """
1569 1569 prompt = self._get_input_buffer_cursor_prompt()
1570 1570 if prompt is None:
1571 1571 return -1
1572 1572 else:
1573 1573 cursor = self._control.textCursor()
1574 1574 return cursor.columnNumber() - len(prompt)
1575 1575
1576 1576 def _get_input_buffer_cursor_line(self):
1577 1577 """ Returns the text of the line of the input buffer that contains the
1578 1578 cursor, or None if there is no such line.
1579 1579 """
1580 1580 prompt = self._get_input_buffer_cursor_prompt()
1581 1581 if prompt is None:
1582 1582 return None
1583 1583 else:
1584 1584 cursor = self._control.textCursor()
1585 1585 text = self._get_block_plain_text(cursor.block())
1586 1586 return text[len(prompt):]
1587
1587
1588 def _get_input_buffer_cursor_pos(self):
1589 """Return the cursor position within the input buffer."""
1590 cursor = self._control.textCursor()
1591 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1592 input_buffer = cursor.selection().toPlainText()
1593
1594 # Don't count continuation prompts
1595 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1596
1588 1597 def _get_input_buffer_cursor_prompt(self):
1589 1598 """ Returns the (plain text) prompt for line of the input buffer that
1590 1599 contains the cursor, or None if there is no such line.
1591 1600 """
1592 1601 if self._executing:
1593 1602 return None
1594 1603 cursor = self._control.textCursor()
1595 1604 if cursor.position() >= self._prompt_pos:
1596 1605 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1597 1606 return self._prompt
1598 1607 else:
1599 1608 return self._continuation_prompt
1600 1609 else:
1601 1610 return None
1602 1611
1603 1612 def _get_last_lines(self, text, num_lines, return_count=False):
1604 1613 """ Return last specified number of lines of text (like `tail -n`).
1605 1614 If return_count is True, returns a tuple of clipped text and the
1606 1615 number of lines in the clipped text.
1607 1616 """
1608 1617 pos = len(text)
1609 1618 if pos < num_lines:
1610 1619 if return_count:
1611 1620 return text, text.count('\n') if return_count else text
1612 1621 else:
1613 1622 return text
1614 1623 i = 0
1615 1624 while i < num_lines:
1616 1625 pos = text.rfind('\n', None, pos)
1617 1626 if pos == -1:
1618 1627 pos = None
1619 1628 break
1620 1629 i += 1
1621 1630 if return_count:
1622 1631 return text[pos:], i
1623 1632 else:
1624 1633 return text[pos:]
1625 1634
1626 1635 def _get_last_lines_from_list(self, text_list, num_lines):
1627 1636 """ Return the list of text clipped to last specified lines.
1628 1637 """
1629 1638 ret = []
1630 1639 lines_pending = num_lines
1631 1640 for text in reversed(text_list):
1632 1641 text, lines_added = self._get_last_lines(text, lines_pending,
1633 1642 return_count=True)
1634 1643 ret.append(text)
1635 1644 lines_pending -= lines_added
1636 1645 if lines_pending <= 0:
1637 1646 break
1638 1647 return ret[::-1]
1639 1648
1640 1649 def _get_prompt_cursor(self):
1641 1650 """ Convenience method that returns a cursor for the prompt position.
1642 1651 """
1643 1652 cursor = self._control.textCursor()
1644 1653 cursor.setPosition(self._prompt_pos)
1645 1654 return cursor
1646 1655
1647 1656 def _get_selection_cursor(self, start, end):
1648 1657 """ Convenience method that returns a cursor with text selected between
1649 1658 the positions 'start' and 'end'.
1650 1659 """
1651 1660 cursor = self._control.textCursor()
1652 1661 cursor.setPosition(start)
1653 1662 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1654 1663 return cursor
1655 1664
1656 1665 def _get_word_start_cursor(self, position):
1657 1666 """ Find the start of the word to the left the given position. If a
1658 1667 sequence of non-word characters precedes the first word, skip over
1659 1668 them. (This emulates the behavior of bash, emacs, etc.)
1660 1669 """
1661 1670 document = self._control.document()
1662 1671 position -= 1
1663 1672 while position >= self._prompt_pos and \
1664 1673 not is_letter_or_number(document.characterAt(position)):
1665 1674 position -= 1
1666 1675 while position >= self._prompt_pos and \
1667 1676 is_letter_or_number(document.characterAt(position)):
1668 1677 position -= 1
1669 1678 cursor = self._control.textCursor()
1670 1679 cursor.setPosition(position + 1)
1671 1680 return cursor
1672 1681
1673 1682 def _get_word_end_cursor(self, position):
1674 1683 """ Find the end of the word to the right the given position. If a
1675 1684 sequence of non-word characters precedes the first word, skip over
1676 1685 them. (This emulates the behavior of bash, emacs, etc.)
1677 1686 """
1678 1687 document = self._control.document()
1679 1688 end = self._get_end_cursor().position()
1680 1689 while position < end and \
1681 1690 not is_letter_or_number(document.characterAt(position)):
1682 1691 position += 1
1683 1692 while position < end and \
1684 1693 is_letter_or_number(document.characterAt(position)):
1685 1694 position += 1
1686 1695 cursor = self._control.textCursor()
1687 1696 cursor.setPosition(position)
1688 1697 return cursor
1689 1698
1690 1699 def _insert_continuation_prompt(self, cursor):
1691 1700 """ Inserts new continuation prompt using the specified cursor.
1692 1701 """
1693 1702 if self._continuation_prompt_html is None:
1694 1703 self._insert_plain_text(cursor, self._continuation_prompt)
1695 1704 else:
1696 1705 self._continuation_prompt = self._insert_html_fetching_plain_text(
1697 1706 cursor, self._continuation_prompt_html)
1698 1707
1699 1708 def _insert_block(self, cursor, block_format=None):
1700 1709 """ Inserts an empty QTextBlock using the specified cursor.
1701 1710 """
1702 1711 if block_format is None:
1703 1712 block_format = QtGui.QTextBlockFormat()
1704 1713 cursor.insertBlock(block_format)
1705 1714
1706 1715 def _insert_html(self, cursor, html):
1707 1716 """ Inserts HTML using the specified cursor in such a way that future
1708 1717 formatting is unaffected.
1709 1718 """
1710 1719 cursor.beginEditBlock()
1711 1720 cursor.insertHtml(html)
1712 1721
1713 1722 # After inserting HTML, the text document "remembers" it's in "html
1714 1723 # mode", which means that subsequent calls adding plain text will result
1715 1724 # in unwanted formatting, lost tab characters, etc. The following code
1716 1725 # hacks around this behavior, which I consider to be a bug in Qt, by
1717 1726 # (crudely) resetting the document's style state.
1718 1727 cursor.movePosition(QtGui.QTextCursor.Left,
1719 1728 QtGui.QTextCursor.KeepAnchor)
1720 1729 if cursor.selection().toPlainText() == ' ':
1721 1730 cursor.removeSelectedText()
1722 1731 else:
1723 1732 cursor.movePosition(QtGui.QTextCursor.Right)
1724 1733 cursor.insertText(' ', QtGui.QTextCharFormat())
1725 1734 cursor.endEditBlock()
1726 1735
1727 1736 def _insert_html_fetching_plain_text(self, cursor, html):
1728 1737 """ Inserts HTML using the specified cursor, then returns its plain text
1729 1738 version.
1730 1739 """
1731 1740 cursor.beginEditBlock()
1732 1741 cursor.removeSelectedText()
1733 1742
1734 1743 start = cursor.position()
1735 1744 self._insert_html(cursor, html)
1736 1745 end = cursor.position()
1737 1746 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1738 1747 text = cursor.selection().toPlainText()
1739 1748
1740 1749 cursor.setPosition(end)
1741 1750 cursor.endEditBlock()
1742 1751 return text
1743 1752
1744 1753 def _insert_plain_text(self, cursor, text, flush=False):
1745 1754 """ Inserts plain text using the specified cursor, processing ANSI codes
1746 1755 if enabled.
1747 1756 """
1748 1757 # maximumBlockCount() can be different from self.buffer_size in
1749 1758 # case input prompt is active.
1750 1759 buffer_size = self._control.document().maximumBlockCount()
1751 1760
1752 1761 if self._executing and not flush and \
1753 1762 self._pending_text_flush_interval.isActive():
1754 1763 self._pending_insert_text.append(text)
1755 1764 if buffer_size > 0:
1756 1765 self._pending_insert_text = self._get_last_lines_from_list(
1757 1766 self._pending_insert_text, buffer_size)
1758 1767 return
1759 1768
1760 1769 if self._executing and not self._pending_text_flush_interval.isActive():
1761 1770 self._pending_text_flush_interval.start()
1762 1771
1763 1772 # Clip the text to last `buffer_size` lines.
1764 1773 if buffer_size > 0:
1765 1774 text = self._get_last_lines(text, buffer_size)
1766 1775
1767 1776 cursor.beginEditBlock()
1768 1777 if self.ansi_codes:
1769 1778 for substring in self._ansi_processor.split_string(text):
1770 1779 for act in self._ansi_processor.actions:
1771 1780
1772 1781 # Unlike real terminal emulators, we don't distinguish
1773 1782 # between the screen and the scrollback buffer. A screen
1774 1783 # erase request clears everything.
1775 1784 if act.action == 'erase' and act.area == 'screen':
1776 1785 cursor.select(QtGui.QTextCursor.Document)
1777 1786 cursor.removeSelectedText()
1778 1787
1779 1788 # Simulate a form feed by scrolling just past the last line.
1780 1789 elif act.action == 'scroll' and act.unit == 'page':
1781 1790 cursor.insertText('\n')
1782 1791 cursor.endEditBlock()
1783 1792 self._set_top_cursor(cursor)
1784 1793 cursor.joinPreviousEditBlock()
1785 1794 cursor.deletePreviousChar()
1786 1795
1787 1796 elif act.action == 'carriage-return':
1788 1797 cursor.movePosition(
1789 1798 cursor.StartOfLine, cursor.KeepAnchor)
1790 1799
1791 1800 elif act.action == 'beep':
1792 1801 QtGui.qApp.beep()
1793 1802
1794 1803 elif act.action == 'backspace':
1795 1804 if not cursor.atBlockStart():
1796 1805 cursor.movePosition(
1797 1806 cursor.PreviousCharacter, cursor.KeepAnchor)
1798 1807
1799 1808 elif act.action == 'newline':
1800 1809 cursor.movePosition(cursor.EndOfLine)
1801 1810
1802 1811 format = self._ansi_processor.get_format()
1803 1812
1804 1813 selection = cursor.selectedText()
1805 1814 if len(selection) == 0:
1806 1815 cursor.insertText(substring, format)
1807 1816 elif substring is not None:
1808 1817 # BS and CR are treated as a change in print
1809 1818 # position, rather than a backwards character
1810 1819 # deletion for output equivalence with (I)Python
1811 1820 # terminal.
1812 1821 if len(substring) >= len(selection):
1813 1822 cursor.insertText(substring, format)
1814 1823 else:
1815 1824 old_text = selection[len(substring):]
1816 1825 cursor.insertText(substring + old_text, format)
1817 1826 cursor.movePosition(cursor.PreviousCharacter,
1818 1827 cursor.KeepAnchor, len(old_text))
1819 1828 else:
1820 1829 cursor.insertText(text)
1821 1830 cursor.endEditBlock()
1822 1831
1823 1832 def _insert_plain_text_into_buffer(self, cursor, text):
1824 1833 """ Inserts text into the input buffer using the specified cursor (which
1825 1834 must be in the input buffer), ensuring that continuation prompts are
1826 1835 inserted as necessary.
1827 1836 """
1828 1837 lines = text.splitlines(True)
1829 1838 if lines:
1830 1839 cursor.beginEditBlock()
1831 1840 cursor.insertText(lines[0])
1832 1841 for line in lines[1:]:
1833 1842 if self._continuation_prompt_html is None:
1834 1843 cursor.insertText(self._continuation_prompt)
1835 1844 else:
1836 1845 self._continuation_prompt = \
1837 1846 self._insert_html_fetching_plain_text(
1838 1847 cursor, self._continuation_prompt_html)
1839 1848 cursor.insertText(line)
1840 1849 cursor.endEditBlock()
1841 1850
1842 1851 def _in_buffer(self, position=None):
1843 1852 """ Returns whether the current cursor (or, if specified, a position) is
1844 1853 inside the editing region.
1845 1854 """
1846 1855 cursor = self._control.textCursor()
1847 1856 if position is None:
1848 1857 position = cursor.position()
1849 1858 else:
1850 1859 cursor.setPosition(position)
1851 1860 line = cursor.blockNumber()
1852 1861 prompt_line = self._get_prompt_cursor().blockNumber()
1853 1862 if line == prompt_line:
1854 1863 return position >= self._prompt_pos
1855 1864 elif line > prompt_line:
1856 1865 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1857 1866 prompt_pos = cursor.position() + len(self._continuation_prompt)
1858 1867 return position >= prompt_pos
1859 1868 return False
1860 1869
1861 1870 def _keep_cursor_in_buffer(self):
1862 1871 """ Ensures that the cursor is inside the editing region. Returns
1863 1872 whether the cursor was moved.
1864 1873 """
1865 1874 moved = not self._in_buffer()
1866 1875 if moved:
1867 1876 cursor = self._control.textCursor()
1868 1877 cursor.movePosition(QtGui.QTextCursor.End)
1869 1878 self._control.setTextCursor(cursor)
1870 1879 return moved
1871 1880
1872 1881 def _keyboard_quit(self):
1873 1882 """ Cancels the current editing task ala Ctrl-G in Emacs.
1874 1883 """
1875 1884 if self._temp_buffer_filled :
1876 1885 self._cancel_completion()
1877 1886 self._clear_temporary_buffer()
1878 1887 else:
1879 1888 self.input_buffer = ''
1880 1889
1881 1890 def _page(self, text, html=False):
1882 1891 """ Displays text using the pager if it exceeds the height of the
1883 1892 viewport.
1884 1893
1885 1894 Parameters
1886 1895 ----------
1887 1896 html : bool, optional (default False)
1888 1897 If set, the text will be interpreted as HTML instead of plain text.
1889 1898 """
1890 1899 line_height = QtGui.QFontMetrics(self.font).height()
1891 1900 minlines = self._control.viewport().height() / line_height
1892 1901 if self.paging != 'none' and \
1893 1902 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1894 1903 if self.paging == 'custom':
1895 1904 self.custom_page_requested.emit(text)
1896 1905 else:
1897 1906 self._page_control.clear()
1898 1907 cursor = self._page_control.textCursor()
1899 1908 if html:
1900 1909 self._insert_html(cursor, text)
1901 1910 else:
1902 1911 self._insert_plain_text(cursor, text)
1903 1912 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1904 1913
1905 1914 self._page_control.viewport().resize(self._control.size())
1906 1915 if self._splitter:
1907 1916 self._page_control.show()
1908 1917 self._page_control.setFocus()
1909 1918 else:
1910 1919 self.layout().setCurrentWidget(self._page_control)
1911 1920 elif html:
1912 1921 self._append_html(text)
1913 1922 else:
1914 1923 self._append_plain_text(text)
1915 1924
1916 1925 def _set_paging(self, paging):
1917 1926 """
1918 1927 Change the pager to `paging` style.
1919 1928
1920 1929 Parameters
1921 1930 ----------
1922 1931 paging : string
1923 1932 Either "hsplit", "vsplit", or "inside"
1924 1933 """
1925 1934 if self._splitter is None:
1926 1935 raise NotImplementedError("""can only switch if --paging=hsplit or
1927 1936 --paging=vsplit is used.""")
1928 1937 if paging == 'hsplit':
1929 1938 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1930 1939 elif paging == 'vsplit':
1931 1940 self._splitter.setOrientation(QtCore.Qt.Vertical)
1932 1941 elif paging == 'inside':
1933 1942 raise NotImplementedError("""switching to 'inside' paging not
1934 1943 supported yet.""")
1935 1944 else:
1936 1945 raise ValueError("unknown paging method '%s'" % paging)
1937 1946 self.paging = paging
1938 1947
1939 1948 def _prompt_finished(self):
1940 1949 """ Called immediately after a prompt is finished, i.e. when some input
1941 1950 will be processed and a new prompt displayed.
1942 1951 """
1943 1952 self._control.setReadOnly(True)
1944 1953 self._prompt_finished_hook()
1945 1954
1946 1955 def _prompt_started(self):
1947 1956 """ Called immediately after a new prompt is displayed.
1948 1957 """
1949 1958 # Temporarily disable the maximum block count to permit undo/redo and
1950 1959 # to ensure that the prompt position does not change due to truncation.
1951 1960 self._control.document().setMaximumBlockCount(0)
1952 1961 self._control.setUndoRedoEnabled(True)
1953 1962
1954 1963 # Work around bug in QPlainTextEdit: input method is not re-enabled
1955 1964 # when read-only is disabled.
1956 1965 self._control.setReadOnly(False)
1957 1966 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1958 1967
1959 1968 if not self._reading:
1960 1969 self._executing = False
1961 1970 self._prompt_started_hook()
1962 1971
1963 1972 # If the input buffer has changed while executing, load it.
1964 1973 if self._input_buffer_pending:
1965 1974 self.input_buffer = self._input_buffer_pending
1966 1975 self._input_buffer_pending = ''
1967 1976
1968 1977 self._control.moveCursor(QtGui.QTextCursor.End)
1969 1978
1970 1979 def _readline(self, prompt='', callback=None):
1971 1980 """ Reads one line of input from the user.
1972 1981
1973 1982 Parameters
1974 1983 ----------
1975 1984 prompt : str, optional
1976 1985 The prompt to print before reading the line.
1977 1986
1978 1987 callback : callable, optional
1979 1988 A callback to execute with the read line. If not specified, input is
1980 1989 read *synchronously* and this method does not return until it has
1981 1990 been read.
1982 1991
1983 1992 Returns
1984 1993 -------
1985 1994 If a callback is specified, returns nothing. Otherwise, returns the
1986 1995 input string with the trailing newline stripped.
1987 1996 """
1988 1997 if self._reading:
1989 1998 raise RuntimeError('Cannot read a line. Widget is already reading.')
1990 1999
1991 2000 if not callback and not self.isVisible():
1992 2001 # If the user cannot see the widget, this function cannot return.
1993 2002 raise RuntimeError('Cannot synchronously read a line if the widget '
1994 2003 'is not visible!')
1995 2004
1996 2005 self._reading = True
1997 2006 self._show_prompt(prompt, newline=False)
1998 2007
1999 2008 if callback is None:
2000 2009 self._reading_callback = None
2001 2010 while self._reading:
2002 2011 QtCore.QCoreApplication.processEvents()
2003 2012 return self._get_input_buffer(force=True).rstrip('\n')
2004 2013
2005 2014 else:
2006 2015 self._reading_callback = lambda: \
2007 2016 callback(self._get_input_buffer(force=True).rstrip('\n'))
2008 2017
2009 2018 def _set_continuation_prompt(self, prompt, html=False):
2010 2019 """ Sets the continuation prompt.
2011 2020
2012 2021 Parameters
2013 2022 ----------
2014 2023 prompt : str
2015 2024 The prompt to show when more input is needed.
2016 2025
2017 2026 html : bool, optional (default False)
2018 2027 If set, the prompt will be inserted as formatted HTML. Otherwise,
2019 2028 the prompt will be treated as plain text, though ANSI color codes
2020 2029 will be handled.
2021 2030 """
2022 2031 if html:
2023 2032 self._continuation_prompt_html = prompt
2024 2033 else:
2025 2034 self._continuation_prompt = prompt
2026 2035 self._continuation_prompt_html = None
2027 2036
2028 2037 def _set_cursor(self, cursor):
2029 2038 """ Convenience method to set the current cursor.
2030 2039 """
2031 2040 self._control.setTextCursor(cursor)
2032 2041
2033 2042 def _set_top_cursor(self, cursor):
2034 2043 """ Scrolls the viewport so that the specified cursor is at the top.
2035 2044 """
2036 2045 scrollbar = self._control.verticalScrollBar()
2037 2046 scrollbar.setValue(scrollbar.maximum())
2038 2047 original_cursor = self._control.textCursor()
2039 2048 self._control.setTextCursor(cursor)
2040 2049 self._control.ensureCursorVisible()
2041 2050 self._control.setTextCursor(original_cursor)
2042 2051
2043 2052 def _show_prompt(self, prompt=None, html=False, newline=True):
2044 2053 """ Writes a new prompt at the end of the buffer.
2045 2054
2046 2055 Parameters
2047 2056 ----------
2048 2057 prompt : str, optional
2049 2058 The prompt to show. If not specified, the previous prompt is used.
2050 2059
2051 2060 html : bool, optional (default False)
2052 2061 Only relevant when a prompt is specified. If set, the prompt will
2053 2062 be inserted as formatted HTML. Otherwise, the prompt will be treated
2054 2063 as plain text, though ANSI color codes will be handled.
2055 2064
2056 2065 newline : bool, optional (default True)
2057 2066 If set, a new line will be written before showing the prompt if
2058 2067 there is not already a newline at the end of the buffer.
2059 2068 """
2060 2069 # Save the current end position to support _append*(before_prompt=True).
2061 2070 self._flush_pending_stream()
2062 2071 cursor = self._get_end_cursor()
2063 2072 self._append_before_prompt_pos = cursor.position()
2064 2073
2065 2074 # Insert a preliminary newline, if necessary.
2066 2075 if newline and cursor.position() > 0:
2067 2076 cursor.movePosition(QtGui.QTextCursor.Left,
2068 2077 QtGui.QTextCursor.KeepAnchor)
2069 2078 if cursor.selection().toPlainText() != '\n':
2070 2079 self._append_block()
2071 2080 self._append_before_prompt_pos += 1
2072 2081
2073 2082 # Write the prompt.
2074 2083 self._append_plain_text(self._prompt_sep)
2075 2084 if prompt is None:
2076 2085 if self._prompt_html is None:
2077 2086 self._append_plain_text(self._prompt)
2078 2087 else:
2079 2088 self._append_html(self._prompt_html)
2080 2089 else:
2081 2090 if html:
2082 2091 self._prompt = self._append_html_fetching_plain_text(prompt)
2083 2092 self._prompt_html = prompt
2084 2093 else:
2085 2094 self._append_plain_text(prompt)
2086 2095 self._prompt = prompt
2087 2096 self._prompt_html = None
2088 2097
2089 2098 self._prompt_pos = self._get_end_cursor().position()
2090 2099 self._prompt_started()
2091 2100
2092 2101 #------ Signal handlers ----------------------------------------------------
2093 2102
2094 2103 def _adjust_scrollbars(self):
2095 2104 """ Expands the vertical scrollbar beyond the range set by Qt.
2096 2105 """
2097 2106 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2098 2107 # and qtextedit.cpp.
2099 2108 document = self._control.document()
2100 2109 scrollbar = self._control.verticalScrollBar()
2101 2110 viewport_height = self._control.viewport().height()
2102 2111 if isinstance(self._control, QtGui.QPlainTextEdit):
2103 2112 maximum = max(0, document.lineCount() - 1)
2104 2113 step = viewport_height / self._control.fontMetrics().lineSpacing()
2105 2114 else:
2106 2115 # QTextEdit does not do line-based layout and blocks will not in
2107 2116 # general have the same height. Therefore it does not make sense to
2108 2117 # attempt to scroll in line height increments.
2109 2118 maximum = document.size().height()
2110 2119 step = viewport_height
2111 2120 diff = maximum - scrollbar.maximum()
2112 2121 scrollbar.setRange(0, maximum)
2113 2122 scrollbar.setPageStep(step)
2114 2123
2115 2124 # Compensate for undesirable scrolling that occurs automatically due to
2116 2125 # maximumBlockCount() text truncation.
2117 2126 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2118 2127 scrollbar.setValue(scrollbar.value() + diff)
2119 2128
2120 2129 def _custom_context_menu_requested(self, pos):
2121 2130 """ Shows a context menu at the given QPoint (in widget coordinates).
2122 2131 """
2123 2132 menu = self._context_menu_make(pos)
2124 2133 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,838 +1,830 b''
1 1 """Frontend widget for the Qt Console"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 from collections import namedtuple
9 9 import sys
10 10 import uuid
11 11
12 12 from IPython.external import qt
13 13 from IPython.external.qt import QtCore, QtGui
14 14 from IPython.utils import py3compat
15 15 from IPython.utils.importstring import import_item
16 16
17 17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 18 from IPython.core.inputtransformer import classic_prompt
19 19 from IPython.core.oinspect import call_tip
20 20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 22 from .bracket_matcher import BracketMatcher
23 23 from .call_tip_widget import CallTipWidget
24 24 from .completion_lexer import CompletionLexer
25 25 from .history_console_widget import HistoryConsoleWidget
26 26 from .pygments_highlighter import PygmentsHighlighter
27 27
28 28
29 29 class FrontendHighlighter(PygmentsHighlighter):
30 30 """ A PygmentsHighlighter that understands and ignores prompts.
31 31 """
32 32
33 33 def __init__(self, frontend, lexer=None):
34 34 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
35 35 self._current_offset = 0
36 36 self._frontend = frontend
37 37 self.highlighting_on = False
38 38
39 39 def highlightBlock(self, string):
40 40 """ Highlight a block of text. Reimplemented to highlight selectively.
41 41 """
42 42 if not self.highlighting_on:
43 43 return
44 44
45 45 # The input to this function is a unicode string that may contain
46 46 # paragraph break characters, non-breaking spaces, etc. Here we acquire
47 47 # the string as plain text so we can compare it.
48 48 current_block = self.currentBlock()
49 49 string = self._frontend._get_block_plain_text(current_block)
50 50
51 51 # Decide whether to check for the regular or continuation prompt.
52 52 if current_block.contains(self._frontend._prompt_pos):
53 53 prompt = self._frontend._prompt
54 54 else:
55 55 prompt = self._frontend._continuation_prompt
56 56
57 57 # Only highlight if we can identify a prompt, but make sure not to
58 58 # highlight the prompt.
59 59 if string.startswith(prompt):
60 60 self._current_offset = len(prompt)
61 61 string = string[len(prompt):]
62 62 super(FrontendHighlighter, self).highlightBlock(string)
63 63
64 64 def rehighlightBlock(self, block):
65 65 """ Reimplemented to temporarily enable highlighting if disabled.
66 66 """
67 67 old = self.highlighting_on
68 68 self.highlighting_on = True
69 69 super(FrontendHighlighter, self).rehighlightBlock(block)
70 70 self.highlighting_on = old
71 71
72 72 def setFormat(self, start, count, format):
73 73 """ Reimplemented to highlight selectively.
74 74 """
75 75 start += self._current_offset
76 76 super(FrontendHighlighter, self).setFormat(start, count, format)
77 77
78 78
79 79 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
80 80 """ A Qt frontend for a generic Python kernel.
81 81 """
82 82
83 83 # The text to show when the kernel is (re)started.
84 84 banner = Unicode(config=True)
85 85
86 86 # An option and corresponding signal for overriding the default kernel
87 87 # interrupt behavior.
88 88 custom_interrupt = Bool(False)
89 89 custom_interrupt_requested = QtCore.Signal()
90 90
91 91 # An option and corresponding signals for overriding the default kernel
92 92 # restart behavior.
93 93 custom_restart = Bool(False)
94 94 custom_restart_kernel_died = QtCore.Signal(float)
95 95 custom_restart_requested = QtCore.Signal()
96 96
97 97 # Whether to automatically show calltips on open-parentheses.
98 98 enable_calltips = Bool(True, config=True,
99 99 help="Whether to draw information calltips on open-parentheses.")
100 100
101 101 clear_on_kernel_restart = Bool(True, config=True,
102 102 help="Whether to clear the console when the kernel is restarted")
103 103
104 104 confirm_restart = Bool(True, config=True,
105 105 help="Whether to ask for user confirmation when restarting kernel")
106 106
107 107 lexer_class = DottedObjectName(config=True,
108 108 help="The pygments lexer class to use."
109 109 )
110 110 def _lexer_class_changed(self, name, old, new):
111 111 lexer_class = import_item(new)
112 112 self.lexer = lexer_class()
113 113
114 114 def _lexer_class_default(self):
115 115 if py3compat.PY3:
116 116 return 'pygments.lexers.Python3Lexer'
117 117 else:
118 118 return 'pygments.lexers.PythonLexer'
119 119
120 120 lexer = Any()
121 121 def _lexer_default(self):
122 122 lexer_class = import_item(self.lexer_class)
123 123 return lexer_class()
124 124
125 125 # Emitted when a user visible 'execute_request' has been submitted to the
126 126 # kernel from the FrontendWidget. Contains the code to be executed.
127 127 executing = QtCore.Signal(object)
128 128
129 129 # Emitted when a user-visible 'execute_reply' has been received from the
130 130 # kernel and processed by the FrontendWidget. Contains the response message.
131 131 executed = QtCore.Signal(object)
132 132
133 133 # Emitted when an exit request has been received from the kernel.
134 134 exit_requested = QtCore.Signal(object)
135 135
136 136 # Protected class variables.
137 137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 138 logical_line_transforms=[],
139 139 python_line_transforms=[],
140 140 )
141 141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 144 _input_splitter_class = InputSplitter
145 145 _local_kernel = False
146 146 _highlighter = Instance(FrontendHighlighter)
147 147
148 148 #---------------------------------------------------------------------------
149 149 # 'object' interface
150 150 #---------------------------------------------------------------------------
151 151
152 152 def __init__(self, *args, **kw):
153 153 super(FrontendWidget, self).__init__(*args, **kw)
154 154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 156 if qt.QT_API == qt.QT_API_PYSIDE:
157 157 import PySide
158 158 if PySide.__version_info__ < (1,0,7):
159 159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 160 self.enable_calltips = False
161 161
162 162 # FrontendWidget protected variables.
163 163 self._bracket_matcher = BracketMatcher(self._control)
164 164 self._call_tip_widget = CallTipWidget(self._control)
165 165 self._completion_lexer = CompletionLexer(self.lexer)
166 166 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
167 167 self._hidden = False
168 168 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
169 169 self._input_splitter = self._input_splitter_class()
170 170 self._kernel_manager = None
171 171 self._kernel_client = None
172 172 self._request_info = {}
173 173 self._request_info['execute'] = {};
174 174 self._callback_dict = {}
175 175
176 176 # Configure the ConsoleWidget.
177 177 self.tab_width = 4
178 178 self._set_continuation_prompt('... ')
179 179
180 180 # Configure the CallTipWidget.
181 181 self._call_tip_widget.setFont(self.font)
182 182 self.font_changed.connect(self._call_tip_widget.setFont)
183 183
184 184 # Configure actions.
185 185 action = self._copy_raw_action
186 186 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
187 187 action.setEnabled(False)
188 188 action.setShortcut(QtGui.QKeySequence(key))
189 189 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
190 190 action.triggered.connect(self.copy_raw)
191 191 self.copy_available.connect(action.setEnabled)
192 192 self.addAction(action)
193 193
194 194 # Connect signal handlers.
195 195 document = self._control.document()
196 196 document.contentsChange.connect(self._document_contents_change)
197 197
198 198 # Set flag for whether we are connected via localhost.
199 199 self._local_kernel = kw.get('local_kernel',
200 200 FrontendWidget._local_kernel)
201 201
202 202 # Whether or not a clear_output call is pending new output.
203 203 self._pending_clearoutput = False
204 204
205 205 #---------------------------------------------------------------------------
206 206 # 'ConsoleWidget' public interface
207 207 #---------------------------------------------------------------------------
208 208
209 209 def copy(self):
210 210 """ Copy the currently selected text to the clipboard, removing prompts.
211 211 """
212 212 if self._page_control is not None and self._page_control.hasFocus():
213 213 self._page_control.copy()
214 214 elif self._control.hasFocus():
215 215 text = self._control.textCursor().selection().toPlainText()
216 216 if text:
217 217 text = self._prompt_transformer.transform_cell(text)
218 218 QtGui.QApplication.clipboard().setText(text)
219 219 else:
220 220 self.log.debug("frontend widget : unknown copy target")
221 221
222 222 #---------------------------------------------------------------------------
223 223 # 'ConsoleWidget' abstract interface
224 224 #---------------------------------------------------------------------------
225 225
226 226 def _is_complete(self, source, interactive):
227 227 """ Returns whether 'source' can be completely processed and a new
228 228 prompt created. When triggered by an Enter/Return key press,
229 229 'interactive' is True; otherwise, it is False.
230 230 """
231 231 self._input_splitter.reset()
232 232 try:
233 233 complete = self._input_splitter.push(source)
234 234 except SyntaxError:
235 235 return True
236 236 if interactive:
237 237 complete = not self._input_splitter.push_accepts_more()
238 238 return complete
239 239
240 240 def _execute(self, source, hidden):
241 241 """ Execute 'source'. If 'hidden', do not show any output.
242 242
243 243 See parent class :meth:`execute` docstring for full details.
244 244 """
245 245 msg_id = self.kernel_client.execute(source, hidden)
246 246 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
247 247 self._hidden = hidden
248 248 if not hidden:
249 249 self.executing.emit(source)
250 250
251 251 def _prompt_started_hook(self):
252 252 """ Called immediately after a new prompt is displayed.
253 253 """
254 254 if not self._reading:
255 255 self._highlighter.highlighting_on = True
256 256
257 257 def _prompt_finished_hook(self):
258 258 """ Called immediately after a prompt is finished, i.e. when some input
259 259 will be processed and a new prompt displayed.
260 260 """
261 261 # Flush all state from the input splitter so the next round of
262 262 # reading input starts with a clean buffer.
263 263 self._input_splitter.reset()
264 264
265 265 if not self._reading:
266 266 self._highlighter.highlighting_on = False
267 267
268 268 def _tab_pressed(self):
269 269 """ Called when the tab key is pressed. Returns whether to continue
270 270 processing the event.
271 271 """
272 272 # Perform tab completion if:
273 273 # 1) The cursor is in the input buffer.
274 274 # 2) There is a non-whitespace character before the cursor.
275 275 text = self._get_input_buffer_cursor_line()
276 276 if text is None:
277 277 return False
278 278 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
279 279 if complete:
280 280 self._complete()
281 281 return not complete
282 282
283 283 #---------------------------------------------------------------------------
284 284 # 'ConsoleWidget' protected interface
285 285 #---------------------------------------------------------------------------
286 286
287 287 def _context_menu_make(self, pos):
288 288 """ Reimplemented to add an action for raw copy.
289 289 """
290 290 menu = super(FrontendWidget, self)._context_menu_make(pos)
291 291 for before_action in menu.actions():
292 292 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
293 293 QtGui.QKeySequence.ExactMatch:
294 294 menu.insertAction(before_action, self._copy_raw_action)
295 295 break
296 296 return menu
297 297
298 298 def request_interrupt_kernel(self):
299 299 if self._executing:
300 300 self.interrupt_kernel()
301 301
302 302 def request_restart_kernel(self):
303 303 message = 'Are you sure you want to restart the kernel?'
304 304 self.restart_kernel(message, now=False)
305 305
306 306 def _event_filter_console_keypress(self, event):
307 307 """ Reimplemented for execution interruption and smart backspace.
308 308 """
309 309 key = event.key()
310 310 if self._control_key_down(event.modifiers(), include_command=False):
311 311
312 312 if key == QtCore.Qt.Key_C and self._executing:
313 313 self.request_interrupt_kernel()
314 314 return True
315 315
316 316 elif key == QtCore.Qt.Key_Period:
317 317 self.request_restart_kernel()
318 318 return True
319 319
320 320 elif not event.modifiers() & QtCore.Qt.AltModifier:
321 321
322 322 # Smart backspace: remove four characters in one backspace if:
323 323 # 1) everything left of the cursor is whitespace
324 324 # 2) the four characters immediately left of the cursor are spaces
325 325 if key == QtCore.Qt.Key_Backspace:
326 326 col = self._get_input_buffer_cursor_column()
327 327 cursor = self._control.textCursor()
328 328 if col > 3 and not cursor.hasSelection():
329 329 text = self._get_input_buffer_cursor_line()[:col]
330 330 if text.endswith(' ') and not text.strip():
331 331 cursor.movePosition(QtGui.QTextCursor.Left,
332 332 QtGui.QTextCursor.KeepAnchor, 4)
333 333 cursor.removeSelectedText()
334 334 return True
335 335
336 336 return super(FrontendWidget, self)._event_filter_console_keypress(event)
337 337
338 338 def _insert_continuation_prompt(self, cursor):
339 339 """ Reimplemented for auto-indentation.
340 340 """
341 341 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
342 342 cursor.insertText(' ' * self._input_splitter.indent_spaces)
343 343
344 344 #---------------------------------------------------------------------------
345 345 # 'BaseFrontendMixin' abstract interface
346 346 #---------------------------------------------------------------------------
347 347 def _handle_clear_output(self, msg):
348 348 """Handle clear output messages."""
349 349 if not self._hidden and self._is_from_this_session(msg):
350 350 wait = msg['content'].get('wait', True)
351 351 if wait:
352 352 self._pending_clearoutput = True
353 353 else:
354 354 self.clear_output()
355 355
356 356 def _handle_complete_reply(self, rep):
357 357 """ Handle replies for tab completion.
358 358 """
359 359 self.log.debug("complete: %s", rep.get('content', ''))
360 360 cursor = self._get_cursor()
361 361 info = self._request_info.get('complete')
362 362 if info and info.id == rep['parent_header']['msg_id'] and \
363 363 info.pos == cursor.position():
364 364 text = '.'.join(self._get_context())
365 365 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
366 366 self._complete_with_items(cursor, rep['content']['matches'])
367 367
368 368 def _silent_exec_callback(self, expr, callback):
369 369 """Silently execute `expr` in the kernel and call `callback` with reply
370 370
371 371 the `expr` is evaluated silently in the kernel (without) output in
372 372 the frontend. Call `callback` with the
373 373 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
374 374
375 375 Parameters
376 376 ----------
377 377 expr : string
378 378 valid string to be executed by the kernel.
379 379 callback : function
380 380 function accepting one argument, as a string. The string will be
381 381 the `repr` of the result of evaluating `expr`
382 382
383 383 The `callback` is called with the `repr()` of the result of `expr` as
384 384 first argument. To get the object, do `eval()` on the passed value.
385 385
386 386 See Also
387 387 --------
388 388 _handle_exec_callback : private method, deal with calling callback with reply
389 389
390 390 """
391 391
392 392 # generate uuid, which would be used as an indication of whether or
393 393 # not the unique request originated from here (can use msg id ?)
394 394 local_uuid = str(uuid.uuid1())
395 395 msg_id = self.kernel_client.execute('',
396 396 silent=True, user_expressions={ local_uuid:expr })
397 397 self._callback_dict[local_uuid] = callback
398 398 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
399 399
400 400 def _handle_exec_callback(self, msg):
401 401 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
402 402
403 403 Parameters
404 404 ----------
405 405 msg : raw message send by the kernel containing an `user_expressions`
406 406 and having a 'silent_exec_callback' kind.
407 407
408 408 Notes
409 409 -----
410 410 This function will look for a `callback` associated with the
411 411 corresponding message id. Association has been made by
412 412 `_silent_exec_callback`. `callback` is then called with the `repr()`
413 413 of the value of corresponding `user_expressions` as argument.
414 414 `callback` is then removed from the known list so that any message
415 415 coming again with the same id won't trigger it.
416 416
417 417 """
418 418
419 419 user_exp = msg['content'].get('user_expressions')
420 420 if not user_exp:
421 421 return
422 422 for expression in user_exp:
423 423 if expression in self._callback_dict:
424 424 self._callback_dict.pop(expression)(user_exp[expression])
425 425
426 426 def _handle_execute_reply(self, msg):
427 427 """ Handles replies for code execution.
428 428 """
429 429 self.log.debug("execute: %s", msg.get('content', ''))
430 430 msg_id = msg['parent_header']['msg_id']
431 431 info = self._request_info['execute'].get(msg_id)
432 432 # unset reading flag, because if execute finished, raw_input can't
433 433 # still be pending.
434 434 self._reading = False
435 435 if info and info.kind == 'user' and not self._hidden:
436 436 # Make sure that all output from the SUB channel has been processed
437 437 # before writing a new prompt.
438 438 self.kernel_client.iopub_channel.flush()
439 439
440 440 # Reset the ANSI style information to prevent bad text in stdout
441 441 # from messing up our colors. We're not a true terminal so we're
442 442 # allowed to do this.
443 443 if self.ansi_codes:
444 444 self._ansi_processor.reset_sgr()
445 445
446 446 content = msg['content']
447 447 status = content['status']
448 448 if status == 'ok':
449 449 self._process_execute_ok(msg)
450 450 elif status == 'error':
451 451 self._process_execute_error(msg)
452 452 elif status == 'aborted':
453 453 self._process_execute_abort(msg)
454 454
455 455 self._show_interpreter_prompt_for_reply(msg)
456 456 self.executed.emit(msg)
457 457 self._request_info['execute'].pop(msg_id)
458 458 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
459 459 self._handle_exec_callback(msg)
460 460 self._request_info['execute'].pop(msg_id)
461 461 else:
462 462 super(FrontendWidget, self)._handle_execute_reply(msg)
463 463
464 464 def _handle_input_request(self, msg):
465 465 """ Handle requests for raw_input.
466 466 """
467 467 self.log.debug("input: %s", msg.get('content', ''))
468 468 if self._hidden:
469 469 raise RuntimeError('Request for raw input during hidden execution.')
470 470
471 471 # Make sure that all output from the SUB channel has been processed
472 472 # before entering readline mode.
473 473 self.kernel_client.iopub_channel.flush()
474 474
475 475 def callback(line):
476 476 self.kernel_client.stdin_channel.input(line)
477 477 if self._reading:
478 478 self.log.debug("Got second input request, assuming first was interrupted.")
479 479 self._reading = False
480 480 self._readline(msg['content']['prompt'], callback=callback)
481 481
482 482 def _kernel_restarted_message(self, died=True):
483 483 msg = "Kernel died, restarting" if died else "Kernel restarting"
484 484 self._append_html("<br>%s<hr><br>" % msg,
485 485 before_prompt=False
486 486 )
487 487
488 488 def _handle_kernel_died(self, since_last_heartbeat):
489 489 """Handle the kernel's death (if we do not own the kernel).
490 490 """
491 491 self.log.warn("kernel died: %s", since_last_heartbeat)
492 492 if self.custom_restart:
493 493 self.custom_restart_kernel_died.emit(since_last_heartbeat)
494 494 else:
495 495 self._kernel_restarted_message(died=True)
496 496 self.reset()
497 497
498 498 def _handle_kernel_restarted(self, died=True):
499 499 """Notice that the autorestarter restarted the kernel.
500 500
501 501 There's nothing to do but show a message.
502 502 """
503 503 self.log.warn("kernel restarted")
504 504 self._kernel_restarted_message(died=died)
505 505 self.reset()
506 506
507 507 def _handle_object_info_reply(self, rep):
508 508 """ Handle replies for call tips.
509 509 """
510 510 self.log.debug("oinfo: %s", rep.get('content', ''))
511 511 cursor = self._get_cursor()
512 512 info = self._request_info.get('call_tip')
513 513 if info and info.id == rep['parent_header']['msg_id'] and \
514 514 info.pos == cursor.position():
515 515 # Get the information for a call tip. For now we format the call
516 516 # line as string, later we can pass False to format_call and
517 517 # syntax-highlight it ourselves for nicer formatting in the
518 518 # calltip.
519 519 content = rep['content']
520 520 # if this is from pykernel, 'docstring' will be the only key
521 521 if content.get('ismagic', False):
522 522 # Don't generate a call-tip for magics. Ideally, we should
523 523 # generate a tooltip, but not on ( like we do for actual
524 524 # callables.
525 525 call_info, doc = None, None
526 526 else:
527 527 call_info, doc = call_tip(content, format_call=True)
528 528 if call_info or doc:
529 529 self._call_tip_widget.show_call_info(call_info, doc)
530 530
531 531 def _handle_execute_result(self, msg):
532 532 """ Handle display hook output.
533 533 """
534 534 self.log.debug("execute_result: %s", msg.get('content', ''))
535 535 if not self._hidden and self._is_from_this_session(msg):
536 536 self.flush_clearoutput()
537 537 text = msg['content']['data']
538 538 self._append_plain_text(text + '\n', before_prompt=True)
539 539
540 540 def _handle_stream(self, msg):
541 541 """ Handle stdout, stderr, and stdin.
542 542 """
543 543 self.log.debug("stream: %s", msg.get('content', ''))
544 544 if not self._hidden and self._is_from_this_session(msg):
545 545 self.flush_clearoutput()
546 546 self.append_stream(msg['content']['data'])
547 547
548 548 def _handle_shutdown_reply(self, msg):
549 549 """ Handle shutdown signal, only if from other console.
550 550 """
551 551 self.log.warn("shutdown: %s", msg.get('content', ''))
552 552 restart = msg.get('content', {}).get('restart', False)
553 553 if not self._hidden and not self._is_from_this_session(msg):
554 554 # got shutdown reply, request came from session other than ours
555 555 if restart:
556 556 # someone restarted the kernel, handle it
557 557 self._handle_kernel_restarted(died=False)
558 558 else:
559 559 # kernel was shutdown permanently
560 560 # this triggers exit_requested if the kernel was local,
561 561 # and a dialog if the kernel was remote,
562 562 # so we don't suddenly clear the qtconsole without asking.
563 563 if self._local_kernel:
564 564 self.exit_requested.emit(self)
565 565 else:
566 566 title = self.window().windowTitle()
567 567 reply = QtGui.QMessageBox.question(self, title,
568 568 "Kernel has been shutdown permanently. "
569 569 "Close the Console?",
570 570 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
571 571 if reply == QtGui.QMessageBox.Yes:
572 572 self.exit_requested.emit(self)
573 573
574 574 def _handle_status(self, msg):
575 575 """Handle status message"""
576 576 # This is where a busy/idle indicator would be triggered,
577 577 # when we make one.
578 578 state = msg['content'].get('execution_state', '')
579 579 if state == 'starting':
580 580 # kernel started while we were running
581 581 if self._executing:
582 582 self._handle_kernel_restarted(died=True)
583 583 elif state == 'idle':
584 584 pass
585 585 elif state == 'busy':
586 586 pass
587 587
588 588 def _started_channels(self):
589 589 """ Called when the KernelManager channels have started listening or
590 590 when the frontend is assigned an already listening KernelManager.
591 591 """
592 592 self.reset(clear=True)
593 593
594 594 #---------------------------------------------------------------------------
595 595 # 'FrontendWidget' public interface
596 596 #---------------------------------------------------------------------------
597 597
598 598 def copy_raw(self):
599 599 """ Copy the currently selected text to the clipboard without attempting
600 600 to remove prompts or otherwise alter the text.
601 601 """
602 602 self._control.copy()
603 603
604 604 def execute_file(self, path, hidden=False):
605 605 """ Attempts to execute file with 'path'. If 'hidden', no output is
606 606 shown.
607 607 """
608 608 self.execute('execfile(%r)' % path, hidden=hidden)
609 609
610 610 def interrupt_kernel(self):
611 611 """ Attempts to interrupt the running kernel.
612 612
613 613 Also unsets _reading flag, to avoid runtime errors
614 614 if raw_input is called again.
615 615 """
616 616 if self.custom_interrupt:
617 617 self._reading = False
618 618 self.custom_interrupt_requested.emit()
619 619 elif self.kernel_manager:
620 620 self._reading = False
621 621 self.kernel_manager.interrupt_kernel()
622 622 else:
623 623 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
624 624
625 625 def reset(self, clear=False):
626 626 """ Resets the widget to its initial state if ``clear`` parameter
627 627 is True, otherwise
628 628 prints a visual indication of the fact that the kernel restarted, but
629 629 does not clear the traces from previous usage of the kernel before it
630 630 was restarted. With ``clear=True``, it is similar to ``%clear``, but
631 631 also re-writes the banner and aborts execution if necessary.
632 632 """
633 633 if self._executing:
634 634 self._executing = False
635 635 self._request_info['execute'] = {}
636 636 self._reading = False
637 637 self._highlighter.highlighting_on = False
638 638
639 639 if clear:
640 640 self._control.clear()
641 641 self._append_plain_text(self.banner)
642 642 # update output marker for stdout/stderr, so that startup
643 643 # messages appear after banner:
644 644 self._append_before_prompt_pos = self._get_cursor().position()
645 645 self._show_interpreter_prompt()
646 646
647 647 def restart_kernel(self, message, now=False):
648 648 """ Attempts to restart the running kernel.
649 649 """
650 650 # FIXME: now should be configurable via a checkbox in the dialog. Right
651 651 # now at least the heartbeat path sets it to True and the manual restart
652 652 # to False. But those should just be the pre-selected states of a
653 653 # checkbox that the user could override if so desired. But I don't know
654 654 # enough Qt to go implementing the checkbox now.
655 655
656 656 if self.custom_restart:
657 657 self.custom_restart_requested.emit()
658 658 return
659 659
660 660 if self.kernel_manager:
661 661 # Pause the heart beat channel to prevent further warnings.
662 662 self.kernel_client.hb_channel.pause()
663 663
664 664 # Prompt the user to restart the kernel. Un-pause the heartbeat if
665 665 # they decline. (If they accept, the heartbeat will be un-paused
666 666 # automatically when the kernel is restarted.)
667 667 if self.confirm_restart:
668 668 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
669 669 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
670 670 message, buttons)
671 671 do_restart = result == QtGui.QMessageBox.Yes
672 672 else:
673 673 # confirm_restart is False, so we don't need to ask user
674 674 # anything, just do the restart
675 675 do_restart = True
676 676 if do_restart:
677 677 try:
678 678 self.kernel_manager.restart_kernel(now=now)
679 679 except RuntimeError as e:
680 680 self._append_plain_text(
681 681 'Error restarting kernel: %s\n' % e,
682 682 before_prompt=True
683 683 )
684 684 else:
685 685 self._append_html("<br>Restarting kernel...\n<hr><br>",
686 686 before_prompt=True,
687 687 )
688 688 else:
689 689 self.kernel_client.hb_channel.unpause()
690 690
691 691 else:
692 692 self._append_plain_text(
693 693 'Cannot restart a Kernel I did not start\n',
694 694 before_prompt=True
695 695 )
696 696
697 697 def append_stream(self, text):
698 698 """Appends text to the output stream."""
699 699 # Most consoles treat tabs as being 8 space characters. Convert tabs
700 700 # to spaces so that output looks as expected regardless of this
701 701 # widget's tab width.
702 702 text = text.expandtabs(8)
703 703 self._append_plain_text(text, before_prompt=True)
704 704 self._control.moveCursor(QtGui.QTextCursor.End)
705 705
706 706 def flush_clearoutput(self):
707 707 """If a clearoutput is pending, execute it."""
708 708 if self._pending_clearoutput:
709 709 self._pending_clearoutput = False
710 710 self.clear_output()
711 711
712 712 def clear_output(self):
713 713 """Clears the current line of output."""
714 714 cursor = self._control.textCursor()
715 715 cursor.beginEditBlock()
716 716 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
717 717 cursor.insertText('')
718 718 cursor.endEditBlock()
719 719
720 720 #---------------------------------------------------------------------------
721 721 # 'FrontendWidget' protected interface
722 722 #---------------------------------------------------------------------------
723 723
724 724 def _call_tip(self):
725 725 """ Shows a call tip, if appropriate, at the current cursor location.
726 726 """
727 727 # Decide if it makes sense to show a call tip
728 728 if not self.enable_calltips:
729 729 return False
730 cursor = self._get_cursor()
731 cursor.movePosition(QtGui.QTextCursor.Left)
732 if cursor.document().characterAt(cursor.position()) != '(':
733 return False
734 context = self._get_context(cursor)
735 if not context:
736 return False
737
730 cursor_pos = self._get_input_buffer_cursor_pos()
731 code = self.input_buffer
738 732 # Send the metadata request to the kernel
739 name = '.'.join(context)
740 msg_id = self.kernel_client.object_info(name)
733 msg_id = self.kernel_client.object_info(code, cursor_pos)
741 734 pos = self._get_cursor().position()
742 735 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
743 736 return True
744 737
745 738 def _complete(self):
746 739 """ Performs completion at the current cursor location.
747 740 """
748 741 context = self._get_context()
749 742 if context:
750 743 # Send the completion request to the kernel
751 744 msg_id = self.kernel_client.complete(
752 '.'.join(context), # text
753 self._get_input_buffer_cursor_line(), # line
754 self._get_input_buffer_cursor_column(), # cursor_pos
755 self.input_buffer) # block
745 code=self.input_buffer,
746 cursor_pos=self._get_input_buffer_cursor_pos(),
747 )
756 748 pos = self._get_cursor().position()
757 749 info = self._CompletionRequest(msg_id, pos)
758 750 self._request_info['complete'] = info
759 751
760 752 def _get_context(self, cursor=None):
761 753 """ Gets the context for the specified cursor (or the current cursor
762 754 if none is specified).
763 755 """
764 756 if cursor is None:
765 757 cursor = self._get_cursor()
766 758 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
767 759 QtGui.QTextCursor.KeepAnchor)
768 760 text = cursor.selection().toPlainText()
769 761 return self._completion_lexer.get_context(text)
770 762
771 763 def _process_execute_abort(self, msg):
772 764 """ Process a reply for an aborted execution request.
773 765 """
774 766 self._append_plain_text("ERROR: execution aborted\n")
775 767
776 768 def _process_execute_error(self, msg):
777 769 """ Process a reply for an execution request that resulted in an error.
778 770 """
779 771 content = msg['content']
780 772 # If a SystemExit is passed along, this means exit() was called - also
781 773 # all the ipython %exit magic syntax of '-k' to be used to keep
782 774 # the kernel running
783 775 if content['ename']=='SystemExit':
784 776 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
785 777 self._keep_kernel_on_exit = keepkernel
786 778 self.exit_requested.emit(self)
787 779 else:
788 780 traceback = ''.join(content['traceback'])
789 781 self._append_plain_text(traceback)
790 782
791 783 def _process_execute_ok(self, msg):
792 784 """ Process a reply for a successful execution request.
793 785 """
794 786 payload = msg['content']['payload']
795 787 for item in payload:
796 788 if not self._process_execute_payload(item):
797 789 warning = 'Warning: received unknown payload of type %s'
798 790 print(warning % repr(item['source']))
799 791
800 792 def _process_execute_payload(self, item):
801 793 """ Process a single payload item from the list of payload items in an
802 794 execution reply. Returns whether the payload was handled.
803 795 """
804 796 # The basic FrontendWidget doesn't handle payloads, as they are a
805 797 # mechanism for going beyond the standard Python interpreter model.
806 798 return False
807 799
808 800 def _show_interpreter_prompt(self):
809 801 """ Shows a prompt for the interpreter.
810 802 """
811 803 self._show_prompt('>>> ')
812 804
813 805 def _show_interpreter_prompt_for_reply(self, msg):
814 806 """ Shows a prompt for the interpreter given an 'execute_reply' message.
815 807 """
816 808 self._show_interpreter_prompt()
817 809
818 810 #------ Signal handlers ----------------------------------------------------
819 811
820 812 def _document_contents_change(self, position, removed, added):
821 813 """ Called whenever the document's content changes. Display a call tip
822 814 if appropriate.
823 815 """
824 816 # Calculate where the cursor should be *after* the change:
825 817 position += added
826 818
827 819 document = self._control.document()
828 820 if position == self._get_cursor().position():
829 821 self._call_tip()
830 822
831 823 #------ Trait default initializers -----------------------------------------
832 824
833 825 def _banner_default(self):
834 826 """ Returns the standard Python banner.
835 827 """
836 828 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
837 829 '"license" for more information.'
838 830 return banner % (sys.version, sys.platform)
@@ -1,590 +1,572 b''
1 1 """A FrontendWidget that emulates the interface of the console IPython.
2 2
3 3 This supports the additional functionality provided by the IPython kernel.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 from collections import namedtuple
10 10 import os.path
11 11 import re
12 12 from subprocess import Popen
13 13 import sys
14 14 import time
15 15 from textwrap import dedent
16 16
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 from IPython.core.inputsplitter import IPythonInputSplitter
20 20 from IPython.core.inputtransformer import ipy_prompt
21 21 from IPython.utils.traitlets import Bool, Unicode
22 22 from .frontend_widget import FrontendWidget
23 23 from . import styles
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Constants
27 27 #-----------------------------------------------------------------------------
28 28
29 29 # Default strings to build and display input and output prompts (and separators
30 30 # in between)
31 31 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
32 32 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
33 33 default_input_sep = '\n'
34 34 default_output_sep = ''
35 35 default_output_sep2 = ''
36 36
37 37 # Base path for most payload sources.
38 38 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
39 39
40 40 if sys.platform.startswith('win'):
41 41 default_editor = 'notepad'
42 42 else:
43 43 default_editor = ''
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # IPythonWidget class
47 47 #-----------------------------------------------------------------------------
48 48
49 49 class IPythonWidget(FrontendWidget):
50 50 """ A FrontendWidget for an IPython kernel.
51 51 """
52 52
53 53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 55 # settings.
56 56 custom_edit = Bool(False)
57 57 custom_edit_requested = QtCore.Signal(object, object)
58 58
59 59 editor = Unicode(default_editor, config=True,
60 60 help="""
61 61 A command for invoking a system text editor. If the string contains a
62 62 {filename} format specifier, it will be used. Otherwise, the filename
63 63 will be appended to the end the command.
64 64 """)
65 65
66 66 editor_line = Unicode(config=True,
67 67 help="""
68 68 The editor command to use when a specific line number is requested. The
69 69 string should contain two format specifiers: {line} and {filename}. If
70 70 this parameter is not specified, the line number option to the %edit
71 71 magic will be ignored.
72 72 """)
73 73
74 74 style_sheet = Unicode(config=True,
75 75 help="""
76 76 A CSS stylesheet. The stylesheet can contain classes for:
77 77 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
78 78 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
79 79 3. IPython: .error, .in-prompt, .out-prompt, etc
80 80 """)
81 81
82 82 syntax_style = Unicode(config=True,
83 83 help="""
84 84 If not empty, use this Pygments style for syntax highlighting.
85 85 Otherwise, the style sheet is queried for Pygments style
86 86 information.
87 87 """)
88 88
89 89 # Prompts.
90 90 in_prompt = Unicode(default_in_prompt, config=True)
91 91 out_prompt = Unicode(default_out_prompt, config=True)
92 92 input_sep = Unicode(default_input_sep, config=True)
93 93 output_sep = Unicode(default_output_sep, config=True)
94 94 output_sep2 = Unicode(default_output_sep2, config=True)
95 95
96 96 # FrontendWidget protected class variables.
97 97 _input_splitter_class = IPythonInputSplitter
98 98 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
99 99 logical_line_transforms=[],
100 100 python_line_transforms=[],
101 101 )
102 102
103 103 # IPythonWidget protected class variables.
104 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 105 _payload_source_edit = 'edit_magic'
106 106 _payload_source_exit = 'ask_exit'
107 107 _payload_source_next_input = 'set_next_input'
108 108 _payload_source_page = 'page'
109 109 _retrying_history_request = False
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(IPythonWidget, self).__init__(*args, **kw)
117 117
118 118 # IPythonWidget protected variables.
119 119 self._payload_handlers = {
120 120 self._payload_source_edit : self._handle_payload_edit,
121 121 self._payload_source_exit : self._handle_payload_exit,
122 122 self._payload_source_page : self._handle_payload_page,
123 123 self._payload_source_next_input : self._handle_payload_next_input }
124 124 self._previous_prompt_obj = None
125 125 self._keep_kernel_on_exit = None
126 126
127 127 # Initialize widget styling.
128 128 if self.style_sheet:
129 129 self._style_sheet_changed()
130 130 self._syntax_style_changed()
131 131 else:
132 132 self.set_default_style()
133 133
134 134 self._guiref_loaded = False
135 135
136 136 #---------------------------------------------------------------------------
137 137 # 'BaseFrontendMixin' abstract interface
138 138 #---------------------------------------------------------------------------
139 139 def _handle_complete_reply(self, rep):
140 140 """ Reimplemented to support IPython's improved completion machinery.
141 141 """
142 142 self.log.debug("complete: %s", rep.get('content', ''))
143 143 cursor = self._get_cursor()
144 144 info = self._request_info.get('complete')
145 145 if info and info.id == rep['parent_header']['msg_id'] and \
146 146 info.pos == cursor.position():
147 147 matches = rep['content']['matches']
148 148 text = rep['content']['matched_text']
149 149 offset = len(text)
150 150
151 151 # Clean up matches with period and path separators if the matched
152 152 # text has not been transformed. This is done by truncating all
153 153 # but the last component and then suitably decreasing the offset
154 154 # between the current cursor position and the start of completion.
155 155 if len(matches) > 1 and matches[0][:offset] == text:
156 156 parts = re.split(r'[./\\]', text)
157 157 sep_count = len(parts) - 1
158 158 if sep_count:
159 159 chop_length = sum(map(len, parts[:sep_count])) + sep_count
160 160 matches = [ match[chop_length:] for match in matches ]
161 161 offset -= chop_length
162 162
163 163 # Move the cursor to the start of the match and complete.
164 164 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
165 165 self._complete_with_items(cursor, matches)
166 166
167 167 def _handle_execute_reply(self, msg):
168 168 """ Reimplemented to support prompt requests.
169 169 """
170 170 msg_id = msg['parent_header'].get('msg_id')
171 171 info = self._request_info['execute'].get(msg_id)
172 172 if info and info.kind == 'prompt':
173 173 content = msg['content']
174 174 if content['status'] == 'aborted':
175 175 self._show_interpreter_prompt()
176 176 else:
177 177 number = content['execution_count'] + 1
178 178 self._show_interpreter_prompt(number)
179 179 self._request_info['execute'].pop(msg_id)
180 180 else:
181 181 super(IPythonWidget, self)._handle_execute_reply(msg)
182 182
183 183 def _handle_history_reply(self, msg):
184 184 """ Implemented to handle history tail replies, which are only supported
185 185 by the IPython kernel.
186 186 """
187 187 content = msg['content']
188 188 if 'history' not in content:
189 189 self.log.error("History request failed: %r"%content)
190 190 if content.get('status', '') == 'aborted' and \
191 191 not self._retrying_history_request:
192 192 # a *different* action caused this request to be aborted, so
193 193 # we should try again.
194 194 self.log.error("Retrying aborted history request")
195 195 # prevent multiple retries of aborted requests:
196 196 self._retrying_history_request = True
197 197 # wait out the kernel's queue flush, which is currently timed at 0.1s
198 198 time.sleep(0.25)
199 199 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
200 200 else:
201 201 self._retrying_history_request = False
202 202 return
203 203 # reset retry flag
204 204 self._retrying_history_request = False
205 205 history_items = content['history']
206 206 self.log.debug("Received history reply with %i entries", len(history_items))
207 207 items = []
208 208 last_cell = u""
209 209 for _, _, cell in history_items:
210 210 cell = cell.rstrip()
211 211 if cell != last_cell:
212 212 items.append(cell)
213 213 last_cell = cell
214 214 self._set_history(items)
215 215
216 216 def _handle_execute_result(self, msg):
217 217 """ Reimplemented for IPython-style "display hook".
218 218 """
219 219 self.log.debug("execute_result: %s", msg.get('content', ''))
220 220 if not self._hidden and self._is_from_this_session(msg):
221 221 self.flush_clearoutput()
222 222 content = msg['content']
223 223 prompt_number = content.get('execution_count', 0)
224 224 data = content['data']
225 225 if 'text/plain' in data:
226 226 self._append_plain_text(self.output_sep, True)
227 227 self._append_html(self._make_out_prompt(prompt_number), True)
228 228 text = data['text/plain']
229 229 # If the repr is multiline, make sure we start on a new line,
230 230 # so that its lines are aligned.
231 231 if "\n" in text and not self.output_sep.endswith("\n"):
232 232 self._append_plain_text('\n', True)
233 233 self._append_plain_text(text + self.output_sep2, True)
234 234
235 235 def _handle_display_data(self, msg):
236 236 """ The base handler for the ``display_data`` message.
237 237 """
238 238 self.log.debug("display: %s", msg.get('content', ''))
239 239 # For now, we don't display data from other frontends, but we
240 240 # eventually will as this allows all frontends to monitor the display
241 241 # data. But we need to figure out how to handle this in the GUI.
242 242 if not self._hidden and self._is_from_this_session(msg):
243 243 self.flush_clearoutput()
244 244 source = msg['content']['source']
245 245 data = msg['content']['data']
246 246 metadata = msg['content']['metadata']
247 247 # In the regular IPythonWidget, we simply print the plain text
248 248 # representation.
249 249 if 'text/plain' in data:
250 250 text = data['text/plain']
251 251 self._append_plain_text(text, True)
252 252 # This newline seems to be needed for text and html output.
253 253 self._append_plain_text(u'\n', True)
254 254
255 255 def _handle_kernel_info_reply(self, rep):
256 256 """ Handle kernel info replies.
257 257 """
258 258 if not self._guiref_loaded:
259 259 if rep['content'].get('language') == 'python':
260 260 self._load_guiref_magic()
261 261 self._guiref_loaded = True
262 262
263 263 def _started_channels(self):
264 264 """Reimplemented to make a history request and load %guiref."""
265 265 super(IPythonWidget, self)._started_channels()
266 266
267 267 # The reply will trigger %guiref load provided language=='python'
268 268 self.kernel_client.kernel_info()
269 269
270 270 self.kernel_client.shell_channel.history(hist_access_type='tail',
271 271 n=1000)
272 272
273 273 def _started_kernel(self):
274 274 """Load %guiref when the kernel starts (if channels are also started).
275 275
276 276 Principally triggered by kernel restart.
277 277 """
278 278 if self.kernel_client.shell_channel is not None:
279 279 self._load_guiref_magic()
280 280
281 281 def _load_guiref_magic(self):
282 282 """Load %guiref magic."""
283 283 self.kernel_client.shell_channel.execute('\n'.join([
284 284 "try:",
285 285 " _usage",
286 286 "except:",
287 287 " from IPython.core import usage as _usage",
288 288 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
289 289 " del _usage",
290 290 ]), silent=True)
291 291
292 292 #---------------------------------------------------------------------------
293 293 # 'ConsoleWidget' public interface
294 294 #---------------------------------------------------------------------------
295 295
296 296 #---------------------------------------------------------------------------
297 297 # 'FrontendWidget' public interface
298 298 #---------------------------------------------------------------------------
299 299
300 300 def execute_file(self, path, hidden=False):
301 301 """ Reimplemented to use the 'run' magic.
302 302 """
303 303 # Use forward slashes on Windows to avoid escaping each separator.
304 304 if sys.platform == 'win32':
305 305 path = os.path.normpath(path).replace('\\', '/')
306 306
307 307 # Perhaps we should not be using %run directly, but while we
308 308 # are, it is necessary to quote or escape filenames containing spaces
309 309 # or quotes.
310 310
311 311 # In earlier code here, to minimize escaping, we sometimes quoted the
312 312 # filename with single quotes. But to do this, this code must be
313 313 # platform-aware, because run uses shlex rather than python string
314 314 # parsing, so that:
315 315 # * In Win: single quotes can be used in the filename without quoting,
316 316 # and we cannot use single quotes to quote the filename.
317 317 # * In *nix: we can escape double quotes in a double quoted filename,
318 318 # but can't escape single quotes in a single quoted filename.
319 319
320 320 # So to keep this code non-platform-specific and simple, we now only
321 321 # use double quotes to quote filenames, and escape when needed:
322 322 if ' ' in path or "'" in path or '"' in path:
323 323 path = '"%s"' % path.replace('"', '\\"')
324 324 self.execute('%%run %s' % path, hidden=hidden)
325 325
326 326 #---------------------------------------------------------------------------
327 327 # 'FrontendWidget' protected interface
328 328 #---------------------------------------------------------------------------
329 329
330 def _complete(self):
331 """ Reimplemented to support IPython's improved completion machinery.
332 """
333 # We let the kernel split the input line, so we *always* send an empty
334 # text field. Readline-based frontends do get a real text field which
335 # they can use.
336 text = ''
337
338 # Send the completion request to the kernel
339 msg_id = self.kernel_client.shell_channel.complete(
340 text, # text
341 self._get_input_buffer_cursor_line(), # line
342 self._get_input_buffer_cursor_column(), # cursor_pos
343 self.input_buffer) # block
344 pos = self._get_cursor().position()
345 info = self._CompletionRequest(msg_id, pos)
346 self._request_info['complete'] = info
347
348 330 def _process_execute_error(self, msg):
349 331 """ Reimplemented for IPython-style traceback formatting.
350 332 """
351 333 content = msg['content']
352 334 traceback = '\n'.join(content['traceback']) + '\n'
353 335 if False:
354 336 # FIXME: For now, tracebacks come as plain text, so we can't use
355 337 # the html renderer yet. Once we refactor ultratb to produce
356 338 # properly styled tracebacks, this branch should be the default
357 339 traceback = traceback.replace(' ', '&nbsp;')
358 340 traceback = traceback.replace('\n', '<br/>')
359 341
360 342 ename = content['ename']
361 343 ename_styled = '<span class="error">%s</span>' % ename
362 344 traceback = traceback.replace(ename, ename_styled)
363 345
364 346 self._append_html(traceback)
365 347 else:
366 348 # This is the fallback for now, using plain text with ansi escapes
367 349 self._append_plain_text(traceback)
368 350
369 351 def _process_execute_payload(self, item):
370 352 """ Reimplemented to dispatch payloads to handler methods.
371 353 """
372 354 handler = self._payload_handlers.get(item['source'])
373 355 if handler is None:
374 356 # We have no handler for this type of payload, simply ignore it
375 357 return False
376 358 else:
377 359 handler(item)
378 360 return True
379 361
380 362 def _show_interpreter_prompt(self, number=None):
381 363 """ Reimplemented for IPython-style prompts.
382 364 """
383 365 # If a number was not specified, make a prompt number request.
384 366 if number is None:
385 367 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
386 368 info = self._ExecutionRequest(msg_id, 'prompt')
387 369 self._request_info['execute'][msg_id] = info
388 370 return
389 371
390 372 # Show a new prompt and save information about it so that it can be
391 373 # updated later if the prompt number turns out to be wrong.
392 374 self._prompt_sep = self.input_sep
393 375 self._show_prompt(self._make_in_prompt(number), html=True)
394 376 block = self._control.document().lastBlock()
395 377 length = len(self._prompt)
396 378 self._previous_prompt_obj = self._PromptBlock(block, length, number)
397 379
398 380 # Update continuation prompt to reflect (possibly) new prompt length.
399 381 self._set_continuation_prompt(
400 382 self._make_continuation_prompt(self._prompt), html=True)
401 383
402 384 def _show_interpreter_prompt_for_reply(self, msg):
403 385 """ Reimplemented for IPython-style prompts.
404 386 """
405 387 # Update the old prompt number if necessary.
406 388 content = msg['content']
407 389 # abort replies do not have any keys:
408 390 if content['status'] == 'aborted':
409 391 if self._previous_prompt_obj:
410 392 previous_prompt_number = self._previous_prompt_obj.number
411 393 else:
412 394 previous_prompt_number = 0
413 395 else:
414 396 previous_prompt_number = content['execution_count']
415 397 if self._previous_prompt_obj and \
416 398 self._previous_prompt_obj.number != previous_prompt_number:
417 399 block = self._previous_prompt_obj.block
418 400
419 401 # Make sure the prompt block has not been erased.
420 402 if block.isValid() and block.text():
421 403
422 404 # Remove the old prompt and insert a new prompt.
423 405 cursor = QtGui.QTextCursor(block)
424 406 cursor.movePosition(QtGui.QTextCursor.Right,
425 407 QtGui.QTextCursor.KeepAnchor,
426 408 self._previous_prompt_obj.length)
427 409 prompt = self._make_in_prompt(previous_prompt_number)
428 410 self._prompt = self._insert_html_fetching_plain_text(
429 411 cursor, prompt)
430 412
431 413 # When the HTML is inserted, Qt blows away the syntax
432 414 # highlighting for the line, so we need to rehighlight it.
433 415 self._highlighter.rehighlightBlock(cursor.block())
434 416
435 417 self._previous_prompt_obj = None
436 418
437 419 # Show a new prompt with the kernel's estimated prompt number.
438 420 self._show_interpreter_prompt(previous_prompt_number + 1)
439 421
440 422 #---------------------------------------------------------------------------
441 423 # 'IPythonWidget' interface
442 424 #---------------------------------------------------------------------------
443 425
444 426 def set_default_style(self, colors='lightbg'):
445 427 """ Sets the widget style to the class defaults.
446 428
447 429 Parameters
448 430 ----------
449 431 colors : str, optional (default lightbg)
450 432 Whether to use the default IPython light background or dark
451 433 background or B&W style.
452 434 """
453 435 colors = colors.lower()
454 436 if colors=='lightbg':
455 437 self.style_sheet = styles.default_light_style_sheet
456 438 self.syntax_style = styles.default_light_syntax_style
457 439 elif colors=='linux':
458 440 self.style_sheet = styles.default_dark_style_sheet
459 441 self.syntax_style = styles.default_dark_syntax_style
460 442 elif colors=='nocolor':
461 443 self.style_sheet = styles.default_bw_style_sheet
462 444 self.syntax_style = styles.default_bw_syntax_style
463 445 else:
464 446 raise KeyError("No such color scheme: %s"%colors)
465 447
466 448 #---------------------------------------------------------------------------
467 449 # 'IPythonWidget' protected interface
468 450 #---------------------------------------------------------------------------
469 451
470 452 def _edit(self, filename, line=None):
471 453 """ Opens a Python script for editing.
472 454
473 455 Parameters
474 456 ----------
475 457 filename : str
476 458 A path to a local system file.
477 459
478 460 line : int, optional
479 461 A line of interest in the file.
480 462 """
481 463 if self.custom_edit:
482 464 self.custom_edit_requested.emit(filename, line)
483 465 elif not self.editor:
484 466 self._append_plain_text('No default editor available.\n'
485 467 'Specify a GUI text editor in the `IPythonWidget.editor` '
486 468 'configurable to enable the %edit magic')
487 469 else:
488 470 try:
489 471 filename = '"%s"' % filename
490 472 if line and self.editor_line:
491 473 command = self.editor_line.format(filename=filename,
492 474 line=line)
493 475 else:
494 476 try:
495 477 command = self.editor.format()
496 478 except KeyError:
497 479 command = self.editor.format(filename=filename)
498 480 else:
499 481 command += ' ' + filename
500 482 except KeyError:
501 483 self._append_plain_text('Invalid editor command.\n')
502 484 else:
503 485 try:
504 486 Popen(command, shell=True)
505 487 except OSError:
506 488 msg = 'Opening editor with command "%s" failed.\n'
507 489 self._append_plain_text(msg % command)
508 490
509 491 def _make_in_prompt(self, number):
510 492 """ Given a prompt number, returns an HTML In prompt.
511 493 """
512 494 try:
513 495 body = self.in_prompt % number
514 496 except TypeError:
515 497 # allow in_prompt to leave out number, e.g. '>>> '
516 498 body = self.in_prompt
517 499 return '<span class="in-prompt">%s</span>' % body
518 500
519 501 def _make_continuation_prompt(self, prompt):
520 502 """ Given a plain text version of an In prompt, returns an HTML
521 503 continuation prompt.
522 504 """
523 505 end_chars = '...: '
524 506 space_count = len(prompt.lstrip('\n')) - len(end_chars)
525 507 body = '&nbsp;' * space_count + end_chars
526 508 return '<span class="in-prompt">%s</span>' % body
527 509
528 510 def _make_out_prompt(self, number):
529 511 """ Given a prompt number, returns an HTML Out prompt.
530 512 """
531 513 body = self.out_prompt % number
532 514 return '<span class="out-prompt">%s</span>' % body
533 515
534 516 #------ Payload handlers --------------------------------------------------
535 517
536 518 # Payload handlers with a generic interface: each takes the opaque payload
537 519 # dict, unpacks it and calls the underlying functions with the necessary
538 520 # arguments.
539 521
540 522 def _handle_payload_edit(self, item):
541 523 self._edit(item['filename'], item['line_number'])
542 524
543 525 def _handle_payload_exit(self, item):
544 526 self._keep_kernel_on_exit = item['keepkernel']
545 527 self.exit_requested.emit(self)
546 528
547 529 def _handle_payload_next_input(self, item):
548 530 self.input_buffer = item['text']
549 531
550 532 def _handle_payload_page(self, item):
551 533 # Since the plain text widget supports only a very small subset of HTML
552 534 # and we have no control over the HTML source, we only page HTML
553 535 # payloads in the rich text widget.
554 536 if item['html'] and self.kind == 'rich':
555 537 self._page(item['html'], html=True)
556 538 else:
557 539 self._page(item['text'], html=False)
558 540
559 541 #------ Trait change handlers --------------------------------------------
560 542
561 543 def _style_sheet_changed(self):
562 544 """ Set the style sheets of the underlying widgets.
563 545 """
564 546 self.setStyleSheet(self.style_sheet)
565 547 if self._control is not None:
566 548 self._control.document().setDefaultStyleSheet(self.style_sheet)
567 549 bg_color = self._control.palette().window().color()
568 550 self._ansi_processor.set_background_color(bg_color)
569 551
570 552 if self._page_control is not None:
571 553 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
572 554
573 555
574 556
575 557 def _syntax_style_changed(self):
576 558 """ Set the style for the syntax highlighter.
577 559 """
578 560 if self._highlighter is None:
579 561 # ignore premature calls
580 562 return
581 563 if self.syntax_style:
582 564 self._highlighter.set_style(self.syntax_style)
583 565 else:
584 566 self._highlighter.set_style_sheet(self.style_sheet)
585 567
586 568 #------ Trait default initializers -----------------------------------------
587 569
588 570 def _banner_default(self):
589 571 from IPython.core.usage import default_gui_banner
590 572 return default_gui_banner
@@ -1,58 +1,63 b''
1 """Adapt readline completer interface to make ZMQ request.
2 """
3 1 # -*- coding: utf-8 -*-
2 """Adapt readline completer interface to make ZMQ request."""
3
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
6
4 7 try:
5 8 from queue import Empty # Py 3
6 9 except ImportError:
7 10 from Queue import Empty # Py 2
8 11
9 12 from IPython.config import Configurable
10 13 from IPython.core.completer import IPCompleter
11 14 from IPython.utils.traitlets import Float
12 15 import IPython.utils.rlineimpl as readline
13 16
14 17 class ZMQCompleter(IPCompleter):
15 18 """Client-side completion machinery.
16 19
17 20 How it works: self.complete will be called multiple times, with
18 21 state=0,1,2,... When state=0 it should compute ALL the completion matches,
19 22 and then return them for each value of state."""
20 23
21 24 timeout = Float(5.0, config=True, help='timeout before completion abort')
22 25
23 26 def __init__(self, shell, client, config=None):
24 27 super(ZMQCompleter,self).__init__(config=config)
25 28
26 29 self.shell = shell
27 30 self.client = client
28 31 self.matches = []
29 32
30 def complete_request(self,text):
33 def complete_request(self, text):
31 34 line = readline.get_line_buffer()
32 35 cursor_pos = readline.get_endidx()
33 36
34 37 # send completion request to kernel
35 38 # Give the kernel up to 0.5s to respond
36 msg_id = self.client.shell_channel.complete(text=text, line=line,
37 cursor_pos=cursor_pos)
39 msg_id = self.client.shell_channel.complete(
40 code=line,
41 cursor_pos=cursor_pos,
42 )
38 43
39 44 msg = self.client.shell_channel.get_msg(timeout=self.timeout)
40 45 if msg['parent_header']['msg_id'] == msg_id:
41 46 return msg["content"]["matches"]
42 47 return []
43 48
44 49 def rlcomplete(self, text, state):
45 50 if state == 0:
46 51 try:
47 52 self.matches = self.complete_request(text)
48 53 except Empty:
49 54 #print('WARNING: Kernel timeout on tab completion.')
50 55 pass
51 56
52 57 try:
53 58 return self.matches[state]
54 59 except IndexError:
55 60 return None
56 61
57 62 def complete(self, text, line, cursor_pos=None):
58 63 return self.rlcomplete(text, 0)
@@ -1,56 +1,63 b''
1 1 """Tests for tokenutil"""
2 2 # Copyright (c) IPython Development Team.
3 3 # Distributed under the terms of the Modified BSD License.
4 4
5 5 import nose.tools as nt
6 6
7 7 from IPython.utils.tokenutil import token_at_cursor
8 8
9 def expect_token(expected, cell, column, line=0):
10 token = token_at_cursor(cell, column, line)
11
12 lines = cell.splitlines()
13 line_with_cursor = '%s|%s' % (lines[line][:column], lines[line][column:])
9 def expect_token(expected, cell, cursor_pos):
10 token = token_at_cursor(cell, cursor_pos)
11 offset = 0
12 for line in cell.splitlines():
13 if offset + len(line) >= cursor_pos:
14 break
15 else:
16 offset += len(line)
17 column = cursor_pos - offset
18 line_with_cursor = '%s|%s' % (line[:column], line[column:])
14 19 line
15 20 nt.assert_equal(token, expected,
16 21 "Excpected %r, got %r in: %s" % (
17 22 expected, token, line_with_cursor)
18 23 )
19 24
20 25 def test_simple():
21 26 cell = "foo"
22 27 for i in range(len(cell)):
23 28 expect_token("foo", cell, i)
24 29
25 30 def test_function():
26 31 cell = "foo(a=5, b='10')"
27 32 expected = 'foo'
28 33 for i in (6,7,8,10,11,12):
29 34 expect_token("foo", cell, i)
30 35
31 36 def test_multiline():
32 37 cell = '\n'.join([
33 38 'a = 5',
34 39 'b = hello("string", there)'
35 40 ])
36 41 expected = 'hello'
37 for i in range(4,9):
38 expect_token(expected, cell, i, 1)
42 start = cell.index(expected)
43 for i in range(start, start + len(expected)):
44 expect_token(expected, cell, i)
39 45 expected = 'there'
40 for i in range(21,27):
41 expect_token(expected, cell, i, 1)
46 start = cell.index(expected)
47 for i in range(start, start + len(expected)):
48 expect_token(expected, cell, i)
42 49
43 50 def test_attrs():
44 51 cell = "foo(a=obj.attr.subattr)"
45 52 expected = 'obj'
46 53 idx = cell.find('obj')
47 54 for i in range(idx, idx + 3):
48 55 expect_token(expected, cell, i)
49 56 idx = idx + 4
50 57 expected = 'obj.attr'
51 58 for i in range(idx, idx + 4):
52 59 expect_token(expected, cell, i)
53 60 idx = idx + 5
54 61 expected = 'obj.attr.subattr'
55 62 for i in range(idx, len(cell)):
56 63 expect_token(expected, cell, i)
@@ -1,80 +1,78 b''
1 1 """Token-related utilities"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import absolute_import, print_function
7 7
8 8 from collections import namedtuple
9 9 from io import StringIO
10 10 from keyword import iskeyword
11 11
12 12 from . import tokenize2
13 13 from .py3compat import cast_unicode_py2
14 14
15 15 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
16 16
17 17 def generate_tokens(readline):
18 18 """wrap generate_tokens to catch EOF errors"""
19 19 try:
20 20 for token in tokenize2.generate_tokens(readline):
21 21 yield token
22 22 except tokenize2.TokenError:
23 23 # catch EOF error
24 24 return
25 25
26 def token_at_cursor(cell, column, line=0):
26 def token_at_cursor(cell, cursor_pos=0):
27 27 """Get the token at a given cursor
28 28
29 29 Used for introspection.
30 30
31 31 Parameters
32 32 ----------
33 33
34 34 cell : unicode
35 35 A block of Python code
36 column : int
37 The column of the cursor offset, where the token should be found
38 line : int, optional
39 The line where the token should be found (optional if cell is a single line)
36 cursor_pos : int
37 The location of the cursor in the block where the token should be found
40 38 """
41 39 cell = cast_unicode_py2(cell)
42 40 names = []
43 41 tokens = []
44 current_line = 0
42 offset = 0
45 43 for tup in generate_tokens(StringIO(cell).readline):
46 44
47 45 tok = Token(*tup)
48 46
49 47 # token, text, start, end, line = tup
50 48 start_col = tok.start[1]
51 49 end_col = tok.end[1]
52 if line == current_line and start_col > column:
50 if offset + start_col > cursor_pos:
53 51 # current token starts after the cursor,
54 52 # don't consume it
55 53 break
56 54
57 55 if tok.token == tokenize2.NAME and not iskeyword(tok.text):
58 56 if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.':
59 57 names[-1] = "%s.%s" % (names[-1], tok.text)
60 58 else:
61 59 names.append(tok.text)
62 60 elif tok.token == tokenize2.OP:
63 61 if tok.text == '=' and names:
64 62 # don't inspect the lhs of an assignment
65 63 names.pop(-1)
66 64
67 if line == current_line and end_col > column:
65 if offset + end_col > cursor_pos:
68 66 # we found the cursor, stop reading
69 67 break
70 68
71 69 tokens.append(tok)
72 70 if tok.token == tokenize2.NEWLINE:
73 current_line += 1
71 offset += len(tok.line)
74 72
75 73 if names:
76 74 return names[-1]
77 75 else:
78 76 return ''
79 77
80 78
General Comments 0
You need to be logged in to leave comments. Login now