##// END OF EJS Templates
update completion_ and objection_info_request...
MinRK -
Show More
@@ -1,526 +1,535 b''
1 //----------------------------------------------------------------------------
1 // Copyright (c) IPython Development Team.
2 // Copyright (C) 2008-2012 The IPython Development Team
2 // Distributed under the terms of the Modified BSD License.
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 //----------------------------------------------------------------------------
7
3
8 //============================================================================
4 //============================================================================
9 // Utilities
5 // Utilities
10 //============================================================================
6 //============================================================================
7
11 IPython.namespace('IPython.utils');
8 IPython.namespace('IPython.utils');
12
9
13 IPython.utils = (function (IPython) {
10 IPython.utils = (function (IPython) {
14 "use strict";
11 "use strict";
15
12
16 IPython.load_extensions = function () {
13 IPython.load_extensions = function () {
17 // load one or more IPython notebook extensions with requirejs
14 // load one or more IPython notebook extensions with requirejs
18
15
19 var extensions = [];
16 var extensions = [];
20 var extension_names = arguments;
17 var extension_names = arguments;
21 for (var i = 0; i < extension_names.length; i++) {
18 for (var i = 0; i < extension_names.length; i++) {
22 extensions.push("nbextensions/" + arguments[i]);
19 extensions.push("nbextensions/" + arguments[i]);
23 }
20 }
24
21
25 require(extensions,
22 require(extensions,
26 function () {
23 function () {
27 for (var i = 0; i < arguments.length; i++) {
24 for (var i = 0; i < arguments.length; i++) {
28 var ext = arguments[i];
25 var ext = arguments[i];
29 var ext_name = extension_names[i];
26 var ext_name = extension_names[i];
30 // success callback
27 // success callback
31 console.log("Loaded extension: " + ext_name);
28 console.log("Loaded extension: " + ext_name);
32 if (ext && ext.load_ipython_extension !== undefined) {
29 if (ext && ext.load_ipython_extension !== undefined) {
33 ext.load_ipython_extension();
30 ext.load_ipython_extension();
34 }
31 }
35 }
32 }
36 },
33 },
37 function (err) {
34 function (err) {
38 // failure callback
35 // failure callback
39 console.log("Failed to load extension(s):", err.requireModules, err);
36 console.log("Failed to load extension(s):", err.requireModules, err);
40 }
37 }
41 );
38 );
42 };
39 };
43
40
44 //============================================================================
41 //============================================================================
45 // Cross-browser RegEx Split
42 // Cross-browser RegEx Split
46 //============================================================================
43 //============================================================================
47
44
48 // This code has been MODIFIED from the code licensed below to not replace the
45 // This code has been MODIFIED from the code licensed below to not replace the
49 // default browser split. The license is reproduced here.
46 // default browser split. The license is reproduced here.
50
47
51 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
48 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
52 /*!
49 /*!
53 * Cross-Browser Split 1.1.1
50 * Cross-Browser Split 1.1.1
54 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
51 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
55 * Available under the MIT License
52 * Available under the MIT License
56 * ECMAScript compliant, uniform cross-browser split method
53 * ECMAScript compliant, uniform cross-browser split method
57 */
54 */
58
55
59 /**
56 /**
60 * Splits a string into an array of strings using a regex or string
57 * Splits a string into an array of strings using a regex or string
61 * separator. Matches of the separator are not included in the result array.
58 * separator. Matches of the separator are not included in the result array.
62 * However, if `separator` is a regex that contains capturing groups,
59 * However, if `separator` is a regex that contains capturing groups,
63 * backreferences are spliced into the result each time `separator` is
60 * backreferences are spliced into the result each time `separator` is
64 * matched. Fixes browser bugs compared to the native
61 * matched. Fixes browser bugs compared to the native
65 * `String.prototype.split` and can be used reliably cross-browser.
62 * `String.prototype.split` and can be used reliably cross-browser.
66 * @param {String} str String to split.
63 * @param {String} str String to split.
67 * @param {RegExp|String} separator Regex or string to use for separating
64 * @param {RegExp|String} separator Regex or string to use for separating
68 * the string.
65 * the string.
69 * @param {Number} [limit] Maximum number of items to include in the result
66 * @param {Number} [limit] Maximum number of items to include in the result
70 * array.
67 * array.
71 * @returns {Array} Array of substrings.
68 * @returns {Array} Array of substrings.
72 * @example
69 * @example
73 *
70 *
74 * // Basic use
71 * // Basic use
75 * regex_split('a b c d', ' ');
72 * regex_split('a b c d', ' ');
76 * // -> ['a', 'b', 'c', 'd']
73 * // -> ['a', 'b', 'c', 'd']
77 *
74 *
78 * // With limit
75 * // With limit
79 * regex_split('a b c d', ' ', 2);
76 * regex_split('a b c d', ' ', 2);
80 * // -> ['a', 'b']
77 * // -> ['a', 'b']
81 *
78 *
82 * // Backreferences in result array
79 * // Backreferences in result array
83 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
80 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
84 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
81 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
85 */
82 */
86 var regex_split = function (str, separator, limit) {
83 var regex_split = function (str, separator, limit) {
87 // If `separator` is not a regex, use `split`
84 // If `separator` is not a regex, use `split`
88 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
85 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
89 return split.call(str, separator, limit);
86 return split.call(str, separator, limit);
90 }
87 }
91 var output = [],
88 var output = [],
92 flags = (separator.ignoreCase ? "i" : "") +
89 flags = (separator.ignoreCase ? "i" : "") +
93 (separator.multiline ? "m" : "") +
90 (separator.multiline ? "m" : "") +
94 (separator.extended ? "x" : "") + // Proposed for ES6
91 (separator.extended ? "x" : "") + // Proposed for ES6
95 (separator.sticky ? "y" : ""), // Firefox 3+
92 (separator.sticky ? "y" : ""), // Firefox 3+
96 lastLastIndex = 0,
93 lastLastIndex = 0,
97 // Make `global` and avoid `lastIndex` issues by working with a copy
94 // Make `global` and avoid `lastIndex` issues by working with a copy
98 separator = new RegExp(separator.source, flags + "g"),
95 separator = new RegExp(separator.source, flags + "g"),
99 separator2, match, lastIndex, lastLength;
96 separator2, match, lastIndex, lastLength;
100 str += ""; // Type-convert
97 str += ""; // Type-convert
101
98
102 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
99 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
103 if (!compliantExecNpcg) {
100 if (!compliantExecNpcg) {
104 // Doesn't need flags gy, but they don't hurt
101 // Doesn't need flags gy, but they don't hurt
105 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
102 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
106 }
103 }
107 /* Values for `limit`, per the spec:
104 /* Values for `limit`, per the spec:
108 * If undefined: 4294967295 // Math.pow(2, 32) - 1
105 * If undefined: 4294967295 // Math.pow(2, 32) - 1
109 * If 0, Infinity, or NaN: 0
106 * If 0, Infinity, or NaN: 0
110 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
107 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
111 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
108 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
112 * If other: Type-convert, then use the above rules
109 * If other: Type-convert, then use the above rules
113 */
110 */
114 limit = typeof(limit) === "undefined" ?
111 limit = typeof(limit) === "undefined" ?
115 -1 >>> 0 : // Math.pow(2, 32) - 1
112 -1 >>> 0 : // Math.pow(2, 32) - 1
116 limit >>> 0; // ToUint32(limit)
113 limit >>> 0; // ToUint32(limit)
117 while (match = separator.exec(str)) {
114 while (match = separator.exec(str)) {
118 // `separator.lastIndex` is not reliable cross-browser
115 // `separator.lastIndex` is not reliable cross-browser
119 lastIndex = match.index + match[0].length;
116 lastIndex = match.index + match[0].length;
120 if (lastIndex > lastLastIndex) {
117 if (lastIndex > lastLastIndex) {
121 output.push(str.slice(lastLastIndex, match.index));
118 output.push(str.slice(lastLastIndex, match.index));
122 // Fix browsers whose `exec` methods don't consistently return `undefined` for
119 // Fix browsers whose `exec` methods don't consistently return `undefined` for
123 // nonparticipating capturing groups
120 // nonparticipating capturing groups
124 if (!compliantExecNpcg && match.length > 1) {
121 if (!compliantExecNpcg && match.length > 1) {
125 match[0].replace(separator2, function () {
122 match[0].replace(separator2, function () {
126 for (var i = 1; i < arguments.length - 2; i++) {
123 for (var i = 1; i < arguments.length - 2; i++) {
127 if (typeof(arguments[i]) === "undefined") {
124 if (typeof(arguments[i]) === "undefined") {
128 match[i] = undefined;
125 match[i] = undefined;
129 }
126 }
130 }
127 }
131 });
128 });
132 }
129 }
133 if (match.length > 1 && match.index < str.length) {
130 if (match.length > 1 && match.index < str.length) {
134 Array.prototype.push.apply(output, match.slice(1));
131 Array.prototype.push.apply(output, match.slice(1));
135 }
132 }
136 lastLength = match[0].length;
133 lastLength = match[0].length;
137 lastLastIndex = lastIndex;
134 lastLastIndex = lastIndex;
138 if (output.length >= limit) {
135 if (output.length >= limit) {
139 break;
136 break;
140 }
137 }
141 }
138 }
142 if (separator.lastIndex === match.index) {
139 if (separator.lastIndex === match.index) {
143 separator.lastIndex++; // Avoid an infinite loop
140 separator.lastIndex++; // Avoid an infinite loop
144 }
141 }
145 }
142 }
146 if (lastLastIndex === str.length) {
143 if (lastLastIndex === str.length) {
147 if (lastLength || !separator.test("")) {
144 if (lastLength || !separator.test("")) {
148 output.push("");
145 output.push("");
149 }
146 }
150 } else {
147 } else {
151 output.push(str.slice(lastLastIndex));
148 output.push(str.slice(lastLastIndex));
152 }
149 }
153 return output.length > limit ? output.slice(0, limit) : output;
150 return output.length > limit ? output.slice(0, limit) : output;
154 };
151 };
155
152
156 //============================================================================
153 //============================================================================
157 // End contributed Cross-browser RegEx Split
154 // End contributed Cross-browser RegEx Split
158 //============================================================================
155 //============================================================================
159
156
160
157
161 var uuid = function () {
158 var uuid = function () {
162 // http://www.ietf.org/rfc/rfc4122.txt
159 // http://www.ietf.org/rfc/rfc4122.txt
163 var s = [];
160 var s = [];
164 var hexDigits = "0123456789ABCDEF";
161 var hexDigits = "0123456789ABCDEF";
165 for (var i = 0; i < 32; i++) {
162 for (var i = 0; i < 32; i++) {
166 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
163 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
167 }
164 }
168 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
165 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
169 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
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 var uuid = s.join("");
168 var uuid = s.join("");
172 return uuid;
169 return uuid;
173 };
170 };
174
171
175
172
176 //Fix raw text to parse correctly in crazy XML
173 //Fix raw text to parse correctly in crazy XML
177 function xmlencode(string) {
174 function xmlencode(string) {
178 return string.replace(/\&/g,'&'+'amp;')
175 return string.replace(/\&/g,'&'+'amp;')
179 .replace(/</g,'&'+'lt;')
176 .replace(/</g,'&'+'lt;')
180 .replace(/>/g,'&'+'gt;')
177 .replace(/>/g,'&'+'gt;')
181 .replace(/\'/g,'&'+'apos;')
178 .replace(/\'/g,'&'+'apos;')
182 .replace(/\"/g,'&'+'quot;')
179 .replace(/\"/g,'&'+'quot;')
183 .replace(/`/g,'&'+'#96;');
180 .replace(/`/g,'&'+'#96;');
184 }
181 }
185
182
186
183
187 //Map from terminal commands to CSS classes
184 //Map from terminal commands to CSS classes
188 var ansi_colormap = {
185 var ansi_colormap = {
189 "01":"ansibold",
186 "01":"ansibold",
190
187
191 "30":"ansiblack",
188 "30":"ansiblack",
192 "31":"ansired",
189 "31":"ansired",
193 "32":"ansigreen",
190 "32":"ansigreen",
194 "33":"ansiyellow",
191 "33":"ansiyellow",
195 "34":"ansiblue",
192 "34":"ansiblue",
196 "35":"ansipurple",
193 "35":"ansipurple",
197 "36":"ansicyan",
194 "36":"ansicyan",
198 "37":"ansigray",
195 "37":"ansigray",
199
196
200 "40":"ansibgblack",
197 "40":"ansibgblack",
201 "41":"ansibgred",
198 "41":"ansibgred",
202 "42":"ansibggreen",
199 "42":"ansibggreen",
203 "43":"ansibgyellow",
200 "43":"ansibgyellow",
204 "44":"ansibgblue",
201 "44":"ansibgblue",
205 "45":"ansibgpurple",
202 "45":"ansibgpurple",
206 "46":"ansibgcyan",
203 "46":"ansibgcyan",
207 "47":"ansibggray"
204 "47":"ansibggray"
208 };
205 };
209
206
210 function _process_numbers(attrs, numbers) {
207 function _process_numbers(attrs, numbers) {
211 // process ansi escapes
208 // process ansi escapes
212 var n = numbers.shift();
209 var n = numbers.shift();
213 if (ansi_colormap[n]) {
210 if (ansi_colormap[n]) {
214 if ( ! attrs["class"] ) {
211 if ( ! attrs["class"] ) {
215 attrs["class"] = ansi_colormap[n];
212 attrs["class"] = ansi_colormap[n];
216 } else {
213 } else {
217 attrs["class"] += " " + ansi_colormap[n];
214 attrs["class"] += " " + ansi_colormap[n];
218 }
215 }
219 } else if (n == "38" || n == "48") {
216 } else if (n == "38" || n == "48") {
220 // VT100 256 color or 24 bit RGB
217 // VT100 256 color or 24 bit RGB
221 if (numbers.length < 2) {
218 if (numbers.length < 2) {
222 console.log("Not enough fields for VT100 color", numbers);
219 console.log("Not enough fields for VT100 color", numbers);
223 return;
220 return;
224 }
221 }
225
222
226 var index_or_rgb = numbers.shift();
223 var index_or_rgb = numbers.shift();
227 var r,g,b;
224 var r,g,b;
228 if (index_or_rgb == "5") {
225 if (index_or_rgb == "5") {
229 // 256 color
226 // 256 color
230 var idx = parseInt(numbers.shift());
227 var idx = parseInt(numbers.shift());
231 if (idx < 16) {
228 if (idx < 16) {
232 // indexed ANSI
229 // indexed ANSI
233 // ignore bright / non-bright distinction
230 // ignore bright / non-bright distinction
234 idx = idx % 8;
231 idx = idx % 8;
235 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
232 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
236 if ( ! attrs["class"] ) {
233 if ( ! attrs["class"] ) {
237 attrs["class"] = ansiclass;
234 attrs["class"] = ansiclass;
238 } else {
235 } else {
239 attrs["class"] += " " + ansiclass;
236 attrs["class"] += " " + ansiclass;
240 }
237 }
241 return;
238 return;
242 } else if (idx < 232) {
239 } else if (idx < 232) {
243 // 216 color 6x6x6 RGB
240 // 216 color 6x6x6 RGB
244 idx = idx - 16;
241 idx = idx - 16;
245 b = idx % 6;
242 b = idx % 6;
246 g = Math.floor(idx / 6) % 6;
243 g = Math.floor(idx / 6) % 6;
247 r = Math.floor(idx / 36) % 6;
244 r = Math.floor(idx / 36) % 6;
248 // convert to rgb
245 // convert to rgb
249 r = (r * 51);
246 r = (r * 51);
250 g = (g * 51);
247 g = (g * 51);
251 b = (b * 51);
248 b = (b * 51);
252 } else {
249 } else {
253 // grayscale
250 // grayscale
254 idx = idx - 231;
251 idx = idx - 231;
255 // it's 1-24 and should *not* include black or white,
252 // it's 1-24 and should *not* include black or white,
256 // so a 26 point scale
253 // so a 26 point scale
257 r = g = b = Math.floor(idx * 256 / 26);
254 r = g = b = Math.floor(idx * 256 / 26);
258 }
255 }
259 } else if (index_or_rgb == "2") {
256 } else if (index_or_rgb == "2") {
260 // Simple 24 bit RGB
257 // Simple 24 bit RGB
261 if (numbers.length > 3) {
258 if (numbers.length > 3) {
262 console.log("Not enough fields for RGB", numbers);
259 console.log("Not enough fields for RGB", numbers);
263 return;
260 return;
264 }
261 }
265 r = numbers.shift();
262 r = numbers.shift();
266 g = numbers.shift();
263 g = numbers.shift();
267 b = numbers.shift();
264 b = numbers.shift();
268 } else {
265 } else {
269 console.log("unrecognized control", numbers);
266 console.log("unrecognized control", numbers);
270 return;
267 return;
271 }
268 }
272 if (r !== undefined) {
269 if (r !== undefined) {
273 // apply the rgb color
270 // apply the rgb color
274 var line;
271 var line;
275 if (n == "38") {
272 if (n == "38") {
276 line = "color: ";
273 line = "color: ";
277 } else {
274 } else {
278 line = "background-color: ";
275 line = "background-color: ";
279 }
276 }
280 line = line + "rgb(" + r + "," + g + "," + b + ");"
277 line = line + "rgb(" + r + "," + g + "," + b + ");"
281 if ( !attrs["style"] ) {
278 if ( !attrs["style"] ) {
282 attrs["style"] = line;
279 attrs["style"] = line;
283 } else {
280 } else {
284 attrs["style"] += " " + line;
281 attrs["style"] += " " + line;
285 }
282 }
286 }
283 }
287 }
284 }
288 }
285 }
289
286
290 function ansispan(str) {
287 function ansispan(str) {
291 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
288 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
292 // regular ansi escapes (using the table above)
289 // regular ansi escapes (using the table above)
293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
290 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
294 if (!pattern) {
291 if (!pattern) {
295 // [(01|22|39|)m close spans
292 // [(01|22|39|)m close spans
296 return "</span>";
293 return "</span>";
297 }
294 }
298 // consume sequence of color escapes
295 // consume sequence of color escapes
299 var numbers = pattern.match(/\d+/g);
296 var numbers = pattern.match(/\d+/g);
300 var attrs = {};
297 var attrs = {};
301 while (numbers.length > 0) {
298 while (numbers.length > 0) {
302 _process_numbers(attrs, numbers);
299 _process_numbers(attrs, numbers);
303 }
300 }
304
301
305 var span = "<span ";
302 var span = "<span ";
306 for (var attr in attrs) {
303 for (var attr in attrs) {
307 var value = attrs[attr];
304 var value = attrs[attr];
308 span = span + " " + attr + '="' + attrs[attr] + '"';
305 span = span + " " + attr + '="' + attrs[attr] + '"';
309 }
306 }
310 return span + ">";
307 return span + ">";
311 });
308 });
312 };
309 };
313
310
314 // Transform ANSI color escape codes into HTML <span> tags with css
311 // Transform ANSI color escape codes into HTML <span> tags with css
315 // classes listed in the above ansi_colormap object. The actual color used
312 // classes listed in the above ansi_colormap object. The actual color used
316 // are set in the css file.
313 // are set in the css file.
317 function fixConsole(txt) {
314 function fixConsole(txt) {
318 txt = xmlencode(txt);
315 txt = xmlencode(txt);
319 var re = /\033\[([\dA-Fa-f;]*?)m/;
316 var re = /\033\[([\dA-Fa-f;]*?)m/;
320 var opened = false;
317 var opened = false;
321 var cmds = [];
318 var cmds = [];
322 var opener = "";
319 var opener = "";
323 var closer = "";
320 var closer = "";
324
321
325 // Strip all ANSI codes that are not color related. Matches
322 // Strip all ANSI codes that are not color related. Matches
326 // all ANSI codes that do not end with "m".
323 // all ANSI codes that do not end with "m".
327 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
324 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
328 txt = txt.replace(ignored_re, "");
325 txt = txt.replace(ignored_re, "");
329
326
330 // color ansi codes
327 // color ansi codes
331 txt = ansispan(txt);
328 txt = ansispan(txt);
332 return txt;
329 return txt;
333 }
330 }
334
331
335 // Remove chunks that should be overridden by the effect of
332 // Remove chunks that should be overridden by the effect of
336 // carriage return characters
333 // carriage return characters
337 function fixCarriageReturn(txt) {
334 function fixCarriageReturn(txt) {
338 var tmp = txt;
335 var tmp = txt;
339 do {
336 do {
340 txt = tmp;
337 txt = tmp;
341 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
338 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
342 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
339 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
343 } while (tmp.length < txt.length);
340 } while (tmp.length < txt.length);
344 return txt;
341 return txt;
345 }
342 }
346
343
347 // Locate any URLs and convert them to a anchor tag
344 // Locate any URLs and convert them to a anchor tag
348 function autoLinkUrls(txt) {
345 function autoLinkUrls(txt) {
349 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
346 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
350 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
347 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
351 }
348 }
352
349
353 var points_to_pixels = function (points) {
350 var points_to_pixels = function (points) {
354 // A reasonably good way of converting between points and pixels.
351 // A reasonably good way of converting between points and pixels.
355 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
352 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
356 $(body).append(test);
353 $(body).append(test);
357 var pixel_per_point = test.width()/10000;
354 var pixel_per_point = test.width()/10000;
358 test.remove();
355 test.remove();
359 return Math.floor(points*pixel_per_point);
356 return Math.floor(points*pixel_per_point);
360 };
357 };
361
358
362 var always_new = function (constructor) {
359 var always_new = function (constructor) {
363 // wrapper around contructor to avoid requiring `var a = new constructor()`
360 // wrapper around contructor to avoid requiring `var a = new constructor()`
364 // useful for passing constructors as callbacks,
361 // useful for passing constructors as callbacks,
365 // not for programmer laziness.
362 // not for programmer laziness.
366 // from http://programmers.stackexchange.com/questions/118798
363 // from http://programmers.stackexchange.com/questions/118798
367 return function () {
364 return function () {
368 var obj = Object.create(constructor.prototype);
365 var obj = Object.create(constructor.prototype);
369 constructor.apply(obj, arguments);
366 constructor.apply(obj, arguments);
370 return obj;
367 return obj;
371 };
368 };
372 };
369 };
373
370
374 var url_path_join = function () {
371 var url_path_join = function () {
375 // join a sequence of url components with '/'
372 // join a sequence of url components with '/'
376 var url = '';
373 var url = '';
377 for (var i = 0; i < arguments.length; i++) {
374 for (var i = 0; i < arguments.length; i++) {
378 if (arguments[i] === '') {
375 if (arguments[i] === '') {
379 continue;
376 continue;
380 }
377 }
381 if (url.length > 0 && url[url.length-1] != '/') {
378 if (url.length > 0 && url[url.length-1] != '/') {
382 url = url + '/' + arguments[i];
379 url = url + '/' + arguments[i];
383 } else {
380 } else {
384 url = url + arguments[i];
381 url = url + arguments[i];
385 }
382 }
386 }
383 }
387 url = url.replace(/\/\/+/, '/');
384 url = url.replace(/\/\/+/, '/');
388 return url;
385 return url;
389 };
386 };
390
387
391 var parse_url = function (url) {
388 var parse_url = function (url) {
392 // an `a` element with an href allows attr-access to the parsed segments of a URL
389 // an `a` element with an href allows attr-access to the parsed segments of a URL
393 // a = parse_url("http://localhost:8888/path/name#hash")
390 // a = parse_url("http://localhost:8888/path/name#hash")
394 // a.protocol = "http:"
391 // a.protocol = "http:"
395 // a.host = "localhost:8888"
392 // a.host = "localhost:8888"
396 // a.hostname = "localhost"
393 // a.hostname = "localhost"
397 // a.port = 8888
394 // a.port = 8888
398 // a.pathname = "/path/name"
395 // a.pathname = "/path/name"
399 // a.hash = "#hash"
396 // a.hash = "#hash"
400 var a = document.createElement("a");
397 var a = document.createElement("a");
401 a.href = url;
398 a.href = url;
402 return a;
399 return a;
403 };
400 };
404
401
405 var encode_uri_components = function (uri) {
402 var encode_uri_components = function (uri) {
406 // encode just the components of a multi-segment uri,
403 // encode just the components of a multi-segment uri,
407 // leaving '/' separators
404 // leaving '/' separators
408 return uri.split('/').map(encodeURIComponent).join('/');
405 return uri.split('/').map(encodeURIComponent).join('/');
409 };
406 };
410
407
411 var url_join_encode = function () {
408 var url_join_encode = function () {
412 // join a sequence of url components with '/',
409 // join a sequence of url components with '/',
413 // encoding each component with encodeURIComponent
410 // encoding each component with encodeURIComponent
414 return encode_uri_components(url_path_join.apply(null, arguments));
411 return encode_uri_components(url_path_join.apply(null, arguments));
415 };
412 };
416
413
417
414
418 var splitext = function (filename) {
415 var splitext = function (filename) {
419 // mimic Python os.path.splitext
416 // mimic Python os.path.splitext
420 // Returns ['base', '.ext']
417 // Returns ['base', '.ext']
421 var idx = filename.lastIndexOf('.');
418 var idx = filename.lastIndexOf('.');
422 if (idx > 0) {
419 if (idx > 0) {
423 return [filename.slice(0, idx), filename.slice(idx)];
420 return [filename.slice(0, idx), filename.slice(idx)];
424 } else {
421 } else {
425 return [filename, ''];
422 return [filename, ''];
426 }
423 }
427 };
424 };
428
425
429
426
430 var escape_html = function (text) {
427 var escape_html = function (text) {
431 // escape text to HTML
428 // escape text to HTML
432 return $("<div/>").text(text).html();
429 return $("<div/>").text(text).html();
433 }
430 };
434
431
435
432
436 var get_body_data = function(key) {
433 var get_body_data = function(key) {
437 // get a url-encoded item from body.data and decode it
434 // get a url-encoded item from body.data and decode it
438 // we should never have any encoded URLs anywhere else in code
435 // we should never have any encoded URLs anywhere else in code
439 // until we are building an actual request
436 // until we are building an actual request
440 return decodeURIComponent($('body').data(key));
437 return decodeURIComponent($('body').data(key));
441 };
438 };
442
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 };
443
451
444 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
452 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
445 var browser = (function() {
453 var browser = (function() {
446 if (typeof navigator === 'undefined') {
454 if (typeof navigator === 'undefined') {
447 // navigator undefined in node
455 // navigator undefined in node
448 return 'None';
456 return 'None';
449 }
457 }
450 var N= navigator.appName, ua= navigator.userAgent, tem;
458 var N= navigator.appName, ua= navigator.userAgent, tem;
451 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
459 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
452 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
460 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
453 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
461 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
454 return M;
462 return M;
455 })();
463 })();
456
464
457 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
465 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
458 var platform = (function () {
466 var platform = (function () {
459 if (typeof navigator === 'undefined') {
467 if (typeof navigator === 'undefined') {
460 // navigator undefined in node
468 // navigator undefined in node
461 return 'None';
469 return 'None';
462 }
470 }
463 var OSName="None";
471 var OSName="None";
464 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
472 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
465 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
473 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
466 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
474 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
467 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
475 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
468 return OSName
476 return OSName;
469 })();
477 })();
470
478
471 var is_or_has = function (a, b) {
479 var is_or_has = function (a, b) {
472 // Is b a child of a or a itself?
480 // Is b a child of a or a itself?
473 return a.has(b).length !==0 || a.is(b);
481 return a.has(b).length !==0 || a.is(b);
474 }
482 };
475
483
476 var is_focused = function (e) {
484 var is_focused = function (e) {
477 // Is element e, or one of its children focused?
485 // Is element e, or one of its children focused?
478 e = $(e);
486 e = $(e);
479 var target = $(document.activeElement);
487 var target = $(document.activeElement);
480 if (target.length > 0) {
488 if (target.length > 0) {
481 if (is_or_has(e, target)) {
489 if (is_or_has(e, target)) {
482 return true;
490 return true;
483 } else {
491 } else {
484 return false;
492 return false;
485 }
493 }
486 } else {
494 } else {
487 return false;
495 return false;
488 }
496 }
489 }
497 };
490
498
491 var log_ajax_error = function (jqXHR, status, error) {
499 var log_ajax_error = function (jqXHR, status, error) {
492 // log ajax failures with informative messages
500 // log ajax failures with informative messages
493 var msg = "API request failed (" + jqXHR.status + "): ";
501 var msg = "API request failed (" + jqXHR.status + "): ";
494 console.log(jqXHR);
502 console.log(jqXHR);
495 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
503 if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
496 msg += jqXHR.responseJSON.message;
504 msg += jqXHR.responseJSON.message;
497 } else {
505 } else {
498 msg += jqXHR.statusText;
506 msg += jqXHR.statusText;
499 }
507 }
500 console.log(msg);
508 console.log(msg);
501 };
509 };
502
510
503 return {
511 return {
504 regex_split : regex_split,
512 regex_split : regex_split,
505 uuid : uuid,
513 uuid : uuid,
506 fixConsole : fixConsole,
514 fixConsole : fixConsole,
507 fixCarriageReturn : fixCarriageReturn,
515 fixCarriageReturn : fixCarriageReturn,
508 autoLinkUrls : autoLinkUrls,
516 autoLinkUrls : autoLinkUrls,
509 points_to_pixels : points_to_pixels,
517 points_to_pixels : points_to_pixels,
510 get_body_data : get_body_data,
518 get_body_data : get_body_data,
511 parse_url : parse_url,
519 parse_url : parse_url,
512 url_path_join : url_path_join,
520 url_path_join : url_path_join,
513 url_join_encode : url_join_encode,
521 url_join_encode : url_join_encode,
514 encode_uri_components : encode_uri_components,
522 encode_uri_components : encode_uri_components,
515 splitext : splitext,
523 splitext : splitext,
516 escape_html : escape_html,
524 escape_html : escape_html,
517 always_new : always_new,
525 always_new : always_new,
526 absolute_cursor_pos : absolute_cursor_pos,
518 browser : browser,
527 browser : browser,
519 platform: platform,
528 platform: platform,
520 is_or_has : is_or_has,
529 is_or_has : is_or_has,
521 is_focused : is_focused,
530 is_focused : is_focused,
522 log_ajax_error : log_ajax_error,
531 log_ajax_error : log_ajax_error,
523 };
532 };
524
533
525 }(IPython));
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 var IPython = (function (IPython) {
8 var IPython = (function (IPython) {
5 // that will prevent us from misspelling
9 // that will prevent us from misspelling
6 "use strict";
10 "use strict";
7
11
8 // easier key mapping
12 // easier key mapping
9 var keycodes = IPython.keyboard.keycodes;
13 var keycodes = IPython.keyboard.keycodes;
10
14
11 function prepend_n_prc(str, n) {
15 function prepend_n_prc(str, n) {
12 for( var i =0 ; i< n ; i++){
16 for( var i =0 ; i< n ; i++){
13 str = '%'+str ;
17 str = '%'+str ;
14 }
18 }
15 return str;
19 return str;
16 };
20 };
17
21
18 function _existing_completion(item, completion_array){
22 function _existing_completion(item, completion_array){
19 for( var i=0; i < completion_array.length; i++) {
23 for( var i=0; i < completion_array.length; i++) {
20 if (completion_array[i].trim().substr(-item.length) == item) {
24 if (completion_array[i].trim().substr(-item.length) == item) {
21 return true;
25 return true;
22 }
26 }
23 }
27 }
24 return false;
28 return false;
25 };
29 };
26
30
27 // what is the common start of all completions
31 // what is the common start of all completions
28 function shared_start(B, drop_prct) {
32 function shared_start(B, drop_prct) {
29 if (B.length == 1) {
33 if (B.length == 1) {
30 return B[0];
34 return B[0];
31 }
35 }
32 var A = [];
36 var A = [];
33 var common;
37 var common;
34 var min_lead_prct = 10;
38 var min_lead_prct = 10;
35 for (var i = 0; i < B.length; i++) {
39 for (var i = 0; i < B.length; i++) {
36 var str = B[i].str;
40 var str = B[i].str;
37 var localmin = 0;
41 var localmin = 0;
38 if(drop_prct === true){
42 if(drop_prct === true){
39 while ( str.substr(0, 1) == '%') {
43 while ( str.substr(0, 1) == '%') {
40 localmin = localmin+1;
44 localmin = localmin+1;
41 str = str.substring(1);
45 str = str.substring(1);
42 }
46 }
43 }
47 }
44 min_lead_prct = Math.min(min_lead_prct, localmin);
48 min_lead_prct = Math.min(min_lead_prct, localmin);
45 A.push(str);
49 A.push(str);
46 }
50 }
47
51
48 if (A.length > 1) {
52 if (A.length > 1) {
49 var tem1, tem2, s;
53 var tem1, tem2, s;
50 A = A.slice(0).sort();
54 A = A.slice(0).sort();
51 tem1 = A[0];
55 tem1 = A[0];
52 s = tem1.length;
56 s = tem1.length;
53 tem2 = A.pop();
57 tem2 = A.pop();
54 while (s && tem2.indexOf(tem1) == -1) {
58 while (s && tem2.indexOf(tem1) == -1) {
55 tem1 = tem1.substring(0, --s);
59 tem1 = tem1.substring(0, --s);
56 }
60 }
57 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
61 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
58 return {
62 return {
59 str:prepend_n_prc('', min_lead_prct),
63 str:prepend_n_prc('', min_lead_prct),
60 type: "computed",
64 type: "computed",
61 from: B[0].from,
65 from: B[0].from,
62 to: B[0].to
66 to: B[0].to
63 };
67 };
64 }
68 }
65 return {
69 return {
66 str: prepend_n_prc(tem1, min_lead_prct),
70 str: prepend_n_prc(tem1, min_lead_prct),
67 type: "computed",
71 type: "computed",
68 from: B[0].from,
72 from: B[0].from,
69 to: B[0].to
73 to: B[0].to
70 };
74 };
71 }
75 }
72 return null;
76 return null;
73 }
77 }
74
78
75
79
76 var Completer = function (cell) {
80 var Completer = function (cell) {
77 this.cell = cell;
81 this.cell = cell;
78 this.editor = cell.code_mirror;
82 this.editor = cell.code_mirror;
79 var that = this;
83 var that = this;
80 $([IPython.events]).on('status_busy.Kernel', function () {
84 $([IPython.events]).on('status_busy.Kernel', function () {
81 that.skip_kernel_completion = true;
85 that.skip_kernel_completion = true;
82 });
86 });
83 $([IPython.events]).on('status_idle.Kernel', function () {
87 $([IPython.events]).on('status_idle.Kernel', function () {
84 that.skip_kernel_completion = false;
88 that.skip_kernel_completion = false;
85 });
89 });
86 };
90 };
87
91
88 Completer.prototype.startCompletion = function () {
92 Completer.prototype.startCompletion = function () {
89 // call for a 'first' completion, that will set the editor and do some
93 // call for a 'first' completion, that will set the editor and do some
90 // special behavior like autopicking if only one completion available.
94 // special behavior like autopicking if only one completion available.
91 if (this.editor.somethingSelected()) return;
95 if (this.editor.somethingSelected()) return;
92 this.done = false;
96 this.done = false;
93 // use to get focus back on opera
97 // use to get focus back on opera
94 this.carry_on_completion(true);
98 this.carry_on_completion(true);
95 };
99 };
96
100
97
101
98 // easy access for julia to monkeypatch
102 // easy access for julia to monkeypatch
99 //
103 //
100 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
104 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
101
105
102 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
106 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
103 return Completer.reinvoke_re.test(pre_cursor);
107 return Completer.reinvoke_re.test(pre_cursor);
104 };
108 };
105
109
106 /**
110 /**
107 *
111 *
108 * pass true as parameter if this is the first invocation of the completer
112 * pass true as parameter if this is the first invocation of the completer
109 * this will prevent the completer to dissmiss itself if it is not on a
113 * this will prevent the completer to dissmiss itself if it is not on a
110 * word boundary like pressing tab after a space, and make it autopick the
114 * word boundary like pressing tab after a space, and make it autopick the
111 * only choice if there is only one which prevent from popping the UI. as
115 * only choice if there is only one which prevent from popping the UI. as
112 * well as fast-forwarding the typing if all completion have a common
116 * well as fast-forwarding the typing if all completion have a common
113 * shared start
117 * shared start
114 **/
118 **/
115 Completer.prototype.carry_on_completion = function (first_invocation) {
119 Completer.prototype.carry_on_completion = function (first_invocation) {
116 // Pass true as parameter if you want the completer to autopick when
120 // Pass true as parameter if you want the completer to autopick when
117 // only one completion. This function is automatically reinvoked at
121 // only one completion. This function is automatically reinvoked at
118 // each keystroke with first_invocation = false
122 // each keystroke with first_invocation = false
119 var cur = this.editor.getCursor();
123 var cur = this.editor.getCursor();
120 var line = this.editor.getLine(cur.line);
124 var line = this.editor.getLine(cur.line);
121 var pre_cursor = this.editor.getRange({
125 var pre_cursor = this.editor.getRange({
122 line: cur.line,
126 line: cur.line,
123 ch: cur.ch - 1
127 ch: cur.ch - 1
124 }, cur);
128 }, cur);
125
129
126 // we need to check that we are still on a word boundary
130 // we need to check that we are still on a word boundary
127 // because while typing the completer is still reinvoking itself
131 // because while typing the completer is still reinvoking itself
128 // so dismiss if we are on a "bad" caracter
132 // so dismiss if we are on a "bad" caracter
129 if (!this.reinvoke(pre_cursor) && !first_invocation) {
133 if (!this.reinvoke(pre_cursor) && !first_invocation) {
130 this.close();
134 this.close();
131 return;
135 return;
132 }
136 }
133
137
134 this.autopick = false;
138 this.autopick = false;
135 if (first_invocation) {
139 if (first_invocation) {
136 this.autopick = true;
140 this.autopick = true;
137 }
141 }
138
142
139 // We want a single cursor position.
143 // We want a single cursor position.
140 if (this.editor.somethingSelected()) {
144 if (this.editor.somethingSelected()) {
141 return;
145 return;
142 }
146 }
143
147
144 // one kernel completion came back, finish_completing will be called with the results
148 // one kernel completion came back, finish_completing will be called with the results
145 // we fork here and directly call finish completing if kernel is busy
149 // we fork here and directly call finish completing if kernel is busy
146 if (this.skip_kernel_completion) {
150 if (this.skip_kernel_completion) {
147 this.finish_completing({
151 this.finish_completing({
148 'matches': [],
152 matches: [],
149 matched_text: ""
153 matched_text: ""
150 });
154 });
151 } else {
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 Completer.prototype.finish_completing = function (msg) {
163 Completer.prototype.finish_completing = function (msg) {
157 // let's build a function that wrap all that stuff into what is needed
164 // let's build a function that wrap all that stuff into what is needed
158 // for the new completer:
165 // for the new completer:
159 var content = msg.content;
166 var content = msg.content;
160 var matched_text = content.matched_text;
167 var matched_text = content.matched_text;
161 var matches = content.matches;
168 var matches = content.matches;
162
169
163 var cur = this.editor.getCursor();
170 var cur = this.editor.getCursor();
164 var results = CodeMirror.contextHint(this.editor);
171 var results = CodeMirror.contextHint(this.editor);
165 var filtered_results = [];
172 var filtered_results = [];
166 //remove results from context completion
173 //remove results from context completion
167 //that are already in kernel completion
174 //that are already in kernel completion
168 for (var i=0; i < results.length; i++) {
175 for (var i=0; i < results.length; i++) {
169 if (!_existing_completion(results[i].str, matches)) {
176 if (!_existing_completion(results[i].str, matches)) {
170 filtered_results.push(results[i]);
177 filtered_results.push(results[i]);
171 }
178 }
172 }
179 }
173
180
174 // append the introspection result, in order, at at the beginning of
181 // append the introspection result, in order, at at the beginning of
175 // the table and compute the replacement range from current cursor
182 // the table and compute the replacement range from current cursor
176 // positon and matched_text length.
183 // positon and matched_text length.
177 for (var i = matches.length - 1; i >= 0; --i) {
184 for (var i = matches.length - 1; i >= 0; --i) {
178 filtered_results.unshift({
185 filtered_results.unshift({
179 str: matches[i],
186 str: matches[i],
180 type: "introspection",
187 type: "introspection",
181 from: {
188 from: {
182 line: cur.line,
189 line: cur.line,
183 ch: cur.ch - matched_text.length
190 ch: cur.ch - matched_text.length
184 },
191 },
185 to: {
192 to: {
186 line: cur.line,
193 line: cur.line,
187 ch: cur.ch
194 ch: cur.ch
188 }
195 }
189 });
196 });
190 }
197 }
191
198
192 // one the 2 sources results have been merge, deal with it
199 // one the 2 sources results have been merge, deal with it
193 this.raw_result = filtered_results;
200 this.raw_result = filtered_results;
194
201
195 // if empty result return
202 // if empty result return
196 if (!this.raw_result || !this.raw_result.length) return;
203 if (!this.raw_result || !this.raw_result.length) return;
197
204
198 // When there is only one completion, use it directly.
205 // When there is only one completion, use it directly.
199 if (this.autopick && this.raw_result.length == 1) {
206 if (this.autopick && this.raw_result.length == 1) {
200 this.insert(this.raw_result[0]);
207 this.insert(this.raw_result[0]);
201 return;
208 return;
202 }
209 }
203
210
204 if (this.raw_result.length == 1) {
211 if (this.raw_result.length == 1) {
205 // test if first and only completion totally matches
212 // test if first and only completion totally matches
206 // what is typed, in this case dismiss
213 // what is typed, in this case dismiss
207 var str = this.raw_result[0].str;
214 var str = this.raw_result[0].str;
208 var pre_cursor = this.editor.getRange({
215 var pre_cursor = this.editor.getRange({
209 line: cur.line,
216 line: cur.line,
210 ch: cur.ch - str.length
217 ch: cur.ch - str.length
211 }, cur);
218 }, cur);
212 if (pre_cursor == str) {
219 if (pre_cursor == str) {
213 this.close();
220 this.close();
214 return;
221 return;
215 }
222 }
216 }
223 }
217
224
218 if (!this.visible) {
225 if (!this.visible) {
219 this.complete = $('<div/>').addClass('completions');
226 this.complete = $('<div/>').addClass('completions');
220 this.complete.attr('id', 'complete');
227 this.complete.attr('id', 'complete');
221
228
222 // Currently webkit doesn't use the size attr correctly. See:
229 // Currently webkit doesn't use the size attr correctly. See:
223 // https://code.google.com/p/chromium/issues/detail?id=4579
230 // https://code.google.com/p/chromium/issues/detail?id=4579
224 this.sel = $('<select/>')
231 this.sel = $('<select/>')
225 .attr('tabindex', -1)
232 .attr('tabindex', -1)
226 .attr('multiple', 'true');
233 .attr('multiple', 'true');
227 this.complete.append(this.sel);
234 this.complete.append(this.sel);
228 this.visible = true;
235 this.visible = true;
229 $('body').append(this.complete);
236 $('body').append(this.complete);
230
237
231 //build the container
238 //build the container
232 var that = this;
239 var that = this;
233 this.sel.dblclick(function () {
240 this.sel.dblclick(function () {
234 that.pick();
241 that.pick();
235 });
242 });
236 this.sel.focus(function () {
243 this.sel.focus(function () {
237 that.editor.focus();
244 that.editor.focus();
238 });
245 });
239 this._handle_keydown = function (cm, event) {
246 this._handle_keydown = function (cm, event) {
240 that.keydown(event);
247 that.keydown(event);
241 };
248 };
242 this.editor.on('keydown', this._handle_keydown);
249 this.editor.on('keydown', this._handle_keydown);
243 this._handle_keypress = function (cm, event) {
250 this._handle_keypress = function (cm, event) {
244 that.keypress(event);
251 that.keypress(event);
245 };
252 };
246 this.editor.on('keypress', this._handle_keypress);
253 this.editor.on('keypress', this._handle_keypress);
247 }
254 }
248 this.sel.attr('size', Math.min(10, this.raw_result.length));
255 this.sel.attr('size', Math.min(10, this.raw_result.length));
249
256
250 // After everything is on the page, compute the postion.
257 // After everything is on the page, compute the postion.
251 // We put it above the code if it is too close to the bottom of the page.
258 // We put it above the code if it is too close to the bottom of the page.
252 cur.ch = cur.ch-matched_text.length;
259 cur.ch = cur.ch-matched_text.length;
253 var pos = this.editor.cursorCoords(cur);
260 var pos = this.editor.cursorCoords(cur);
254 var left = pos.left-3;
261 var left = pos.left-3;
255 var top;
262 var top;
256 var cheight = this.complete.height();
263 var cheight = this.complete.height();
257 var wheight = $(window).height();
264 var wheight = $(window).height();
258 if (pos.bottom+cheight+5 > wheight) {
265 if (pos.bottom+cheight+5 > wheight) {
259 top = pos.top-cheight-4;
266 top = pos.top-cheight-4;
260 } else {
267 } else {
261 top = pos.bottom+1;
268 top = pos.bottom+1;
262 }
269 }
263 this.complete.css('left', left + 'px');
270 this.complete.css('left', left + 'px');
264 this.complete.css('top', top + 'px');
271 this.complete.css('top', top + 'px');
265
272
266 // Clear and fill the list.
273 // Clear and fill the list.
267 this.sel.text('');
274 this.sel.text('');
268 this.build_gui_list(this.raw_result);
275 this.build_gui_list(this.raw_result);
269 return true;
276 return true;
270 };
277 };
271
278
272 Completer.prototype.insert = function (completion) {
279 Completer.prototype.insert = function (completion) {
273 this.editor.replaceRange(completion.str, completion.from, completion.to);
280 this.editor.replaceRange(completion.str, completion.from, completion.to);
274 };
281 };
275
282
276 Completer.prototype.build_gui_list = function (completions) {
283 Completer.prototype.build_gui_list = function (completions) {
277 for (var i = 0; i < completions.length; ++i) {
284 for (var i = 0; i < completions.length; ++i) {
278 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
285 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
279 this.sel.append(opt);
286 this.sel.append(opt);
280 }
287 }
281 this.sel.children().first().attr('selected', 'true');
288 this.sel.children().first().attr('selected', 'true');
282 this.sel.scrollTop(0);
289 this.sel.scrollTop(0);
283 };
290 };
284
291
285 Completer.prototype.close = function () {
292 Completer.prototype.close = function () {
286 this.done = true;
293 this.done = true;
287 $('#complete').remove();
294 $('#complete').remove();
288 this.editor.off('keydown', this._handle_keydown);
295 this.editor.off('keydown', this._handle_keydown);
289 this.editor.off('keypress', this._handle_keypress);
296 this.editor.off('keypress', this._handle_keypress);
290 this.visible = false;
297 this.visible = false;
291 };
298 };
292
299
293 Completer.prototype.pick = function () {
300 Completer.prototype.pick = function () {
294 this.insert(this.raw_result[this.sel[0].selectedIndex]);
301 this.insert(this.raw_result[this.sel[0].selectedIndex]);
295 this.close();
302 this.close();
296 };
303 };
297
304
298 Completer.prototype.keydown = function (event) {
305 Completer.prototype.keydown = function (event) {
299 var code = event.keyCode;
306 var code = event.keyCode;
300 var that = this;
307 var that = this;
301
308
302 // Enter
309 // Enter
303 if (code == keycodes.enter) {
310 if (code == keycodes.enter) {
304 CodeMirror.e_stop(event);
311 CodeMirror.e_stop(event);
305 this.pick();
312 this.pick();
306 // Escape or backspace
313 // Escape or backspace
307 } else if (code == keycodes.esc || code == keycodes.backspace) {
314 } else if (code == keycodes.esc || code == keycodes.backspace) {
308 CodeMirror.e_stop(event);
315 CodeMirror.e_stop(event);
309 this.close();
316 this.close();
310 } else if (code == keycodes.tab) {
317 } else if (code == keycodes.tab) {
311 //all the fastforwarding operation,
318 //all the fastforwarding operation,
312 //Check that shared start is not null which can append with prefixed completion
319 //Check that shared start is not null which can append with prefixed completion
313 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
320 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
314 // to erase py
321 // to erase py
315 var sh = shared_start(this.raw_result, true);
322 var sh = shared_start(this.raw_result, true);
316 if (sh) {
323 if (sh) {
317 this.insert(sh);
324 this.insert(sh);
318 }
325 }
319 this.close();
326 this.close();
320 //reinvoke self
327 //reinvoke self
321 setTimeout(function () {
328 setTimeout(function () {
322 that.carry_on_completion();
329 that.carry_on_completion();
323 }, 50);
330 }, 50);
324 } else if (code == keycodes.up || code == keycodes.down) {
331 } else if (code == keycodes.up || code == keycodes.down) {
325 // need to do that to be able to move the arrow
332 // need to do that to be able to move the arrow
326 // when on the first or last line ofo a code cell
333 // when on the first or last line ofo a code cell
327 CodeMirror.e_stop(event);
334 CodeMirror.e_stop(event);
328
335
329 var options = this.sel.find('option');
336 var options = this.sel.find('option');
330 var index = this.sel[0].selectedIndex;
337 var index = this.sel[0].selectedIndex;
331 if (code == keycodes.up) {
338 if (code == keycodes.up) {
332 index--;
339 index--;
333 }
340 }
334 if (code == keycodes.down) {
341 if (code == keycodes.down) {
335 index++;
342 index++;
336 }
343 }
337 index = Math.min(Math.max(index, 0), options.length-1);
344 index = Math.min(Math.max(index, 0), options.length-1);
338 this.sel[0].selectedIndex = index;
345 this.sel[0].selectedIndex = index;
339 } else if (code == keycodes.left || code == keycodes.right) {
346 } else if (code == keycodes.left || code == keycodes.right) {
340 this.close();
347 this.close();
341 }
348 }
342 };
349 };
343
350
344 Completer.prototype.keypress = function (event) {
351 Completer.prototype.keypress = function (event) {
345 // FIXME: This is a band-aid.
352 // FIXME: This is a band-aid.
346 // on keypress, trigger insertion of a single character.
353 // on keypress, trigger insertion of a single character.
347 // This simulates the old behavior of completion as you type,
354 // This simulates the old behavior of completion as you type,
348 // before events were disconnected and CodeMirror stopped
355 // before events were disconnected and CodeMirror stopped
349 // receiving events while the completer is focused.
356 // receiving events while the completer is focused.
350
357
351 var that = this;
358 var that = this;
352 var code = event.keyCode;
359 var code = event.keyCode;
353
360
354 // don't handle keypress if it's not a character (arrows on FF)
361 // don't handle keypress if it's not a character (arrows on FF)
355 // or ENTER/TAB
362 // or ENTER/TAB
356 if (event.charCode === 0 ||
363 if (event.charCode === 0 ||
357 code == keycodes.tab ||
364 code == keycodes.tab ||
358 code == keycodes.enter
365 code == keycodes.enter
359 ) return;
366 ) return;
360
367
361 this.close();
368 this.close();
362 this.editor.focus();
369 this.editor.focus();
363 setTimeout(function () {
370 setTimeout(function () {
364 that.carry_on_completion();
371 that.carry_on_completion();
365 }, 50);
372 }, 50);
366 };
373 };
367 IPython.Completer = Completer;
374 IPython.Completer = Completer;
368
375
369 return IPython;
376 return IPython;
370 }(IPython));
377 }(IPython));
@@ -1,387 +1,350 b''
1 //----------------------------------------------------------------------------
1 // Copyright (c) IPython Development Team.
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Distributed under the terms of the Modified BSD License.
3 //
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 //----------------------------------------------------------------------------
7 //============================================================================
4 //============================================================================
8 // Tooltip
5 // Tooltip
9 //============================================================================
6 //============================================================================
10 //
7 //
11 // you can set the autocall time by setting `IPython.tooltip.time_before_tooltip` in ms
8 // you can set the autocall time by setting `IPython.tooltip.time_before_tooltip` in ms
12 //
9 //
13 // you can configure the differents action of pressing shift-tab several times in a row by
10 // you can configure the differents action of pressing shift-tab several times in a row by
14 // setting/appending different fonction in the array
11 // setting/appending different fonction in the array
15 // IPython.tooltip.tabs_functions
12 // IPython.tooltip.tabs_functions
16 //
13 //
17 // eg :
14 // eg :
18 // IPython.tooltip.tabs_functions[4] = function (){console.log('this is the action of the 4th tab pressing')}
15 // IPython.tooltip.tabs_functions[4] = function (){console.log('this is the action of the 4th tab pressing')}
19 //
16 //
20 var IPython = (function (IPython) {
17 var IPython = (function (IPython) {
21 "use strict";
18 "use strict";
22
19
23 var utils = IPython.utils;
20 var utils = IPython.utils;
24
21
25 // tooltip constructor
22 // tooltip constructor
26 var Tooltip = function () {
23 var Tooltip = function () {
27 var that = this;
24 var that = this;
28 this.time_before_tooltip = 1200;
25 this.time_before_tooltip = 1200;
29
26
30 // handle to html
27 // handle to html
31 this.tooltip = $('#tooltip');
28 this.tooltip = $('#tooltip');
32 this._hidden = true;
29 this._hidden = true;
33
30
34 // variable for consecutive call
31 // variable for consecutive call
35 this._old_cell = null;
32 this._old_cell = null;
36 this._old_request = null;
33 this._old_request = null;
37 this._consecutive_counter = 0;
34 this._consecutive_counter = 0;
38
35
39 // 'sticky ?'
36 // 'sticky ?'
40 this._sticky = false;
37 this._sticky = false;
41
38
42 // display tooltip if the docstring is empty?
39 // display tooltip if the docstring is empty?
43 this._hide_if_no_docstring = false;
40 this._hide_if_no_docstring = false;
44
41
45 // contain the button in the upper right corner
42 // contain the button in the upper right corner
46 this.buttons = $('<div/>').addClass('tooltipbuttons');
43 this.buttons = $('<div/>').addClass('tooltipbuttons');
47
44
48 // will contain the docstring
45 // will contain the docstring
49 this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
46 this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
50
47
51 // build the buttons menu on the upper right
48 // build the buttons menu on the upper right
52 // expand the tooltip to see more
49 // expand the tooltip to see more
53 var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
50 var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
54 .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () {
51 .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () {
55 that.expand();
52 that.expand();
56 }).append(
53 }).append(
57 $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
54 $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
58
55
59 // open in pager
56 // open in pager
60 var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press shift-tab 4 times)');
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 var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
58 var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
62 morelink.append(morespan);
59 morelink.append(morespan);
63 morelink.click(function () {
60 morelink.click(function () {
64 that.showInPager(that._old_cell);
61 that.showInPager(that._old_cell);
65 });
62 });
66
63
67 // close the tooltip
64 // close the tooltip
68 var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
65 var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
69 var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
66 var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
70 closelink.append(closespan);
67 closelink.append(closespan);
71 closelink.click(function () {
68 closelink.click(function () {
72 that.remove_and_cancel_tooltip(true);
69 that.remove_and_cancel_tooltip(true);
73 });
70 });
74
71
75 this._clocklink = $('<a/>').attr('href', "#");
72 this._clocklink = $('<a/>').attr('href', "#");
76 this._clocklink.attr('role', "button");
73 this._clocklink.attr('role', "button");
77 this._clocklink.addClass('ui-button');
74 this._clocklink.addClass('ui-button');
78 this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds');
75 this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds');
79 var clockspan = $('<span/>').text('Close');
76 var clockspan = $('<span/>').text('Close');
80 clockspan.addClass('ui-icon');
77 clockspan.addClass('ui-icon');
81 clockspan.addClass('ui-icon-clock');
78 clockspan.addClass('ui-icon-clock');
82 this._clocklink.append(clockspan);
79 this._clocklink.append(clockspan);
83 this._clocklink.click(function () {
80 this._clocklink.click(function () {
84 that.cancel_stick();
81 that.cancel_stick();
85 });
82 });
86
83
87
84
88
85
89
86
90 //construct the tooltip
87 //construct the tooltip
91 // add in the reverse order you want them to appear
88 // add in the reverse order you want them to appear
92 this.buttons.append(closelink);
89 this.buttons.append(closelink);
93 this.buttons.append(expandlink);
90 this.buttons.append(expandlink);
94 this.buttons.append(morelink);
91 this.buttons.append(morelink);
95 this.buttons.append(this._clocklink);
92 this.buttons.append(this._clocklink);
96 this._clocklink.hide();
93 this._clocklink.hide();
97
94
98
95
99 // we need a phony element to make the small arrow
96 // we need a phony element to make the small arrow
100 // of the tooltip in css
97 // of the tooltip in css
101 // we will move the arrow later
98 // we will move the arrow later
102 this.arrow = $('<div/>').addClass('pretooltiparrow');
99 this.arrow = $('<div/>').addClass('pretooltiparrow');
103 this.tooltip.append(this.buttons);
100 this.tooltip.append(this.buttons);
104 this.tooltip.append(this.arrow);
101 this.tooltip.append(this.arrow);
105 this.tooltip.append(this.text);
102 this.tooltip.append(this.text);
106
103
107 // function that will be called if you press tab 1, 2, 3... times in a row
104 // function that will be called if you press tab 1, 2, 3... times in a row
108 this.tabs_functions = [function (cell, text) {
105 this.tabs_functions = [function (cell, text, cursor) {
109 that._request_tooltip(cell, text);
106 that._request_tooltip(cell, text, cursor);
110 }, function () {
107 }, function () {
111 that.expand();
108 that.expand();
112 }, function () {
109 }, function () {
113 that.stick();
110 that.stick();
114 }, function (cell) {
111 }, function (cell) {
115 that.cancel_stick();
112 that.cancel_stick();
116 that.showInPager(cell);
113 that.showInPager(cell);
117 }];
114 }];
118 // call after all the tabs function above have bee call to clean their effects
115 // call after all the tabs function above have bee call to clean their effects
119 // if necessary
116 // if necessary
120 this.reset_tabs_function = function (cell, text) {
117 this.reset_tabs_function = function (cell, text) {
121 this._old_cell = (cell) ? cell : null;
118 this._old_cell = (cell) ? cell : null;
122 this._old_request = (text) ? text : null;
119 this._old_request = (text) ? text : null;
123 this._consecutive_counter = 0;
120 this._consecutive_counter = 0;
124 };
121 };
125 };
122 };
126
123
127 Tooltip.prototype.is_visible = function () {
124 Tooltip.prototype.is_visible = function () {
128 return !this._hidden;
125 return !this._hidden;
129 };
126 };
130
127
131 Tooltip.prototype.showInPager = function (cell) {
128 Tooltip.prototype.showInPager = function (cell) {
132 // reexecute last call in pager by appending ? to show back in pager
129 // reexecute last call in pager by appending ? to show back in pager
133 var that = this;
130 var that = this;
134 var callbacks = {'shell' : {
131 var payload = {};
135 'payload' : {
132 payload.text = that._reply.content.data['text/plain'];
136 'page' : $.proxy(cell._open_with_pager, cell)
133
137 }
134 $([IPython.events]).trigger('open_with_text.Pager', payload);
138 }
139 };
140 cell.kernel.execute(that.name + '?', callbacks, {'silent': false, 'store_history': true});
141 this.remove_and_cancel_tooltip();
135 this.remove_and_cancel_tooltip();
142 };
136 };
143
137
144 // grow the tooltip verticaly
138 // grow the tooltip verticaly
145 Tooltip.prototype.expand = function () {
139 Tooltip.prototype.expand = function () {
146 this.text.removeClass('smalltooltip');
140 this.text.removeClass('smalltooltip');
147 this.text.addClass('bigtooltip');
141 this.text.addClass('bigtooltip');
148 $('#expanbutton').hide('slow');
142 $('#expanbutton').hide('slow');
149 };
143 };
150
144
151 // deal with all the logic of hiding the tooltip
145 // deal with all the logic of hiding the tooltip
152 // and reset it's status
146 // and reset it's status
153 Tooltip.prototype._hide = function () {
147 Tooltip.prototype._hide = function () {
154 this._hidden = true;
148 this._hidden = true;
155 this.tooltip.fadeOut('fast');
149 this.tooltip.fadeOut('fast');
156 $('#expanbutton').show('slow');
150 $('#expanbutton').show('slow');
157 this.text.removeClass('bigtooltip');
151 this.text.removeClass('bigtooltip');
158 this.text.addClass('smalltooltip');
152 this.text.addClass('smalltooltip');
159 // keep scroll top to be sure to always see the first line
153 // keep scroll top to be sure to always see the first line
160 this.text.scrollTop(0);
154 this.text.scrollTop(0);
161 this.code_mirror = null;
155 this.code_mirror = null;
162 };
156 };
163
157
164 // return true on successfully removing a visible tooltip; otherwise return
158 // return true on successfully removing a visible tooltip; otherwise return
165 // false.
159 // false.
166 Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
160 Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
167 // note that we don't handle closing directly inside the calltip
161 // note that we don't handle closing directly inside the calltip
168 // as in the completer, because it is not focusable, so won't
162 // as in the completer, because it is not focusable, so won't
169 // get the event.
163 // get the event.
170 this.cancel_pending();
164 this.cancel_pending();
171 if (!this._hidden) {
165 if (!this._hidden) {
172 if (force || !this._sticky) {
166 if (force || !this._sticky) {
173 this.cancel_stick();
167 this.cancel_stick();
174 this._hide();
168 this._hide();
175 }
169 }
176 this.reset_tabs_function();
170 this.reset_tabs_function();
177 return true;
171 return true;
178 } else {
172 } else {
179 return false;
173 return false;
180 }
174 }
181 };
175 };
182
176
183 // cancel autocall done after '(' for example.
177 // cancel autocall done after '(' for example.
184 Tooltip.prototype.cancel_pending = function () {
178 Tooltip.prototype.cancel_pending = function () {
185 if (this._tooltip_timeout !== null) {
179 if (this._tooltip_timeout !== null) {
186 clearTimeout(this._tooltip_timeout);
180 clearTimeout(this._tooltip_timeout);
187 this._tooltip_timeout = null;
181 this._tooltip_timeout = null;
188 }
182 }
189 };
183 };
190
184
191 // will trigger tooltip after timeout
185 // will trigger tooltip after timeout
192 Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
186 Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
193 var that = this;
187 var that = this;
194 this._tooltip_timeout = setTimeout(function () {
188 this._tooltip_timeout = setTimeout(function () {
195 that.request(cell, hide_if_no_docstring);
189 that.request(cell, hide_if_no_docstring);
196 }, that.time_before_tooltip);
190 }, that.time_before_tooltip);
197 };
191 };
198
192
199 // easy access for julia monkey patching.
193 // easy access for julia monkey patching.
200 Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
194 Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
201
195
202 Tooltip.prototype.extract_oir_token = function(line){
196 Tooltip.prototype.extract_oir_token = function(line){
203 // use internally just to make the request to the kernel
197 // use internally just to make the request to the kernel
204 // Feel free to shorten this logic if you are better
198 // Feel free to shorten this logic if you are better
205 // than me in regEx
199 // than me in regEx
206 // basicaly you shoul be able to get xxx.xxx.xxx from
200 // basicaly you shoul be able to get xxx.xxx.xxx from
207 // something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
201 // something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
208 // remove everything between matchin bracket (need to iterate)
202 // remove everything between matchin bracket (need to iterate)
209 var matchBracket = /\([^\(\)]+\)/g;
203 var matchBracket = /\([^\(\)]+\)/g;
210 var endBracket = /\([^\(]*$/g;
204 var endBracket = /\([^\(]*$/g;
211 var oldline = line;
205 var oldline = line;
212
206
213 line = line.replace(matchBracket, "");
207 line = line.replace(matchBracket, "");
214 while (oldline != line) {
208 while (oldline != line) {
215 oldline = line;
209 oldline = line;
216 line = line.replace(matchBracket, "");
210 line = line.replace(matchBracket, "");
217 }
211 }
218 // remove everything after last open bracket
212 // remove everything after last open bracket
219 line = line.replace(endBracket, "");
213 line = line.replace(endBracket, "");
220 // reset the regex object
214 // reset the regex object
221 Tooltip.last_token_re.lastIndex = 0;
215 Tooltip.last_token_re.lastIndex = 0;
222 return Tooltip.last_token_re.exec(line);
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 var callbacks = $.proxy(this._show, this);
220 var callbacks = $.proxy(this._show, this);
227 var oir_token = this.extract_oir_token(line);
221 var msg_id = cell.kernel.object_info(text, cursor_pos, callbacks);
228 var msg_id = cell.kernel.object_info(oir_token, callbacks);
229 };
222 };
230
223
231 // make an imediate completion request
224 // make an imediate completion request
232 Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
225 Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
233 // request(codecell)
226 // request(codecell)
234 // Deal with extracting the text from the cell and counting
227 // Deal with extracting the text from the cell and counting
235 // call in a row
228 // call in a row
236 this.cancel_pending();
229 this.cancel_pending();
237 var editor = cell.code_mirror;
230 var editor = cell.code_mirror;
238 var cursor = editor.getCursor();
231 var cursor = editor.getCursor();
239 var text = editor.getRange({
232 var cursor_pos = IPython.utils.absolute_cursor_pos(editor, cursor);
240 line: cursor.line,
233 var text = cell.get_text();
241 ch: 0
242 }, cursor).trim();
243
234
244 this._hide_if_no_docstring = hide_if_no_docstring;
235 this._hide_if_no_docstring = hide_if_no_docstring;
245
236
246 if(editor.somethingSelected()){
237 if(editor.somethingSelected()){
247 text = editor.getSelection();
238 text = editor.getSelection();
248 }
239 }
249
240
250 // need a permanent handel to code_mirror for future auto recall
241 // need a permanent handel to code_mirror for future auto recall
251 this.code_mirror = editor;
242 this.code_mirror = editor;
252
243
253 // now we treat the different number of keypress
244 // now we treat the different number of keypress
254 // first if same cell, same text, increment counter by 1
245 // first if same cell, same text, increment counter by 1
255 if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
246 if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
256 this._consecutive_counter++;
247 this._consecutive_counter++;
257 } else {
248 } else {
258 // else reset
249 // else reset
259 this.cancel_stick();
250 this.cancel_stick();
260 this.reset_tabs_function (cell, text);
251 this.reset_tabs_function (cell, text);
261 }
252 }
262
253
263 // don't do anything if line beggin with '(' or is empty
254 // don't do anything if line begins with '(' or is empty
264 if (text === "" || text === "(") {
255 // if (text === "" || text === "(") {
265 return;
256 // return;
266 }
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 // then if we are at the end of list function, reset
261 // then if we are at the end of list function, reset
271 if (this._consecutive_counter == this.tabs_functions.length) {
262 if (this._consecutive_counter == this.tabs_functions.length) {
272 this.reset_tabs_function (cell, text);
263 this.reset_tabs_function (cell, text, cursor);
273 }
264 }
274
265
275 return;
266 return;
276 };
267 };
277
268
278 // cancel the option of having the tooltip to stick
269 // cancel the option of having the tooltip to stick
279 Tooltip.prototype.cancel_stick = function () {
270 Tooltip.prototype.cancel_stick = function () {
280 clearTimeout(this._stick_timeout);
271 clearTimeout(this._stick_timeout);
281 this._stick_timeout = null;
272 this._stick_timeout = null;
282 this._clocklink.hide('slow');
273 this._clocklink.hide('slow');
283 this._sticky = false;
274 this._sticky = false;
284 };
275 };
285
276
286 // put the tooltip in a sicky state for 10 seconds
277 // put the tooltip in a sicky state for 10 seconds
287 // it won't be removed by remove_and_cancell() unless you called with
278 // it won't be removed by remove_and_cancell() unless you called with
288 // the first parameter set to true.
279 // the first parameter set to true.
289 // remove_and_cancell_tooltip(true)
280 // remove_and_cancell_tooltip(true)
290 Tooltip.prototype.stick = function (time) {
281 Tooltip.prototype.stick = function (time) {
291 time = (time !== undefined) ? time : 10;
282 time = (time !== undefined) ? time : 10;
292 var that = this;
283 var that = this;
293 this._sticky = true;
284 this._sticky = true;
294 this._clocklink.show('slow');
285 this._clocklink.show('slow');
295 this._stick_timeout = setTimeout(function () {
286 this._stick_timeout = setTimeout(function () {
296 that._sticky = false;
287 that._sticky = false;
297 that._clocklink.hide('slow');
288 that._clocklink.hide('slow');
298 }, time * 1000);
289 }, time * 1000);
299 };
290 };
300
291
301 // should be called with the kernel reply to actually show the tooltip
292 // should be called with the kernel reply to actually show the tooltip
302 Tooltip.prototype._show = function (reply) {
293 Tooltip.prototype._show = function (reply) {
303 // move the bubble if it is not hidden
294 // move the bubble if it is not hidden
304 // otherwise fade it
295 // otherwise fade it
296 this._reply = reply;
305 var content = reply.content;
297 var content = reply.content;
306 if (!content.found) {
298 if (!content.found) {
307 // object not found, nothing to show
299 // object not found, nothing to show
308 return;
300 return;
309 }
301 }
310 this.name = content.name;
302 this.name = content.name;
311
303
312 // do some math to have the tooltip arrow on more or less on left or right
304 // do some math to have the tooltip arrow on more or less on left or right
313 // width of the editor
305 // width of the editor
314 var w = $(this.code_mirror.getScrollerElement()).width();
306 var w = $(this.code_mirror.getScrollerElement()).width();
315 // ofset of the editor
307 // ofset of the editor
316 var o = $(this.code_mirror.getScrollerElement()).offset();
308 var o = $(this.code_mirror.getScrollerElement()).offset();
317
309
318 // whatever anchor/head order but arrow at mid x selection
310 // whatever anchor/head order but arrow at mid x selection
319 var anchor = this.code_mirror.cursorCoords(false);
311 var anchor = this.code_mirror.cursorCoords(false);
320 var head = this.code_mirror.cursorCoords(true);
312 var head = this.code_mirror.cursorCoords(true);
321 var xinit = (head.left+anchor.left)/2;
313 var xinit = (head.left+anchor.left)/2;
322 var xinter = o.left + (xinit - o.left) / w * (w - 450);
314 var xinter = o.left + (xinit - o.left) / w * (w - 450);
323 var posarrowleft = xinit - xinter;
315 var posarrowleft = xinit - xinter;
324
316
325 if (this._hidden === false) {
317 if (this._hidden === false) {
326 this.tooltip.animate({
318 this.tooltip.animate({
327 'left': xinter - 30 + 'px',
319 'left': xinter - 30 + 'px',
328 'top': (head.bottom + 10) + 'px'
320 'top': (head.bottom + 10) + 'px'
329 });
321 });
330 } else {
322 } else {
331 this.tooltip.css({
323 this.tooltip.css({
332 'left': xinter - 30 + 'px'
324 'left': xinter - 30 + 'px'
333 });
325 });
334 this.tooltip.css({
326 this.tooltip.css({
335 'top': (head.bottom + 10) + 'px'
327 'top': (head.bottom + 10) + 'px'
336 });
328 });
337 }
329 }
338 this.arrow.animate({
330 this.arrow.animate({
339 'left': posarrowleft + 'px'
331 'left': posarrowleft + 'px'
340 });
332 });
341
333
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
368 this._hidden = false;
334 this._hidden = false;
369 this.tooltip.fadeIn('fast');
335 this.tooltip.fadeIn('fast');
370 this.text.children().remove();
336 this.text.children().remove();
371
337
338 // This should support rich data types, but only text/plain for now
372 // Any HTML within the docstring is escaped by the fixConsole() method.
339 // Any HTML within the docstring is escaped by the fixConsole() method.
373 var pre = $('<pre/>').html(utils.fixConsole(docstring));
340 var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain']));
374 if (defstring) {
375 var defstring_html = $('<pre/>').html(utils.fixConsole(defstring));
376 this.text.append(defstring_html);
377 }
378 this.text.append(pre);
341 this.text.append(pre);
379 // keep scroll top to be sure to always see the first line
342 // keep scroll top to be sure to always see the first line
380 this.text.scrollTop(0);
343 this.text.scrollTop(0);
381 };
344 };
382
345
383 IPython.Tooltip = Tooltip;
346 IPython.Tooltip = Tooltip;
384
347
385 return IPython;
348 return IPython;
386
349
387 }(IPython));
350 }(IPython));
@@ -1,621 +1,618 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 //============================================================================
4 //============================================================================
5 // Kernel
5 // Kernel
6 //============================================================================
6 //============================================================================
7
7
8 /**
8 /**
9 * @module IPython
9 * @module IPython
10 * @namespace IPython
10 * @namespace IPython
11 * @submodule Kernel
11 * @submodule Kernel
12 */
12 */
13
13
14 var IPython = (function (IPython) {
14 var IPython = (function (IPython) {
15 "use strict";
15 "use strict";
16
16
17 var utils = IPython.utils;
17 var utils = IPython.utils;
18
18
19 // Initialization and connection.
19 // Initialization and connection.
20 /**
20 /**
21 * A Kernel Class to communicate with the Python kernel
21 * A Kernel Class to communicate with the Python kernel
22 * @Class Kernel
22 * @Class Kernel
23 */
23 */
24 var Kernel = function (kernel_service_url) {
24 var Kernel = function (kernel_service_url) {
25 this.kernel_id = null;
25 this.kernel_id = null;
26 this.shell_channel = null;
26 this.shell_channel = null;
27 this.iopub_channel = null;
27 this.iopub_channel = null;
28 this.stdin_channel = null;
28 this.stdin_channel = null;
29 this.kernel_service_url = kernel_service_url;
29 this.kernel_service_url = kernel_service_url;
30 this.running = false;
30 this.running = false;
31 this.username = "username";
31 this.username = "username";
32 this.session_id = utils.uuid();
32 this.session_id = utils.uuid();
33 this._msg_callbacks = {};
33 this._msg_callbacks = {};
34 this.post = $.post;
34 this.post = $.post;
35
35
36 if (typeof(WebSocket) !== 'undefined') {
36 if (typeof(WebSocket) !== 'undefined') {
37 this.WebSocket = WebSocket;
37 this.WebSocket = WebSocket;
38 } else if (typeof(MozWebSocket) !== 'undefined') {
38 } else if (typeof(MozWebSocket) !== 'undefined') {
39 this.WebSocket = MozWebSocket;
39 this.WebSocket = MozWebSocket;
40 } else {
40 } else {
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.');
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 this.bind_events();
44 this.bind_events();
45 this.init_iopub_handlers();
45 this.init_iopub_handlers();
46 this.comm_manager = new IPython.CommManager(this);
46 this.comm_manager = new IPython.CommManager(this);
47 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
47 this.widget_manager = new IPython.WidgetManager(this.comm_manager);
48
48
49 this.last_msg_id = null;
49 this.last_msg_id = null;
50 this.last_msg_callbacks = {};
50 this.last_msg_callbacks = {};
51 };
51 };
52
52
53
53
54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
54 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
55 var msg = {
55 var msg = {
56 header : {
56 header : {
57 msg_id : utils.uuid(),
57 msg_id : utils.uuid(),
58 username : this.username,
58 username : this.username,
59 session : this.session_id,
59 session : this.session_id,
60 msg_type : msg_type
60 msg_type : msg_type
61 },
61 },
62 metadata : metadata || {},
62 metadata : metadata || {},
63 content : content,
63 content : content,
64 parent_header : {}
64 parent_header : {}
65 };
65 };
66 return msg;
66 return msg;
67 };
67 };
68
68
69 Kernel.prototype.bind_events = function () {
69 Kernel.prototype.bind_events = function () {
70 var that = this;
70 var that = this;
71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
71 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
72 that.send_input_reply(data);
72 that.send_input_reply(data);
73 });
73 });
74 };
74 };
75
75
76 // Initialize the iopub handlers
76 // Initialize the iopub handlers
77
77
78 Kernel.prototype.init_iopub_handlers = function () {
78 Kernel.prototype.init_iopub_handlers = function () {
79 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
79 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
80 this._iopub_handlers = {};
80 this._iopub_handlers = {};
81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
81 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
82 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
83
83
84 for (var i=0; i < output_msg_types.length; i++) {
84 for (var i=0; i < output_msg_types.length; i++) {
85 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
85 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
86 }
86 }
87 };
87 };
88
88
89 /**
89 /**
90 * Start the Python kernel
90 * Start the Python kernel
91 * @method start
91 * @method start
92 */
92 */
93 Kernel.prototype.start = function (params) {
93 Kernel.prototype.start = function (params) {
94 params = params || {};
94 params = params || {};
95 if (!this.running) {
95 if (!this.running) {
96 var qs = $.param(params);
96 var qs = $.param(params);
97 this.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
97 this.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
98 $.proxy(this._kernel_started, this),
98 $.proxy(this._kernel_started, this),
99 'json'
99 'json'
100 );
100 );
101 }
101 }
102 };
102 };
103
103
104 /**
104 /**
105 * Restart the python kernel.
105 * Restart the python kernel.
106 *
106 *
107 * Emit a 'status_restarting.Kernel' event with
107 * Emit a 'status_restarting.Kernel' event with
108 * the current object as parameter
108 * the current object as parameter
109 *
109 *
110 * @method restart
110 * @method restart
111 */
111 */
112 Kernel.prototype.restart = function () {
112 Kernel.prototype.restart = function () {
113 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
113 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
114 if (this.running) {
114 if (this.running) {
115 this.stop_channels();
115 this.stop_channels();
116 this.post(utils.url_join_encode(this.kernel_url, "restart"),
116 this.post(utils.url_join_encode(this.kernel_url, "restart"),
117 $.proxy(this._kernel_started, this),
117 $.proxy(this._kernel_started, this),
118 'json'
118 'json'
119 );
119 );
120 }
120 }
121 };
121 };
122
122
123
123
124 Kernel.prototype._kernel_started = function (json) {
124 Kernel.prototype._kernel_started = function (json) {
125 console.log("Kernel started: ", json.id);
125 console.log("Kernel started: ", json.id);
126 this.running = true;
126 this.running = true;
127 this.kernel_id = json.id;
127 this.kernel_id = json.id;
128 // trailing 's' in https will become wss for secure web sockets
128 // trailing 's' in https will become wss for secure web sockets
129 this.ws_host = location.protocol.replace('http', 'ws') + "//" + location.host;
129 this.ws_host = location.protocol.replace('http', 'ws') + "//" + location.host;
130 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
130 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
131 this.start_channels();
131 this.start_channels();
132 };
132 };
133
133
134
134
135 Kernel.prototype._websocket_closed = function(ws_url, early) {
135 Kernel.prototype._websocket_closed = function(ws_url, early) {
136 this.stop_channels();
136 this.stop_channels();
137 $([IPython.events]).trigger('websocket_closed.Kernel',
137 $([IPython.events]).trigger('websocket_closed.Kernel',
138 {ws_url: ws_url, kernel: this, early: early}
138 {ws_url: ws_url, kernel: this, early: early}
139 );
139 );
140 };
140 };
141
141
142 /**
142 /**
143 * Start the `shell`and `iopub` channels.
143 * Start the `shell`and `iopub` channels.
144 * Will stop and restart them if they already exist.
144 * Will stop and restart them if they already exist.
145 *
145 *
146 * @method start_channels
146 * @method start_channels
147 */
147 */
148 Kernel.prototype.start_channels = function () {
148 Kernel.prototype.start_channels = function () {
149 var that = this;
149 var that = this;
150 this.stop_channels();
150 this.stop_channels();
151 var ws_host_url = this.ws_host + this.kernel_url;
151 var ws_host_url = this.ws_host + this.kernel_url;
152 console.log("Starting WebSockets:", ws_host_url);
152 console.log("Starting WebSockets:", ws_host_url);
153 this.shell_channel = new this.WebSocket(
153 this.shell_channel = new this.WebSocket(
154 this.ws_host + utils.url_join_encode(this.kernel_url, "shell")
154 this.ws_host + utils.url_join_encode(this.kernel_url, "shell")
155 );
155 );
156 this.stdin_channel = new this.WebSocket(
156 this.stdin_channel = new this.WebSocket(
157 this.ws_host + utils.url_join_encode(this.kernel_url, "stdin")
157 this.ws_host + utils.url_join_encode(this.kernel_url, "stdin")
158 );
158 );
159 this.iopub_channel = new this.WebSocket(
159 this.iopub_channel = new this.WebSocket(
160 this.ws_host + utils.url_join_encode(this.kernel_url, "iopub")
160 this.ws_host + utils.url_join_encode(this.kernel_url, "iopub")
161 );
161 );
162
162
163 var already_called_onclose = false; // only alert once
163 var already_called_onclose = false; // only alert once
164 var ws_closed_early = function(evt){
164 var ws_closed_early = function(evt){
165 if (already_called_onclose){
165 if (already_called_onclose){
166 return;
166 return;
167 }
167 }
168 already_called_onclose = true;
168 already_called_onclose = true;
169 if ( ! evt.wasClean ){
169 if ( ! evt.wasClean ){
170 that._websocket_closed(ws_host_url, true);
170 that._websocket_closed(ws_host_url, true);
171 }
171 }
172 };
172 };
173 var ws_closed_late = function(evt){
173 var ws_closed_late = function(evt){
174 if (already_called_onclose){
174 if (already_called_onclose){
175 return;
175 return;
176 }
176 }
177 already_called_onclose = true;
177 already_called_onclose = true;
178 if ( ! evt.wasClean ){
178 if ( ! evt.wasClean ){
179 that._websocket_closed(ws_host_url, false);
179 that._websocket_closed(ws_host_url, false);
180 }
180 }
181 };
181 };
182 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
182 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
183 for (var i=0; i < channels.length; i++) {
183 for (var i=0; i < channels.length; i++) {
184 channels[i].onopen = $.proxy(this._ws_opened, this);
184 channels[i].onopen = $.proxy(this._ws_opened, this);
185 channels[i].onclose = ws_closed_early;
185 channels[i].onclose = ws_closed_early;
186 }
186 }
187 // switch from early-close to late-close message after 1s
187 // switch from early-close to late-close message after 1s
188 setTimeout(function() {
188 setTimeout(function() {
189 for (var i=0; i < channels.length; i++) {
189 for (var i=0; i < channels.length; i++) {
190 if (channels[i] !== null) {
190 if (channels[i] !== null) {
191 channels[i].onclose = ws_closed_late;
191 channels[i].onclose = ws_closed_late;
192 }
192 }
193 }
193 }
194 }, 1000);
194 }, 1000);
195 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
195 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
196 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
196 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
197 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
197 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
198 };
198 };
199
199
200 /**
200 /**
201 * Handle a websocket entering the open state
201 * Handle a websocket entering the open state
202 * sends session and cookie authentication info as first message.
202 * sends session and cookie authentication info as first message.
203 * Once all sockets are open, signal the Kernel.status_started event.
203 * Once all sockets are open, signal the Kernel.status_started event.
204 * @method _ws_opened
204 * @method _ws_opened
205 */
205 */
206 Kernel.prototype._ws_opened = function (evt) {
206 Kernel.prototype._ws_opened = function (evt) {
207 // send the session id so the Session object Python-side
207 // send the session id so the Session object Python-side
208 // has the same identity
208 // has the same identity
209 evt.target.send(this.session_id + ':' + document.cookie);
209 evt.target.send(this.session_id + ':' + document.cookie);
210
210
211 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
211 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
212 for (var i=0; i < channels.length; i++) {
212 for (var i=0; i < channels.length; i++) {
213 // if any channel is not ready, don't trigger event.
213 // if any channel is not ready, don't trigger event.
214 if ( !channels[i].readyState ) return;
214 if ( !channels[i].readyState ) return;
215 }
215 }
216 // all events ready, trigger started event.
216 // all events ready, trigger started event.
217 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
217 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
218 };
218 };
219
219
220 /**
220 /**
221 * Stop the websocket channels.
221 * Stop the websocket channels.
222 * @method stop_channels
222 * @method stop_channels
223 */
223 */
224 Kernel.prototype.stop_channels = function () {
224 Kernel.prototype.stop_channels = function () {
225 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
225 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
226 for (var i=0; i < channels.length; i++) {
226 for (var i=0; i < channels.length; i++) {
227 if ( channels[i] !== null ) {
227 if ( channels[i] !== null ) {
228 channels[i].onclose = null;
228 channels[i].onclose = null;
229 channels[i].close();
229 channels[i].close();
230 }
230 }
231 }
231 }
232 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
232 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
233 };
233 };
234
234
235 // Main public methods.
235 // Main public methods.
236
236
237 // send a message on the Kernel's shell channel
237 // send a message on the Kernel's shell channel
238 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
238 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
239 var msg = this._get_msg(msg_type, content, metadata);
239 var msg = this._get_msg(msg_type, content, metadata);
240 this.shell_channel.send(JSON.stringify(msg));
240 this.shell_channel.send(JSON.stringify(msg));
241 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
241 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
242 return msg.header.msg_id;
242 return msg.header.msg_id;
243 };
243 };
244
244
245 /**
245 /**
246 * Get kernel info
246 * Get kernel info
247 *
247 *
248 * @param callback {function}
248 * @param callback {function}
249 * @method object_info
249 * @method object_info
250 *
250 *
251 * When calling this method, pass a callback function that expects one argument.
251 * When calling this method, pass a callback function that expects one argument.
252 * The callback will be passed the complete `kernel_info_reply` message documented
252 * The callback will be passed the complete `kernel_info_reply` message documented
253 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
253 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
254 */
254 */
255 Kernel.prototype.kernel_info = function (callback) {
255 Kernel.prototype.kernel_info = function (callback) {
256 var callbacks;
256 var callbacks;
257 if (callback) {
257 if (callback) {
258 callbacks = { shell : { reply : callback } };
258 callbacks = { shell : { reply : callback } };
259 }
259 }
260 return this.send_shell_message("kernel_info_request", {}, callbacks);
260 return this.send_shell_message("kernel_info_request", {}, callbacks);
261 };
261 };
262
262
263 /**
263 /**
264 * Get info on an object
264 * Get info on an object
265 *
265 *
266 * @param objname {string}
266 * @param code {string}
267 * @param cursor_pos {integer}
267 * @param callback {function}
268 * @param callback {function}
268 * @method object_info
269 * @method object_info
269 *
270 *
270 * When calling this method, pass a callback function that expects one argument.
271 * When calling this method, pass a callback function that expects one argument.
271 * The callback will be passed the complete `object_info_reply` message documented
272 * The callback will be passed the complete `object_info_reply` message documented
272 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
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 var callbacks;
276 var callbacks;
276 if (callback) {
277 if (callback) {
277 callbacks = { shell : { reply : callback } };
278 callbacks = { shell : { reply : callback } };
278 }
279 }
279
280
280 if (typeof(objname) !== null && objname !== null) {
281 var content = {
281 var content = {
282 oname : objname.toString(),
282 code : code,
283 cursor_pos : cursor_pos,
283 detail_level : 0,
284 detail_level : 0,
284 };
285 };
285 return this.send_shell_message("object_info_request", content, callbacks);
286 return this.send_shell_message("object_info_request", content, callbacks);
286 }
287 return;
288 };
287 };
289
288
290 /**
289 /**
291 * Execute given code into kernel, and pass result to callback.
290 * Execute given code into kernel, and pass result to callback.
292 *
291 *
293 * @async
292 * @async
294 * @method execute
293 * @method execute
295 * @param {string} code
294 * @param {string} code
296 * @param [callbacks] {Object} With the following keys (all optional)
295 * @param [callbacks] {Object} With the following keys (all optional)
297 * @param callbacks.shell.reply {function}
296 * @param callbacks.shell.reply {function}
298 * @param callbacks.shell.payload.[payload_name] {function}
297 * @param callbacks.shell.payload.[payload_name] {function}
299 * @param callbacks.iopub.output {function}
298 * @param callbacks.iopub.output {function}
300 * @param callbacks.iopub.clear_output {function}
299 * @param callbacks.iopub.clear_output {function}
301 * @param callbacks.input {function}
300 * @param callbacks.input {function}
302 * @param {object} [options]
301 * @param {object} [options]
303 * @param [options.silent=false] {Boolean}
302 * @param [options.silent=false] {Boolean}
304 * @param [options.user_expressions=empty_dict] {Dict}
303 * @param [options.user_expressions=empty_dict] {Dict}
305 * @param [options.allow_stdin=false] {Boolean} true|false
304 * @param [options.allow_stdin=false] {Boolean} true|false
306 *
305 *
307 * @example
306 * @example
308 *
307 *
309 * The options object should contain the options for the execute call. Its default
308 * The options object should contain the options for the execute call. Its default
310 * values are:
309 * values are:
311 *
310 *
312 * options = {
311 * options = {
313 * silent : true,
312 * silent : true,
314 * user_expressions : {},
313 * user_expressions : {},
315 * allow_stdin : false
314 * allow_stdin : false
316 * }
315 * }
317 *
316 *
318 * When calling this method pass a callbacks structure of the form:
317 * When calling this method pass a callbacks structure of the form:
319 *
318 *
320 * callbacks = {
319 * callbacks = {
321 * shell : {
320 * shell : {
322 * reply : execute_reply_callback,
321 * reply : execute_reply_callback,
323 * payload : {
322 * payload : {
324 * set_next_input : set_next_input_callback,
323 * set_next_input : set_next_input_callback,
325 * }
324 * }
326 * },
325 * },
327 * iopub : {
326 * iopub : {
328 * output : output_callback,
327 * output : output_callback,
329 * clear_output : clear_output_callback,
328 * clear_output : clear_output_callback,
330 * },
329 * },
331 * input : raw_input_callback
330 * input : raw_input_callback
332 * }
331 * }
333 *
332 *
334 * Each callback will be passed the entire message as a single arugment.
333 * Each callback will be passed the entire message as a single arugment.
335 * Payload handlers will be passed the corresponding payload and the execute_reply message.
334 * Payload handlers will be passed the corresponding payload and the execute_reply message.
336 */
335 */
337 Kernel.prototype.execute = function (code, callbacks, options) {
336 Kernel.prototype.execute = function (code, callbacks, options) {
338
337
339 var content = {
338 var content = {
340 code : code,
339 code : code,
341 silent : true,
340 silent : true,
342 store_history : false,
341 store_history : false,
343 user_expressions : {},
342 user_expressions : {},
344 allow_stdin : false
343 allow_stdin : false
345 };
344 };
346 callbacks = callbacks || {};
345 callbacks = callbacks || {};
347 if (callbacks.input !== undefined) {
346 if (callbacks.input !== undefined) {
348 content.allow_stdin = true;
347 content.allow_stdin = true;
349 }
348 }
350 $.extend(true, content, options);
349 $.extend(true, content, options);
351 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
350 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
352 return this.send_shell_message("execute_request", content, callbacks);
351 return this.send_shell_message("execute_request", content, callbacks);
353 };
352 };
354
353
355 /**
354 /**
356 * When calling this method, pass a function to be called with the `complete_reply` message
355 * When calling this method, pass a function to be called with the `complete_reply` message
357 * as its only argument when it arrives.
356 * as its only argument when it arrives.
358 *
357 *
359 * `complete_reply` is documented
358 * `complete_reply` is documented
360 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
359 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
361 *
360 *
362 * @method complete
361 * @method complete
363 * @param line {integer}
362 * @param code {string}
364 * @param cursor_pos {integer}
363 * @param cursor_pos {integer}
365 * @param callback {function}
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 var callbacks;
368 var callbacks;
370 if (callback) {
369 if (callback) {
371 callbacks = { shell : { reply : callback } };
370 callbacks = { shell : { reply : callback } };
372 }
371 }
373 var content = {
372 var content = {
374 text : '',
373 code : code,
375 line : line,
374 cursor_pos : cursor_pos,
376 block : null,
377 cursor_pos : cursor_pos
378 };
375 };
379 return this.send_shell_message("complete_request", content, callbacks);
376 return this.send_shell_message("complete_request", content, callbacks);
380 };
377 };
381
378
382
379
383 Kernel.prototype.interrupt = function () {
380 Kernel.prototype.interrupt = function () {
384 if (this.running) {
381 if (this.running) {
385 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
382 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
386 this.post(utils.url_join_encode(this.kernel_url, "interrupt"));
383 this.post(utils.url_join_encode(this.kernel_url, "interrupt"));
387 }
384 }
388 };
385 };
389
386
390
387
391 Kernel.prototype.kill = function () {
388 Kernel.prototype.kill = function () {
392 if (this.running) {
389 if (this.running) {
393 this.running = false;
390 this.running = false;
394 var settings = {
391 var settings = {
395 cache : false,
392 cache : false,
396 type : "DELETE",
393 type : "DELETE",
397 error : utils.log_ajax_error,
394 error : utils.log_ajax_error,
398 };
395 };
399 $.ajax(utils.url_join_encode(this.kernel_url), settings);
396 $.ajax(utils.url_join_encode(this.kernel_url), settings);
400 }
397 }
401 };
398 };
402
399
403 Kernel.prototype.send_input_reply = function (input) {
400 Kernel.prototype.send_input_reply = function (input) {
404 var content = {
401 var content = {
405 value : input,
402 value : input,
406 };
403 };
407 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
404 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
408 var msg = this._get_msg("input_reply", content);
405 var msg = this._get_msg("input_reply", content);
409 this.stdin_channel.send(JSON.stringify(msg));
406 this.stdin_channel.send(JSON.stringify(msg));
410 return msg.header.msg_id;
407 return msg.header.msg_id;
411 };
408 };
412
409
413
410
414 // Reply handlers
411 // Reply handlers
415
412
416 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
413 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
417 this._iopub_handlers[msg_type] = callback;
414 this._iopub_handlers[msg_type] = callback;
418 };
415 };
419
416
420 Kernel.prototype.get_iopub_handler = function (msg_type) {
417 Kernel.prototype.get_iopub_handler = function (msg_type) {
421 // get iopub handler for a specific message type
418 // get iopub handler for a specific message type
422 return this._iopub_handlers[msg_type];
419 return this._iopub_handlers[msg_type];
423 };
420 };
424
421
425
422
426 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
423 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
427 // get callbacks for a specific message
424 // get callbacks for a specific message
428 if (msg_id == this.last_msg_id) {
425 if (msg_id == this.last_msg_id) {
429 return this.last_msg_callbacks;
426 return this.last_msg_callbacks;
430 } else {
427 } else {
431 return this._msg_callbacks[msg_id];
428 return this._msg_callbacks[msg_id];
432 }
429 }
433 };
430 };
434
431
435
432
436 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
433 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
437 if (this._msg_callbacks[msg_id] !== undefined ) {
434 if (this._msg_callbacks[msg_id] !== undefined ) {
438 delete this._msg_callbacks[msg_id];
435 delete this._msg_callbacks[msg_id];
439 }
436 }
440 };
437 };
441
438
442 Kernel.prototype._finish_shell = function (msg_id) {
439 Kernel.prototype._finish_shell = function (msg_id) {
443 var callbacks = this._msg_callbacks[msg_id];
440 var callbacks = this._msg_callbacks[msg_id];
444 if (callbacks !== undefined) {
441 if (callbacks !== undefined) {
445 callbacks.shell_done = true;
442 callbacks.shell_done = true;
446 if (callbacks.iopub_done) {
443 if (callbacks.iopub_done) {
447 this.clear_callbacks_for_msg(msg_id);
444 this.clear_callbacks_for_msg(msg_id);
448 }
445 }
449 }
446 }
450 };
447 };
451
448
452 Kernel.prototype._finish_iopub = function (msg_id) {
449 Kernel.prototype._finish_iopub = function (msg_id) {
453 var callbacks = this._msg_callbacks[msg_id];
450 var callbacks = this._msg_callbacks[msg_id];
454 if (callbacks !== undefined) {
451 if (callbacks !== undefined) {
455 callbacks.iopub_done = true;
452 callbacks.iopub_done = true;
456 if (!callbacks.shell_done) {
453 if (!callbacks.shell_done) {
457 this.clear_callbacks_for_msg(msg_id);
454 this.clear_callbacks_for_msg(msg_id);
458 }
455 }
459 }
456 }
460 };
457 };
461
458
462 /* Set callbacks for a particular message.
459 /* Set callbacks for a particular message.
463 * Callbacks should be a struct of the following form:
460 * Callbacks should be a struct of the following form:
464 * shell : {
461 * shell : {
465 *
462 *
466 * }
463 * }
467
464
468 */
465 */
469 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
466 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
470 this.last_msg_id = msg_id;
467 this.last_msg_id = msg_id;
471 if (callbacks) {
468 if (callbacks) {
472 // shallow-copy mapping, because we will modify it at the top level
469 // shallow-copy mapping, because we will modify it at the top level
473 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
470 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
474 cbcopy.shell = callbacks.shell;
471 cbcopy.shell = callbacks.shell;
475 cbcopy.iopub = callbacks.iopub;
472 cbcopy.iopub = callbacks.iopub;
476 cbcopy.input = callbacks.input;
473 cbcopy.input = callbacks.input;
477 cbcopy.shell_done = (!callbacks.shell);
474 cbcopy.shell_done = (!callbacks.shell);
478 cbcopy.iopub_done = (!callbacks.iopub);
475 cbcopy.iopub_done = (!callbacks.iopub);
479 } else {
476 } else {
480 this.last_msg_callbacks = {};
477 this.last_msg_callbacks = {};
481 }
478 }
482 };
479 };
483
480
484
481
485 Kernel.prototype._handle_shell_reply = function (e) {
482 Kernel.prototype._handle_shell_reply = function (e) {
486 var reply = $.parseJSON(e.data);
483 var reply = $.parseJSON(e.data);
487 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
484 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
488 var content = reply.content;
485 var content = reply.content;
489 var metadata = reply.metadata;
486 var metadata = reply.metadata;
490 var parent_id = reply.parent_header.msg_id;
487 var parent_id = reply.parent_header.msg_id;
491 var callbacks = this.get_callbacks_for_msg(parent_id);
488 var callbacks = this.get_callbacks_for_msg(parent_id);
492 if (!callbacks || !callbacks.shell) {
489 if (!callbacks || !callbacks.shell) {
493 return;
490 return;
494 }
491 }
495 var shell_callbacks = callbacks.shell;
492 var shell_callbacks = callbacks.shell;
496
493
497 // signal that shell callbacks are done
494 // signal that shell callbacks are done
498 this._finish_shell(parent_id);
495 this._finish_shell(parent_id);
499
496
500 if (shell_callbacks.reply !== undefined) {
497 if (shell_callbacks.reply !== undefined) {
501 shell_callbacks.reply(reply);
498 shell_callbacks.reply(reply);
502 }
499 }
503 if (content.payload && shell_callbacks.payload) {
500 if (content.payload && shell_callbacks.payload) {
504 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
501 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
505 }
502 }
506 };
503 };
507
504
508
505
509 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
506 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
510 var l = payloads.length;
507 var l = payloads.length;
511 // Payloads are handled by triggering events because we don't want the Kernel
508 // Payloads are handled by triggering events because we don't want the Kernel
512 // to depend on the Notebook or Pager classes.
509 // to depend on the Notebook or Pager classes.
513 for (var i=0; i<l; i++) {
510 for (var i=0; i<l; i++) {
514 var payload = payloads[i];
511 var payload = payloads[i];
515 var callback = payload_callbacks[payload.source];
512 var callback = payload_callbacks[payload.source];
516 if (callback) {
513 if (callback) {
517 callback(payload, msg);
514 callback(payload, msg);
518 }
515 }
519 }
516 }
520 };
517 };
521
518
522 Kernel.prototype._handle_status_message = function (msg) {
519 Kernel.prototype._handle_status_message = function (msg) {
523 var execution_state = msg.content.execution_state;
520 var execution_state = msg.content.execution_state;
524 var parent_id = msg.parent_header.msg_id;
521 var parent_id = msg.parent_header.msg_id;
525
522
526 // dispatch status msg callbacks, if any
523 // dispatch status msg callbacks, if any
527 var callbacks = this.get_callbacks_for_msg(parent_id);
524 var callbacks = this.get_callbacks_for_msg(parent_id);
528 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
525 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
529 try {
526 try {
530 callbacks.iopub.status(msg);
527 callbacks.iopub.status(msg);
531 } catch (e) {
528 } catch (e) {
532 console.log("Exception in status msg handler", e, e.stack);
529 console.log("Exception in status msg handler", e, e.stack);
533 }
530 }
534 }
531 }
535
532
536 if (execution_state === 'busy') {
533 if (execution_state === 'busy') {
537 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
534 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
538 } else if (execution_state === 'idle') {
535 } else if (execution_state === 'idle') {
539 // signal that iopub callbacks are (probably) done
536 // signal that iopub callbacks are (probably) done
540 // async output may still arrive,
537 // async output may still arrive,
541 // but only for the most recent request
538 // but only for the most recent request
542 this._finish_iopub(parent_id);
539 this._finish_iopub(parent_id);
543
540
544 // trigger status_idle event
541 // trigger status_idle event
545 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
542 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
546 } else if (execution_state === 'restarting') {
543 } else if (execution_state === 'restarting') {
547 // autorestarting is distinct from restarting,
544 // autorestarting is distinct from restarting,
548 // in that it means the kernel died and the server is restarting it.
545 // in that it means the kernel died and the server is restarting it.
549 // status_restarting sets the notification widget,
546 // status_restarting sets the notification widget,
550 // autorestart shows the more prominent dialog.
547 // autorestart shows the more prominent dialog.
551 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
548 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
552 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
549 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
553 } else if (execution_state === 'dead') {
550 } else if (execution_state === 'dead') {
554 this.stop_channels();
551 this.stop_channels();
555 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
552 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
556 }
553 }
557 };
554 };
558
555
559
556
560 // handle clear_output message
557 // handle clear_output message
561 Kernel.prototype._handle_clear_output = function (msg) {
558 Kernel.prototype._handle_clear_output = function (msg) {
562 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
559 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
563 if (!callbacks || !callbacks.iopub) {
560 if (!callbacks || !callbacks.iopub) {
564 return;
561 return;
565 }
562 }
566 var callback = callbacks.iopub.clear_output;
563 var callback = callbacks.iopub.clear_output;
567 if (callback) {
564 if (callback) {
568 callback(msg);
565 callback(msg);
569 }
566 }
570 };
567 };
571
568
572
569
573 // handle an output message (execute_result, display_data, etc.)
570 // handle an output message (execute_result, display_data, etc.)
574 Kernel.prototype._handle_output_message = function (msg) {
571 Kernel.prototype._handle_output_message = function (msg) {
575 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
572 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
576 if (!callbacks || !callbacks.iopub) {
573 if (!callbacks || !callbacks.iopub) {
577 return;
574 return;
578 }
575 }
579 var callback = callbacks.iopub.output;
576 var callback = callbacks.iopub.output;
580 if (callback) {
577 if (callback) {
581 callback(msg);
578 callback(msg);
582 }
579 }
583 };
580 };
584
581
585 // dispatch IOPub messages to respective handlers.
582 // dispatch IOPub messages to respective handlers.
586 // each message type should have a handler.
583 // each message type should have a handler.
587 Kernel.prototype._handle_iopub_message = function (e) {
584 Kernel.prototype._handle_iopub_message = function (e) {
588 var msg = $.parseJSON(e.data);
585 var msg = $.parseJSON(e.data);
589
586
590 var handler = this.get_iopub_handler(msg.header.msg_type);
587 var handler = this.get_iopub_handler(msg.header.msg_type);
591 if (handler !== undefined) {
588 if (handler !== undefined) {
592 handler(msg);
589 handler(msg);
593 }
590 }
594 };
591 };
595
592
596
593
597 Kernel.prototype._handle_input_request = function (e) {
594 Kernel.prototype._handle_input_request = function (e) {
598 var request = $.parseJSON(e.data);
595 var request = $.parseJSON(e.data);
599 var header = request.header;
596 var header = request.header;
600 var content = request.content;
597 var content = request.content;
601 var metadata = request.metadata;
598 var metadata = request.metadata;
602 var msg_type = header.msg_type;
599 var msg_type = header.msg_type;
603 if (msg_type !== 'input_request') {
600 if (msg_type !== 'input_request') {
604 console.log("Invalid input request!", request);
601 console.log("Invalid input request!", request);
605 return;
602 return;
606 }
603 }
607 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
604 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
608 if (callbacks) {
605 if (callbacks) {
609 if (callbacks.input) {
606 if (callbacks.input) {
610 callbacks.input(request);
607 callbacks.input(request);
611 }
608 }
612 }
609 }
613 };
610 };
614
611
615
612
616 IPython.Kernel = Kernel;
613 IPython.Kernel = Kernel;
617
614
618 return IPython;
615 return IPython;
619
616
620 }(IPython));
617 }(IPython));
621
618
@@ -1,618 +1,620 b''
1 """Base classes to manage a Client's interaction with a running kernel"""
1 """Base classes to manage a Client's interaction with a running kernel"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import atexit
8 import atexit
9 import errno
9 import errno
10 from threading import Thread
10 from threading import Thread
11 import time
11 import time
12
12
13 import zmq
13 import zmq
14 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
14 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
15 # during garbage collection of threads at exit:
15 # during garbage collection of threads at exit:
16 from zmq import ZMQError
16 from zmq import ZMQError
17 from zmq.eventloop import ioloop, zmqstream
17 from zmq.eventloop import ioloop, zmqstream
18
18
19 # Local imports
19 # Local imports
20 from .channelsabc import (
20 from .channelsabc import (
21 ShellChannelABC, IOPubChannelABC,
21 ShellChannelABC, IOPubChannelABC,
22 HBChannelABC, StdInChannelABC,
22 HBChannelABC, StdInChannelABC,
23 )
23 )
24 from IPython.utils.py3compat import string_types, iteritems
24 from IPython.utils.py3compat import string_types, iteritems
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Constants and exceptions
27 # Constants and exceptions
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 class InvalidPortNumber(Exception):
30 class InvalidPortNumber(Exception):
31 pass
31 pass
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Utility functions
34 # Utility functions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 # some utilities to validate message structure, these might get moved elsewhere
37 # some utilities to validate message structure, these might get moved elsewhere
38 # if they prove to have more generic utility
38 # if they prove to have more generic utility
39
39
40 def validate_string_list(lst):
40 def validate_string_list(lst):
41 """Validate that the input is a list of strings.
41 """Validate that the input is a list of strings.
42
42
43 Raises ValueError if not."""
43 Raises ValueError if not."""
44 if not isinstance(lst, list):
44 if not isinstance(lst, list):
45 raise ValueError('input %r must be a list' % lst)
45 raise ValueError('input %r must be a list' % lst)
46 for x in lst:
46 for x in lst:
47 if not isinstance(x, string_types):
47 if not isinstance(x, string_types):
48 raise ValueError('element %r in list must be a string' % x)
48 raise ValueError('element %r in list must be a string' % x)
49
49
50
50
51 def validate_string_dict(dct):
51 def validate_string_dict(dct):
52 """Validate that the input is a dict with string keys and values.
52 """Validate that the input is a dict with string keys and values.
53
53
54 Raises ValueError if not."""
54 Raises ValueError if not."""
55 for k,v in iteritems(dct):
55 for k,v in iteritems(dct):
56 if not isinstance(k, string_types):
56 if not isinstance(k, string_types):
57 raise ValueError('key %r in dict must be a string' % k)
57 raise ValueError('key %r in dict must be a string' % k)
58 if not isinstance(v, string_types):
58 if not isinstance(v, string_types):
59 raise ValueError('value %r in dict must be a string' % v)
59 raise ValueError('value %r in dict must be a string' % v)
60
60
61
61
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63 # ZMQ Socket Channel classes
63 # ZMQ Socket Channel classes
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65
65
66 class ZMQSocketChannel(Thread):
66 class ZMQSocketChannel(Thread):
67 """The base class for the channels that use ZMQ sockets."""
67 """The base class for the channels that use ZMQ sockets."""
68 context = None
68 context = None
69 session = None
69 session = None
70 socket = None
70 socket = None
71 ioloop = None
71 ioloop = None
72 stream = None
72 stream = None
73 _address = None
73 _address = None
74 _exiting = False
74 _exiting = False
75 proxy_methods = []
75 proxy_methods = []
76
76
77 def __init__(self, context, session, address):
77 def __init__(self, context, session, address):
78 """Create a channel.
78 """Create a channel.
79
79
80 Parameters
80 Parameters
81 ----------
81 ----------
82 context : :class:`zmq.Context`
82 context : :class:`zmq.Context`
83 The ZMQ context to use.
83 The ZMQ context to use.
84 session : :class:`session.Session`
84 session : :class:`session.Session`
85 The session to use.
85 The session to use.
86 address : zmq url
86 address : zmq url
87 Standard (ip, port) tuple that the kernel is listening on.
87 Standard (ip, port) tuple that the kernel is listening on.
88 """
88 """
89 super(ZMQSocketChannel, self).__init__()
89 super(ZMQSocketChannel, self).__init__()
90 self.daemon = True
90 self.daemon = True
91
91
92 self.context = context
92 self.context = context
93 self.session = session
93 self.session = session
94 if isinstance(address, tuple):
94 if isinstance(address, tuple):
95 if address[1] == 0:
95 if address[1] == 0:
96 message = 'The port number for a channel cannot be 0.'
96 message = 'The port number for a channel cannot be 0.'
97 raise InvalidPortNumber(message)
97 raise InvalidPortNumber(message)
98 address = "tcp://%s:%i" % address
98 address = "tcp://%s:%i" % address
99 self._address = address
99 self._address = address
100 atexit.register(self._notice_exit)
100 atexit.register(self._notice_exit)
101
101
102 def _notice_exit(self):
102 def _notice_exit(self):
103 self._exiting = True
103 self._exiting = True
104
104
105 def _run_loop(self):
105 def _run_loop(self):
106 """Run my loop, ignoring EINTR events in the poller"""
106 """Run my loop, ignoring EINTR events in the poller"""
107 while True:
107 while True:
108 try:
108 try:
109 self.ioloop.start()
109 self.ioloop.start()
110 except ZMQError as e:
110 except ZMQError as e:
111 if e.errno == errno.EINTR:
111 if e.errno == errno.EINTR:
112 continue
112 continue
113 else:
113 else:
114 raise
114 raise
115 except Exception:
115 except Exception:
116 if self._exiting:
116 if self._exiting:
117 break
117 break
118 else:
118 else:
119 raise
119 raise
120 else:
120 else:
121 break
121 break
122
122
123 def stop(self):
123 def stop(self):
124 """Stop the channel's event loop and join its thread.
124 """Stop the channel's event loop and join its thread.
125
125
126 This calls :meth:`~threading.Thread.join` and returns when the thread
126 This calls :meth:`~threading.Thread.join` and returns when the thread
127 terminates. :class:`RuntimeError` will be raised if
127 terminates. :class:`RuntimeError` will be raised if
128 :meth:`~threading.Thread.start` is called again.
128 :meth:`~threading.Thread.start` is called again.
129 """
129 """
130 if self.ioloop is not None:
130 if self.ioloop is not None:
131 self.ioloop.stop()
131 self.ioloop.stop()
132 self.join()
132 self.join()
133 self.close()
133 self.close()
134
134
135 def close(self):
135 def close(self):
136 if self.ioloop is not None:
136 if self.ioloop is not None:
137 try:
137 try:
138 self.ioloop.close(all_fds=True)
138 self.ioloop.close(all_fds=True)
139 except Exception:
139 except Exception:
140 pass
140 pass
141 if self.socket is not None:
141 if self.socket is not None:
142 try:
142 try:
143 self.socket.close(linger=0)
143 self.socket.close(linger=0)
144 except Exception:
144 except Exception:
145 pass
145 pass
146 self.socket = None
146 self.socket = None
147
147
148 @property
148 @property
149 def address(self):
149 def address(self):
150 """Get the channel's address as a zmq url string.
150 """Get the channel's address as a zmq url string.
151
151
152 These URLS have the form: 'tcp://127.0.0.1:5555'.
152 These URLS have the form: 'tcp://127.0.0.1:5555'.
153 """
153 """
154 return self._address
154 return self._address
155
155
156 def _queue_send(self, msg):
156 def _queue_send(self, msg):
157 """Queue a message to be sent from the IOLoop's thread.
157 """Queue a message to be sent from the IOLoop's thread.
158
158
159 Parameters
159 Parameters
160 ----------
160 ----------
161 msg : message to send
161 msg : message to send
162
162
163 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
163 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
164 thread control of the action.
164 thread control of the action.
165 """
165 """
166 def thread_send():
166 def thread_send():
167 self.session.send(self.stream, msg)
167 self.session.send(self.stream, msg)
168 self.ioloop.add_callback(thread_send)
168 self.ioloop.add_callback(thread_send)
169
169
170 def _handle_recv(self, msg):
170 def _handle_recv(self, msg):
171 """Callback for stream.on_recv.
171 """Callback for stream.on_recv.
172
172
173 Unpacks message, and calls handlers with it.
173 Unpacks message, and calls handlers with it.
174 """
174 """
175 ident,smsg = self.session.feed_identities(msg)
175 ident,smsg = self.session.feed_identities(msg)
176 self.call_handlers(self.session.unserialize(smsg))
176 self.call_handlers(self.session.unserialize(smsg))
177
177
178
178
179
179
180 class ShellChannel(ZMQSocketChannel):
180 class ShellChannel(ZMQSocketChannel):
181 """The shell channel for issuing request/replies to the kernel."""
181 """The shell channel for issuing request/replies to the kernel."""
182
182
183 command_queue = None
183 command_queue = None
184 # flag for whether execute requests should be allowed to call raw_input:
184 # flag for whether execute requests should be allowed to call raw_input:
185 allow_stdin = True
185 allow_stdin = True
186 proxy_methods = [
186 proxy_methods = [
187 'execute',
187 'execute',
188 'complete',
188 'complete',
189 'object_info',
189 'object_info',
190 'history',
190 'history',
191 'kernel_info',
191 'kernel_info',
192 'shutdown',
192 'shutdown',
193 ]
193 ]
194
194
195 def __init__(self, context, session, address):
195 def __init__(self, context, session, address):
196 super(ShellChannel, self).__init__(context, session, address)
196 super(ShellChannel, self).__init__(context, session, address)
197 self.ioloop = ioloop.IOLoop()
197 self.ioloop = ioloop.IOLoop()
198
198
199 def run(self):
199 def run(self):
200 """The thread's main activity. Call start() instead."""
200 """The thread's main activity. Call start() instead."""
201 self.socket = self.context.socket(zmq.DEALER)
201 self.socket = self.context.socket(zmq.DEALER)
202 self.socket.linger = 1000
202 self.socket.linger = 1000
203 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
203 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
204 self.socket.connect(self.address)
204 self.socket.connect(self.address)
205 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
205 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
206 self.stream.on_recv(self._handle_recv)
206 self.stream.on_recv(self._handle_recv)
207 self._run_loop()
207 self._run_loop()
208
208
209 def call_handlers(self, msg):
209 def call_handlers(self, msg):
210 """This method is called in the ioloop thread when a message arrives.
210 """This method is called in the ioloop thread when a message arrives.
211
211
212 Subclasses should override this method to handle incoming messages.
212 Subclasses should override this method to handle incoming messages.
213 It is important to remember that this method is called in the thread
213 It is important to remember that this method is called in the thread
214 so that some logic must be done to ensure that the application level
214 so that some logic must be done to ensure that the application level
215 handlers are called in the application thread.
215 handlers are called in the application thread.
216 """
216 """
217 raise NotImplementedError('call_handlers must be defined in a subclass.')
217 raise NotImplementedError('call_handlers must be defined in a subclass.')
218
218
219 def execute(self, code, silent=False, store_history=True,
219 def execute(self, code, silent=False, store_history=True,
220 user_expressions=None, allow_stdin=None):
220 user_expressions=None, allow_stdin=None):
221 """Execute code in the kernel.
221 """Execute code in the kernel.
222
222
223 Parameters
223 Parameters
224 ----------
224 ----------
225 code : str
225 code : str
226 A string of Python code.
226 A string of Python code.
227
227
228 silent : bool, optional (default False)
228 silent : bool, optional (default False)
229 If set, the kernel will execute the code as quietly possible, and
229 If set, the kernel will execute the code as quietly possible, and
230 will force store_history to be False.
230 will force store_history to be False.
231
231
232 store_history : bool, optional (default True)
232 store_history : bool, optional (default True)
233 If set, the kernel will store command history. This is forced
233 If set, the kernel will store command history. This is forced
234 to be False if silent is True.
234 to be False if silent is True.
235
235
236 user_expressions : dict, optional
236 user_expressions : dict, optional
237 A dict mapping names to expressions to be evaluated in the user's
237 A dict mapping names to expressions to be evaluated in the user's
238 dict. The expression values are returned as strings formatted using
238 dict. The expression values are returned as strings formatted using
239 :func:`repr`.
239 :func:`repr`.
240
240
241 allow_stdin : bool, optional (default self.allow_stdin)
241 allow_stdin : bool, optional (default self.allow_stdin)
242 Flag for whether the kernel can send stdin requests to frontends.
242 Flag for whether the kernel can send stdin requests to frontends.
243
243
244 Some frontends (e.g. the Notebook) do not support stdin requests.
244 Some frontends (e.g. the Notebook) do not support stdin requests.
245 If raw_input is called from code executed from such a frontend, a
245 If raw_input is called from code executed from such a frontend, a
246 StdinNotImplementedError will be raised.
246 StdinNotImplementedError will be raised.
247
247
248 Returns
248 Returns
249 -------
249 -------
250 The msg_id of the message sent.
250 The msg_id of the message sent.
251 """
251 """
252 if user_expressions is None:
252 if user_expressions is None:
253 user_expressions = {}
253 user_expressions = {}
254 if allow_stdin is None:
254 if allow_stdin is None:
255 allow_stdin = self.allow_stdin
255 allow_stdin = self.allow_stdin
256
256
257
257
258 # Don't waste network traffic if inputs are invalid
258 # Don't waste network traffic if inputs are invalid
259 if not isinstance(code, string_types):
259 if not isinstance(code, string_types):
260 raise ValueError('code %r must be a string' % code)
260 raise ValueError('code %r must be a string' % code)
261 validate_string_dict(user_expressions)
261 validate_string_dict(user_expressions)
262
262
263 # Create class for content/msg creation. Related to, but possibly
263 # Create class for content/msg creation. Related to, but possibly
264 # not in Session.
264 # not in Session.
265 content = dict(code=code, silent=silent, store_history=store_history,
265 content = dict(code=code, silent=silent, store_history=store_history,
266 user_expressions=user_expressions,
266 user_expressions=user_expressions,
267 allow_stdin=allow_stdin,
267 allow_stdin=allow_stdin,
268 )
268 )
269 msg = self.session.msg('execute_request', content)
269 msg = self.session.msg('execute_request', content)
270 self._queue_send(msg)
270 self._queue_send(msg)
271 return msg['header']['msg_id']
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 """Tab complete text in the kernel's namespace.
274 """Tab complete text in the kernel's namespace.
275
275
276 Parameters
276 Parameters
277 ----------
277 ----------
278 text : str
278 code : str
279 The text to complete.
279 The context in which completion is requested.
280 line : str
280 Can be anything between a variable name and an entire cell.
281 The full line of text that is the surrounding context for the
281 cursor_pos : int, optional
282 text to complete.
282 The position of the cursor in the block of code where the completion was requested.
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.
288
283
289 Returns
284 Returns
290 -------
285 -------
291 The msg_id of the message sent.
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 msg = self.session.msg('complete_request', content)
289 msg = self.session.msg('complete_request', content)
295 self._queue_send(msg)
290 self._queue_send(msg)
296 return msg['header']['msg_id']
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 """Get metadata information about an object in the kernel's namespace.
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 Parameters
298 Parameters
302 ----------
299 ----------
303 oname : str
300 code : str
304 A string specifying the object name.
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 detail_level : int, optional
305 detail_level : int, optional
306 The level of detail for the introspection (0-2)
306 The level of detail for the introspection (0-2)
307
307
308 Returns
308 Returns
309 -------
309 -------
310 The msg_id of the message sent.
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 msg = self.session.msg('object_info_request', content)
315 msg = self.session.msg('object_info_request', content)
314 self._queue_send(msg)
316 self._queue_send(msg)
315 return msg['header']['msg_id']
317 return msg['header']['msg_id']
316
318
317 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
319 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
318 """Get entries from the kernel's history list.
320 """Get entries from the kernel's history list.
319
321
320 Parameters
322 Parameters
321 ----------
323 ----------
322 raw : bool
324 raw : bool
323 If True, return the raw input.
325 If True, return the raw input.
324 output : bool
326 output : bool
325 If True, then return the output as well.
327 If True, then return the output as well.
326 hist_access_type : str
328 hist_access_type : str
327 'range' (fill in session, start and stop params), 'tail' (fill in n)
329 'range' (fill in session, start and stop params), 'tail' (fill in n)
328 or 'search' (fill in pattern param).
330 or 'search' (fill in pattern param).
329
331
330 session : int
332 session : int
331 For a range request, the session from which to get lines. Session
333 For a range request, the session from which to get lines. Session
332 numbers are positive integers; negative ones count back from the
334 numbers are positive integers; negative ones count back from the
333 current session.
335 current session.
334 start : int
336 start : int
335 The first line number of a history range.
337 The first line number of a history range.
336 stop : int
338 stop : int
337 The final (excluded) line number of a history range.
339 The final (excluded) line number of a history range.
338
340
339 n : int
341 n : int
340 The number of lines of history to get for a tail request.
342 The number of lines of history to get for a tail request.
341
343
342 pattern : str
344 pattern : str
343 The glob-syntax pattern for a search request.
345 The glob-syntax pattern for a search request.
344
346
345 Returns
347 Returns
346 -------
348 -------
347 The msg_id of the message sent.
349 The msg_id of the message sent.
348 """
350 """
349 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
351 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
350 **kwargs)
352 **kwargs)
351 msg = self.session.msg('history_request', content)
353 msg = self.session.msg('history_request', content)
352 self._queue_send(msg)
354 self._queue_send(msg)
353 return msg['header']['msg_id']
355 return msg['header']['msg_id']
354
356
355 def kernel_info(self):
357 def kernel_info(self):
356 """Request kernel info."""
358 """Request kernel info."""
357 msg = self.session.msg('kernel_info_request')
359 msg = self.session.msg('kernel_info_request')
358 self._queue_send(msg)
360 self._queue_send(msg)
359 return msg['header']['msg_id']
361 return msg['header']['msg_id']
360
362
361 def shutdown(self, restart=False):
363 def shutdown(self, restart=False):
362 """Request an immediate kernel shutdown.
364 """Request an immediate kernel shutdown.
363
365
364 Upon receipt of the (empty) reply, client code can safely assume that
366 Upon receipt of the (empty) reply, client code can safely assume that
365 the kernel has shut down and it's safe to forcefully terminate it if
367 the kernel has shut down and it's safe to forcefully terminate it if
366 it's still alive.
368 it's still alive.
367
369
368 The kernel will send the reply via a function registered with Python's
370 The kernel will send the reply via a function registered with Python's
369 atexit module, ensuring it's truly done as the kernel is done with all
371 atexit module, ensuring it's truly done as the kernel is done with all
370 normal operation.
372 normal operation.
371 """
373 """
372 # Send quit message to kernel. Once we implement kernel-side setattr,
374 # Send quit message to kernel. Once we implement kernel-side setattr,
373 # this should probably be done that way, but for now this will do.
375 # this should probably be done that way, but for now this will do.
374 msg = self.session.msg('shutdown_request', {'restart':restart})
376 msg = self.session.msg('shutdown_request', {'restart':restart})
375 self._queue_send(msg)
377 self._queue_send(msg)
376 return msg['header']['msg_id']
378 return msg['header']['msg_id']
377
379
378
380
379
381
380 class IOPubChannel(ZMQSocketChannel):
382 class IOPubChannel(ZMQSocketChannel):
381 """The iopub channel which listens for messages that the kernel publishes.
383 """The iopub channel which listens for messages that the kernel publishes.
382
384
383 This channel is where all output is published to frontends.
385 This channel is where all output is published to frontends.
384 """
386 """
385
387
386 def __init__(self, context, session, address):
388 def __init__(self, context, session, address):
387 super(IOPubChannel, self).__init__(context, session, address)
389 super(IOPubChannel, self).__init__(context, session, address)
388 self.ioloop = ioloop.IOLoop()
390 self.ioloop = ioloop.IOLoop()
389
391
390 def run(self):
392 def run(self):
391 """The thread's main activity. Call start() instead."""
393 """The thread's main activity. Call start() instead."""
392 self.socket = self.context.socket(zmq.SUB)
394 self.socket = self.context.socket(zmq.SUB)
393 self.socket.linger = 1000
395 self.socket.linger = 1000
394 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
396 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
395 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
397 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
396 self.socket.connect(self.address)
398 self.socket.connect(self.address)
397 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
399 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
398 self.stream.on_recv(self._handle_recv)
400 self.stream.on_recv(self._handle_recv)
399 self._run_loop()
401 self._run_loop()
400
402
401 def call_handlers(self, msg):
403 def call_handlers(self, msg):
402 """This method is called in the ioloop thread when a message arrives.
404 """This method is called in the ioloop thread when a message arrives.
403
405
404 Subclasses should override this method to handle incoming messages.
406 Subclasses should override this method to handle incoming messages.
405 It is important to remember that this method is called in the thread
407 It is important to remember that this method is called in the thread
406 so that some logic must be done to ensure that the application leve
408 so that some logic must be done to ensure that the application leve
407 handlers are called in the application thread.
409 handlers are called in the application thread.
408 """
410 """
409 raise NotImplementedError('call_handlers must be defined in a subclass.')
411 raise NotImplementedError('call_handlers must be defined in a subclass.')
410
412
411 def flush(self, timeout=1.0):
413 def flush(self, timeout=1.0):
412 """Immediately processes all pending messages on the iopub channel.
414 """Immediately processes all pending messages on the iopub channel.
413
415
414 Callers should use this method to ensure that :meth:`call_handlers`
416 Callers should use this method to ensure that :meth:`call_handlers`
415 has been called for all messages that have been received on the
417 has been called for all messages that have been received on the
416 0MQ SUB socket of this channel.
418 0MQ SUB socket of this channel.
417
419
418 This method is thread safe.
420 This method is thread safe.
419
421
420 Parameters
422 Parameters
421 ----------
423 ----------
422 timeout : float, optional
424 timeout : float, optional
423 The maximum amount of time to spend flushing, in seconds. The
425 The maximum amount of time to spend flushing, in seconds. The
424 default is one second.
426 default is one second.
425 """
427 """
426 # We do the IOLoop callback process twice to ensure that the IOLoop
428 # We do the IOLoop callback process twice to ensure that the IOLoop
427 # gets to perform at least one full poll.
429 # gets to perform at least one full poll.
428 stop_time = time.time() + timeout
430 stop_time = time.time() + timeout
429 for i in range(2):
431 for i in range(2):
430 self._flushed = False
432 self._flushed = False
431 self.ioloop.add_callback(self._flush)
433 self.ioloop.add_callback(self._flush)
432 while not self._flushed and time.time() < stop_time:
434 while not self._flushed and time.time() < stop_time:
433 time.sleep(0.01)
435 time.sleep(0.01)
434
436
435 def _flush(self):
437 def _flush(self):
436 """Callback for :method:`self.flush`."""
438 """Callback for :method:`self.flush`."""
437 self.stream.flush()
439 self.stream.flush()
438 self._flushed = True
440 self._flushed = True
439
441
440
442
441 class StdInChannel(ZMQSocketChannel):
443 class StdInChannel(ZMQSocketChannel):
442 """The stdin channel to handle raw_input requests that the kernel makes."""
444 """The stdin channel to handle raw_input requests that the kernel makes."""
443
445
444 msg_queue = None
446 msg_queue = None
445 proxy_methods = ['input']
447 proxy_methods = ['input']
446
448
447 def __init__(self, context, session, address):
449 def __init__(self, context, session, address):
448 super(StdInChannel, self).__init__(context, session, address)
450 super(StdInChannel, self).__init__(context, session, address)
449 self.ioloop = ioloop.IOLoop()
451 self.ioloop = ioloop.IOLoop()
450
452
451 def run(self):
453 def run(self):
452 """The thread's main activity. Call start() instead."""
454 """The thread's main activity. Call start() instead."""
453 self.socket = self.context.socket(zmq.DEALER)
455 self.socket = self.context.socket(zmq.DEALER)
454 self.socket.linger = 1000
456 self.socket.linger = 1000
455 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
457 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
456 self.socket.connect(self.address)
458 self.socket.connect(self.address)
457 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
459 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
458 self.stream.on_recv(self._handle_recv)
460 self.stream.on_recv(self._handle_recv)
459 self._run_loop()
461 self._run_loop()
460
462
461 def call_handlers(self, msg):
463 def call_handlers(self, msg):
462 """This method is called in the ioloop thread when a message arrives.
464 """This method is called in the ioloop thread when a message arrives.
463
465
464 Subclasses should override this method to handle incoming messages.
466 Subclasses should override this method to handle incoming messages.
465 It is important to remember that this method is called in the thread
467 It is important to remember that this method is called in the thread
466 so that some logic must be done to ensure that the application leve
468 so that some logic must be done to ensure that the application leve
467 handlers are called in the application thread.
469 handlers are called in the application thread.
468 """
470 """
469 raise NotImplementedError('call_handlers must be defined in a subclass.')
471 raise NotImplementedError('call_handlers must be defined in a subclass.')
470
472
471 def input(self, string):
473 def input(self, string):
472 """Send a string of raw input to the kernel."""
474 """Send a string of raw input to the kernel."""
473 content = dict(value=string)
475 content = dict(value=string)
474 msg = self.session.msg('input_reply', content)
476 msg = self.session.msg('input_reply', content)
475 self._queue_send(msg)
477 self._queue_send(msg)
476
478
477
479
478 class HBChannel(ZMQSocketChannel):
480 class HBChannel(ZMQSocketChannel):
479 """The heartbeat channel which monitors the kernel heartbeat.
481 """The heartbeat channel which monitors the kernel heartbeat.
480
482
481 Note that the heartbeat channel is paused by default. As long as you start
483 Note that the heartbeat channel is paused by default. As long as you start
482 this channel, the kernel manager will ensure that it is paused and un-paused
484 this channel, the kernel manager will ensure that it is paused and un-paused
483 as appropriate.
485 as appropriate.
484 """
486 """
485
487
486 time_to_dead = 3.0
488 time_to_dead = 3.0
487 socket = None
489 socket = None
488 poller = None
490 poller = None
489 _running = None
491 _running = None
490 _pause = None
492 _pause = None
491 _beating = None
493 _beating = None
492
494
493 def __init__(self, context, session, address):
495 def __init__(self, context, session, address):
494 super(HBChannel, self).__init__(context, session, address)
496 super(HBChannel, self).__init__(context, session, address)
495 self._running = False
497 self._running = False
496 self._pause =True
498 self._pause =True
497 self.poller = zmq.Poller()
499 self.poller = zmq.Poller()
498
500
499 def _create_socket(self):
501 def _create_socket(self):
500 if self.socket is not None:
502 if self.socket is not None:
501 # close previous socket, before opening a new one
503 # close previous socket, before opening a new one
502 self.poller.unregister(self.socket)
504 self.poller.unregister(self.socket)
503 self.socket.close()
505 self.socket.close()
504 self.socket = self.context.socket(zmq.REQ)
506 self.socket = self.context.socket(zmq.REQ)
505 self.socket.linger = 1000
507 self.socket.linger = 1000
506 self.socket.connect(self.address)
508 self.socket.connect(self.address)
507
509
508 self.poller.register(self.socket, zmq.POLLIN)
510 self.poller.register(self.socket, zmq.POLLIN)
509
511
510 def _poll(self, start_time):
512 def _poll(self, start_time):
511 """poll for heartbeat replies until we reach self.time_to_dead.
513 """poll for heartbeat replies until we reach self.time_to_dead.
512
514
513 Ignores interrupts, and returns the result of poll(), which
515 Ignores interrupts, and returns the result of poll(), which
514 will be an empty list if no messages arrived before the timeout,
516 will be an empty list if no messages arrived before the timeout,
515 or the event tuple if there is a message to receive.
517 or the event tuple if there is a message to receive.
516 """
518 """
517
519
518 until_dead = self.time_to_dead - (time.time() - start_time)
520 until_dead = self.time_to_dead - (time.time() - start_time)
519 # ensure poll at least once
521 # ensure poll at least once
520 until_dead = max(until_dead, 1e-3)
522 until_dead = max(until_dead, 1e-3)
521 events = []
523 events = []
522 while True:
524 while True:
523 try:
525 try:
524 events = self.poller.poll(1000 * until_dead)
526 events = self.poller.poll(1000 * until_dead)
525 except ZMQError as e:
527 except ZMQError as e:
526 if e.errno == errno.EINTR:
528 if e.errno == errno.EINTR:
527 # ignore interrupts during heartbeat
529 # ignore interrupts during heartbeat
528 # this may never actually happen
530 # this may never actually happen
529 until_dead = self.time_to_dead - (time.time() - start_time)
531 until_dead = self.time_to_dead - (time.time() - start_time)
530 until_dead = max(until_dead, 1e-3)
532 until_dead = max(until_dead, 1e-3)
531 pass
533 pass
532 else:
534 else:
533 raise
535 raise
534 except Exception:
536 except Exception:
535 if self._exiting:
537 if self._exiting:
536 break
538 break
537 else:
539 else:
538 raise
540 raise
539 else:
541 else:
540 break
542 break
541 return events
543 return events
542
544
543 def run(self):
545 def run(self):
544 """The thread's main activity. Call start() instead."""
546 """The thread's main activity. Call start() instead."""
545 self._create_socket()
547 self._create_socket()
546 self._running = True
548 self._running = True
547 self._beating = True
549 self._beating = True
548
550
549 while self._running:
551 while self._running:
550 if self._pause:
552 if self._pause:
551 # just sleep, and skip the rest of the loop
553 # just sleep, and skip the rest of the loop
552 time.sleep(self.time_to_dead)
554 time.sleep(self.time_to_dead)
553 continue
555 continue
554
556
555 since_last_heartbeat = 0.0
557 since_last_heartbeat = 0.0
556 # io.rprint('Ping from HB channel') # dbg
558 # io.rprint('Ping from HB channel') # dbg
557 # no need to catch EFSM here, because the previous event was
559 # no need to catch EFSM here, because the previous event was
558 # either a recv or connect, which cannot be followed by EFSM
560 # either a recv or connect, which cannot be followed by EFSM
559 self.socket.send(b'ping')
561 self.socket.send(b'ping')
560 request_time = time.time()
562 request_time = time.time()
561 ready = self._poll(request_time)
563 ready = self._poll(request_time)
562 if ready:
564 if ready:
563 self._beating = True
565 self._beating = True
564 # the poll above guarantees we have something to recv
566 # the poll above guarantees we have something to recv
565 self.socket.recv()
567 self.socket.recv()
566 # sleep the remainder of the cycle
568 # sleep the remainder of the cycle
567 remainder = self.time_to_dead - (time.time() - request_time)
569 remainder = self.time_to_dead - (time.time() - request_time)
568 if remainder > 0:
570 if remainder > 0:
569 time.sleep(remainder)
571 time.sleep(remainder)
570 continue
572 continue
571 else:
573 else:
572 # nothing was received within the time limit, signal heart failure
574 # nothing was received within the time limit, signal heart failure
573 self._beating = False
575 self._beating = False
574 since_last_heartbeat = time.time() - request_time
576 since_last_heartbeat = time.time() - request_time
575 self.call_handlers(since_last_heartbeat)
577 self.call_handlers(since_last_heartbeat)
576 # and close/reopen the socket, because the REQ/REP cycle has been broken
578 # and close/reopen the socket, because the REQ/REP cycle has been broken
577 self._create_socket()
579 self._create_socket()
578 continue
580 continue
579
581
580 def pause(self):
582 def pause(self):
581 """Pause the heartbeat."""
583 """Pause the heartbeat."""
582 self._pause = True
584 self._pause = True
583
585
584 def unpause(self):
586 def unpause(self):
585 """Unpause the heartbeat."""
587 """Unpause the heartbeat."""
586 self._pause = False
588 self._pause = False
587
589
588 def is_beating(self):
590 def is_beating(self):
589 """Is the heartbeat running and responsive (and not paused)."""
591 """Is the heartbeat running and responsive (and not paused)."""
590 if self.is_alive() and not self._pause and self._beating:
592 if self.is_alive() and not self._pause and self._beating:
591 return True
593 return True
592 else:
594 else:
593 return False
595 return False
594
596
595 def stop(self):
597 def stop(self):
596 """Stop the channel's event loop and join its thread."""
598 """Stop the channel's event loop and join its thread."""
597 self._running = False
599 self._running = False
598 super(HBChannel, self).stop()
600 super(HBChannel, self).stop()
599
601
600 def call_handlers(self, since_last_heartbeat):
602 def call_handlers(self, since_last_heartbeat):
601 """This method is called in the ioloop thread when a message arrives.
603 """This method is called in the ioloop thread when a message arrives.
602
604
603 Subclasses should override this method to handle incoming messages.
605 Subclasses should override this method to handle incoming messages.
604 It is important to remember that this method is called in the thread
606 It is important to remember that this method is called in the thread
605 so that some logic must be done to ensure that the application level
607 so that some logic must be done to ensure that the application level
606 handlers are called in the application thread.
608 handlers are called in the application thread.
607 """
609 """
608 raise NotImplementedError('call_handlers must be defined in a subclass.')
610 raise NotImplementedError('call_handlers must be defined in a subclass.')
609
611
610
612
611 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
613 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
612 # ABC Registration
614 # ABC Registration
613 #-----------------------------------------------------------------------------
615 #-----------------------------------------------------------------------------
614
616
615 ShellChannelABC.register(ShellChannel)
617 ShellChannelABC.register(ShellChannel)
616 IOPubChannelABC.register(IOPubChannel)
618 IOPubChannelABC.register(IOPubChannel)
617 HBChannelABC.register(HBChannel)
619 HBChannelABC.register(HBChannel)
618 StdInChannelABC.register(StdInChannel)
620 StdInChannelABC.register(StdInChannel)
@@ -1,190 +1,192 b''
1 """A kernel client for in-process kernels."""
1 """A kernel client for in-process kernels."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from IPython.kernel.channelsabc import (
6 from IPython.kernel.channelsabc import (
7 ShellChannelABC, IOPubChannelABC,
7 ShellChannelABC, IOPubChannelABC,
8 HBChannelABC, StdInChannelABC,
8 HBChannelABC, StdInChannelABC,
9 )
9 )
10
10
11 from .socket import DummySocket
11 from .socket import DummySocket
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Channel classes
14 # Channel classes
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 class InProcessChannel(object):
17 class InProcessChannel(object):
18 """Base class for in-process channels."""
18 """Base class for in-process channels."""
19 proxy_methods = []
19 proxy_methods = []
20
20
21 def __init__(self, client=None):
21 def __init__(self, client=None):
22 super(InProcessChannel, self).__init__()
22 super(InProcessChannel, self).__init__()
23 self.client = client
23 self.client = client
24 self._is_alive = False
24 self._is_alive = False
25
25
26 #--------------------------------------------------------------------------
26 #--------------------------------------------------------------------------
27 # Channel interface
27 # Channel interface
28 #--------------------------------------------------------------------------
28 #--------------------------------------------------------------------------
29
29
30 def is_alive(self):
30 def is_alive(self):
31 return self._is_alive
31 return self._is_alive
32
32
33 def start(self):
33 def start(self):
34 self._is_alive = True
34 self._is_alive = True
35
35
36 def stop(self):
36 def stop(self):
37 self._is_alive = False
37 self._is_alive = False
38
38
39 def call_handlers(self, msg):
39 def call_handlers(self, msg):
40 """ This method is called in the main thread when a message arrives.
40 """ This method is called in the main thread when a message arrives.
41
41
42 Subclasses should override this method to handle incoming messages.
42 Subclasses should override this method to handle incoming messages.
43 """
43 """
44 raise NotImplementedError('call_handlers must be defined in a subclass.')
44 raise NotImplementedError('call_handlers must be defined in a subclass.')
45
45
46 #--------------------------------------------------------------------------
46 #--------------------------------------------------------------------------
47 # InProcessChannel interface
47 # InProcessChannel interface
48 #--------------------------------------------------------------------------
48 #--------------------------------------------------------------------------
49
49
50 def call_handlers_later(self, *args, **kwds):
50 def call_handlers_later(self, *args, **kwds):
51 """ Call the message handlers later.
51 """ Call the message handlers later.
52
52
53 The default implementation just calls the handlers immediately, but this
53 The default implementation just calls the handlers immediately, but this
54 method exists so that GUI toolkits can defer calling the handlers until
54 method exists so that GUI toolkits can defer calling the handlers until
55 after the event loop has run, as expected by GUI frontends.
55 after the event loop has run, as expected by GUI frontends.
56 """
56 """
57 self.call_handlers(*args, **kwds)
57 self.call_handlers(*args, **kwds)
58
58
59 def process_events(self):
59 def process_events(self):
60 """ Process any pending GUI events.
60 """ Process any pending GUI events.
61
61
62 This method will be never be called from a frontend without an event
62 This method will be never be called from a frontend without an event
63 loop (e.g., a terminal frontend).
63 loop (e.g., a terminal frontend).
64 """
64 """
65 raise NotImplementedError
65 raise NotImplementedError
66
66
67
67
68 class InProcessShellChannel(InProcessChannel):
68 class InProcessShellChannel(InProcessChannel):
69 """See `IPython.kernel.channels.ShellChannel` for docstrings."""
69 """See `IPython.kernel.channels.ShellChannel` for docstrings."""
70
70
71 # flag for whether execute requests should be allowed to call raw_input
71 # flag for whether execute requests should be allowed to call raw_input
72 allow_stdin = True
72 allow_stdin = True
73 proxy_methods = [
73 proxy_methods = [
74 'execute',
74 'execute',
75 'complete',
75 'complete',
76 'object_info',
76 'object_info',
77 'history',
77 'history',
78 'shutdown',
78 'shutdown',
79 'kernel_info',
79 'kernel_info',
80 ]
80 ]
81
81
82 #--------------------------------------------------------------------------
82 #--------------------------------------------------------------------------
83 # ShellChannel interface
83 # ShellChannel interface
84 #--------------------------------------------------------------------------
84 #--------------------------------------------------------------------------
85
85
86 def execute(self, code, silent=False, store_history=True,
86 def execute(self, code, silent=False, store_history=True,
87 user_expressions={}, allow_stdin=None):
87 user_expressions={}, allow_stdin=None):
88 if allow_stdin is None:
88 if allow_stdin is None:
89 allow_stdin = self.allow_stdin
89 allow_stdin = self.allow_stdin
90 content = dict(code=code, silent=silent, store_history=store_history,
90 content = dict(code=code, silent=silent, store_history=store_history,
91 user_expressions=user_expressions,
91 user_expressions=user_expressions,
92 allow_stdin=allow_stdin)
92 allow_stdin=allow_stdin)
93 msg = self.client.session.msg('execute_request', content)
93 msg = self.client.session.msg('execute_request', content)
94 self._dispatch_to_kernel(msg)
94 self._dispatch_to_kernel(msg)
95 return msg['header']['msg_id']
95 return msg['header']['msg_id']
96
96
97 def complete(self, text, line, cursor_pos, block=None):
97 def complete(self, code, cursor_pos=0):
98 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
98 content = dict(code=code, cursor_pos=cursor_pos)
99 msg = self.client.session.msg('complete_request', content)
99 msg = self.client.session.msg('complete_request', content)
100 self._dispatch_to_kernel(msg)
100 self._dispatch_to_kernel(msg)
101 return msg['header']['msg_id']
101 return msg['header']['msg_id']
102
102
103 def object_info(self, oname, detail_level=0):
103 def object_info(self, code, cursor_pos=0, detail_level=0):
104 content = dict(oname=oname, detail_level=detail_level)
104 content = dict(code=code, cursor_pos=cursor_pos,
105 detail_level=detail_level,
106 )
105 msg = self.client.session.msg('object_info_request', content)
107 msg = self.client.session.msg('object_info_request', content)
106 self._dispatch_to_kernel(msg)
108 self._dispatch_to_kernel(msg)
107 return msg['header']['msg_id']
109 return msg['header']['msg_id']
108
110
109 def history(self, raw=True, output=False, hist_access_type='range', **kwds):
111 def history(self, raw=True, output=False, hist_access_type='range', **kwds):
110 content = dict(raw=raw, output=output,
112 content = dict(raw=raw, output=output,
111 hist_access_type=hist_access_type, **kwds)
113 hist_access_type=hist_access_type, **kwds)
112 msg = self.client.session.msg('history_request', content)
114 msg = self.client.session.msg('history_request', content)
113 self._dispatch_to_kernel(msg)
115 self._dispatch_to_kernel(msg)
114 return msg['header']['msg_id']
116 return msg['header']['msg_id']
115
117
116 def shutdown(self, restart=False):
118 def shutdown(self, restart=False):
117 # FIXME: What to do here?
119 # FIXME: What to do here?
118 raise NotImplementedError('Cannot shutdown in-process kernel')
120 raise NotImplementedError('Cannot shutdown in-process kernel')
119
121
120 def kernel_info(self):
122 def kernel_info(self):
121 """Request kernel info."""
123 """Request kernel info."""
122 msg = self.client.session.msg('kernel_info_request')
124 msg = self.client.session.msg('kernel_info_request')
123 self._dispatch_to_kernel(msg)
125 self._dispatch_to_kernel(msg)
124 return msg['header']['msg_id']
126 return msg['header']['msg_id']
125
127
126 #--------------------------------------------------------------------------
128 #--------------------------------------------------------------------------
127 # Protected interface
129 # Protected interface
128 #--------------------------------------------------------------------------
130 #--------------------------------------------------------------------------
129
131
130 def _dispatch_to_kernel(self, msg):
132 def _dispatch_to_kernel(self, msg):
131 """ Send a message to the kernel and handle a reply.
133 """ Send a message to the kernel and handle a reply.
132 """
134 """
133 kernel = self.client.kernel
135 kernel = self.client.kernel
134 if kernel is None:
136 if kernel is None:
135 raise RuntimeError('Cannot send request. No kernel exists.')
137 raise RuntimeError('Cannot send request. No kernel exists.')
136
138
137 stream = DummySocket()
139 stream = DummySocket()
138 self.client.session.send(stream, msg)
140 self.client.session.send(stream, msg)
139 msg_parts = stream.recv_multipart()
141 msg_parts = stream.recv_multipart()
140 kernel.dispatch_shell(stream, msg_parts)
142 kernel.dispatch_shell(stream, msg_parts)
141
143
142 idents, reply_msg = self.client.session.recv(stream, copy=False)
144 idents, reply_msg = self.client.session.recv(stream, copy=False)
143 self.call_handlers_later(reply_msg)
145 self.call_handlers_later(reply_msg)
144
146
145
147
146 class InProcessIOPubChannel(InProcessChannel):
148 class InProcessIOPubChannel(InProcessChannel):
147 """See `IPython.kernel.channels.IOPubChannel` for docstrings."""
149 """See `IPython.kernel.channels.IOPubChannel` for docstrings."""
148
150
149 def flush(self, timeout=1.0):
151 def flush(self, timeout=1.0):
150 pass
152 pass
151
153
152
154
153 class InProcessStdInChannel(InProcessChannel):
155 class InProcessStdInChannel(InProcessChannel):
154 """See `IPython.kernel.channels.StdInChannel` for docstrings."""
156 """See `IPython.kernel.channels.StdInChannel` for docstrings."""
155
157
156 proxy_methods = ['input']
158 proxy_methods = ['input']
157
159
158 def input(self, string):
160 def input(self, string):
159 kernel = self.client.kernel
161 kernel = self.client.kernel
160 if kernel is None:
162 if kernel is None:
161 raise RuntimeError('Cannot send input reply. No kernel exists.')
163 raise RuntimeError('Cannot send input reply. No kernel exists.')
162 kernel.raw_input_str = string
164 kernel.raw_input_str = string
163
165
164
166
165 class InProcessHBChannel(InProcessChannel):
167 class InProcessHBChannel(InProcessChannel):
166 """See `IPython.kernel.channels.HBChannel` for docstrings."""
168 """See `IPython.kernel.channels.HBChannel` for docstrings."""
167
169
168 time_to_dead = 3.0
170 time_to_dead = 3.0
169
171
170 def __init__(self, *args, **kwds):
172 def __init__(self, *args, **kwds):
171 super(InProcessHBChannel, self).__init__(*args, **kwds)
173 super(InProcessHBChannel, self).__init__(*args, **kwds)
172 self._pause = True
174 self._pause = True
173
175
174 def pause(self):
176 def pause(self):
175 self._pause = True
177 self._pause = True
176
178
177 def unpause(self):
179 def unpause(self):
178 self._pause = False
180 self._pause = False
179
181
180 def is_beating(self):
182 def is_beating(self):
181 return not self._pause
183 return not self._pause
182
184
183 #-----------------------------------------------------------------------------
185 #-----------------------------------------------------------------------------
184 # ABC Registration
186 # ABC Registration
185 #-----------------------------------------------------------------------------
187 #-----------------------------------------------------------------------------
186
188
187 ShellChannelABC.register(InProcessShellChannel)
189 ShellChannelABC.register(InProcessShellChannel)
188 IOPubChannelABC.register(InProcessIOPubChannel)
190 IOPubChannelABC.register(InProcessIOPubChannel)
189 HBChannelABC.register(InProcessHBChannel)
191 HBChannelABC.register(InProcessHBChannel)
190 StdInChannelABC.register(InProcessStdInChannel)
192 StdInChannelABC.register(InProcessStdInChannel)
@@ -1,111 +1,105 b''
1 #-------------------------------------------------------------------------------
1 # Copyright (c) IPython Development Team.
2 # Copyright (C) 2012 The IPython Development Team
2 # Distributed under the terms of the Modified BSD License.
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 #-------------------------------------------------------------------------------
7
3
8 #-----------------------------------------------------------------------------
9 # Imports
10 #-----------------------------------------------------------------------------
11 from __future__ import print_function
4 from __future__ import print_function
12
5
13 # Standard library imports
14 import unittest
6 import unittest
15
7
16 # Local imports
17 from IPython.kernel.inprocess.blocking import BlockingInProcessKernelClient
8 from IPython.kernel.inprocess.blocking import BlockingInProcessKernelClient
18 from IPython.kernel.inprocess.manager import InProcessKernelManager
9 from IPython.kernel.inprocess.manager import InProcessKernelManager
19
10
20 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
21 # Test case
12 # Test case
22 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
23
14
24 class InProcessKernelManagerTestCase(unittest.TestCase):
15 class InProcessKernelManagerTestCase(unittest.TestCase):
25
16
26 def test_interface(self):
17 def test_interface(self):
27 """ Does the in-process kernel manager implement the basic KM interface?
18 """ Does the in-process kernel manager implement the basic KM interface?
28 """
19 """
29 km = InProcessKernelManager()
20 km = InProcessKernelManager()
30 self.assert_(not km.has_kernel)
21 self.assert_(not km.has_kernel)
31
22
32 km.start_kernel()
23 km.start_kernel()
33 self.assert_(km.has_kernel)
24 self.assert_(km.has_kernel)
34 self.assert_(km.kernel is not None)
25 self.assert_(km.kernel is not None)
35
26
36 kc = BlockingInProcessKernelClient(kernel=km.kernel)
27 kc = BlockingInProcessKernelClient(kernel=km.kernel)
37 self.assert_(not kc.channels_running)
28 self.assert_(not kc.channels_running)
38
29
39 kc.start_channels()
30 kc.start_channels()
40 self.assert_(kc.channels_running)
31 self.assert_(kc.channels_running)
41
32
42 old_kernel = km.kernel
33 old_kernel = km.kernel
43 km.restart_kernel()
34 km.restart_kernel()
44 self.assert_(km.kernel is not None)
35 self.assert_(km.kernel is not None)
45 self.assertNotEquals(km.kernel, old_kernel)
36 self.assertNotEquals(km.kernel, old_kernel)
46
37
47 km.shutdown_kernel()
38 km.shutdown_kernel()
48 self.assert_(not km.has_kernel)
39 self.assert_(not km.has_kernel)
49
40
50 self.assertRaises(NotImplementedError, km.interrupt_kernel)
41 self.assertRaises(NotImplementedError, km.interrupt_kernel)
51 self.assertRaises(NotImplementedError, km.signal_kernel, 9)
42 self.assertRaises(NotImplementedError, km.signal_kernel, 9)
52
43
53 kc.stop_channels()
44 kc.stop_channels()
54 self.assert_(not kc.channels_running)
45 self.assert_(not kc.channels_running)
55
46
56 def test_execute(self):
47 def test_execute(self):
57 """ Does executing code in an in-process kernel work?
48 """ Does executing code in an in-process kernel work?
58 """
49 """
59 km = InProcessKernelManager()
50 km = InProcessKernelManager()
60 km.start_kernel()
51 km.start_kernel()
61 kc = BlockingInProcessKernelClient(kernel=km.kernel)
52 kc = BlockingInProcessKernelClient(kernel=km.kernel)
62 kc.start_channels()
53 kc.start_channels()
63 kc.execute('foo = 1')
54 kc.execute('foo = 1')
64 self.assertEquals(km.kernel.shell.user_ns['foo'], 1)
55 self.assertEquals(km.kernel.shell.user_ns['foo'], 1)
65
56
66 def test_complete(self):
57 def test_complete(self):
67 """ Does requesting completion from an in-process kernel work?
58 """ Does requesting completion from an in-process kernel work?
68 """
59 """
69 km = InProcessKernelManager()
60 km = InProcessKernelManager()
70 km.start_kernel()
61 km.start_kernel()
71 kc = BlockingInProcessKernelClient(kernel=km.kernel)
62 kc = BlockingInProcessKernelClient(kernel=km.kernel)
72 kc.start_channels()
63 kc.start_channels()
73 km.kernel.shell.push({'my_bar': 0, 'my_baz': 1})
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 msg = kc.get_shell_msg()
66 msg = kc.get_shell_msg()
76 self.assertEqual(msg['header']['msg_type'], 'complete_reply')
67 self.assertEqual(msg['header']['msg_type'], 'complete_reply')
77 self.assertEqual(sorted(msg['content']['matches']),
68 self.assertEqual(sorted(msg['content']['matches']),
78 ['my_bar', 'my_baz'])
69 ['my_bar', 'my_baz'])
79
70
80 def test_object_info(self):
71 def test_object_info(self):
81 """ Does requesting object information from an in-process kernel work?
72 """ Does requesting object information from an in-process kernel work?
82 """
73 """
83 km = InProcessKernelManager()
74 km = InProcessKernelManager()
84 km.start_kernel()
75 km.start_kernel()
85 kc = BlockingInProcessKernelClient(kernel=km.kernel)
76 kc = BlockingInProcessKernelClient(kernel=km.kernel)
86 kc.start_channels()
77 kc.start_channels()
87 km.kernel.shell.user_ns['foo'] = 1
78 km.kernel.shell.user_ns['foo'] = 1
88 kc.object_info('foo')
79 kc.object_info('foo')
89 msg = kc.get_shell_msg()
80 msg = kc.get_shell_msg()
90 self.assertEquals(msg['header']['msg_type'], 'object_info_reply')
81 self.assertEqual(msg['header']['msg_type'], 'object_info_reply')
91 self.assertEquals(msg['content']['name'], 'foo')
82 content = msg['content']
92 self.assertEquals(msg['content']['type_name'], 'int')
83 assert content['found']
84 self.assertEqual(content['name'], 'foo')
85 text = content['data']['text/plain']
86 self.assertIn('int', text)
93
87
94 def test_history(self):
88 def test_history(self):
95 """ Does requesting history from an in-process kernel work?
89 """ Does requesting history from an in-process kernel work?
96 """
90 """
97 km = InProcessKernelManager()
91 km = InProcessKernelManager()
98 km.start_kernel()
92 km.start_kernel()
99 kc = BlockingInProcessKernelClient(kernel=km.kernel)
93 kc = BlockingInProcessKernelClient(kernel=km.kernel)
100 kc.start_channels()
94 kc.start_channels()
101 kc.execute('%who')
95 kc.execute('%who')
102 kc.history(hist_access_type='tail', n=1)
96 kc.history(hist_access_type='tail', n=1)
103 msg = kc.shell_channel.get_msgs()[-1]
97 msg = kc.shell_channel.get_msgs()[-1]
104 self.assertEquals(msg['header']['msg_type'], 'history_reply')
98 self.assertEquals(msg['header']['msg_type'], 'history_reply')
105 history = msg['content']['history']
99 history = msg['content']['history']
106 self.assertEquals(len(history), 1)
100 self.assertEquals(len(history), 1)
107 self.assertEquals(history[0][2], '%who')
101 self.assertEquals(history[0][2], '%who')
108
102
109
103
110 if __name__ == '__main__':
104 if __name__ == '__main__':
111 unittest.main()
105 unittest.main()
@@ -1,420 +1,399 b''
1 """Test suite for our zeromq-based message specification."""
1 """Test suite for our zeromq-based message specification."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import re
6 import re
7 from distutils.version import LooseVersion as V
7 from distutils.version import LooseVersion as V
8 from subprocess import PIPE
8 from subprocess import PIPE
9 try:
9 try:
10 from queue import Empty # Py 3
10 from queue import Empty # Py 3
11 except ImportError:
11 except ImportError:
12 from Queue import Empty # Py 2
12 from Queue import Empty # Py 2
13
13
14 import nose.tools as nt
14 import nose.tools as nt
15
15
16 from IPython.kernel import KernelManager
16 from IPython.kernel import KernelManager
17
17
18 from IPython.utils.traitlets import (
18 from IPython.utils.traitlets import (
19 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
19 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
20 )
20 )
21 from IPython.utils.py3compat import string_types, iteritems
21 from IPython.utils.py3compat import string_types, iteritems
22
22
23 from .utils import TIMEOUT, start_global_kernel, flush_channels, execute
23 from .utils import TIMEOUT, start_global_kernel, flush_channels, execute
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Globals
26 # Globals
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 KC = None
28 KC = None
29
29
30 def setup():
30 def setup():
31 global KC
31 global KC
32 KC = start_global_kernel()
32 KC = start_global_kernel()
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Message Spec References
35 # Message Spec References
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class Reference(HasTraits):
38 class Reference(HasTraits):
39
39
40 """
40 """
41 Base class for message spec specification testing.
41 Base class for message spec specification testing.
42
42
43 This class is the core of the message specification test. The
43 This class is the core of the message specification test. The
44 idea is that child classes implement trait attributes for each
44 idea is that child classes implement trait attributes for each
45 message keys, so that message keys can be tested against these
45 message keys, so that message keys can be tested against these
46 traits using :meth:`check` method.
46 traits using :meth:`check` method.
47
47
48 """
48 """
49
49
50 def check(self, d):
50 def check(self, d):
51 """validate a dict against our traits"""
51 """validate a dict against our traits"""
52 for key in self.trait_names():
52 for key in self.trait_names():
53 nt.assert_in(key, d)
53 nt.assert_in(key, d)
54 # FIXME: always allow None, probably not a good idea
54 # FIXME: always allow None, probably not a good idea
55 if d[key] is None:
55 if d[key] is None:
56 continue
56 continue
57 try:
57 try:
58 setattr(self, key, d[key])
58 setattr(self, key, d[key])
59 except TraitError as e:
59 except TraitError as e:
60 assert False, str(e)
60 assert False, str(e)
61
61
62 class Version(Unicode):
62 class Version(Unicode):
63 def validate(self, obj, value):
63 def validate(self, obj, value):
64 min_version = self.default_value
64 min_version = self.default_value
65 if V(value) < V(min_version):
65 if V(value) < V(min_version):
66 raise TraitError("bad version: %s < %s" % (value, min_version))
66 raise TraitError("bad version: %s < %s" % (value, min_version))
67
67
68 class RMessage(Reference):
68 class RMessage(Reference):
69 msg_id = Unicode()
69 msg_id = Unicode()
70 msg_type = Unicode()
70 msg_type = Unicode()
71 header = Dict()
71 header = Dict()
72 parent_header = Dict()
72 parent_header = Dict()
73 content = Dict()
73 content = Dict()
74
74
75 def check(self, d):
75 def check(self, d):
76 super(RMessage, self).check(d)
76 super(RMessage, self).check(d)
77 RHeader().check(self.header)
77 RHeader().check(self.header)
78 if self.parent_header:
78 if self.parent_header:
79 RHeader().check(self.parent_header)
79 RHeader().check(self.parent_header)
80
80
81 class RHeader(Reference):
81 class RHeader(Reference):
82 msg_id = Unicode()
82 msg_id = Unicode()
83 msg_type = Unicode()
83 msg_type = Unicode()
84 session = Unicode()
84 session = Unicode()
85 username = Unicode()
85 username = Unicode()
86 version = Version('5.0')
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 class ExecuteReply(Reference):
100 class ExecuteReply(Reference):
90 execution_count = Integer()
101 execution_count = Integer()
91 status = Enum((u'ok', u'error'))
102 status = Enum((u'ok', u'error'))
92
103
93 def check(self, d):
104 def check(self, d):
94 Reference.check(self, d)
105 Reference.check(self, d)
95 if d['status'] == 'ok':
106 if d['status'] == 'ok':
96 ExecuteReplyOkay().check(d)
107 ExecuteReplyOkay().check(d)
97 elif d['status'] == 'error':
108 elif d['status'] == 'error':
98 ExecuteReplyError().check(d)
109 ExecuteReplyError().check(d)
99
110
100
111
101 class ExecuteReplyOkay(Reference):
112 class ExecuteReplyOkay(Reference):
102 payload = List(Dict)
113 payload = List(Dict)
103 user_expressions = Dict()
114 user_expressions = Dict()
104
115
105
116
106 class ExecuteReplyError(Reference):
117 class ExecuteReplyError(Reference):
107 ename = Unicode()
118 ename = Unicode()
108 evalue = Unicode()
119 evalue = Unicode()
109 traceback = List(Unicode)
120 traceback = List(Unicode)
110
121
111
122
112 class OInfoReply(Reference):
123 class OInfoReply(MimeBundle):
113 name = Unicode()
124 name = Unicode()
114 found = Bool()
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 class ArgSpec(Reference):
128 class ArgSpec(Reference):
140 args = List(Unicode)
129 args = List(Unicode)
141 varargs = Unicode()
130 varargs = Unicode()
142 varkw = Unicode()
131 varkw = Unicode()
143 defaults = List()
132 defaults = List()
144
133
145
134
146 class Status(Reference):
135 class Status(Reference):
147 execution_state = Enum((u'busy', u'idle', u'starting'))
136 execution_state = Enum((u'busy', u'idle', u'starting'))
148
137
149
138
150 class CompleteReply(Reference):
139 class CompleteReply(Reference):
151 matches = List(Unicode)
140 matches = List(Unicode)
152
141
153
142
154 class KernelInfoReply(Reference):
143 class KernelInfoReply(Reference):
155 protocol_version = Version('5.0')
144 protocol_version = Version('5.0')
156 ipython_version = Version('2.0')
145 ipython_version = Version('2.0')
157 language_version = Version('2.7')
146 language_version = Version('2.7')
158 language = Unicode()
147 language = Unicode()
159
148
160
149
161 # IOPub messages
150 # IOPub messages
162
151
163 class ExecuteInput(Reference):
152 class ExecuteInput(Reference):
164 code = Unicode()
153 code = Unicode()
165 execution_count = Integer()
154 execution_count = Integer()
166
155
167
156
168 Error = ExecuteReplyError
157 Error = ExecuteReplyError
169
158
170
159
171 class Stream(Reference):
160 class Stream(Reference):
172 name = Enum((u'stdout', u'stderr'))
161 name = Enum((u'stdout', u'stderr'))
173 data = Unicode()
162 data = Unicode()
174
163
175
164
176 mime_pat = re.compile(r'\w+/\w+')
165 class DisplayData(MimeBundle):
177
178 class DisplayData(Reference):
179 source = Unicode()
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 execution_count = Integer()
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 references = {
173 references = {
198 'execute_reply' : ExecuteReply(),
174 'execute_reply' : ExecuteReply(),
199 'object_info_reply' : OInfoReply(),
175 'object_info_reply' : OInfoReply(),
200 'status' : Status(),
176 'status' : Status(),
201 'complete_reply' : CompleteReply(),
177 'complete_reply' : CompleteReply(),
202 'kernel_info_reply': KernelInfoReply(),
178 'kernel_info_reply': KernelInfoReply(),
203 'execute_input' : ExecuteInput(),
179 'execute_input' : ExecuteInput(),
204 'execute_result' : ExecuteResult(),
180 'execute_result' : ExecuteResult(),
205 'error' : Error(),
181 'error' : Error(),
206 'stream' : Stream(),
182 'stream' : Stream(),
207 'display_data' : DisplayData(),
183 'display_data' : DisplayData(),
208 'header' : RHeader(),
184 'header' : RHeader(),
209 }
185 }
210 """
186 """
211 Specifications of `content` part of the reply messages.
187 Specifications of `content` part of the reply messages.
212 """
188 """
213
189
214
190
215 def validate_message(msg, msg_type=None, parent=None):
191 def validate_message(msg, msg_type=None, parent=None):
216 """validate a message
192 """validate a message
217
193
218 This is a generator, and must be iterated through to actually
194 This is a generator, and must be iterated through to actually
219 trigger each test.
195 trigger each test.
220
196
221 If msg_type and/or parent are given, the msg_type and/or parent msg_id
197 If msg_type and/or parent are given, the msg_type and/or parent msg_id
222 are compared with the given values.
198 are compared with the given values.
223 """
199 """
224 RMessage().check(msg)
200 RMessage().check(msg)
225 if msg_type:
201 if msg_type:
226 nt.assert_equal(msg['msg_type'], msg_type)
202 nt.assert_equal(msg['msg_type'], msg_type)
227 if parent:
203 if parent:
228 nt.assert_equal(msg['parent_header']['msg_id'], parent)
204 nt.assert_equal(msg['parent_header']['msg_id'], parent)
229 content = msg['content']
205 content = msg['content']
230 ref = references[msg['msg_type']]
206 ref = references[msg['msg_type']]
231 ref.check(content)
207 ref.check(content)
232
208
233
209
234 #-----------------------------------------------------------------------------
210 #-----------------------------------------------------------------------------
235 # Tests
211 # Tests
236 #-----------------------------------------------------------------------------
212 #-----------------------------------------------------------------------------
237
213
238 # Shell channel
214 # Shell channel
239
215
240 def test_execute():
216 def test_execute():
241 flush_channels()
217 flush_channels()
242
218
243 msg_id = KC.execute(code='x=1')
219 msg_id = KC.execute(code='x=1')
244 reply = KC.get_shell_msg(timeout=TIMEOUT)
220 reply = KC.get_shell_msg(timeout=TIMEOUT)
245 validate_message(reply, 'execute_reply', msg_id)
221 validate_message(reply, 'execute_reply', msg_id)
246
222
247
223
248 def test_execute_silent():
224 def test_execute_silent():
249 flush_channels()
225 flush_channels()
250 msg_id, reply = execute(code='x=1', silent=True)
226 msg_id, reply = execute(code='x=1', silent=True)
251
227
252 # flush status=idle
228 # flush status=idle
253 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
229 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
254 validate_message(status, 'status', msg_id)
230 validate_message(status, 'status', msg_id)
255 nt.assert_equal(status['content']['execution_state'], 'idle')
231 nt.assert_equal(status['content']['execution_state'], 'idle')
256
232
257 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
233 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
258 count = reply['execution_count']
234 count = reply['execution_count']
259
235
260 msg_id, reply = execute(code='x=2', silent=True)
236 msg_id, reply = execute(code='x=2', silent=True)
261
237
262 # flush status=idle
238 # flush status=idle
263 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
239 status = KC.iopub_channel.get_msg(timeout=TIMEOUT)
264 validate_message(status, 'status', msg_id)
240 validate_message(status, 'status', msg_id)
265 nt.assert_equal(status['content']['execution_state'], 'idle')
241 nt.assert_equal(status['content']['execution_state'], 'idle')
266
242
267 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
243 nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
268 count_2 = reply['execution_count']
244 count_2 = reply['execution_count']
269 nt.assert_equal(count_2, count)
245 nt.assert_equal(count_2, count)
270
246
271
247
272 def test_execute_error():
248 def test_execute_error():
273 flush_channels()
249 flush_channels()
274
250
275 msg_id, reply = execute(code='1/0')
251 msg_id, reply = execute(code='1/0')
276 nt.assert_equal(reply['status'], 'error')
252 nt.assert_equal(reply['status'], 'error')
277 nt.assert_equal(reply['ename'], 'ZeroDivisionError')
253 nt.assert_equal(reply['ename'], 'ZeroDivisionError')
278
254
279 error = KC.iopub_channel.get_msg(timeout=TIMEOUT)
255 error = KC.iopub_channel.get_msg(timeout=TIMEOUT)
280 validate_message(error, 'error', msg_id)
256 validate_message(error, 'error', msg_id)
281
257
282
258
283 def test_execute_inc():
259 def test_execute_inc():
284 """execute request should increment execution_count"""
260 """execute request should increment execution_count"""
285 flush_channels()
261 flush_channels()
286
262
287 msg_id, reply = execute(code='x=1')
263 msg_id, reply = execute(code='x=1')
288 count = reply['execution_count']
264 count = reply['execution_count']
289
265
290 flush_channels()
266 flush_channels()
291
267
292 msg_id, reply = execute(code='x=2')
268 msg_id, reply = execute(code='x=2')
293 count_2 = reply['execution_count']
269 count_2 = reply['execution_count']
294 nt.assert_equal(count_2, count+1)
270 nt.assert_equal(count_2, count+1)
295
271
296
272
297 def test_user_expressions():
273 def test_user_expressions():
298 flush_channels()
274 flush_channels()
299
275
300 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
276 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
301 user_expressions = reply['user_expressions']
277 user_expressions = reply['user_expressions']
302 nt.assert_equal(user_expressions, {u'foo': {
278 nt.assert_equal(user_expressions, {u'foo': {
303 u'status': u'ok',
279 u'status': u'ok',
304 u'data': {u'text/plain': u'2'},
280 u'data': {u'text/plain': u'2'},
305 u'metadata': {},
281 u'metadata': {},
306 }})
282 }})
307
283
308
284
309 def test_user_expressions_fail():
285 def test_user_expressions_fail():
310 flush_channels()
286 flush_channels()
311
287
312 msg_id, reply = execute(code='x=0', user_expressions=dict(foo='nosuchname'))
288 msg_id, reply = execute(code='x=0', user_expressions=dict(foo='nosuchname'))
313 user_expressions = reply['user_expressions']
289 user_expressions = reply['user_expressions']
314 foo = user_expressions['foo']
290 foo = user_expressions['foo']
315 nt.assert_equal(foo['status'], 'error')
291 nt.assert_equal(foo['status'], 'error')
316 nt.assert_equal(foo['ename'], 'NameError')
292 nt.assert_equal(foo['ename'], 'NameError')
317
293
318
294
319 def test_oinfo():
295 def test_oinfo():
320 flush_channels()
296 flush_channels()
321
297
322 msg_id = KC.object_info('a')
298 msg_id = KC.object_info('a')
323 reply = KC.get_shell_msg(timeout=TIMEOUT)
299 reply = KC.get_shell_msg(timeout=TIMEOUT)
324 validate_message(reply, 'object_info_reply', msg_id)
300 validate_message(reply, 'object_info_reply', msg_id)
325
301
326
302
327 def test_oinfo_found():
303 def test_oinfo_found():
328 flush_channels()
304 flush_channels()
329
305
330 msg_id, reply = execute(code='a=5')
306 msg_id, reply = execute(code='a=5')
331
307
332 msg_id = KC.object_info('a')
308 msg_id = KC.object_info('a')
333 reply = KC.get_shell_msg(timeout=TIMEOUT)
309 reply = KC.get_shell_msg(timeout=TIMEOUT)
334 validate_message(reply, 'object_info_reply', msg_id)
310 validate_message(reply, 'object_info_reply', msg_id)
335 content = reply['content']
311 content = reply['content']
336 assert content['found']
312 assert content['found']
337 argspec = content['argspec']
313 nt.assert_equal(content['name'], 'a')
338 nt.assert_is(argspec, None)
314 text = content['data']['text/plain']
315 nt.assert_in('Type:', text)
316 nt.assert_in('Docstring:', text)
339
317
340
318
341 def test_oinfo_detail():
319 def test_oinfo_detail():
342 flush_channels()
320 flush_channels()
343
321
344 msg_id, reply = execute(code='ip=get_ipython()')
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 reply = KC.get_shell_msg(timeout=TIMEOUT)
325 reply = KC.get_shell_msg(timeout=TIMEOUT)
348 validate_message(reply, 'object_info_reply', msg_id)
326 validate_message(reply, 'object_info_reply', msg_id)
349 content = reply['content']
327 content = reply['content']
350 assert content['found']
328 assert content['found']
351 argspec = content['argspec']
329 nt.assert_equal(content['name'], 'ip.object_inspect')
352 nt.assert_is_instance(argspec, dict, "expected non-empty argspec dict, got %r" % argspec)
330 text = content['data']['text/plain']
353 nt.assert_equal(argspec['defaults'], [0])
331 nt.assert_in('Definition:', text)
332 nt.assert_in('Source:', text)
354
333
355
334
356 def test_oinfo_not_found():
335 def test_oinfo_not_found():
357 flush_channels()
336 flush_channels()
358
337
359 msg_id = KC.object_info('dne')
338 msg_id = KC.object_info('dne')
360 reply = KC.get_shell_msg(timeout=TIMEOUT)
339 reply = KC.get_shell_msg(timeout=TIMEOUT)
361 validate_message(reply, 'object_info_reply', msg_id)
340 validate_message(reply, 'object_info_reply', msg_id)
362 content = reply['content']
341 content = reply['content']
363 nt.assert_false(content['found'])
342 nt.assert_false(content['found'])
364
343
365
344
366 def test_complete():
345 def test_complete():
367 flush_channels()
346 flush_channels()
368
347
369 msg_id, reply = execute(code="alpha = albert = 5")
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 reply = KC.get_shell_msg(timeout=TIMEOUT)
351 reply = KC.get_shell_msg(timeout=TIMEOUT)
373 validate_message(reply, 'complete_reply', msg_id)
352 validate_message(reply, 'complete_reply', msg_id)
374 matches = reply['content']['matches']
353 matches = reply['content']['matches']
375 for name in ('alpha', 'albert'):
354 for name in ('alpha', 'albert'):
376 nt.assert_in(name, matches)
355 nt.assert_in(name, matches)
377
356
378
357
379 def test_kernel_info_request():
358 def test_kernel_info_request():
380 flush_channels()
359 flush_channels()
381
360
382 msg_id = KC.kernel_info()
361 msg_id = KC.kernel_info()
383 reply = KC.get_shell_msg(timeout=TIMEOUT)
362 reply = KC.get_shell_msg(timeout=TIMEOUT)
384 validate_message(reply, 'kernel_info_reply', msg_id)
363 validate_message(reply, 'kernel_info_reply', msg_id)
385
364
386
365
387 def test_single_payload():
366 def test_single_payload():
388 flush_channels()
367 flush_channels()
389 msg_id, reply = execute(code="for i in range(3):\n"+
368 msg_id, reply = execute(code="for i in range(3):\n"+
390 " x=range?\n")
369 " x=range?\n")
391 payload = reply['payload']
370 payload = reply['payload']
392 next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"]
371 next_input_pls = [pl for pl in payload if pl["source"] == "set_next_input"]
393 nt.assert_equal(len(next_input_pls), 1)
372 nt.assert_equal(len(next_input_pls), 1)
394
373
395
374
396 # IOPub channel
375 # IOPub channel
397
376
398
377
399 def test_stream():
378 def test_stream():
400 flush_channels()
379 flush_channels()
401
380
402 msg_id, reply = execute("print('hi')")
381 msg_id, reply = execute("print('hi')")
403
382
404 stdout = KC.iopub_channel.get_msg(timeout=TIMEOUT)
383 stdout = KC.iopub_channel.get_msg(timeout=TIMEOUT)
405 validate_message(stdout, 'stream', msg_id)
384 validate_message(stdout, 'stream', msg_id)
406 content = stdout['content']
385 content = stdout['content']
407 nt.assert_equal(content['name'], u'stdout')
386 nt.assert_equal(content['name'], u'stdout')
408 nt.assert_equal(content['data'], u'hi\n')
387 nt.assert_equal(content['data'], u'hi\n')
409
388
410
389
411 def test_display_data():
390 def test_display_data():
412 flush_channels()
391 flush_channels()
413
392
414 msg_id, reply = execute("from IPython.core.display import display; display(1)")
393 msg_id, reply = execute("from IPython.core.display import display; display(1)")
415
394
416 display = KC.iopub_channel.get_msg(timeout=TIMEOUT)
395 display = KC.iopub_channel.get_msg(timeout=TIMEOUT)
417 validate_message(display, 'display_data', parent=msg_id)
396 validate_message(display, 'display_data', parent=msg_id)
418 data = display['content']['data']
397 data = display['content']['data']
419 nt.assert_equal(data['text/plain'], u'1')
398 nt.assert_equal(data['text/plain'], u'1')
420
399
@@ -1,846 +1,850 b''
1 #!/usr/bin/env python
2 """An interactive kernel that talks to frontends over 0MQ."""
1 """An interactive kernel that talks to frontends over 0MQ."""
3
2
4 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
6
5
7 from __future__ import print_function
6 from __future__ import print_function
8
7
9 import getpass
8 import getpass
10 import sys
9 import sys
11 import time
10 import time
12 import traceback
11 import traceback
13 import logging
12 import logging
14 import uuid
13 import uuid
15
14
16 from datetime import datetime
15 from datetime import datetime
17 from signal import (
16 from signal import (
18 signal, default_int_handler, SIGINT
17 signal, default_int_handler, SIGINT
19 )
18 )
20
19
21 import zmq
20 import zmq
22 from zmq.eventloop import ioloop
21 from zmq.eventloop import ioloop
23 from zmq.eventloop.zmqstream import ZMQStream
22 from zmq.eventloop.zmqstream import ZMQStream
24
23
25 from IPython.config.configurable import Configurable
24 from IPython.config.configurable import Configurable
26 from IPython.core.error import StdinNotImplementedError
25 from IPython.core.error import StdinNotImplementedError
27 from IPython.core import release
26 from IPython.core import release
28 from IPython.utils import py3compat
27 from IPython.utils import py3compat
29 from IPython.utils.py3compat import builtin_mod, unicode_type, string_types
28 from IPython.utils.py3compat import builtin_mod, unicode_type, string_types
30 from IPython.utils.jsonutil import json_clean
29 from IPython.utils.jsonutil import json_clean
30 from IPython.utils.tokenutil import token_at_cursor
31 from IPython.utils.traitlets import (
31 from IPython.utils.traitlets import (
32 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
32 Any, Instance, Float, Dict, List, Set, Integer, Unicode,
33 Type, Bool,
33 Type, Bool,
34 )
34 )
35
35
36 from .serialize import serialize_object, unpack_apply_message
36 from .serialize import serialize_object, unpack_apply_message
37 from .session import Session
37 from .session import Session
38 from .zmqshell import ZMQInteractiveShell
38 from .zmqshell import ZMQInteractiveShell
39
39
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Main kernel class
42 # Main kernel class
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 protocol_version = release.kernel_protocol_version
45 protocol_version = release.kernel_protocol_version
46 ipython_version = release.version
46 ipython_version = release.version
47 language_version = sys.version.split()[0]
47 language_version = sys.version.split()[0]
48
48
49
49
50 class Kernel(Configurable):
50 class Kernel(Configurable):
51
51
52 #---------------------------------------------------------------------------
52 #---------------------------------------------------------------------------
53 # Kernel interface
53 # Kernel interface
54 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
55
55
56 # attribute to override with a GUI
56 # attribute to override with a GUI
57 eventloop = Any(None)
57 eventloop = Any(None)
58 def _eventloop_changed(self, name, old, new):
58 def _eventloop_changed(self, name, old, new):
59 """schedule call to eventloop from IOLoop"""
59 """schedule call to eventloop from IOLoop"""
60 loop = ioloop.IOLoop.instance()
60 loop = ioloop.IOLoop.instance()
61 loop.add_callback(self.enter_eventloop)
61 loop.add_callback(self.enter_eventloop)
62
62
63 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
63 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
64 shell_class = Type(ZMQInteractiveShell)
64 shell_class = Type(ZMQInteractiveShell)
65
65
66 session = Instance(Session)
66 session = Instance(Session)
67 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
67 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
68 shell_streams = List()
68 shell_streams = List()
69 control_stream = Instance(ZMQStream)
69 control_stream = Instance(ZMQStream)
70 iopub_socket = Instance(zmq.Socket)
70 iopub_socket = Instance(zmq.Socket)
71 stdin_socket = Instance(zmq.Socket)
71 stdin_socket = Instance(zmq.Socket)
72 log = Instance(logging.Logger)
72 log = Instance(logging.Logger)
73
73
74 user_module = Any()
74 user_module = Any()
75 def _user_module_changed(self, name, old, new):
75 def _user_module_changed(self, name, old, new):
76 if self.shell is not None:
76 if self.shell is not None:
77 self.shell.user_module = new
77 self.shell.user_module = new
78
78
79 user_ns = Instance(dict, args=None, allow_none=True)
79 user_ns = Instance(dict, args=None, allow_none=True)
80 def _user_ns_changed(self, name, old, new):
80 def _user_ns_changed(self, name, old, new):
81 if self.shell is not None:
81 if self.shell is not None:
82 self.shell.user_ns = new
82 self.shell.user_ns = new
83 self.shell.init_user_ns()
83 self.shell.init_user_ns()
84
84
85 # identities:
85 # identities:
86 int_id = Integer(-1)
86 int_id = Integer(-1)
87 ident = Unicode()
87 ident = Unicode()
88
88
89 def _ident_default(self):
89 def _ident_default(self):
90 return unicode_type(uuid.uuid4())
90 return unicode_type(uuid.uuid4())
91
91
92 # Private interface
92 # Private interface
93
93
94 _darwin_app_nap = Bool(True, config=True,
94 _darwin_app_nap = Bool(True, config=True,
95 help="""Whether to use appnope for compatiblity with OS X App Nap.
95 help="""Whether to use appnope for compatiblity with OS X App Nap.
96
96
97 Only affects OS X >= 10.9.
97 Only affects OS X >= 10.9.
98 """
98 """
99 )
99 )
100
100
101 # track associations with current request
101 # track associations with current request
102 _allow_stdin = Bool(False)
102 _allow_stdin = Bool(False)
103 _parent_header = Dict()
103 _parent_header = Dict()
104 _parent_ident = Any(b'')
104 _parent_ident = Any(b'')
105 # Time to sleep after flushing the stdout/err buffers in each execute
105 # Time to sleep after flushing the stdout/err buffers in each execute
106 # cycle. While this introduces a hard limit on the minimal latency of the
106 # cycle. While this introduces a hard limit on the minimal latency of the
107 # execute cycle, it helps prevent output synchronization problems for
107 # execute cycle, it helps prevent output synchronization problems for
108 # clients.
108 # clients.
109 # Units are in seconds. The minimum zmq latency on local host is probably
109 # Units are in seconds. The minimum zmq latency on local host is probably
110 # ~150 microseconds, set this to 500us for now. We may need to increase it
110 # ~150 microseconds, set this to 500us for now. We may need to increase it
111 # a little if it's not enough after more interactive testing.
111 # a little if it's not enough after more interactive testing.
112 _execute_sleep = Float(0.0005, config=True)
112 _execute_sleep = Float(0.0005, config=True)
113
113
114 # Frequency of the kernel's event loop.
114 # Frequency of the kernel's event loop.
115 # Units are in seconds, kernel subclasses for GUI toolkits may need to
115 # Units are in seconds, kernel subclasses for GUI toolkits may need to
116 # adapt to milliseconds.
116 # adapt to milliseconds.
117 _poll_interval = Float(0.05, config=True)
117 _poll_interval = Float(0.05, config=True)
118
118
119 # If the shutdown was requested over the network, we leave here the
119 # If the shutdown was requested over the network, we leave here the
120 # necessary reply message so it can be sent by our registered atexit
120 # necessary reply message so it can be sent by our registered atexit
121 # handler. This ensures that the reply is only sent to clients truly at
121 # handler. This ensures that the reply is only sent to clients truly at
122 # the end of our shutdown process (which happens after the underlying
122 # the end of our shutdown process (which happens after the underlying
123 # IPython shell's own shutdown).
123 # IPython shell's own shutdown).
124 _shutdown_message = None
124 _shutdown_message = None
125
125
126 # This is a dict of port number that the kernel is listening on. It is set
126 # This is a dict of port number that the kernel is listening on. It is set
127 # by record_ports and used by connect_request.
127 # by record_ports and used by connect_request.
128 _recorded_ports = Dict()
128 _recorded_ports = Dict()
129
129
130 # A reference to the Python builtin 'raw_input' function.
130 # A reference to the Python builtin 'raw_input' function.
131 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
131 # (i.e., __builtin__.raw_input for Python 2.7, builtins.input for Python 3)
132 _sys_raw_input = Any()
132 _sys_raw_input = Any()
133 _sys_eval_input = Any()
133 _sys_eval_input = Any()
134
134
135 # set of aborted msg_ids
135 # set of aborted msg_ids
136 aborted = Set()
136 aborted = Set()
137
137
138
138
139 def __init__(self, **kwargs):
139 def __init__(self, **kwargs):
140 super(Kernel, self).__init__(**kwargs)
140 super(Kernel, self).__init__(**kwargs)
141
141
142 # Initialize the InteractiveShell subclass
142 # Initialize the InteractiveShell subclass
143 self.shell = self.shell_class.instance(parent=self,
143 self.shell = self.shell_class.instance(parent=self,
144 profile_dir = self.profile_dir,
144 profile_dir = self.profile_dir,
145 user_module = self.user_module,
145 user_module = self.user_module,
146 user_ns = self.user_ns,
146 user_ns = self.user_ns,
147 kernel = self,
147 kernel = self,
148 )
148 )
149 self.shell.displayhook.session = self.session
149 self.shell.displayhook.session = self.session
150 self.shell.displayhook.pub_socket = self.iopub_socket
150 self.shell.displayhook.pub_socket = self.iopub_socket
151 self.shell.displayhook.topic = self._topic('execute_result')
151 self.shell.displayhook.topic = self._topic('execute_result')
152 self.shell.display_pub.session = self.session
152 self.shell.display_pub.session = self.session
153 self.shell.display_pub.pub_socket = self.iopub_socket
153 self.shell.display_pub.pub_socket = self.iopub_socket
154 self.shell.data_pub.session = self.session
154 self.shell.data_pub.session = self.session
155 self.shell.data_pub.pub_socket = self.iopub_socket
155 self.shell.data_pub.pub_socket = self.iopub_socket
156
156
157 # TMP - hack while developing
157 # TMP - hack while developing
158 self.shell._reply_content = None
158 self.shell._reply_content = None
159
159
160 # Build dict of handlers for message types
160 # Build dict of handlers for message types
161 msg_types = [ 'execute_request', 'complete_request',
161 msg_types = [ 'execute_request', 'complete_request',
162 'object_info_request', 'history_request',
162 'object_info_request', 'history_request',
163 'kernel_info_request',
163 'kernel_info_request',
164 'connect_request', 'shutdown_request',
164 'connect_request', 'shutdown_request',
165 'apply_request',
165 'apply_request',
166 ]
166 ]
167 self.shell_handlers = {}
167 self.shell_handlers = {}
168 for msg_type in msg_types:
168 for msg_type in msg_types:
169 self.shell_handlers[msg_type] = getattr(self, msg_type)
169 self.shell_handlers[msg_type] = getattr(self, msg_type)
170
170
171 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
171 comm_msg_types = [ 'comm_open', 'comm_msg', 'comm_close' ]
172 comm_manager = self.shell.comm_manager
172 comm_manager = self.shell.comm_manager
173 for msg_type in comm_msg_types:
173 for msg_type in comm_msg_types:
174 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
174 self.shell_handlers[msg_type] = getattr(comm_manager, msg_type)
175
175
176 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
176 control_msg_types = msg_types + [ 'clear_request', 'abort_request' ]
177 self.control_handlers = {}
177 self.control_handlers = {}
178 for msg_type in control_msg_types:
178 for msg_type in control_msg_types:
179 self.control_handlers[msg_type] = getattr(self, msg_type)
179 self.control_handlers[msg_type] = getattr(self, msg_type)
180
180
181
181
182 def dispatch_control(self, msg):
182 def dispatch_control(self, msg):
183 """dispatch control requests"""
183 """dispatch control requests"""
184 idents,msg = self.session.feed_identities(msg, copy=False)
184 idents,msg = self.session.feed_identities(msg, copy=False)
185 try:
185 try:
186 msg = self.session.unserialize(msg, content=True, copy=False)
186 msg = self.session.unserialize(msg, content=True, copy=False)
187 except:
187 except:
188 self.log.error("Invalid Control Message", exc_info=True)
188 self.log.error("Invalid Control Message", exc_info=True)
189 return
189 return
190
190
191 self.log.debug("Control received: %s", msg)
191 self.log.debug("Control received: %s", msg)
192
192
193 header = msg['header']
193 header = msg['header']
194 msg_id = header['msg_id']
194 msg_id = header['msg_id']
195 msg_type = header['msg_type']
195 msg_type = header['msg_type']
196
196
197 handler = self.control_handlers.get(msg_type, None)
197 handler = self.control_handlers.get(msg_type, None)
198 if handler is None:
198 if handler is None:
199 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
199 self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
200 else:
200 else:
201 try:
201 try:
202 handler(self.control_stream, idents, msg)
202 handler(self.control_stream, idents, msg)
203 except Exception:
203 except Exception:
204 self.log.error("Exception in control handler:", exc_info=True)
204 self.log.error("Exception in control handler:", exc_info=True)
205
205
206 def dispatch_shell(self, stream, msg):
206 def dispatch_shell(self, stream, msg):
207 """dispatch shell requests"""
207 """dispatch shell requests"""
208 # flush control requests first
208 # flush control requests first
209 if self.control_stream:
209 if self.control_stream:
210 self.control_stream.flush()
210 self.control_stream.flush()
211
211
212 idents,msg = self.session.feed_identities(msg, copy=False)
212 idents,msg = self.session.feed_identities(msg, copy=False)
213 try:
213 try:
214 msg = self.session.unserialize(msg, content=True, copy=False)
214 msg = self.session.unserialize(msg, content=True, copy=False)
215 except:
215 except:
216 self.log.error("Invalid Message", exc_info=True)
216 self.log.error("Invalid Message", exc_info=True)
217 return
217 return
218
218
219 header = msg['header']
219 header = msg['header']
220 msg_id = header['msg_id']
220 msg_id = header['msg_id']
221 msg_type = msg['header']['msg_type']
221 msg_type = msg['header']['msg_type']
222
222
223 # Print some info about this message and leave a '--->' marker, so it's
223 # Print some info about this message and leave a '--->' marker, so it's
224 # easier to trace visually the message chain when debugging. Each
224 # easier to trace visually the message chain when debugging. Each
225 # handler prints its message at the end.
225 # handler prints its message at the end.
226 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
226 self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
227 self.log.debug(' Content: %s\n --->\n ', msg['content'])
227 self.log.debug(' Content: %s\n --->\n ', msg['content'])
228
228
229 if msg_id in self.aborted:
229 if msg_id in self.aborted:
230 self.aborted.remove(msg_id)
230 self.aborted.remove(msg_id)
231 # is it safe to assume a msg_id will not be resubmitted?
231 # is it safe to assume a msg_id will not be resubmitted?
232 reply_type = msg_type.split('_')[0] + '_reply'
232 reply_type = msg_type.split('_')[0] + '_reply'
233 status = {'status' : 'aborted'}
233 status = {'status' : 'aborted'}
234 md = {'engine' : self.ident}
234 md = {'engine' : self.ident}
235 md.update(status)
235 md.update(status)
236 reply_msg = self.session.send(stream, reply_type, metadata=md,
236 reply_msg = self.session.send(stream, reply_type, metadata=md,
237 content=status, parent=msg, ident=idents)
237 content=status, parent=msg, ident=idents)
238 return
238 return
239
239
240 handler = self.shell_handlers.get(msg_type, None)
240 handler = self.shell_handlers.get(msg_type, None)
241 if handler is None:
241 if handler is None:
242 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
242 self.log.error("UNKNOWN MESSAGE TYPE: %r", msg_type)
243 else:
243 else:
244 # ensure default_int_handler during handler call
244 # ensure default_int_handler during handler call
245 sig = signal(SIGINT, default_int_handler)
245 sig = signal(SIGINT, default_int_handler)
246 self.log.debug("%s: %s", msg_type, msg)
246 try:
247 try:
247 handler(stream, idents, msg)
248 handler(stream, idents, msg)
248 except Exception:
249 except Exception:
249 self.log.error("Exception in message handler:", exc_info=True)
250 self.log.error("Exception in message handler:", exc_info=True)
250 finally:
251 finally:
251 signal(SIGINT, sig)
252 signal(SIGINT, sig)
252
253
253 def enter_eventloop(self):
254 def enter_eventloop(self):
254 """enter eventloop"""
255 """enter eventloop"""
255 self.log.info("entering eventloop %s", self.eventloop)
256 self.log.info("entering eventloop %s", self.eventloop)
256 for stream in self.shell_streams:
257 for stream in self.shell_streams:
257 # flush any pending replies,
258 # flush any pending replies,
258 # which may be skipped by entering the eventloop
259 # which may be skipped by entering the eventloop
259 stream.flush(zmq.POLLOUT)
260 stream.flush(zmq.POLLOUT)
260 # restore default_int_handler
261 # restore default_int_handler
261 signal(SIGINT, default_int_handler)
262 signal(SIGINT, default_int_handler)
262 while self.eventloop is not None:
263 while self.eventloop is not None:
263 try:
264 try:
264 self.eventloop(self)
265 self.eventloop(self)
265 except KeyboardInterrupt:
266 except KeyboardInterrupt:
266 # Ctrl-C shouldn't crash the kernel
267 # Ctrl-C shouldn't crash the kernel
267 self.log.error("KeyboardInterrupt caught in kernel")
268 self.log.error("KeyboardInterrupt caught in kernel")
268 continue
269 continue
269 else:
270 else:
270 # eventloop exited cleanly, this means we should stop (right?)
271 # eventloop exited cleanly, this means we should stop (right?)
271 self.eventloop = None
272 self.eventloop = None
272 break
273 break
273 self.log.info("exiting eventloop")
274 self.log.info("exiting eventloop")
274
275
275 def start(self):
276 def start(self):
276 """register dispatchers for streams"""
277 """register dispatchers for streams"""
277 self.shell.exit_now = False
278 self.shell.exit_now = False
278 if self.control_stream:
279 if self.control_stream:
279 self.control_stream.on_recv(self.dispatch_control, copy=False)
280 self.control_stream.on_recv(self.dispatch_control, copy=False)
280
281
281 def make_dispatcher(stream):
282 def make_dispatcher(stream):
282 def dispatcher(msg):
283 def dispatcher(msg):
283 return self.dispatch_shell(stream, msg)
284 return self.dispatch_shell(stream, msg)
284 return dispatcher
285 return dispatcher
285
286
286 for s in self.shell_streams:
287 for s in self.shell_streams:
287 s.on_recv(make_dispatcher(s), copy=False)
288 s.on_recv(make_dispatcher(s), copy=False)
288
289
289 # publish idle status
290 # publish idle status
290 self._publish_status('starting')
291 self._publish_status('starting')
291
292
292 def do_one_iteration(self):
293 def do_one_iteration(self):
293 """step eventloop just once"""
294 """step eventloop just once"""
294 if self.control_stream:
295 if self.control_stream:
295 self.control_stream.flush()
296 self.control_stream.flush()
296 for stream in self.shell_streams:
297 for stream in self.shell_streams:
297 # handle at most one request per iteration
298 # handle at most one request per iteration
298 stream.flush(zmq.POLLIN, 1)
299 stream.flush(zmq.POLLIN, 1)
299 stream.flush(zmq.POLLOUT)
300 stream.flush(zmq.POLLOUT)
300
301
301
302
302 def record_ports(self, ports):
303 def record_ports(self, ports):
303 """Record the ports that this kernel is using.
304 """Record the ports that this kernel is using.
304
305
305 The creator of the Kernel instance must call this methods if they
306 The creator of the Kernel instance must call this methods if they
306 want the :meth:`connect_request` method to return the port numbers.
307 want the :meth:`connect_request` method to return the port numbers.
307 """
308 """
308 self._recorded_ports = ports
309 self._recorded_ports = ports
309
310
310 #---------------------------------------------------------------------------
311 #---------------------------------------------------------------------------
311 # Kernel request handlers
312 # Kernel request handlers
312 #---------------------------------------------------------------------------
313 #---------------------------------------------------------------------------
313
314
314 def _make_metadata(self, other=None):
315 def _make_metadata(self, other=None):
315 """init metadata dict, for execute/apply_reply"""
316 """init metadata dict, for execute/apply_reply"""
316 new_md = {
317 new_md = {
317 'dependencies_met' : True,
318 'dependencies_met' : True,
318 'engine' : self.ident,
319 'engine' : self.ident,
319 'started': datetime.now(),
320 'started': datetime.now(),
320 }
321 }
321 if other:
322 if other:
322 new_md.update(other)
323 new_md.update(other)
323 return new_md
324 return new_md
324
325
325 def _publish_execute_input(self, code, parent, execution_count):
326 def _publish_execute_input(self, code, parent, execution_count):
326 """Publish the code request on the iopub stream."""
327 """Publish the code request on the iopub stream."""
327
328
328 self.session.send(self.iopub_socket, u'execute_input',
329 self.session.send(self.iopub_socket, u'execute_input',
329 {u'code':code, u'execution_count': execution_count},
330 {u'code':code, u'execution_count': execution_count},
330 parent=parent, ident=self._topic('execute_input')
331 parent=parent, ident=self._topic('execute_input')
331 )
332 )
332
333
333 def _publish_status(self, status, parent=None):
334 def _publish_status(self, status, parent=None):
334 """send status (busy/idle) on IOPub"""
335 """send status (busy/idle) on IOPub"""
335 self.session.send(self.iopub_socket,
336 self.session.send(self.iopub_socket,
336 u'status',
337 u'status',
337 {u'execution_state': status},
338 {u'execution_state': status},
338 parent=parent,
339 parent=parent,
339 ident=self._topic('status'),
340 ident=self._topic('status'),
340 )
341 )
341
342
342 def _forward_input(self, allow_stdin=False):
343 def _forward_input(self, allow_stdin=False):
343 """Forward raw_input and getpass to the current frontend.
344 """Forward raw_input and getpass to the current frontend.
344
345
345 via input_request
346 via input_request
346 """
347 """
347 self._allow_stdin = allow_stdin
348 self._allow_stdin = allow_stdin
348
349
349 if py3compat.PY3:
350 if py3compat.PY3:
350 self._sys_raw_input = builtin_mod.input
351 self._sys_raw_input = builtin_mod.input
351 builtin_mod.input = self.raw_input
352 builtin_mod.input = self.raw_input
352 else:
353 else:
353 self._sys_raw_input = builtin_mod.raw_input
354 self._sys_raw_input = builtin_mod.raw_input
354 self._sys_eval_input = builtin_mod.input
355 self._sys_eval_input = builtin_mod.input
355 builtin_mod.raw_input = self.raw_input
356 builtin_mod.raw_input = self.raw_input
356 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
357 builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt))
357 self._save_getpass = getpass.getpass
358 self._save_getpass = getpass.getpass
358 getpass.getpass = self.getpass
359 getpass.getpass = self.getpass
359
360
360 def _restore_input(self):
361 def _restore_input(self):
361 """Restore raw_input, getpass"""
362 """Restore raw_input, getpass"""
362 if py3compat.PY3:
363 if py3compat.PY3:
363 builtin_mod.input = self._sys_raw_input
364 builtin_mod.input = self._sys_raw_input
364 else:
365 else:
365 builtin_mod.raw_input = self._sys_raw_input
366 builtin_mod.raw_input = self._sys_raw_input
366 builtin_mod.input = self._sys_eval_input
367 builtin_mod.input = self._sys_eval_input
367
368
368 getpass.getpass = self._save_getpass
369 getpass.getpass = self._save_getpass
369
370
370 def set_parent(self, ident, parent):
371 def set_parent(self, ident, parent):
371 """Record the parent state
372 """Record the parent state
372
373
373 For associating side effects with their requests.
374 For associating side effects with their requests.
374 """
375 """
375 self._parent_ident = ident
376 self._parent_ident = ident
376 self._parent_header = parent
377 self._parent_header = parent
377 self.shell.set_parent(parent)
378 self.shell.set_parent(parent)
378
379
379 def execute_request(self, stream, ident, parent):
380 def execute_request(self, stream, ident, parent):
380 """handle an execute_request"""
381 """handle an execute_request"""
381
382
382 self._publish_status(u'busy', parent)
383 self._publish_status(u'busy', parent)
383
384
384 try:
385 try:
385 content = parent[u'content']
386 content = parent[u'content']
386 code = py3compat.cast_unicode_py2(content[u'code'])
387 code = py3compat.cast_unicode_py2(content[u'code'])
387 silent = content[u'silent']
388 silent = content[u'silent']
388 store_history = content.get(u'store_history', not silent)
389 store_history = content.get(u'store_history', not silent)
389 except:
390 except:
390 self.log.error("Got bad msg: ")
391 self.log.error("Got bad msg: ")
391 self.log.error("%s", parent)
392 self.log.error("%s", parent)
392 return
393 return
393
394
394 md = self._make_metadata(parent['metadata'])
395 md = self._make_metadata(parent['metadata'])
395
396
396 shell = self.shell # we'll need this a lot here
397 shell = self.shell # we'll need this a lot here
397
398
398 self._forward_input(content.get('allow_stdin', False))
399 self._forward_input(content.get('allow_stdin', False))
399 # Set the parent message of the display hook and out streams.
400 # Set the parent message of the display hook and out streams.
400 self.set_parent(ident, parent)
401 self.set_parent(ident, parent)
401
402
402 # Re-broadcast our input for the benefit of listening clients, and
403 # Re-broadcast our input for the benefit of listening clients, and
403 # start computing output
404 # start computing output
404 if not silent:
405 if not silent:
405 self._publish_execute_input(code, parent, shell.execution_count)
406 self._publish_execute_input(code, parent, shell.execution_count)
406
407
407 reply_content = {}
408 reply_content = {}
408 # FIXME: the shell calls the exception handler itself.
409 # FIXME: the shell calls the exception handler itself.
409 shell._reply_content = None
410 shell._reply_content = None
410 try:
411 try:
411 shell.run_cell(code, store_history=store_history, silent=silent)
412 shell.run_cell(code, store_history=store_history, silent=silent)
412 except:
413 except:
413 status = u'error'
414 status = u'error'
414 # FIXME: this code right now isn't being used yet by default,
415 # FIXME: this code right now isn't being used yet by default,
415 # because the run_cell() call above directly fires off exception
416 # because the run_cell() call above directly fires off exception
416 # reporting. This code, therefore, is only active in the scenario
417 # reporting. This code, therefore, is only active in the scenario
417 # where runlines itself has an unhandled exception. We need to
418 # where runlines itself has an unhandled exception. We need to
418 # uniformize this, for all exception construction to come from a
419 # uniformize this, for all exception construction to come from a
419 # single location in the codbase.
420 # single location in the codbase.
420 etype, evalue, tb = sys.exc_info()
421 etype, evalue, tb = sys.exc_info()
421 tb_list = traceback.format_exception(etype, evalue, tb)
422 tb_list = traceback.format_exception(etype, evalue, tb)
422 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
423 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
423 else:
424 else:
424 status = u'ok'
425 status = u'ok'
425 finally:
426 finally:
426 self._restore_input()
427 self._restore_input()
427
428
428 reply_content[u'status'] = status
429 reply_content[u'status'] = status
429
430
430 # Return the execution counter so clients can display prompts
431 # Return the execution counter so clients can display prompts
431 reply_content['execution_count'] = shell.execution_count - 1
432 reply_content['execution_count'] = shell.execution_count - 1
432
433
433 # FIXME - fish exception info out of shell, possibly left there by
434 # FIXME - fish exception info out of shell, possibly left there by
434 # runlines. We'll need to clean up this logic later.
435 # runlines. We'll need to clean up this logic later.
435 if shell._reply_content is not None:
436 if shell._reply_content is not None:
436 reply_content.update(shell._reply_content)
437 reply_content.update(shell._reply_content)
437 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
438 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute')
438 reply_content['engine_info'] = e_info
439 reply_content['engine_info'] = e_info
439 # reset after use
440 # reset after use
440 shell._reply_content = None
441 shell._reply_content = None
441
442
442 if 'traceback' in reply_content:
443 if 'traceback' in reply_content:
443 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
444 self.log.info("Exception in execute request:\n%s", '\n'.join(reply_content['traceback']))
444
445
445
446
446 # At this point, we can tell whether the main code execution succeeded
447 # At this point, we can tell whether the main code execution succeeded
447 # or not. If it did, we proceed to evaluate user_expressions
448 # or not. If it did, we proceed to evaluate user_expressions
448 if reply_content['status'] == 'ok':
449 if reply_content['status'] == 'ok':
449 reply_content[u'user_expressions'] = \
450 reply_content[u'user_expressions'] = \
450 shell.user_expressions(content.get(u'user_expressions', {}))
451 shell.user_expressions(content.get(u'user_expressions', {}))
451 else:
452 else:
452 # If there was an error, don't even try to compute expressions
453 # If there was an error, don't even try to compute expressions
453 reply_content[u'user_expressions'] = {}
454 reply_content[u'user_expressions'] = {}
454
455
455 # Payloads should be retrieved regardless of outcome, so we can both
456 # Payloads should be retrieved regardless of outcome, so we can both
456 # recover partial output (that could have been generated early in a
457 # recover partial output (that could have been generated early in a
457 # block, before an error) and clear the payload system always.
458 # block, before an error) and clear the payload system always.
458 reply_content[u'payload'] = shell.payload_manager.read_payload()
459 reply_content[u'payload'] = shell.payload_manager.read_payload()
459 # Be agressive about clearing the payload because we don't want
460 # Be agressive about clearing the payload because we don't want
460 # it to sit in memory until the next execute_request comes in.
461 # it to sit in memory until the next execute_request comes in.
461 shell.payload_manager.clear_payload()
462 shell.payload_manager.clear_payload()
462
463
463 # Flush output before sending the reply.
464 # Flush output before sending the reply.
464 sys.stdout.flush()
465 sys.stdout.flush()
465 sys.stderr.flush()
466 sys.stderr.flush()
466 # FIXME: on rare occasions, the flush doesn't seem to make it to the
467 # FIXME: on rare occasions, the flush doesn't seem to make it to the
467 # clients... This seems to mitigate the problem, but we definitely need
468 # clients... This seems to mitigate the problem, but we definitely need
468 # to better understand what's going on.
469 # to better understand what's going on.
469 if self._execute_sleep:
470 if self._execute_sleep:
470 time.sleep(self._execute_sleep)
471 time.sleep(self._execute_sleep)
471
472
472 # Send the reply.
473 # Send the reply.
473 reply_content = json_clean(reply_content)
474 reply_content = json_clean(reply_content)
474
475
475 md['status'] = reply_content['status']
476 md['status'] = reply_content['status']
476 if reply_content['status'] == 'error' and \
477 if reply_content['status'] == 'error' and \
477 reply_content['ename'] == 'UnmetDependency':
478 reply_content['ename'] == 'UnmetDependency':
478 md['dependencies_met'] = False
479 md['dependencies_met'] = False
479
480
480 reply_msg = self.session.send(stream, u'execute_reply',
481 reply_msg = self.session.send(stream, u'execute_reply',
481 reply_content, parent, metadata=md,
482 reply_content, parent, metadata=md,
482 ident=ident)
483 ident=ident)
483
484
484 self.log.debug("%s", reply_msg)
485 self.log.debug("%s", reply_msg)
485
486
486 if not silent and reply_msg['content']['status'] == u'error':
487 if not silent and reply_msg['content']['status'] == u'error':
487 self._abort_queues()
488 self._abort_queues()
488
489
489 self._publish_status(u'idle', parent)
490 self._publish_status(u'idle', parent)
490
491
491 def complete_request(self, stream, ident, parent):
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 matches = {'matches' : matches,
498 matches = {'matches' : matches,
494 'matched_text' : txt,
499 'matched_text' : txt,
495 'status' : 'ok'}
500 'status' : 'ok'}
496 matches = json_clean(matches)
501 matches = json_clean(matches)
497 completion_msg = self.session.send(stream, 'complete_reply',
502 completion_msg = self.session.send(stream, 'complete_reply',
498 matches, parent, ident)
503 matches, parent, ident)
499 self.log.debug("%s", completion_msg)
504 self.log.debug("%s", completion_msg)
500
505
501 def object_info_request(self, stream, ident, parent):
506 def object_info_request(self, stream, ident, parent):
502 content = parent['content']
507 content = parent['content']
503 object_info = self.shell.object_inspect(content['oname'],
508
504 detail_level = content.get('detail_level', 0)
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),
505 )
521 )
522 reply_content['data']['text/plain'] = info_text
506 # Before we send this object over, we scrub it for JSON usage
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 msg = self.session.send(stream, 'object_info_reply',
525 msg = self.session.send(stream, 'object_info_reply',
509 oinfo, parent, ident)
526 reply_content, parent, ident)
510 self.log.debug("%s", msg)
527 self.log.debug("%s", msg)
511
528
512 def history_request(self, stream, ident, parent):
529 def history_request(self, stream, ident, parent):
513 # We need to pull these out, as passing **kwargs doesn't work with
530 # We need to pull these out, as passing **kwargs doesn't work with
514 # unicode keys before Python 2.6.5.
531 # unicode keys before Python 2.6.5.
515 hist_access_type = parent['content']['hist_access_type']
532 hist_access_type = parent['content']['hist_access_type']
516 raw = parent['content']['raw']
533 raw = parent['content']['raw']
517 output = parent['content']['output']
534 output = parent['content']['output']
518 if hist_access_type == 'tail':
535 if hist_access_type == 'tail':
519 n = parent['content']['n']
536 n = parent['content']['n']
520 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
537 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
521 include_latest=True)
538 include_latest=True)
522
539
523 elif hist_access_type == 'range':
540 elif hist_access_type == 'range':
524 session = parent['content']['session']
541 session = parent['content']['session']
525 start = parent['content']['start']
542 start = parent['content']['start']
526 stop = parent['content']['stop']
543 stop = parent['content']['stop']
527 hist = self.shell.history_manager.get_range(session, start, stop,
544 hist = self.shell.history_manager.get_range(session, start, stop,
528 raw=raw, output=output)
545 raw=raw, output=output)
529
546
530 elif hist_access_type == 'search':
547 elif hist_access_type == 'search':
531 n = parent['content'].get('n')
548 n = parent['content'].get('n')
532 unique = parent['content'].get('unique', False)
549 unique = parent['content'].get('unique', False)
533 pattern = parent['content']['pattern']
550 pattern = parent['content']['pattern']
534 hist = self.shell.history_manager.search(
551 hist = self.shell.history_manager.search(
535 pattern, raw=raw, output=output, n=n, unique=unique)
552 pattern, raw=raw, output=output, n=n, unique=unique)
536
553
537 else:
554 else:
538 hist = []
555 hist = []
539 hist = list(hist)
556 hist = list(hist)
540 content = {'history' : hist}
557 content = {'history' : hist}
541 content = json_clean(content)
558 content = json_clean(content)
542 msg = self.session.send(stream, 'history_reply',
559 msg = self.session.send(stream, 'history_reply',
543 content, parent, ident)
560 content, parent, ident)
544 self.log.debug("Sending history reply with %i entries", len(hist))
561 self.log.debug("Sending history reply with %i entries", len(hist))
545
562
546 def connect_request(self, stream, ident, parent):
563 def connect_request(self, stream, ident, parent):
547 if self._recorded_ports is not None:
564 if self._recorded_ports is not None:
548 content = self._recorded_ports.copy()
565 content = self._recorded_ports.copy()
549 else:
566 else:
550 content = {}
567 content = {}
551 msg = self.session.send(stream, 'connect_reply',
568 msg = self.session.send(stream, 'connect_reply',
552 content, parent, ident)
569 content, parent, ident)
553 self.log.debug("%s", msg)
570 self.log.debug("%s", msg)
554
571
555 def kernel_info_request(self, stream, ident, parent):
572 def kernel_info_request(self, stream, ident, parent):
556 vinfo = {
573 vinfo = {
557 'protocol_version': protocol_version,
574 'protocol_version': protocol_version,
558 'ipython_version': ipython_version,
575 'ipython_version': ipython_version,
559 'language_version': language_version,
576 'language_version': language_version,
560 'language': 'python',
577 'language': 'python',
561 }
578 }
562 msg = self.session.send(stream, 'kernel_info_reply',
579 msg = self.session.send(stream, 'kernel_info_reply',
563 vinfo, parent, ident)
580 vinfo, parent, ident)
564 self.log.debug("%s", msg)
581 self.log.debug("%s", msg)
565
582
566 def shutdown_request(self, stream, ident, parent):
583 def shutdown_request(self, stream, ident, parent):
567 self.shell.exit_now = True
584 self.shell.exit_now = True
568 content = dict(status='ok')
585 content = dict(status='ok')
569 content.update(parent['content'])
586 content.update(parent['content'])
570 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
587 self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
571 # same content, but different msg_id for broadcasting on IOPub
588 # same content, but different msg_id for broadcasting on IOPub
572 self._shutdown_message = self.session.msg(u'shutdown_reply',
589 self._shutdown_message = self.session.msg(u'shutdown_reply',
573 content, parent
590 content, parent
574 )
591 )
575
592
576 self._at_shutdown()
593 self._at_shutdown()
577 # call sys.exit after a short delay
594 # call sys.exit after a short delay
578 loop = ioloop.IOLoop.instance()
595 loop = ioloop.IOLoop.instance()
579 loop.add_timeout(time.time()+0.1, loop.stop)
596 loop.add_timeout(time.time()+0.1, loop.stop)
580
597
581 #---------------------------------------------------------------------------
598 #---------------------------------------------------------------------------
582 # Engine methods
599 # Engine methods
583 #---------------------------------------------------------------------------
600 #---------------------------------------------------------------------------
584
601
585 def apply_request(self, stream, ident, parent):
602 def apply_request(self, stream, ident, parent):
586 try:
603 try:
587 content = parent[u'content']
604 content = parent[u'content']
588 bufs = parent[u'buffers']
605 bufs = parent[u'buffers']
589 msg_id = parent['header']['msg_id']
606 msg_id = parent['header']['msg_id']
590 except:
607 except:
591 self.log.error("Got bad msg: %s", parent, exc_info=True)
608 self.log.error("Got bad msg: %s", parent, exc_info=True)
592 return
609 return
593
610
594 self._publish_status(u'busy', parent)
611 self._publish_status(u'busy', parent)
595
612
596 # Set the parent message of the display hook and out streams.
613 # Set the parent message of the display hook and out streams.
597 shell = self.shell
614 shell = self.shell
598 shell.set_parent(parent)
615 shell.set_parent(parent)
599
616
600 # execute_input_msg = self.session.msg(u'execute_input',{u'code':code}, parent=parent)
617 # execute_input_msg = self.session.msg(u'execute_input',{u'code':code}, parent=parent)
601 # self.iopub_socket.send(execute_input_msg)
618 # self.iopub_socket.send(execute_input_msg)
602 # self.session.send(self.iopub_socket, u'execute_input', {u'code':code},parent=parent)
619 # self.session.send(self.iopub_socket, u'execute_input', {u'code':code},parent=parent)
603 md = self._make_metadata(parent['metadata'])
620 md = self._make_metadata(parent['metadata'])
604 try:
621 try:
605 working = shell.user_ns
622 working = shell.user_ns
606
623
607 prefix = "_"+str(msg_id).replace("-","")+"_"
624 prefix = "_"+str(msg_id).replace("-","")+"_"
608
625
609 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
626 f,args,kwargs = unpack_apply_message(bufs, working, copy=False)
610
627
611 fname = getattr(f, '__name__', 'f')
628 fname = getattr(f, '__name__', 'f')
612
629
613 fname = prefix+"f"
630 fname = prefix+"f"
614 argname = prefix+"args"
631 argname = prefix+"args"
615 kwargname = prefix+"kwargs"
632 kwargname = prefix+"kwargs"
616 resultname = prefix+"result"
633 resultname = prefix+"result"
617
634
618 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
635 ns = { fname : f, argname : args, kwargname : kwargs , resultname : None }
619 # print ns
636 # print ns
620 working.update(ns)
637 working.update(ns)
621 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
638 code = "%s = %s(*%s,**%s)" % (resultname, fname, argname, kwargname)
622 try:
639 try:
623 exec(code, shell.user_global_ns, shell.user_ns)
640 exec(code, shell.user_global_ns, shell.user_ns)
624 result = working.get(resultname)
641 result = working.get(resultname)
625 finally:
642 finally:
626 for key in ns:
643 for key in ns:
627 working.pop(key)
644 working.pop(key)
628
645
629 result_buf = serialize_object(result,
646 result_buf = serialize_object(result,
630 buffer_threshold=self.session.buffer_threshold,
647 buffer_threshold=self.session.buffer_threshold,
631 item_threshold=self.session.item_threshold,
648 item_threshold=self.session.item_threshold,
632 )
649 )
633
650
634 except:
651 except:
635 # invoke IPython traceback formatting
652 # invoke IPython traceback formatting
636 shell.showtraceback()
653 shell.showtraceback()
637 # FIXME - fish exception info out of shell, possibly left there by
654 # FIXME - fish exception info out of shell, possibly left there by
638 # run_code. We'll need to clean up this logic later.
655 # run_code. We'll need to clean up this logic later.
639 reply_content = {}
656 reply_content = {}
640 if shell._reply_content is not None:
657 if shell._reply_content is not None:
641 reply_content.update(shell._reply_content)
658 reply_content.update(shell._reply_content)
642 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
659 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='apply')
643 reply_content['engine_info'] = e_info
660 reply_content['engine_info'] = e_info
644 # reset after use
661 # reset after use
645 shell._reply_content = None
662 shell._reply_content = None
646
663
647 self.session.send(self.iopub_socket, u'error', reply_content, parent=parent,
664 self.session.send(self.iopub_socket, u'error', reply_content, parent=parent,
648 ident=self._topic('error'))
665 ident=self._topic('error'))
649 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
666 self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback']))
650 result_buf = []
667 result_buf = []
651
668
652 if reply_content['ename'] == 'UnmetDependency':
669 if reply_content['ename'] == 'UnmetDependency':
653 md['dependencies_met'] = False
670 md['dependencies_met'] = False
654 else:
671 else:
655 reply_content = {'status' : 'ok'}
672 reply_content = {'status' : 'ok'}
656
673
657 # put 'ok'/'error' status in header, for scheduler introspection:
674 # put 'ok'/'error' status in header, for scheduler introspection:
658 md['status'] = reply_content['status']
675 md['status'] = reply_content['status']
659
676
660 # flush i/o
677 # flush i/o
661 sys.stdout.flush()
678 sys.stdout.flush()
662 sys.stderr.flush()
679 sys.stderr.flush()
663
680
664 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
681 reply_msg = self.session.send(stream, u'apply_reply', reply_content,
665 parent=parent, ident=ident,buffers=result_buf, metadata=md)
682 parent=parent, ident=ident,buffers=result_buf, metadata=md)
666
683
667 self._publish_status(u'idle', parent)
684 self._publish_status(u'idle', parent)
668
685
669 #---------------------------------------------------------------------------
686 #---------------------------------------------------------------------------
670 # Control messages
687 # Control messages
671 #---------------------------------------------------------------------------
688 #---------------------------------------------------------------------------
672
689
673 def abort_request(self, stream, ident, parent):
690 def abort_request(self, stream, ident, parent):
674 """abort a specifig msg by id"""
691 """abort a specifig msg by id"""
675 msg_ids = parent['content'].get('msg_ids', None)
692 msg_ids = parent['content'].get('msg_ids', None)
676 if isinstance(msg_ids, string_types):
693 if isinstance(msg_ids, string_types):
677 msg_ids = [msg_ids]
694 msg_ids = [msg_ids]
678 if not msg_ids:
695 if not msg_ids:
679 self.abort_queues()
696 self.abort_queues()
680 for mid in msg_ids:
697 for mid in msg_ids:
681 self.aborted.add(str(mid))
698 self.aborted.add(str(mid))
682
699
683 content = dict(status='ok')
700 content = dict(status='ok')
684 reply_msg = self.session.send(stream, 'abort_reply', content=content,
701 reply_msg = self.session.send(stream, 'abort_reply', content=content,
685 parent=parent, ident=ident)
702 parent=parent, ident=ident)
686 self.log.debug("%s", reply_msg)
703 self.log.debug("%s", reply_msg)
687
704
688 def clear_request(self, stream, idents, parent):
705 def clear_request(self, stream, idents, parent):
689 """Clear our namespace."""
706 """Clear our namespace."""
690 self.shell.reset(False)
707 self.shell.reset(False)
691 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
708 msg = self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
692 content = dict(status='ok'))
709 content = dict(status='ok'))
693
710
694
711
695 #---------------------------------------------------------------------------
712 #---------------------------------------------------------------------------
696 # Protected interface
713 # Protected interface
697 #---------------------------------------------------------------------------
714 #---------------------------------------------------------------------------
698
715
699 def _wrap_exception(self, method=None):
716 def _wrap_exception(self, method=None):
700 # import here, because _wrap_exception is only used in parallel,
717 # import here, because _wrap_exception is only used in parallel,
701 # and parallel has higher min pyzmq version
718 # and parallel has higher min pyzmq version
702 from IPython.parallel.error import wrap_exception
719 from IPython.parallel.error import wrap_exception
703 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
720 e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method=method)
704 content = wrap_exception(e_info)
721 content = wrap_exception(e_info)
705 return content
722 return content
706
723
707 def _topic(self, topic):
724 def _topic(self, topic):
708 """prefixed topic for IOPub messages"""
725 """prefixed topic for IOPub messages"""
709 if self.int_id >= 0:
726 if self.int_id >= 0:
710 base = "engine.%i" % self.int_id
727 base = "engine.%i" % self.int_id
711 else:
728 else:
712 base = "kernel.%s" % self.ident
729 base = "kernel.%s" % self.ident
713
730
714 return py3compat.cast_bytes("%s.%s" % (base, topic))
731 return py3compat.cast_bytes("%s.%s" % (base, topic))
715
732
716 def _abort_queues(self):
733 def _abort_queues(self):
717 for stream in self.shell_streams:
734 for stream in self.shell_streams:
718 if stream:
735 if stream:
719 self._abort_queue(stream)
736 self._abort_queue(stream)
720
737
721 def _abort_queue(self, stream):
738 def _abort_queue(self, stream):
722 poller = zmq.Poller()
739 poller = zmq.Poller()
723 poller.register(stream.socket, zmq.POLLIN)
740 poller.register(stream.socket, zmq.POLLIN)
724 while True:
741 while True:
725 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
742 idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
726 if msg is None:
743 if msg is None:
727 return
744 return
728
745
729 self.log.info("Aborting:")
746 self.log.info("Aborting:")
730 self.log.info("%s", msg)
747 self.log.info("%s", msg)
731 msg_type = msg['header']['msg_type']
748 msg_type = msg['header']['msg_type']
732 reply_type = msg_type.split('_')[0] + '_reply'
749 reply_type = msg_type.split('_')[0] + '_reply'
733
750
734 status = {'status' : 'aborted'}
751 status = {'status' : 'aborted'}
735 md = {'engine' : self.ident}
752 md = {'engine' : self.ident}
736 md.update(status)
753 md.update(status)
737 reply_msg = self.session.send(stream, reply_type, metadata=md,
754 reply_msg = self.session.send(stream, reply_type, metadata=md,
738 content=status, parent=msg, ident=idents)
755 content=status, parent=msg, ident=idents)
739 self.log.debug("%s", reply_msg)
756 self.log.debug("%s", reply_msg)
740 # We need to wait a bit for requests to come in. This can probably
757 # We need to wait a bit for requests to come in. This can probably
741 # be set shorter for true asynchronous clients.
758 # be set shorter for true asynchronous clients.
742 poller.poll(50)
759 poller.poll(50)
743
760
744
761
745 def _no_raw_input(self):
762 def _no_raw_input(self):
746 """Raise StdinNotImplentedError if active frontend doesn't support
763 """Raise StdinNotImplentedError if active frontend doesn't support
747 stdin."""
764 stdin."""
748 raise StdinNotImplementedError("raw_input was called, but this "
765 raise StdinNotImplementedError("raw_input was called, but this "
749 "frontend does not support stdin.")
766 "frontend does not support stdin.")
750
767
751 def getpass(self, prompt=''):
768 def getpass(self, prompt=''):
752 """Forward getpass to frontends
769 """Forward getpass to frontends
753
770
754 Raises
771 Raises
755 ------
772 ------
756 StdinNotImplentedError if active frontend doesn't support stdin.
773 StdinNotImplentedError if active frontend doesn't support stdin.
757 """
774 """
758 if not self._allow_stdin:
775 if not self._allow_stdin:
759 raise StdinNotImplementedError(
776 raise StdinNotImplementedError(
760 "getpass was called, but this frontend does not support input requests."
777 "getpass was called, but this frontend does not support input requests."
761 )
778 )
762 return self._input_request(prompt,
779 return self._input_request(prompt,
763 self._parent_ident,
780 self._parent_ident,
764 self._parent_header,
781 self._parent_header,
765 password=True,
782 password=True,
766 )
783 )
767
784
768 def raw_input(self, prompt=''):
785 def raw_input(self, prompt=''):
769 """Forward raw_input to frontends
786 """Forward raw_input to frontends
770
787
771 Raises
788 Raises
772 ------
789 ------
773 StdinNotImplentedError if active frontend doesn't support stdin.
790 StdinNotImplentedError if active frontend doesn't support stdin.
774 """
791 """
775 if not self._allow_stdin:
792 if not self._allow_stdin:
776 raise StdinNotImplementedError(
793 raise StdinNotImplementedError(
777 "raw_input was called, but this frontend does not support input requests."
794 "raw_input was called, but this frontend does not support input requests."
778 )
795 )
779 return self._input_request(prompt,
796 return self._input_request(prompt,
780 self._parent_ident,
797 self._parent_ident,
781 self._parent_header,
798 self._parent_header,
782 password=False,
799 password=False,
783 )
800 )
784
801
785 def _input_request(self, prompt, ident, parent, password=False):
802 def _input_request(self, prompt, ident, parent, password=False):
786 # Flush output before making the request.
803 # Flush output before making the request.
787 sys.stderr.flush()
804 sys.stderr.flush()
788 sys.stdout.flush()
805 sys.stdout.flush()
789 # flush the stdin socket, to purge stale replies
806 # flush the stdin socket, to purge stale replies
790 while True:
807 while True:
791 try:
808 try:
792 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
809 self.stdin_socket.recv_multipart(zmq.NOBLOCK)
793 except zmq.ZMQError as e:
810 except zmq.ZMQError as e:
794 if e.errno == zmq.EAGAIN:
811 if e.errno == zmq.EAGAIN:
795 break
812 break
796 else:
813 else:
797 raise
814 raise
798
815
799 # Send the input request.
816 # Send the input request.
800 content = json_clean(dict(prompt=prompt, password=password))
817 content = json_clean(dict(prompt=prompt, password=password))
801 self.session.send(self.stdin_socket, u'input_request', content, parent,
818 self.session.send(self.stdin_socket, u'input_request', content, parent,
802 ident=ident)
819 ident=ident)
803
820
804 # Await a response.
821 # Await a response.
805 while True:
822 while True:
806 try:
823 try:
807 ident, reply = self.session.recv(self.stdin_socket, 0)
824 ident, reply = self.session.recv(self.stdin_socket, 0)
808 except Exception:
825 except Exception:
809 self.log.warn("Invalid Message:", exc_info=True)
826 self.log.warn("Invalid Message:", exc_info=True)
810 except KeyboardInterrupt:
827 except KeyboardInterrupt:
811 # re-raise KeyboardInterrupt, to truncate traceback
828 # re-raise KeyboardInterrupt, to truncate traceback
812 raise KeyboardInterrupt
829 raise KeyboardInterrupt
813 else:
830 else:
814 break
831 break
815 try:
832 try:
816 value = py3compat.unicode_to_str(reply['content']['value'])
833 value = py3compat.unicode_to_str(reply['content']['value'])
817 except:
834 except:
818 self.log.error("Bad input_reply: %s", parent)
835 self.log.error("Bad input_reply: %s", parent)
819 value = ''
836 value = ''
820 if value == '\x04':
837 if value == '\x04':
821 # EOF
838 # EOF
822 raise EOFError
839 raise EOFError
823 return value
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 def _at_shutdown(self):
842 def _at_shutdown(self):
839 """Actions taken at shutdown by the kernel, called by python's atexit.
843 """Actions taken at shutdown by the kernel, called by python's atexit.
840 """
844 """
841 # io.rprint("Kernel at_shutdown") # dbg
845 # io.rprint("Kernel at_shutdown") # dbg
842 if self._shutdown_message is not None:
846 if self._shutdown_message is not None:
843 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
847 self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
844 self.log.debug("%s", self._shutdown_message)
848 self.log.debug("%s", self._shutdown_message)
845 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
849 [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]
846
850
@@ -1,194 +1,198 b''
1 """test IPython.embed_kernel()"""
1 """test IPython.embed_kernel()"""
2
2
3 #-------------------------------------------------------------------------------
3 #-------------------------------------------------------------------------------
4 # Copyright (C) 2012 The IPython Development Team
4 # Copyright (C) 2012 The IPython Development Team
5 #
5 #
6 # Distributed under the terms of the BSD License. The full license is in
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
7 # the file COPYING, distributed as part of this software.
8 #-------------------------------------------------------------------------------
8 #-------------------------------------------------------------------------------
9
9
10 #-------------------------------------------------------------------------------
10 #-------------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-------------------------------------------------------------------------------
12 #-------------------------------------------------------------------------------
13
13
14 import os
14 import os
15 import shutil
15 import shutil
16 import sys
16 import sys
17 import tempfile
17 import tempfile
18 import time
18 import time
19
19
20 from contextlib import contextmanager
20 from contextlib import contextmanager
21 from subprocess import Popen, PIPE
21 from subprocess import Popen, PIPE
22
22
23 import nose.tools as nt
23 import nose.tools as nt
24
24
25 from IPython.kernel import BlockingKernelClient
25 from IPython.kernel import BlockingKernelClient
26 from IPython.utils import path, py3compat
26 from IPython.utils import path, py3compat
27 from IPython.utils.py3compat import unicode_type
27 from IPython.utils.py3compat import unicode_type
28
28
29 #-------------------------------------------------------------------------------
29 #-------------------------------------------------------------------------------
30 # Tests
30 # Tests
31 #-------------------------------------------------------------------------------
31 #-------------------------------------------------------------------------------
32
32
33 SETUP_TIMEOUT = 60
33 SETUP_TIMEOUT = 60
34 TIMEOUT = 15
34 TIMEOUT = 15
35
35
36 def setup():
36 def setup():
37 """setup temporary IPYTHONDIR for tests"""
37 """setup temporary IPYTHONDIR for tests"""
38 global IPYTHONDIR
38 global IPYTHONDIR
39 global env
39 global env
40 global save_get_ipython_dir
40 global save_get_ipython_dir
41
41
42 IPYTHONDIR = tempfile.mkdtemp()
42 IPYTHONDIR = tempfile.mkdtemp()
43
43
44 env = os.environ.copy()
44 env = os.environ.copy()
45 env["IPYTHONDIR"] = IPYTHONDIR
45 env["IPYTHONDIR"] = IPYTHONDIR
46
46
47 save_get_ipython_dir = path.get_ipython_dir
47 save_get_ipython_dir = path.get_ipython_dir
48 path.get_ipython_dir = lambda : IPYTHONDIR
48 path.get_ipython_dir = lambda : IPYTHONDIR
49
49
50
50
51 def teardown():
51 def teardown():
52 path.get_ipython_dir = save_get_ipython_dir
52 path.get_ipython_dir = save_get_ipython_dir
53
53
54 try:
54 try:
55 shutil.rmtree(IPYTHONDIR)
55 shutil.rmtree(IPYTHONDIR)
56 except (OSError, IOError):
56 except (OSError, IOError):
57 # no such file
57 # no such file
58 pass
58 pass
59
59
60
60
61 @contextmanager
61 @contextmanager
62 def setup_kernel(cmd):
62 def setup_kernel(cmd):
63 """start an embedded kernel in a subprocess, and wait for it to be ready
63 """start an embedded kernel in a subprocess, and wait for it to be ready
64
64
65 Returns
65 Returns
66 -------
66 -------
67 kernel_manager: connected KernelManager instance
67 kernel_manager: connected KernelManager instance
68 """
68 """
69 kernel = Popen([sys.executable, '-c', cmd], stdout=PIPE, stderr=PIPE, env=env)
69 kernel = Popen([sys.executable, '-c', cmd], stdout=PIPE, stderr=PIPE, env=env)
70 connection_file = os.path.join(IPYTHONDIR,
70 connection_file = os.path.join(IPYTHONDIR,
71 'profile_default',
71 'profile_default',
72 'security',
72 'security',
73 'kernel-%i.json' % kernel.pid
73 'kernel-%i.json' % kernel.pid
74 )
74 )
75 # wait for connection file to exist, timeout after 5s
75 # wait for connection file to exist, timeout after 5s
76 tic = time.time()
76 tic = time.time()
77 while not os.path.exists(connection_file) \
77 while not os.path.exists(connection_file) \
78 and kernel.poll() is None \
78 and kernel.poll() is None \
79 and time.time() < tic + SETUP_TIMEOUT:
79 and time.time() < tic + SETUP_TIMEOUT:
80 time.sleep(0.1)
80 time.sleep(0.1)
81
81
82 if kernel.poll() is not None:
82 if kernel.poll() is not None:
83 o,e = kernel.communicate()
83 o,e = kernel.communicate()
84 e = py3compat.cast_unicode(e)
84 e = py3compat.cast_unicode(e)
85 raise IOError("Kernel failed to start:\n%s" % e)
85 raise IOError("Kernel failed to start:\n%s" % e)
86
86
87 if not os.path.exists(connection_file):
87 if not os.path.exists(connection_file):
88 if kernel.poll() is None:
88 if kernel.poll() is None:
89 kernel.terminate()
89 kernel.terminate()
90 raise IOError("Connection file %r never arrived" % connection_file)
90 raise IOError("Connection file %r never arrived" % connection_file)
91
91
92 client = BlockingKernelClient(connection_file=connection_file)
92 client = BlockingKernelClient(connection_file=connection_file)
93 client.load_connection_file()
93 client.load_connection_file()
94 client.start_channels()
94 client.start_channels()
95
95
96 try:
96 try:
97 yield client
97 yield client
98 finally:
98 finally:
99 client.stop_channels()
99 client.stop_channels()
100 kernel.terminate()
100 kernel.terminate()
101
101
102 def test_embed_kernel_basic():
102 def test_embed_kernel_basic():
103 """IPython.embed_kernel() is basically functional"""
103 """IPython.embed_kernel() is basically functional"""
104 cmd = '\n'.join([
104 cmd = '\n'.join([
105 'from IPython import embed_kernel',
105 'from IPython import embed_kernel',
106 'def go():',
106 'def go():',
107 ' a=5',
107 ' a=5',
108 ' b="hi there"',
108 ' b="hi there"',
109 ' embed_kernel()',
109 ' embed_kernel()',
110 'go()',
110 'go()',
111 '',
111 '',
112 ])
112 ])
113
113
114 with setup_kernel(cmd) as client:
114 with setup_kernel(cmd) as client:
115 # oinfo a (int)
115 # oinfo a (int)
116 msg_id = client.object_info('a')
116 msg_id = client.object_info('a')
117 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
117 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
118 content = msg['content']
118 content = msg['content']
119 nt.assert_true(content['found'])
119 nt.assert_true(content['found'])
120
120
121 msg_id = client.execute("c=a*2")
121 msg_id = client.execute("c=a*2")
122 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
122 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
123 content = msg['content']
123 content = msg['content']
124 nt.assert_equal(content['status'], u'ok')
124 nt.assert_equal(content['status'], u'ok')
125
125
126 # oinfo c (should be 10)
126 # oinfo c (should be 10)
127 msg_id = client.object_info('c')
127 msg_id = client.object_info('c')
128 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
128 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
129 content = msg['content']
129 content = msg['content']
130 nt.assert_true(content['found'])
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 def test_embed_kernel_namespace():
134 def test_embed_kernel_namespace():
134 """IPython.embed_kernel() inherits calling namespace"""
135 """IPython.embed_kernel() inherits calling namespace"""
135 cmd = '\n'.join([
136 cmd = '\n'.join([
136 'from IPython import embed_kernel',
137 'from IPython import embed_kernel',
137 'def go():',
138 'def go():',
138 ' a=5',
139 ' a=5',
139 ' b="hi there"',
140 ' b="hi there"',
140 ' embed_kernel()',
141 ' embed_kernel()',
141 'go()',
142 'go()',
142 '',
143 '',
143 ])
144 ])
144
145
145 with setup_kernel(cmd) as client:
146 with setup_kernel(cmd) as client:
146 # oinfo a (int)
147 # oinfo a (int)
147 msg_id = client.object_info('a')
148 msg_id = client.object_info('a')
148 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
149 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
149 content = msg['content']
150 content = msg['content']
150 nt.assert_true(content['found'])
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 # oinfo b (str)
155 # oinfo b (str)
154 msg_id = client.object_info('b')
156 msg_id = client.object_info('b')
155 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
157 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
156 content = msg['content']
158 content = msg['content']
157 nt.assert_true(content['found'])
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 # oinfo c (undefined)
163 # oinfo c (undefined)
161 msg_id = client.object_info('c')
164 msg_id = client.object_info('c')
162 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
165 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
163 content = msg['content']
166 content = msg['content']
164 nt.assert_false(content['found'])
167 nt.assert_false(content['found'])
165
168
166 def test_embed_kernel_reentrant():
169 def test_embed_kernel_reentrant():
167 """IPython.embed_kernel() can be called multiple times"""
170 """IPython.embed_kernel() can be called multiple times"""
168 cmd = '\n'.join([
171 cmd = '\n'.join([
169 'from IPython import embed_kernel',
172 'from IPython import embed_kernel',
170 'count = 0',
173 'count = 0',
171 'def go():',
174 'def go():',
172 ' global count',
175 ' global count',
173 ' embed_kernel()',
176 ' embed_kernel()',
174 ' count = count + 1',
177 ' count = count + 1',
175 '',
178 '',
176 'while True:'
179 'while True:'
177 ' go()',
180 ' go()',
178 '',
181 '',
179 ])
182 ])
180
183
181 with setup_kernel(cmd) as client:
184 with setup_kernel(cmd) as client:
182 for i in range(5):
185 for i in range(5):
183 msg_id = client.object_info('count')
186 msg_id = client.object_info('count')
184 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
187 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
185 content = msg['content']
188 content = msg['content']
186 nt.assert_true(content['found'])
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 # exit from embed_kernel
193 # exit from embed_kernel
190 client.execute("get_ipython().exit_now = True")
194 client.execute("get_ipython().exit_now = True")
191 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
195 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
192 time.sleep(0.2)
196 time.sleep(0.2)
193
197
194
198
@@ -1,45 +1,48 b''
1 import nose.tools as nt
1 import nose.tools as nt
2
2
3 from .test_embed_kernel import setup, teardown, setup_kernel
3 from .test_embed_kernel import setup, teardown, setup_kernel
4
4
5 TIMEOUT = 15
5 TIMEOUT = 15
6
6
7 def test_ipython_start_kernel_userns():
7 def test_ipython_start_kernel_userns():
8 cmd = ('from IPython import start_kernel\n'
8 cmd = ('from IPython import start_kernel\n'
9 'ns = {"tre": 123}\n'
9 'ns = {"tre": 123}\n'
10 'start_kernel(user_ns=ns)')
10 'start_kernel(user_ns=ns)')
11
11
12 with setup_kernel(cmd) as client:
12 with setup_kernel(cmd) as client:
13 msg_id = client.object_info('tre')
13 msg_id = client.object_info('tre')
14 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
14 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
15 content = msg['content']
15 content = msg['content']
16 assert content['found']
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 # user_module should be an instance of DummyMod
20 # user_module should be an instance of DummyMod
20 msg_id = client.execute("usermod = get_ipython().user_module")
21 msg_id = client.execute("usermod = get_ipython().user_module")
21 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
22 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
22 content = msg['content']
23 content = msg['content']
23 nt.assert_equal(content['status'], u'ok')
24 nt.assert_equal(content['status'], u'ok')
24 msg_id = client.object_info('usermod')
25 msg_id = client.object_info('usermod')
25 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
26 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
26 content = msg['content']
27 content = msg['content']
27 assert content['found']
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 def test_ipython_start_kernel_no_userns():
32 def test_ipython_start_kernel_no_userns():
31 # Issue #4188 - user_ns should be passed to shell as None, not {}
33 # Issue #4188 - user_ns should be passed to shell as None, not {}
32 cmd = ('from IPython import start_kernel\n'
34 cmd = ('from IPython import start_kernel\n'
33 'start_kernel()')
35 'start_kernel()')
34
36
35 with setup_kernel(cmd) as client:
37 with setup_kernel(cmd) as client:
36 # user_module should not be an instance of DummyMod
38 # user_module should not be an instance of DummyMod
37 msg_id = client.execute("usermod = get_ipython().user_module")
39 msg_id = client.execute("usermod = get_ipython().user_module")
38 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
40 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
39 content = msg['content']
41 content = msg['content']
40 nt.assert_equal(content['status'], u'ok')
42 nt.assert_equal(content['status'], u'ok')
41 msg_id = client.object_info('usermod')
43 msg_id = client.object_info('usermod')
42 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
44 msg = client.get_shell_msg(block=True, timeout=TIMEOUT)
43 content = msg['content']
45 content = msg['content']
44 assert content['found']
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 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os.path
8 import os.path
9 import re
9 import re
10 import sys
10 import sys
11 from textwrap import dedent
11 from textwrap import dedent
12 import time
12 import time
13 from unicodedata import category
13 from unicodedata import category
14 import webbrowser
14 import webbrowser
15
15
16 # System library imports
16 # System library imports
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 # Local imports
19 # Local imports
20 from IPython.config.configurable import LoggingConfigurable
20 from IPython.config.configurable import LoggingConfigurable
21 from IPython.core.inputsplitter import ESC_SEQUENCES
21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 from IPython.qt.rich_text import HtmlExporter
22 from IPython.qt.rich_text import HtmlExporter
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 from IPython.utils.text import columnize
24 from IPython.utils.text import columnize
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 from .ansi_code_processor import QtAnsiCodeProcessor
26 from .ansi_code_processor import QtAnsiCodeProcessor
27 from .completion_widget import CompletionWidget
27 from .completion_widget import CompletionWidget
28 from .completion_html import CompletionHtml
28 from .completion_html import CompletionHtml
29 from .completion_plain import CompletionPlain
29 from .completion_plain import CompletionPlain
30 from .kill_ring import QtKillRing
30 from .kill_ring import QtKillRing
31
31
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Functions
34 # Functions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39
39
40 def commonprefix(items):
40 def commonprefix(items):
41 """Get common prefix for completions
41 """Get common prefix for completions
42
42
43 Return the longest common prefix of a list of strings, but with special
43 Return the longest common prefix of a list of strings, but with special
44 treatment of escape characters that might precede commands in IPython,
44 treatment of escape characters that might precede commands in IPython,
45 such as %magic functions. Used in tab completion.
45 such as %magic functions. Used in tab completion.
46
46
47 For a more general function, see os.path.commonprefix
47 For a more general function, see os.path.commonprefix
48 """
48 """
49 # the last item will always have the least leading % symbol
49 # the last item will always have the least leading % symbol
50 # min / max are first/last in alphabetical order
50 # min / max are first/last in alphabetical order
51 first_match = ESCAPE_RE.match(min(items))
51 first_match = ESCAPE_RE.match(min(items))
52 last_match = ESCAPE_RE.match(max(items))
52 last_match = ESCAPE_RE.match(max(items))
53 # common suffix is (common prefix of reversed items) reversed
53 # common suffix is (common prefix of reversed items) reversed
54 if first_match and last_match:
54 if first_match and last_match:
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 else:
56 else:
57 prefix = ''
57 prefix = ''
58
58
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 return prefix+os.path.commonprefix(items)
60 return prefix+os.path.commonprefix(items)
61
61
62 def is_letter_or_number(char):
62 def is_letter_or_number(char):
63 """ Returns whether the specified unicode character is a letter or a number.
63 """ Returns whether the specified unicode character is a letter or a number.
64 """
64 """
65 cat = category(char)
65 cat = category(char)
66 return cat.startswith('L') or cat.startswith('N')
66 return cat.startswith('L') or cat.startswith('N')
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Classes
69 # Classes
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
73 """ An abstract base class for console-type widgets. This class has
73 """ An abstract base class for console-type widgets. This class has
74 functionality for:
74 functionality for:
75
75
76 * Maintaining a prompt and editing region
76 * Maintaining a prompt and editing region
77 * Providing the traditional Unix-style console keyboard shortcuts
77 * Providing the traditional Unix-style console keyboard shortcuts
78 * Performing tab completion
78 * Performing tab completion
79 * Paging text
79 * Paging text
80 * Handling ANSI escape codes
80 * Handling ANSI escape codes
81
81
82 ConsoleWidget also provides a number of utility methods that will be
82 ConsoleWidget also provides a number of utility methods that will be
83 convenient to implementors of a console-style widget.
83 convenient to implementors of a console-style widget.
84 """
84 """
85
85
86 #------ Configuration ------------------------------------------------------
86 #------ Configuration ------------------------------------------------------
87
87
88 ansi_codes = Bool(True, config=True,
88 ansi_codes = Bool(True, config=True,
89 help="Whether to process ANSI escape codes."
89 help="Whether to process ANSI escape codes."
90 )
90 )
91 buffer_size = Integer(500, config=True,
91 buffer_size = Integer(500, config=True,
92 help="""
92 help="""
93 The maximum number of lines of text before truncation. Specifying a
93 The maximum number of lines of text before truncation. Specifying a
94 non-positive number disables text truncation (not recommended).
94 non-positive number disables text truncation (not recommended).
95 """
95 """
96 )
96 )
97 execute_on_complete_input = Bool(True, config=True,
97 execute_on_complete_input = Bool(True, config=True,
98 help="""Whether to automatically execute on syntactically complete input.
98 help="""Whether to automatically execute on syntactically complete input.
99
99
100 If False, Shift-Enter is required to submit each execution.
100 If False, Shift-Enter is required to submit each execution.
101 Disabling this is mainly useful for non-Python kernels,
101 Disabling this is mainly useful for non-Python kernels,
102 where the completion check would be wrong.
102 where the completion check would be wrong.
103 """
103 """
104 )
104 )
105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
106 default_value = 'ncurses',
106 default_value = 'ncurses',
107 help="""
107 help="""
108 The type of completer to use. Valid values are:
108 The type of completer to use. Valid values are:
109
109
110 'plain' : Show the available completion as a text list
110 'plain' : Show the available completion as a text list
111 Below the editing area.
111 Below the editing area.
112 'droplist': Show the completion in a drop down list navigable
112 'droplist': Show the completion in a drop down list navigable
113 by the arrow keys, and from which you can select
113 by the arrow keys, and from which you can select
114 completion by pressing Return.
114 completion by pressing Return.
115 'ncurses' : Show the completion as a text list which is navigable by
115 'ncurses' : Show the completion as a text list which is navigable by
116 `tab` and arrow keys.
116 `tab` and arrow keys.
117 """
117 """
118 )
118 )
119 # NOTE: this value can only be specified during initialization.
119 # NOTE: this value can only be specified during initialization.
120 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
120 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
121 help="""
121 help="""
122 The type of underlying text widget to use. Valid values are 'plain',
122 The type of underlying text widget to use. Valid values are 'plain',
123 which specifies a QPlainTextEdit, and 'rich', which specifies a
123 which specifies a QPlainTextEdit, and 'rich', which specifies a
124 QTextEdit.
124 QTextEdit.
125 """
125 """
126 )
126 )
127 # NOTE: this value can only be specified during initialization.
127 # NOTE: this value can only be specified during initialization.
128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
129 default_value='inside', config=True,
129 default_value='inside', config=True,
130 help="""
130 help="""
131 The type of paging to use. Valid values are:
131 The type of paging to use. Valid values are:
132
132
133 'inside'
133 'inside'
134 The widget pages like a traditional terminal.
134 The widget pages like a traditional terminal.
135 'hsplit'
135 'hsplit'
136 When paging is requested, the widget is split horizontally. The top
136 When paging is requested, the widget is split horizontally. The top
137 pane contains the console, and the bottom pane contains the paged text.
137 pane contains the console, and the bottom pane contains the paged text.
138 'vsplit'
138 'vsplit'
139 Similar to 'hsplit', except that a vertical splitter is used.
139 Similar to 'hsplit', except that a vertical splitter is used.
140 'custom'
140 'custom'
141 No action is taken by the widget beyond emitting a
141 No action is taken by the widget beyond emitting a
142 'custom_page_requested(str)' signal.
142 'custom_page_requested(str)' signal.
143 'none'
143 'none'
144 The text is written directly to the console.
144 The text is written directly to the console.
145 """)
145 """)
146
146
147 font_family = Unicode(config=True,
147 font_family = Unicode(config=True,
148 help="""The font family to use for the console.
148 help="""The font family to use for the console.
149 On OSX this defaults to Monaco, on Windows the default is
149 On OSX this defaults to Monaco, on Windows the default is
150 Consolas with fallback of Courier, and on other platforms
150 Consolas with fallback of Courier, and on other platforms
151 the default is Monospace.
151 the default is Monospace.
152 """)
152 """)
153 def _font_family_default(self):
153 def _font_family_default(self):
154 if sys.platform == 'win32':
154 if sys.platform == 'win32':
155 # Consolas ships with Vista/Win7, fallback to Courier if needed
155 # Consolas ships with Vista/Win7, fallback to Courier if needed
156 return 'Consolas'
156 return 'Consolas'
157 elif sys.platform == 'darwin':
157 elif sys.platform == 'darwin':
158 # OSX always has Monaco, no need for a fallback
158 # OSX always has Monaco, no need for a fallback
159 return 'Monaco'
159 return 'Monaco'
160 else:
160 else:
161 # Monospace should always exist, no need for a fallback
161 # Monospace should always exist, no need for a fallback
162 return 'Monospace'
162 return 'Monospace'
163
163
164 font_size = Integer(config=True,
164 font_size = Integer(config=True,
165 help="""The font size. If unconfigured, Qt will be entrusted
165 help="""The font size. If unconfigured, Qt will be entrusted
166 with the size of the font.
166 with the size of the font.
167 """)
167 """)
168
168
169 width = Integer(81, config=True,
169 width = Integer(81, config=True,
170 help="""The width of the console at start time in number
170 help="""The width of the console at start time in number
171 of characters (will double with `hsplit` paging)
171 of characters (will double with `hsplit` paging)
172 """)
172 """)
173
173
174 height = Integer(25, config=True,
174 height = Integer(25, config=True,
175 help="""The height of the console at start time in number
175 help="""The height of the console at start time in number
176 of characters (will double with `vsplit` paging)
176 of characters (will double with `vsplit` paging)
177 """)
177 """)
178
178
179 # Whether to override ShortcutEvents for the keybindings defined by this
179 # Whether to override ShortcutEvents for the keybindings defined by this
180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
182 override_shortcuts = Bool(False)
182 override_shortcuts = Bool(False)
183
183
184 # ------ Custom Qt Widgets -------------------------------------------------
184 # ------ Custom Qt Widgets -------------------------------------------------
185
185
186 # For other projects to easily override the Qt widgets used by the console
186 # For other projects to easily override the Qt widgets used by the console
187 # (e.g. Spyder)
187 # (e.g. Spyder)
188 custom_control = None
188 custom_control = None
189 custom_page_control = None
189 custom_page_control = None
190
190
191 #------ Signals ------------------------------------------------------------
191 #------ Signals ------------------------------------------------------------
192
192
193 # Signals that indicate ConsoleWidget state.
193 # Signals that indicate ConsoleWidget state.
194 copy_available = QtCore.Signal(bool)
194 copy_available = QtCore.Signal(bool)
195 redo_available = QtCore.Signal(bool)
195 redo_available = QtCore.Signal(bool)
196 undo_available = QtCore.Signal(bool)
196 undo_available = QtCore.Signal(bool)
197
197
198 # Signal emitted when paging is needed and the paging style has been
198 # Signal emitted when paging is needed and the paging style has been
199 # specified as 'custom'.
199 # specified as 'custom'.
200 custom_page_requested = QtCore.Signal(object)
200 custom_page_requested = QtCore.Signal(object)
201
201
202 # Signal emitted when the font is changed.
202 # Signal emitted when the font is changed.
203 font_changed = QtCore.Signal(QtGui.QFont)
203 font_changed = QtCore.Signal(QtGui.QFont)
204
204
205 #------ Protected class variables ------------------------------------------
205 #------ Protected class variables ------------------------------------------
206
206
207 # control handles
207 # control handles
208 _control = None
208 _control = None
209 _page_control = None
209 _page_control = None
210 _splitter = None
210 _splitter = None
211
211
212 # When the control key is down, these keys are mapped.
212 # When the control key is down, these keys are mapped.
213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
219 if not sys.platform == 'darwin':
219 if not sys.platform == 'darwin':
220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
221 # cursor to the bottom of the buffer.
221 # cursor to the bottom of the buffer.
222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
223
223
224 # The shortcuts defined by this widget. We need to keep track of these to
224 # The shortcuts defined by this widget. We need to keep track of these to
225 # support 'override_shortcuts' above.
225 # support 'override_shortcuts' above.
226 _shortcuts = set(_ctrl_down_remap.keys()) | \
226 _shortcuts = set(_ctrl_down_remap.keys()) | \
227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
228 QtCore.Qt.Key_V }
228 QtCore.Qt.Key_V }
229
229
230 _temp_buffer_filled = False
230 _temp_buffer_filled = False
231
231
232 #---------------------------------------------------------------------------
232 #---------------------------------------------------------------------------
233 # 'QObject' interface
233 # 'QObject' interface
234 #---------------------------------------------------------------------------
234 #---------------------------------------------------------------------------
235
235
236 def __init__(self, parent=None, **kw):
236 def __init__(self, parent=None, **kw):
237 """ Create a ConsoleWidget.
237 """ Create a ConsoleWidget.
238
238
239 Parameters
239 Parameters
240 ----------
240 ----------
241 parent : QWidget, optional [default None]
241 parent : QWidget, optional [default None]
242 The parent for this widget.
242 The parent for this widget.
243 """
243 """
244 QtGui.QWidget.__init__(self, parent)
244 QtGui.QWidget.__init__(self, parent)
245 LoggingConfigurable.__init__(self, **kw)
245 LoggingConfigurable.__init__(self, **kw)
246
246
247 # While scrolling the pager on Mac OS X, it tears badly. The
247 # While scrolling the pager on Mac OS X, it tears badly. The
248 # NativeGesture is platform and perhaps build-specific hence
248 # NativeGesture is platform and perhaps build-specific hence
249 # we take adequate precautions here.
249 # we take adequate precautions here.
250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
251 if hasattr(QtCore.QEvent, 'NativeGesture'):
251 if hasattr(QtCore.QEvent, 'NativeGesture'):
252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
253
253
254 # Create the layout and underlying text widget.
254 # Create the layout and underlying text widget.
255 layout = QtGui.QStackedLayout(self)
255 layout = QtGui.QStackedLayout(self)
256 layout.setContentsMargins(0, 0, 0, 0)
256 layout.setContentsMargins(0, 0, 0, 0)
257 self._control = self._create_control()
257 self._control = self._create_control()
258 if self.paging in ('hsplit', 'vsplit'):
258 if self.paging in ('hsplit', 'vsplit'):
259 self._splitter = QtGui.QSplitter()
259 self._splitter = QtGui.QSplitter()
260 if self.paging == 'hsplit':
260 if self.paging == 'hsplit':
261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
262 else:
262 else:
263 self._splitter.setOrientation(QtCore.Qt.Vertical)
263 self._splitter.setOrientation(QtCore.Qt.Vertical)
264 self._splitter.addWidget(self._control)
264 self._splitter.addWidget(self._control)
265 layout.addWidget(self._splitter)
265 layout.addWidget(self._splitter)
266 else:
266 else:
267 layout.addWidget(self._control)
267 layout.addWidget(self._control)
268
268
269 # Create the paging widget, if necessary.
269 # Create the paging widget, if necessary.
270 if self.paging in ('inside', 'hsplit', 'vsplit'):
270 if self.paging in ('inside', 'hsplit', 'vsplit'):
271 self._page_control = self._create_page_control()
271 self._page_control = self._create_page_control()
272 if self._splitter:
272 if self._splitter:
273 self._page_control.hide()
273 self._page_control.hide()
274 self._splitter.addWidget(self._page_control)
274 self._splitter.addWidget(self._page_control)
275 else:
275 else:
276 layout.addWidget(self._page_control)
276 layout.addWidget(self._page_control)
277
277
278 # Initialize protected variables. Some variables contain useful state
278 # Initialize protected variables. Some variables contain useful state
279 # information for subclasses; they should be considered read-only.
279 # information for subclasses; they should be considered read-only.
280 self._append_before_prompt_pos = 0
280 self._append_before_prompt_pos = 0
281 self._ansi_processor = QtAnsiCodeProcessor()
281 self._ansi_processor = QtAnsiCodeProcessor()
282 if self.gui_completion == 'ncurses':
282 if self.gui_completion == 'ncurses':
283 self._completion_widget = CompletionHtml(self)
283 self._completion_widget = CompletionHtml(self)
284 elif self.gui_completion == 'droplist':
284 elif self.gui_completion == 'droplist':
285 self._completion_widget = CompletionWidget(self)
285 self._completion_widget = CompletionWidget(self)
286 elif self.gui_completion == 'plain':
286 elif self.gui_completion == 'plain':
287 self._completion_widget = CompletionPlain(self)
287 self._completion_widget = CompletionPlain(self)
288
288
289 self._continuation_prompt = '> '
289 self._continuation_prompt = '> '
290 self._continuation_prompt_html = None
290 self._continuation_prompt_html = None
291 self._executing = False
291 self._executing = False
292 self._filter_resize = False
292 self._filter_resize = False
293 self._html_exporter = HtmlExporter(self._control)
293 self._html_exporter = HtmlExporter(self._control)
294 self._input_buffer_executing = ''
294 self._input_buffer_executing = ''
295 self._input_buffer_pending = ''
295 self._input_buffer_pending = ''
296 self._kill_ring = QtKillRing(self._control)
296 self._kill_ring = QtKillRing(self._control)
297 self._prompt = ''
297 self._prompt = ''
298 self._prompt_html = None
298 self._prompt_html = None
299 self._prompt_pos = 0
299 self._prompt_pos = 0
300 self._prompt_sep = ''
300 self._prompt_sep = ''
301 self._reading = False
301 self._reading = False
302 self._reading_callback = None
302 self._reading_callback = None
303 self._tab_width = 8
303 self._tab_width = 8
304
304
305 # List of strings pending to be appended as plain text in the widget.
305 # List of strings pending to be appended as plain text in the widget.
306 # The text is not immediately inserted when available to not
306 # The text is not immediately inserted when available to not
307 # choke the Qt event loop with paint events for the widget in
307 # choke the Qt event loop with paint events for the widget in
308 # case of lots of output from kernel.
308 # case of lots of output from kernel.
309 self._pending_insert_text = []
309 self._pending_insert_text = []
310
310
311 # Timer to flush the pending stream messages. The interval is adjusted
311 # Timer to flush the pending stream messages. The interval is adjusted
312 # later based on actual time taken for flushing a screen (buffer_size)
312 # later based on actual time taken for flushing a screen (buffer_size)
313 # of output text.
313 # of output text.
314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
315 self._pending_text_flush_interval.setInterval(100)
315 self._pending_text_flush_interval.setInterval(100)
316 self._pending_text_flush_interval.setSingleShot(True)
316 self._pending_text_flush_interval.setSingleShot(True)
317 self._pending_text_flush_interval.timeout.connect(
317 self._pending_text_flush_interval.timeout.connect(
318 self._on_flush_pending_stream_timer)
318 self._on_flush_pending_stream_timer)
319
319
320 # Set a monospaced font.
320 # Set a monospaced font.
321 self.reset_font()
321 self.reset_font()
322
322
323 # Configure actions.
323 # Configure actions.
324 action = QtGui.QAction('Print', None)
324 action = QtGui.QAction('Print', None)
325 action.setEnabled(True)
325 action.setEnabled(True)
326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
328 # Only override the default if there is a collision.
328 # Only override the default if there is a collision.
329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
330 printkey = "Ctrl+Shift+P"
330 printkey = "Ctrl+Shift+P"
331 action.setShortcut(printkey)
331 action.setShortcut(printkey)
332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
333 action.triggered.connect(self.print_)
333 action.triggered.connect(self.print_)
334 self.addAction(action)
334 self.addAction(action)
335 self.print_action = action
335 self.print_action = action
336
336
337 action = QtGui.QAction('Save as HTML/XML', None)
337 action = QtGui.QAction('Save as HTML/XML', None)
338 action.setShortcut(QtGui.QKeySequence.Save)
338 action.setShortcut(QtGui.QKeySequence.Save)
339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
340 action.triggered.connect(self.export_html)
340 action.triggered.connect(self.export_html)
341 self.addAction(action)
341 self.addAction(action)
342 self.export_action = action
342 self.export_action = action
343
343
344 action = QtGui.QAction('Select All', None)
344 action = QtGui.QAction('Select All', None)
345 action.setEnabled(True)
345 action.setEnabled(True)
346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
348 # Only override the default if there is a collision.
348 # Only override the default if there is a collision.
349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
350 selectall = "Ctrl+Shift+A"
350 selectall = "Ctrl+Shift+A"
351 action.setShortcut(selectall)
351 action.setShortcut(selectall)
352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
353 action.triggered.connect(self.select_all)
353 action.triggered.connect(self.select_all)
354 self.addAction(action)
354 self.addAction(action)
355 self.select_all_action = action
355 self.select_all_action = action
356
356
357 self.increase_font_size = QtGui.QAction("Bigger Font",
357 self.increase_font_size = QtGui.QAction("Bigger Font",
358 self,
358 self,
359 shortcut=QtGui.QKeySequence.ZoomIn,
359 shortcut=QtGui.QKeySequence.ZoomIn,
360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
361 statusTip="Increase the font size by one point",
361 statusTip="Increase the font size by one point",
362 triggered=self._increase_font_size)
362 triggered=self._increase_font_size)
363 self.addAction(self.increase_font_size)
363 self.addAction(self.increase_font_size)
364
364
365 self.decrease_font_size = QtGui.QAction("Smaller Font",
365 self.decrease_font_size = QtGui.QAction("Smaller Font",
366 self,
366 self,
367 shortcut=QtGui.QKeySequence.ZoomOut,
367 shortcut=QtGui.QKeySequence.ZoomOut,
368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
369 statusTip="Decrease the font size by one point",
369 statusTip="Decrease the font size by one point",
370 triggered=self._decrease_font_size)
370 triggered=self._decrease_font_size)
371 self.addAction(self.decrease_font_size)
371 self.addAction(self.decrease_font_size)
372
372
373 self.reset_font_size = QtGui.QAction("Normal Font",
373 self.reset_font_size = QtGui.QAction("Normal Font",
374 self,
374 self,
375 shortcut="Ctrl+0",
375 shortcut="Ctrl+0",
376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
377 statusTip="Restore the Normal font size",
377 statusTip="Restore the Normal font size",
378 triggered=self.reset_font)
378 triggered=self.reset_font)
379 self.addAction(self.reset_font_size)
379 self.addAction(self.reset_font_size)
380
380
381 # Accept drag and drop events here. Drops were already turned off
381 # Accept drag and drop events here. Drops were already turned off
382 # in self._control when that widget was created.
382 # in self._control when that widget was created.
383 self.setAcceptDrops(True)
383 self.setAcceptDrops(True)
384
384
385 #---------------------------------------------------------------------------
385 #---------------------------------------------------------------------------
386 # Drag and drop support
386 # Drag and drop support
387 #---------------------------------------------------------------------------
387 #---------------------------------------------------------------------------
388
388
389 def dragEnterEvent(self, e):
389 def dragEnterEvent(self, e):
390 if e.mimeData().hasUrls():
390 if e.mimeData().hasUrls():
391 # The link action should indicate to that the drop will insert
391 # The link action should indicate to that the drop will insert
392 # the file anme.
392 # the file anme.
393 e.setDropAction(QtCore.Qt.LinkAction)
393 e.setDropAction(QtCore.Qt.LinkAction)
394 e.accept()
394 e.accept()
395 elif e.mimeData().hasText():
395 elif e.mimeData().hasText():
396 # By changing the action to copy we don't need to worry about
396 # By changing the action to copy we don't need to worry about
397 # the user accidentally moving text around in the widget.
397 # the user accidentally moving text around in the widget.
398 e.setDropAction(QtCore.Qt.CopyAction)
398 e.setDropAction(QtCore.Qt.CopyAction)
399 e.accept()
399 e.accept()
400
400
401 def dragMoveEvent(self, e):
401 def dragMoveEvent(self, e):
402 if e.mimeData().hasUrls():
402 if e.mimeData().hasUrls():
403 pass
403 pass
404 elif e.mimeData().hasText():
404 elif e.mimeData().hasText():
405 cursor = self._control.cursorForPosition(e.pos())
405 cursor = self._control.cursorForPosition(e.pos())
406 if self._in_buffer(cursor.position()):
406 if self._in_buffer(cursor.position()):
407 e.setDropAction(QtCore.Qt.CopyAction)
407 e.setDropAction(QtCore.Qt.CopyAction)
408 self._control.setTextCursor(cursor)
408 self._control.setTextCursor(cursor)
409 else:
409 else:
410 e.setDropAction(QtCore.Qt.IgnoreAction)
410 e.setDropAction(QtCore.Qt.IgnoreAction)
411 e.accept()
411 e.accept()
412
412
413 def dropEvent(self, e):
413 def dropEvent(self, e):
414 if e.mimeData().hasUrls():
414 if e.mimeData().hasUrls():
415 self._keep_cursor_in_buffer()
415 self._keep_cursor_in_buffer()
416 cursor = self._control.textCursor()
416 cursor = self._control.textCursor()
417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
419 for f in filenames)
419 for f in filenames)
420 self._insert_plain_text_into_buffer(cursor, text)
420 self._insert_plain_text_into_buffer(cursor, text)
421 elif e.mimeData().hasText():
421 elif e.mimeData().hasText():
422 cursor = self._control.cursorForPosition(e.pos())
422 cursor = self._control.cursorForPosition(e.pos())
423 if self._in_buffer(cursor.position()):
423 if self._in_buffer(cursor.position()):
424 text = e.mimeData().text()
424 text = e.mimeData().text()
425 self._insert_plain_text_into_buffer(cursor, text)
425 self._insert_plain_text_into_buffer(cursor, text)
426
426
427 def eventFilter(self, obj, event):
427 def eventFilter(self, obj, event):
428 """ Reimplemented to ensure a console-like behavior in the underlying
428 """ Reimplemented to ensure a console-like behavior in the underlying
429 text widgets.
429 text widgets.
430 """
430 """
431 etype = event.type()
431 etype = event.type()
432 if etype == QtCore.QEvent.KeyPress:
432 if etype == QtCore.QEvent.KeyPress:
433
433
434 # Re-map keys for all filtered widgets.
434 # Re-map keys for all filtered widgets.
435 key = event.key()
435 key = event.key()
436 if self._control_key_down(event.modifiers()) and \
436 if self._control_key_down(event.modifiers()) and \
437 key in self._ctrl_down_remap:
437 key in self._ctrl_down_remap:
438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
439 self._ctrl_down_remap[key],
439 self._ctrl_down_remap[key],
440 QtCore.Qt.NoModifier)
440 QtCore.Qt.NoModifier)
441 QtGui.qApp.sendEvent(obj, new_event)
441 QtGui.qApp.sendEvent(obj, new_event)
442 return True
442 return True
443
443
444 elif obj == self._control:
444 elif obj == self._control:
445 return self._event_filter_console_keypress(event)
445 return self._event_filter_console_keypress(event)
446
446
447 elif obj == self._page_control:
447 elif obj == self._page_control:
448 return self._event_filter_page_keypress(event)
448 return self._event_filter_page_keypress(event)
449
449
450 # Make middle-click paste safe.
450 # Make middle-click paste safe.
451 elif etype == QtCore.QEvent.MouseButtonRelease and \
451 elif etype == QtCore.QEvent.MouseButtonRelease and \
452 event.button() == QtCore.Qt.MidButton and \
452 event.button() == QtCore.Qt.MidButton and \
453 obj == self._control.viewport():
453 obj == self._control.viewport():
454 cursor = self._control.cursorForPosition(event.pos())
454 cursor = self._control.cursorForPosition(event.pos())
455 self._control.setTextCursor(cursor)
455 self._control.setTextCursor(cursor)
456 self.paste(QtGui.QClipboard.Selection)
456 self.paste(QtGui.QClipboard.Selection)
457 return True
457 return True
458
458
459 # Manually adjust the scrollbars *after* a resize event is dispatched.
459 # Manually adjust the scrollbars *after* a resize event is dispatched.
460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
461 self._filter_resize = True
461 self._filter_resize = True
462 QtGui.qApp.sendEvent(obj, event)
462 QtGui.qApp.sendEvent(obj, event)
463 self._adjust_scrollbars()
463 self._adjust_scrollbars()
464 self._filter_resize = False
464 self._filter_resize = False
465 return True
465 return True
466
466
467 # Override shortcuts for all filtered widgets.
467 # Override shortcuts for all filtered widgets.
468 elif etype == QtCore.QEvent.ShortcutOverride and \
468 elif etype == QtCore.QEvent.ShortcutOverride and \
469 self.override_shortcuts and \
469 self.override_shortcuts and \
470 self._control_key_down(event.modifiers()) and \
470 self._control_key_down(event.modifiers()) and \
471 event.key() in self._shortcuts:
471 event.key() in self._shortcuts:
472 event.accept()
472 event.accept()
473
473
474 # Handle scrolling of the vsplit pager. This hack attempts to solve
474 # Handle scrolling of the vsplit pager. This hack attempts to solve
475 # problems with tearing of the help text inside the pager window. This
475 # problems with tearing of the help text inside the pager window. This
476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
477 # perfect but makes the pager more usable.
477 # perfect but makes the pager more usable.
478 elif etype in self._pager_scroll_events and \
478 elif etype in self._pager_scroll_events and \
479 obj == self._page_control:
479 obj == self._page_control:
480 self._page_control.repaint()
480 self._page_control.repaint()
481 return True
481 return True
482
482
483 elif etype == QtCore.QEvent.MouseMove:
483 elif etype == QtCore.QEvent.MouseMove:
484 anchor = self._control.anchorAt(event.pos())
484 anchor = self._control.anchorAt(event.pos())
485 QtGui.QToolTip.showText(event.globalPos(), anchor)
485 QtGui.QToolTip.showText(event.globalPos(), anchor)
486
486
487 return super(ConsoleWidget, self).eventFilter(obj, event)
487 return super(ConsoleWidget, self).eventFilter(obj, event)
488
488
489 #---------------------------------------------------------------------------
489 #---------------------------------------------------------------------------
490 # 'QWidget' interface
490 # 'QWidget' interface
491 #---------------------------------------------------------------------------
491 #---------------------------------------------------------------------------
492
492
493 def sizeHint(self):
493 def sizeHint(self):
494 """ Reimplemented to suggest a size that is 80 characters wide and
494 """ Reimplemented to suggest a size that is 80 characters wide and
495 25 lines high.
495 25 lines high.
496 """
496 """
497 font_metrics = QtGui.QFontMetrics(self.font)
497 font_metrics = QtGui.QFontMetrics(self.font)
498 margin = (self._control.frameWidth() +
498 margin = (self._control.frameWidth() +
499 self._control.document().documentMargin()) * 2
499 self._control.document().documentMargin()) * 2
500 style = self.style()
500 style = self.style()
501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
502
502
503 # Note 1: Despite my best efforts to take the various margins into
503 # Note 1: Despite my best efforts to take the various margins into
504 # account, the width is still coming out a bit too small, so we include
504 # account, the width is still coming out a bit too small, so we include
505 # a fudge factor of one character here.
505 # a fudge factor of one character here.
506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
507 # to a Qt bug on certain Mac OS systems where it returns 0.
507 # to a Qt bug on certain Mac OS systems where it returns 0.
508 width = font_metrics.width(' ') * self.width + margin
508 width = font_metrics.width(' ') * self.width + margin
509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
510 if self.paging == 'hsplit':
510 if self.paging == 'hsplit':
511 width = width * 2 + splitwidth
511 width = width * 2 + splitwidth
512
512
513 height = font_metrics.height() * self.height + margin
513 height = font_metrics.height() * self.height + margin
514 if self.paging == 'vsplit':
514 if self.paging == 'vsplit':
515 height = height * 2 + splitwidth
515 height = height * 2 + splitwidth
516
516
517 return QtCore.QSize(width, height)
517 return QtCore.QSize(width, height)
518
518
519 #---------------------------------------------------------------------------
519 #---------------------------------------------------------------------------
520 # 'ConsoleWidget' public interface
520 # 'ConsoleWidget' public interface
521 #---------------------------------------------------------------------------
521 #---------------------------------------------------------------------------
522
522
523 def can_copy(self):
523 def can_copy(self):
524 """ Returns whether text can be copied to the clipboard.
524 """ Returns whether text can be copied to the clipboard.
525 """
525 """
526 return self._control.textCursor().hasSelection()
526 return self._control.textCursor().hasSelection()
527
527
528 def can_cut(self):
528 def can_cut(self):
529 """ Returns whether text can be cut to the clipboard.
529 """ Returns whether text can be cut to the clipboard.
530 """
530 """
531 cursor = self._control.textCursor()
531 cursor = self._control.textCursor()
532 return (cursor.hasSelection() and
532 return (cursor.hasSelection() and
533 self._in_buffer(cursor.anchor()) and
533 self._in_buffer(cursor.anchor()) and
534 self._in_buffer(cursor.position()))
534 self._in_buffer(cursor.position()))
535
535
536 def can_paste(self):
536 def can_paste(self):
537 """ Returns whether text can be pasted from the clipboard.
537 """ Returns whether text can be pasted from the clipboard.
538 """
538 """
539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
540 return bool(QtGui.QApplication.clipboard().text())
540 return bool(QtGui.QApplication.clipboard().text())
541 return False
541 return False
542
542
543 def clear(self, keep_input=True):
543 def clear(self, keep_input=True):
544 """ Clear the console.
544 """ Clear the console.
545
545
546 Parameters
546 Parameters
547 ----------
547 ----------
548 keep_input : bool, optional (default True)
548 keep_input : bool, optional (default True)
549 If set, restores the old input buffer if a new prompt is written.
549 If set, restores the old input buffer if a new prompt is written.
550 """
550 """
551 if self._executing:
551 if self._executing:
552 self._control.clear()
552 self._control.clear()
553 else:
553 else:
554 if keep_input:
554 if keep_input:
555 input_buffer = self.input_buffer
555 input_buffer = self.input_buffer
556 self._control.clear()
556 self._control.clear()
557 self._show_prompt()
557 self._show_prompt()
558 if keep_input:
558 if keep_input:
559 self.input_buffer = input_buffer
559 self.input_buffer = input_buffer
560
560
561 def copy(self):
561 def copy(self):
562 """ Copy the currently selected text to the clipboard.
562 """ Copy the currently selected text to the clipboard.
563 """
563 """
564 self.layout().currentWidget().copy()
564 self.layout().currentWidget().copy()
565
565
566 def copy_anchor(self, anchor):
566 def copy_anchor(self, anchor):
567 """ Copy anchor text to the clipboard
567 """ Copy anchor text to the clipboard
568 """
568 """
569 QtGui.QApplication.clipboard().setText(anchor)
569 QtGui.QApplication.clipboard().setText(anchor)
570
570
571 def cut(self):
571 def cut(self):
572 """ Copy the currently selected text to the clipboard and delete it
572 """ Copy the currently selected text to the clipboard and delete it
573 if it's inside the input buffer.
573 if it's inside the input buffer.
574 """
574 """
575 self.copy()
575 self.copy()
576 if self.can_cut():
576 if self.can_cut():
577 self._control.textCursor().removeSelectedText()
577 self._control.textCursor().removeSelectedText()
578
578
579 def execute(self, source=None, hidden=False, interactive=False):
579 def execute(self, source=None, hidden=False, interactive=False):
580 """ Executes source or the input buffer, possibly prompting for more
580 """ Executes source or the input buffer, possibly prompting for more
581 input.
581 input.
582
582
583 Parameters
583 Parameters
584 ----------
584 ----------
585 source : str, optional
585 source : str, optional
586
586
587 The source to execute. If not specified, the input buffer will be
587 The source to execute. If not specified, the input buffer will be
588 used. If specified and 'hidden' is False, the input buffer will be
588 used. If specified and 'hidden' is False, the input buffer will be
589 replaced with the source before execution.
589 replaced with the source before execution.
590
590
591 hidden : bool, optional (default False)
591 hidden : bool, optional (default False)
592
592
593 If set, no output will be shown and the prompt will not be modified.
593 If set, no output will be shown and the prompt will not be modified.
594 In other words, it will be completely invisible to the user that
594 In other words, it will be completely invisible to the user that
595 an execution has occurred.
595 an execution has occurred.
596
596
597 interactive : bool, optional (default False)
597 interactive : bool, optional (default False)
598
598
599 Whether the console is to treat the source as having been manually
599 Whether the console is to treat the source as having been manually
600 entered by the user. The effect of this parameter depends on the
600 entered by the user. The effect of this parameter depends on the
601 subclass implementation.
601 subclass implementation.
602
602
603 Raises
603 Raises
604 ------
604 ------
605 RuntimeError
605 RuntimeError
606 If incomplete input is given and 'hidden' is True. In this case,
606 If incomplete input is given and 'hidden' is True. In this case,
607 it is not possible to prompt for more input.
607 it is not possible to prompt for more input.
608
608
609 Returns
609 Returns
610 -------
610 -------
611 A boolean indicating whether the source was executed.
611 A boolean indicating whether the source was executed.
612 """
612 """
613 # WARNING: The order in which things happen here is very particular, in
613 # WARNING: The order in which things happen here is very particular, in
614 # large part because our syntax highlighting is fragile. If you change
614 # large part because our syntax highlighting is fragile. If you change
615 # something, test carefully!
615 # something, test carefully!
616
616
617 # Decide what to execute.
617 # Decide what to execute.
618 if source is None:
618 if source is None:
619 source = self.input_buffer
619 source = self.input_buffer
620 if not hidden:
620 if not hidden:
621 # A newline is appended later, but it should be considered part
621 # A newline is appended later, but it should be considered part
622 # of the input buffer.
622 # of the input buffer.
623 source += '\n'
623 source += '\n'
624 elif not hidden:
624 elif not hidden:
625 self.input_buffer = source
625 self.input_buffer = source
626
626
627 # Execute the source or show a continuation prompt if it is incomplete.
627 # Execute the source or show a continuation prompt if it is incomplete.
628 if self.execute_on_complete_input:
628 if self.execute_on_complete_input:
629 complete = self._is_complete(source, interactive)
629 complete = self._is_complete(source, interactive)
630 else:
630 else:
631 complete = not interactive
631 complete = not interactive
632 if hidden:
632 if hidden:
633 if complete or not self.execute_on_complete_input:
633 if complete or not self.execute_on_complete_input:
634 self._execute(source, hidden)
634 self._execute(source, hidden)
635 else:
635 else:
636 error = 'Incomplete noninteractive input: "%s"'
636 error = 'Incomplete noninteractive input: "%s"'
637 raise RuntimeError(error % source)
637 raise RuntimeError(error % source)
638 else:
638 else:
639 if complete:
639 if complete:
640 self._append_plain_text('\n')
640 self._append_plain_text('\n')
641 self._input_buffer_executing = self.input_buffer
641 self._input_buffer_executing = self.input_buffer
642 self._executing = True
642 self._executing = True
643 self._prompt_finished()
643 self._prompt_finished()
644
644
645 # The maximum block count is only in effect during execution.
645 # The maximum block count is only in effect during execution.
646 # This ensures that _prompt_pos does not become invalid due to
646 # This ensures that _prompt_pos does not become invalid due to
647 # text truncation.
647 # text truncation.
648 self._control.document().setMaximumBlockCount(self.buffer_size)
648 self._control.document().setMaximumBlockCount(self.buffer_size)
649
649
650 # Setting a positive maximum block count will automatically
650 # Setting a positive maximum block count will automatically
651 # disable the undo/redo history, but just to be safe:
651 # disable the undo/redo history, but just to be safe:
652 self._control.setUndoRedoEnabled(False)
652 self._control.setUndoRedoEnabled(False)
653
653
654 # Perform actual execution.
654 # Perform actual execution.
655 self._execute(source, hidden)
655 self._execute(source, hidden)
656
656
657 else:
657 else:
658 # Do this inside an edit block so continuation prompts are
658 # Do this inside an edit block so continuation prompts are
659 # removed seamlessly via undo/redo.
659 # removed seamlessly via undo/redo.
660 cursor = self._get_end_cursor()
660 cursor = self._get_end_cursor()
661 cursor.beginEditBlock()
661 cursor.beginEditBlock()
662 cursor.insertText('\n')
662 cursor.insertText('\n')
663 self._insert_continuation_prompt(cursor)
663 self._insert_continuation_prompt(cursor)
664 cursor.endEditBlock()
664 cursor.endEditBlock()
665
665
666 # Do not do this inside the edit block. It works as expected
666 # Do not do this inside the edit block. It works as expected
667 # when using a QPlainTextEdit control, but does not have an
667 # when using a QPlainTextEdit control, but does not have an
668 # effect when using a QTextEdit. I believe this is a Qt bug.
668 # effect when using a QTextEdit. I believe this is a Qt bug.
669 self._control.moveCursor(QtGui.QTextCursor.End)
669 self._control.moveCursor(QtGui.QTextCursor.End)
670
670
671 return complete
671 return complete
672
672
673 def export_html(self):
673 def export_html(self):
674 """ Shows a dialog to export HTML/XML in various formats.
674 """ Shows a dialog to export HTML/XML in various formats.
675 """
675 """
676 self._html_exporter.export()
676 self._html_exporter.export()
677
677
678 def _get_input_buffer(self, force=False):
678 def _get_input_buffer(self, force=False):
679 """ The text that the user has entered entered at the current prompt.
679 """ The text that the user has entered entered at the current prompt.
680
680
681 If the console is currently executing, the text that is executing will
681 If the console is currently executing, the text that is executing will
682 always be returned.
682 always be returned.
683 """
683 """
684 # If we're executing, the input buffer may not even exist anymore due to
684 # If we're executing, the input buffer may not even exist anymore due to
685 # the limit imposed by 'buffer_size'. Therefore, we store it.
685 # the limit imposed by 'buffer_size'. Therefore, we store it.
686 if self._executing and not force:
686 if self._executing and not force:
687 return self._input_buffer_executing
687 return self._input_buffer_executing
688
688
689 cursor = self._get_end_cursor()
689 cursor = self._get_end_cursor()
690 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
690 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
691 input_buffer = cursor.selection().toPlainText()
691 input_buffer = cursor.selection().toPlainText()
692
692
693 # Strip out continuation prompts.
693 # Strip out continuation prompts.
694 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
694 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
695
695
696 def _set_input_buffer(self, string):
696 def _set_input_buffer(self, string):
697 """ Sets the text in the input buffer.
697 """ Sets the text in the input buffer.
698
698
699 If the console is currently executing, this call has no *immediate*
699 If the console is currently executing, this call has no *immediate*
700 effect. When the execution is finished, the input buffer will be updated
700 effect. When the execution is finished, the input buffer will be updated
701 appropriately.
701 appropriately.
702 """
702 """
703 # If we're executing, store the text for later.
703 # If we're executing, store the text for later.
704 if self._executing:
704 if self._executing:
705 self._input_buffer_pending = string
705 self._input_buffer_pending = string
706 return
706 return
707
707
708 # Remove old text.
708 # Remove old text.
709 cursor = self._get_end_cursor()
709 cursor = self._get_end_cursor()
710 cursor.beginEditBlock()
710 cursor.beginEditBlock()
711 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
711 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
712 cursor.removeSelectedText()
712 cursor.removeSelectedText()
713
713
714 # Insert new text with continuation prompts.
714 # Insert new text with continuation prompts.
715 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
715 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
716 cursor.endEditBlock()
716 cursor.endEditBlock()
717 self._control.moveCursor(QtGui.QTextCursor.End)
717 self._control.moveCursor(QtGui.QTextCursor.End)
718
718
719 input_buffer = property(_get_input_buffer, _set_input_buffer)
719 input_buffer = property(_get_input_buffer, _set_input_buffer)
720
720
721 def _get_font(self):
721 def _get_font(self):
722 """ The base font being used by the ConsoleWidget.
722 """ The base font being used by the ConsoleWidget.
723 """
723 """
724 return self._control.document().defaultFont()
724 return self._control.document().defaultFont()
725
725
726 def _set_font(self, font):
726 def _set_font(self, font):
727 """ Sets the base font for the ConsoleWidget to the specified QFont.
727 """ Sets the base font for the ConsoleWidget to the specified QFont.
728 """
728 """
729 font_metrics = QtGui.QFontMetrics(font)
729 font_metrics = QtGui.QFontMetrics(font)
730 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
730 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
731
731
732 self._completion_widget.setFont(font)
732 self._completion_widget.setFont(font)
733 self._control.document().setDefaultFont(font)
733 self._control.document().setDefaultFont(font)
734 if self._page_control:
734 if self._page_control:
735 self._page_control.document().setDefaultFont(font)
735 self._page_control.document().setDefaultFont(font)
736
736
737 self.font_changed.emit(font)
737 self.font_changed.emit(font)
738
738
739 font = property(_get_font, _set_font)
739 font = property(_get_font, _set_font)
740
740
741 def open_anchor(self, anchor):
741 def open_anchor(self, anchor):
742 """ Open selected anchor in the default webbrowser
742 """ Open selected anchor in the default webbrowser
743 """
743 """
744 webbrowser.open( anchor )
744 webbrowser.open( anchor )
745
745
746 def paste(self, mode=QtGui.QClipboard.Clipboard):
746 def paste(self, mode=QtGui.QClipboard.Clipboard):
747 """ Paste the contents of the clipboard into the input region.
747 """ Paste the contents of the clipboard into the input region.
748
748
749 Parameters
749 Parameters
750 ----------
750 ----------
751 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
751 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
752
752
753 Controls which part of the system clipboard is used. This can be
753 Controls which part of the system clipboard is used. This can be
754 used to access the selection clipboard in X11 and the Find buffer
754 used to access the selection clipboard in X11 and the Find buffer
755 in Mac OS. By default, the regular clipboard is used.
755 in Mac OS. By default, the regular clipboard is used.
756 """
756 """
757 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
757 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
758 # Make sure the paste is safe.
758 # Make sure the paste is safe.
759 self._keep_cursor_in_buffer()
759 self._keep_cursor_in_buffer()
760 cursor = self._control.textCursor()
760 cursor = self._control.textCursor()
761
761
762 # Remove any trailing newline, which confuses the GUI and forces the
762 # Remove any trailing newline, which confuses the GUI and forces the
763 # user to backspace.
763 # user to backspace.
764 text = QtGui.QApplication.clipboard().text(mode).rstrip()
764 text = QtGui.QApplication.clipboard().text(mode).rstrip()
765 self._insert_plain_text_into_buffer(cursor, dedent(text))
765 self._insert_plain_text_into_buffer(cursor, dedent(text))
766
766
767 def print_(self, printer = None):
767 def print_(self, printer = None):
768 """ Print the contents of the ConsoleWidget to the specified QPrinter.
768 """ Print the contents of the ConsoleWidget to the specified QPrinter.
769 """
769 """
770 if (not printer):
770 if (not printer):
771 printer = QtGui.QPrinter()
771 printer = QtGui.QPrinter()
772 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
772 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
773 return
773 return
774 self._control.print_(printer)
774 self._control.print_(printer)
775
775
776 def prompt_to_top(self):
776 def prompt_to_top(self):
777 """ Moves the prompt to the top of the viewport.
777 """ Moves the prompt to the top of the viewport.
778 """
778 """
779 if not self._executing:
779 if not self._executing:
780 prompt_cursor = self._get_prompt_cursor()
780 prompt_cursor = self._get_prompt_cursor()
781 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
781 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
782 self._set_cursor(prompt_cursor)
782 self._set_cursor(prompt_cursor)
783 self._set_top_cursor(prompt_cursor)
783 self._set_top_cursor(prompt_cursor)
784
784
785 def redo(self):
785 def redo(self):
786 """ Redo the last operation. If there is no operation to redo, nothing
786 """ Redo the last operation. If there is no operation to redo, nothing
787 happens.
787 happens.
788 """
788 """
789 self._control.redo()
789 self._control.redo()
790
790
791 def reset_font(self):
791 def reset_font(self):
792 """ Sets the font to the default fixed-width font for this platform.
792 """ Sets the font to the default fixed-width font for this platform.
793 """
793 """
794 if sys.platform == 'win32':
794 if sys.platform == 'win32':
795 # Consolas ships with Vista/Win7, fallback to Courier if needed
795 # Consolas ships with Vista/Win7, fallback to Courier if needed
796 fallback = 'Courier'
796 fallback = 'Courier'
797 elif sys.platform == 'darwin':
797 elif sys.platform == 'darwin':
798 # OSX always has Monaco
798 # OSX always has Monaco
799 fallback = 'Monaco'
799 fallback = 'Monaco'
800 else:
800 else:
801 # Monospace should always exist
801 # Monospace should always exist
802 fallback = 'Monospace'
802 fallback = 'Monospace'
803 font = get_font(self.font_family, fallback)
803 font = get_font(self.font_family, fallback)
804 if self.font_size:
804 if self.font_size:
805 font.setPointSize(self.font_size)
805 font.setPointSize(self.font_size)
806 else:
806 else:
807 font.setPointSize(QtGui.qApp.font().pointSize())
807 font.setPointSize(QtGui.qApp.font().pointSize())
808 font.setStyleHint(QtGui.QFont.TypeWriter)
808 font.setStyleHint(QtGui.QFont.TypeWriter)
809 self._set_font(font)
809 self._set_font(font)
810
810
811 def change_font_size(self, delta):
811 def change_font_size(self, delta):
812 """Change the font size by the specified amount (in points).
812 """Change the font size by the specified amount (in points).
813 """
813 """
814 font = self.font
814 font = self.font
815 size = max(font.pointSize() + delta, 1) # minimum 1 point
815 size = max(font.pointSize() + delta, 1) # minimum 1 point
816 font.setPointSize(size)
816 font.setPointSize(size)
817 self._set_font(font)
817 self._set_font(font)
818
818
819 def _increase_font_size(self):
819 def _increase_font_size(self):
820 self.change_font_size(1)
820 self.change_font_size(1)
821
821
822 def _decrease_font_size(self):
822 def _decrease_font_size(self):
823 self.change_font_size(-1)
823 self.change_font_size(-1)
824
824
825 def select_all(self):
825 def select_all(self):
826 """ Selects all the text in the buffer.
826 """ Selects all the text in the buffer.
827 """
827 """
828 self._control.selectAll()
828 self._control.selectAll()
829
829
830 def _get_tab_width(self):
830 def _get_tab_width(self):
831 """ The width (in terms of space characters) for tab characters.
831 """ The width (in terms of space characters) for tab characters.
832 """
832 """
833 return self._tab_width
833 return self._tab_width
834
834
835 def _set_tab_width(self, tab_width):
835 def _set_tab_width(self, tab_width):
836 """ Sets the width (in terms of space characters) for tab characters.
836 """ Sets the width (in terms of space characters) for tab characters.
837 """
837 """
838 font_metrics = QtGui.QFontMetrics(self.font)
838 font_metrics = QtGui.QFontMetrics(self.font)
839 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
839 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
840
840
841 self._tab_width = tab_width
841 self._tab_width = tab_width
842
842
843 tab_width = property(_get_tab_width, _set_tab_width)
843 tab_width = property(_get_tab_width, _set_tab_width)
844
844
845 def undo(self):
845 def undo(self):
846 """ Undo the last operation. If there is no operation to undo, nothing
846 """ Undo the last operation. If there is no operation to undo, nothing
847 happens.
847 happens.
848 """
848 """
849 self._control.undo()
849 self._control.undo()
850
850
851 #---------------------------------------------------------------------------
851 #---------------------------------------------------------------------------
852 # 'ConsoleWidget' abstract interface
852 # 'ConsoleWidget' abstract interface
853 #---------------------------------------------------------------------------
853 #---------------------------------------------------------------------------
854
854
855 def _is_complete(self, source, interactive):
855 def _is_complete(self, source, interactive):
856 """ Returns whether 'source' can be executed. When triggered by an
856 """ Returns whether 'source' can be executed. When triggered by an
857 Enter/Return key press, 'interactive' is True; otherwise, it is
857 Enter/Return key press, 'interactive' is True; otherwise, it is
858 False.
858 False.
859 """
859 """
860 raise NotImplementedError
860 raise NotImplementedError
861
861
862 def _execute(self, source, hidden):
862 def _execute(self, source, hidden):
863 """ Execute 'source'. If 'hidden', do not show any output.
863 """ Execute 'source'. If 'hidden', do not show any output.
864 """
864 """
865 raise NotImplementedError
865 raise NotImplementedError
866
866
867 def _prompt_started_hook(self):
867 def _prompt_started_hook(self):
868 """ Called immediately after a new prompt is displayed.
868 """ Called immediately after a new prompt is displayed.
869 """
869 """
870 pass
870 pass
871
871
872 def _prompt_finished_hook(self):
872 def _prompt_finished_hook(self):
873 """ Called immediately after a prompt is finished, i.e. when some input
873 """ Called immediately after a prompt is finished, i.e. when some input
874 will be processed and a new prompt displayed.
874 will be processed and a new prompt displayed.
875 """
875 """
876 pass
876 pass
877
877
878 def _up_pressed(self, shift_modifier):
878 def _up_pressed(self, shift_modifier):
879 """ Called when the up key is pressed. Returns whether to continue
879 """ Called when the up key is pressed. Returns whether to continue
880 processing the event.
880 processing the event.
881 """
881 """
882 return True
882 return True
883
883
884 def _down_pressed(self, shift_modifier):
884 def _down_pressed(self, shift_modifier):
885 """ Called when the down key is pressed. Returns whether to continue
885 """ Called when the down key is pressed. Returns whether to continue
886 processing the event.
886 processing the event.
887 """
887 """
888 return True
888 return True
889
889
890 def _tab_pressed(self):
890 def _tab_pressed(self):
891 """ Called when the tab key is pressed. Returns whether to continue
891 """ Called when the tab key is pressed. Returns whether to continue
892 processing the event.
892 processing the event.
893 """
893 """
894 return False
894 return False
895
895
896 #--------------------------------------------------------------------------
896 #--------------------------------------------------------------------------
897 # 'ConsoleWidget' protected interface
897 # 'ConsoleWidget' protected interface
898 #--------------------------------------------------------------------------
898 #--------------------------------------------------------------------------
899
899
900 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
900 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
901 """ A low-level method for appending content to the end of the buffer.
901 """ A low-level method for appending content to the end of the buffer.
902
902
903 If 'before_prompt' is enabled, the content will be inserted before the
903 If 'before_prompt' is enabled, the content will be inserted before the
904 current prompt, if there is one.
904 current prompt, if there is one.
905 """
905 """
906 # Determine where to insert the content.
906 # Determine where to insert the content.
907 cursor = self._control.textCursor()
907 cursor = self._control.textCursor()
908 if before_prompt and (self._reading or not self._executing):
908 if before_prompt and (self._reading or not self._executing):
909 self._flush_pending_stream()
909 self._flush_pending_stream()
910 cursor.setPosition(self._append_before_prompt_pos)
910 cursor.setPosition(self._append_before_prompt_pos)
911 else:
911 else:
912 if insert != self._insert_plain_text:
912 if insert != self._insert_plain_text:
913 self._flush_pending_stream()
913 self._flush_pending_stream()
914 cursor.movePosition(QtGui.QTextCursor.End)
914 cursor.movePosition(QtGui.QTextCursor.End)
915 start_pos = cursor.position()
915 start_pos = cursor.position()
916
916
917 # Perform the insertion.
917 # Perform the insertion.
918 result = insert(cursor, input, *args, **kwargs)
918 result = insert(cursor, input, *args, **kwargs)
919
919
920 # Adjust the prompt position if we have inserted before it. This is safe
920 # Adjust the prompt position if we have inserted before it. This is safe
921 # because buffer truncation is disabled when not executing.
921 # because buffer truncation is disabled when not executing.
922 if before_prompt and not self._executing:
922 if before_prompt and not self._executing:
923 diff = cursor.position() - start_pos
923 diff = cursor.position() - start_pos
924 self._append_before_prompt_pos += diff
924 self._append_before_prompt_pos += diff
925 self._prompt_pos += diff
925 self._prompt_pos += diff
926
926
927 return result
927 return result
928
928
929 def _append_block(self, block_format=None, before_prompt=False):
929 def _append_block(self, block_format=None, before_prompt=False):
930 """ Appends an new QTextBlock to the end of the console buffer.
930 """ Appends an new QTextBlock to the end of the console buffer.
931 """
931 """
932 self._append_custom(self._insert_block, block_format, before_prompt)
932 self._append_custom(self._insert_block, block_format, before_prompt)
933
933
934 def _append_html(self, html, before_prompt=False):
934 def _append_html(self, html, before_prompt=False):
935 """ Appends HTML at the end of the console buffer.
935 """ Appends HTML at the end of the console buffer.
936 """
936 """
937 self._append_custom(self._insert_html, html, before_prompt)
937 self._append_custom(self._insert_html, html, before_prompt)
938
938
939 def _append_html_fetching_plain_text(self, html, before_prompt=False):
939 def _append_html_fetching_plain_text(self, html, before_prompt=False):
940 """ Appends HTML, then returns the plain text version of it.
940 """ Appends HTML, then returns the plain text version of it.
941 """
941 """
942 return self._append_custom(self._insert_html_fetching_plain_text,
942 return self._append_custom(self._insert_html_fetching_plain_text,
943 html, before_prompt)
943 html, before_prompt)
944
944
945 def _append_plain_text(self, text, before_prompt=False):
945 def _append_plain_text(self, text, before_prompt=False):
946 """ Appends plain text, processing ANSI codes if enabled.
946 """ Appends plain text, processing ANSI codes if enabled.
947 """
947 """
948 self._append_custom(self._insert_plain_text, text, before_prompt)
948 self._append_custom(self._insert_plain_text, text, before_prompt)
949
949
950 def _cancel_completion(self):
950 def _cancel_completion(self):
951 """ If text completion is progress, cancel it.
951 """ If text completion is progress, cancel it.
952 """
952 """
953 self._completion_widget.cancel_completion()
953 self._completion_widget.cancel_completion()
954
954
955 def _clear_temporary_buffer(self):
955 def _clear_temporary_buffer(self):
956 """ Clears the "temporary text" buffer, i.e. all the text following
956 """ Clears the "temporary text" buffer, i.e. all the text following
957 the prompt region.
957 the prompt region.
958 """
958 """
959 # Select and remove all text below the input buffer.
959 # Select and remove all text below the input buffer.
960 cursor = self._get_prompt_cursor()
960 cursor = self._get_prompt_cursor()
961 prompt = self._continuation_prompt.lstrip()
961 prompt = self._continuation_prompt.lstrip()
962 if(self._temp_buffer_filled):
962 if(self._temp_buffer_filled):
963 self._temp_buffer_filled = False
963 self._temp_buffer_filled = False
964 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
964 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
965 temp_cursor = QtGui.QTextCursor(cursor)
965 temp_cursor = QtGui.QTextCursor(cursor)
966 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
966 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
967 text = temp_cursor.selection().toPlainText().lstrip()
967 text = temp_cursor.selection().toPlainText().lstrip()
968 if not text.startswith(prompt):
968 if not text.startswith(prompt):
969 break
969 break
970 else:
970 else:
971 # We've reached the end of the input buffer and no text follows.
971 # We've reached the end of the input buffer and no text follows.
972 return
972 return
973 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
973 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
974 cursor.movePosition(QtGui.QTextCursor.End,
974 cursor.movePosition(QtGui.QTextCursor.End,
975 QtGui.QTextCursor.KeepAnchor)
975 QtGui.QTextCursor.KeepAnchor)
976 cursor.removeSelectedText()
976 cursor.removeSelectedText()
977
977
978 # After doing this, we have no choice but to clear the undo/redo
978 # After doing this, we have no choice but to clear the undo/redo
979 # history. Otherwise, the text is not "temporary" at all, because it
979 # history. Otherwise, the text is not "temporary" at all, because it
980 # can be recalled with undo/redo. Unfortunately, Qt does not expose
980 # can be recalled with undo/redo. Unfortunately, Qt does not expose
981 # fine-grained control to the undo/redo system.
981 # fine-grained control to the undo/redo system.
982 if self._control.isUndoRedoEnabled():
982 if self._control.isUndoRedoEnabled():
983 self._control.setUndoRedoEnabled(False)
983 self._control.setUndoRedoEnabled(False)
984 self._control.setUndoRedoEnabled(True)
984 self._control.setUndoRedoEnabled(True)
985
985
986 def _complete_with_items(self, cursor, items):
986 def _complete_with_items(self, cursor, items):
987 """ Performs completion with 'items' at the specified cursor location.
987 """ Performs completion with 'items' at the specified cursor location.
988 """
988 """
989 self._cancel_completion()
989 self._cancel_completion()
990
990
991 if len(items) == 1:
991 if len(items) == 1:
992 cursor.setPosition(self._control.textCursor().position(),
992 cursor.setPosition(self._control.textCursor().position(),
993 QtGui.QTextCursor.KeepAnchor)
993 QtGui.QTextCursor.KeepAnchor)
994 cursor.insertText(items[0])
994 cursor.insertText(items[0])
995
995
996 elif len(items) > 1:
996 elif len(items) > 1:
997 current_pos = self._control.textCursor().position()
997 current_pos = self._control.textCursor().position()
998 prefix = commonprefix(items)
998 prefix = commonprefix(items)
999 if prefix:
999 if prefix:
1000 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1000 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1001 cursor.insertText(prefix)
1001 cursor.insertText(prefix)
1002 current_pos = cursor.position()
1002 current_pos = cursor.position()
1003
1003
1004 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1004 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1005 self._completion_widget.show_items(cursor, items)
1005 self._completion_widget.show_items(cursor, items)
1006
1006
1007
1007
1008 def _fill_temporary_buffer(self, cursor, text, html=False):
1008 def _fill_temporary_buffer(self, cursor, text, html=False):
1009 """fill the area below the active editting zone with text"""
1009 """fill the area below the active editting zone with text"""
1010
1010
1011 current_pos = self._control.textCursor().position()
1011 current_pos = self._control.textCursor().position()
1012
1012
1013 cursor.beginEditBlock()
1013 cursor.beginEditBlock()
1014 self._append_plain_text('\n')
1014 self._append_plain_text('\n')
1015 self._page(text, html=html)
1015 self._page(text, html=html)
1016 cursor.endEditBlock()
1016 cursor.endEditBlock()
1017
1017
1018 cursor.setPosition(current_pos)
1018 cursor.setPosition(current_pos)
1019 self._control.moveCursor(QtGui.QTextCursor.End)
1019 self._control.moveCursor(QtGui.QTextCursor.End)
1020 self._control.setTextCursor(cursor)
1020 self._control.setTextCursor(cursor)
1021
1021
1022 self._temp_buffer_filled = True
1022 self._temp_buffer_filled = True
1023
1023
1024
1024
1025 def _context_menu_make(self, pos):
1025 def _context_menu_make(self, pos):
1026 """ Creates a context menu for the given QPoint (in widget coordinates).
1026 """ Creates a context menu for the given QPoint (in widget coordinates).
1027 """
1027 """
1028 menu = QtGui.QMenu(self)
1028 menu = QtGui.QMenu(self)
1029
1029
1030 self.cut_action = menu.addAction('Cut', self.cut)
1030 self.cut_action = menu.addAction('Cut', self.cut)
1031 self.cut_action.setEnabled(self.can_cut())
1031 self.cut_action.setEnabled(self.can_cut())
1032 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1032 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1033
1033
1034 self.copy_action = menu.addAction('Copy', self.copy)
1034 self.copy_action = menu.addAction('Copy', self.copy)
1035 self.copy_action.setEnabled(self.can_copy())
1035 self.copy_action.setEnabled(self.can_copy())
1036 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1036 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1037
1037
1038 self.paste_action = menu.addAction('Paste', self.paste)
1038 self.paste_action = menu.addAction('Paste', self.paste)
1039 self.paste_action.setEnabled(self.can_paste())
1039 self.paste_action.setEnabled(self.can_paste())
1040 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1040 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1041
1041
1042 anchor = self._control.anchorAt(pos)
1042 anchor = self._control.anchorAt(pos)
1043 if anchor:
1043 if anchor:
1044 menu.addSeparator()
1044 menu.addSeparator()
1045 self.copy_link_action = menu.addAction(
1045 self.copy_link_action = menu.addAction(
1046 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1046 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1047 self.open_link_action = menu.addAction(
1047 self.open_link_action = menu.addAction(
1048 'Open Link', lambda: self.open_anchor(anchor=anchor))
1048 'Open Link', lambda: self.open_anchor(anchor=anchor))
1049
1049
1050 menu.addSeparator()
1050 menu.addSeparator()
1051 menu.addAction(self.select_all_action)
1051 menu.addAction(self.select_all_action)
1052
1052
1053 menu.addSeparator()
1053 menu.addSeparator()
1054 menu.addAction(self.export_action)
1054 menu.addAction(self.export_action)
1055 menu.addAction(self.print_action)
1055 menu.addAction(self.print_action)
1056
1056
1057 return menu
1057 return menu
1058
1058
1059 def _control_key_down(self, modifiers, include_command=False):
1059 def _control_key_down(self, modifiers, include_command=False):
1060 """ Given a KeyboardModifiers flags object, return whether the Control
1060 """ Given a KeyboardModifiers flags object, return whether the Control
1061 key is down.
1061 key is down.
1062
1062
1063 Parameters
1063 Parameters
1064 ----------
1064 ----------
1065 include_command : bool, optional (default True)
1065 include_command : bool, optional (default True)
1066 Whether to treat the Command key as a (mutually exclusive) synonym
1066 Whether to treat the Command key as a (mutually exclusive) synonym
1067 for Control when in Mac OS.
1067 for Control when in Mac OS.
1068 """
1068 """
1069 # Note that on Mac OS, ControlModifier corresponds to the Command key
1069 # Note that on Mac OS, ControlModifier corresponds to the Command key
1070 # while MetaModifier corresponds to the Control key.
1070 # while MetaModifier corresponds to the Control key.
1071 if sys.platform == 'darwin':
1071 if sys.platform == 'darwin':
1072 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1072 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1073 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1073 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1074 else:
1074 else:
1075 return bool(modifiers & QtCore.Qt.ControlModifier)
1075 return bool(modifiers & QtCore.Qt.ControlModifier)
1076
1076
1077 def _create_control(self):
1077 def _create_control(self):
1078 """ Creates and connects the underlying text widget.
1078 """ Creates and connects the underlying text widget.
1079 """
1079 """
1080 # Create the underlying control.
1080 # Create the underlying control.
1081 if self.custom_control:
1081 if self.custom_control:
1082 control = self.custom_control()
1082 control = self.custom_control()
1083 elif self.kind == 'plain':
1083 elif self.kind == 'plain':
1084 control = QtGui.QPlainTextEdit()
1084 control = QtGui.QPlainTextEdit()
1085 elif self.kind == 'rich':
1085 elif self.kind == 'rich':
1086 control = QtGui.QTextEdit()
1086 control = QtGui.QTextEdit()
1087 control.setAcceptRichText(False)
1087 control.setAcceptRichText(False)
1088 control.setMouseTracking(True)
1088 control.setMouseTracking(True)
1089
1089
1090 # Prevent the widget from handling drops, as we already provide
1090 # Prevent the widget from handling drops, as we already provide
1091 # the logic in this class.
1091 # the logic in this class.
1092 control.setAcceptDrops(False)
1092 control.setAcceptDrops(False)
1093
1093
1094 # Install event filters. The filter on the viewport is needed for
1094 # Install event filters. The filter on the viewport is needed for
1095 # mouse events.
1095 # mouse events.
1096 control.installEventFilter(self)
1096 control.installEventFilter(self)
1097 control.viewport().installEventFilter(self)
1097 control.viewport().installEventFilter(self)
1098
1098
1099 # Connect signals.
1099 # Connect signals.
1100 control.customContextMenuRequested.connect(
1100 control.customContextMenuRequested.connect(
1101 self._custom_context_menu_requested)
1101 self._custom_context_menu_requested)
1102 control.copyAvailable.connect(self.copy_available)
1102 control.copyAvailable.connect(self.copy_available)
1103 control.redoAvailable.connect(self.redo_available)
1103 control.redoAvailable.connect(self.redo_available)
1104 control.undoAvailable.connect(self.undo_available)
1104 control.undoAvailable.connect(self.undo_available)
1105
1105
1106 # Hijack the document size change signal to prevent Qt from adjusting
1106 # Hijack the document size change signal to prevent Qt from adjusting
1107 # the viewport's scrollbar. We are relying on an implementation detail
1107 # the viewport's scrollbar. We are relying on an implementation detail
1108 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1108 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1109 # this functionality we cannot create a nice terminal interface.
1109 # this functionality we cannot create a nice terminal interface.
1110 layout = control.document().documentLayout()
1110 layout = control.document().documentLayout()
1111 layout.documentSizeChanged.disconnect()
1111 layout.documentSizeChanged.disconnect()
1112 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1112 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1113
1113
1114 # Configure the control.
1114 # Configure the control.
1115 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1115 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1116 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1116 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1117 control.setReadOnly(True)
1117 control.setReadOnly(True)
1118 control.setUndoRedoEnabled(False)
1118 control.setUndoRedoEnabled(False)
1119 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1119 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1120 return control
1120 return control
1121
1121
1122 def _create_page_control(self):
1122 def _create_page_control(self):
1123 """ Creates and connects the underlying paging widget.
1123 """ Creates and connects the underlying paging widget.
1124 """
1124 """
1125 if self.custom_page_control:
1125 if self.custom_page_control:
1126 control = self.custom_page_control()
1126 control = self.custom_page_control()
1127 elif self.kind == 'plain':
1127 elif self.kind == 'plain':
1128 control = QtGui.QPlainTextEdit()
1128 control = QtGui.QPlainTextEdit()
1129 elif self.kind == 'rich':
1129 elif self.kind == 'rich':
1130 control = QtGui.QTextEdit()
1130 control = QtGui.QTextEdit()
1131 control.installEventFilter(self)
1131 control.installEventFilter(self)
1132 viewport = control.viewport()
1132 viewport = control.viewport()
1133 viewport.installEventFilter(self)
1133 viewport.installEventFilter(self)
1134 control.setReadOnly(True)
1134 control.setReadOnly(True)
1135 control.setUndoRedoEnabled(False)
1135 control.setUndoRedoEnabled(False)
1136 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1136 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1137 return control
1137 return control
1138
1138
1139 def _event_filter_console_keypress(self, event):
1139 def _event_filter_console_keypress(self, event):
1140 """ Filter key events for the underlying text widget to create a
1140 """ Filter key events for the underlying text widget to create a
1141 console-like interface.
1141 console-like interface.
1142 """
1142 """
1143 intercepted = False
1143 intercepted = False
1144 cursor = self._control.textCursor()
1144 cursor = self._control.textCursor()
1145 position = cursor.position()
1145 position = cursor.position()
1146 key = event.key()
1146 key = event.key()
1147 ctrl_down = self._control_key_down(event.modifiers())
1147 ctrl_down = self._control_key_down(event.modifiers())
1148 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1148 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1149 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1149 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1150
1150
1151 #------ Special sequences ----------------------------------------------
1151 #------ Special sequences ----------------------------------------------
1152
1152
1153 if event.matches(QtGui.QKeySequence.Copy):
1153 if event.matches(QtGui.QKeySequence.Copy):
1154 self.copy()
1154 self.copy()
1155 intercepted = True
1155 intercepted = True
1156
1156
1157 elif event.matches(QtGui.QKeySequence.Cut):
1157 elif event.matches(QtGui.QKeySequence.Cut):
1158 self.cut()
1158 self.cut()
1159 intercepted = True
1159 intercepted = True
1160
1160
1161 elif event.matches(QtGui.QKeySequence.Paste):
1161 elif event.matches(QtGui.QKeySequence.Paste):
1162 self.paste()
1162 self.paste()
1163 intercepted = True
1163 intercepted = True
1164
1164
1165 #------ Special modifier logic -----------------------------------------
1165 #------ Special modifier logic -----------------------------------------
1166
1166
1167 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1167 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1168 intercepted = True
1168 intercepted = True
1169
1169
1170 # Special handling when tab completing in text mode.
1170 # Special handling when tab completing in text mode.
1171 self._cancel_completion()
1171 self._cancel_completion()
1172
1172
1173 if self._in_buffer(position):
1173 if self._in_buffer(position):
1174 # Special handling when a reading a line of raw input.
1174 # Special handling when a reading a line of raw input.
1175 if self._reading:
1175 if self._reading:
1176 self._append_plain_text('\n')
1176 self._append_plain_text('\n')
1177 self._reading = False
1177 self._reading = False
1178 if self._reading_callback:
1178 if self._reading_callback:
1179 self._reading_callback()
1179 self._reading_callback()
1180
1180
1181 # If the input buffer is a single line or there is only
1181 # If the input buffer is a single line or there is only
1182 # whitespace after the cursor, execute. Otherwise, split the
1182 # whitespace after the cursor, execute. Otherwise, split the
1183 # line with a continuation prompt.
1183 # line with a continuation prompt.
1184 elif not self._executing:
1184 elif not self._executing:
1185 cursor.movePosition(QtGui.QTextCursor.End,
1185 cursor.movePosition(QtGui.QTextCursor.End,
1186 QtGui.QTextCursor.KeepAnchor)
1186 QtGui.QTextCursor.KeepAnchor)
1187 at_end = len(cursor.selectedText().strip()) == 0
1187 at_end = len(cursor.selectedText().strip()) == 0
1188 single_line = (self._get_end_cursor().blockNumber() ==
1188 single_line = (self._get_end_cursor().blockNumber() ==
1189 self._get_prompt_cursor().blockNumber())
1189 self._get_prompt_cursor().blockNumber())
1190 if (at_end or shift_down or single_line) and not ctrl_down:
1190 if (at_end or shift_down or single_line) and not ctrl_down:
1191 self.execute(interactive = not shift_down)
1191 self.execute(interactive = not shift_down)
1192 else:
1192 else:
1193 # Do this inside an edit block for clean undo/redo.
1193 # Do this inside an edit block for clean undo/redo.
1194 cursor.beginEditBlock()
1194 cursor.beginEditBlock()
1195 cursor.setPosition(position)
1195 cursor.setPosition(position)
1196 cursor.insertText('\n')
1196 cursor.insertText('\n')
1197 self._insert_continuation_prompt(cursor)
1197 self._insert_continuation_prompt(cursor)
1198 cursor.endEditBlock()
1198 cursor.endEditBlock()
1199
1199
1200 # Ensure that the whole input buffer is visible.
1200 # Ensure that the whole input buffer is visible.
1201 # FIXME: This will not be usable if the input buffer is
1201 # FIXME: This will not be usable if the input buffer is
1202 # taller than the console widget.
1202 # taller than the console widget.
1203 self._control.moveCursor(QtGui.QTextCursor.End)
1203 self._control.moveCursor(QtGui.QTextCursor.End)
1204 self._control.setTextCursor(cursor)
1204 self._control.setTextCursor(cursor)
1205
1205
1206 #------ Control/Cmd modifier -------------------------------------------
1206 #------ Control/Cmd modifier -------------------------------------------
1207
1207
1208 elif ctrl_down:
1208 elif ctrl_down:
1209 if key == QtCore.Qt.Key_G:
1209 if key == QtCore.Qt.Key_G:
1210 self._keyboard_quit()
1210 self._keyboard_quit()
1211 intercepted = True
1211 intercepted = True
1212
1212
1213 elif key == QtCore.Qt.Key_K:
1213 elif key == QtCore.Qt.Key_K:
1214 if self._in_buffer(position):
1214 if self._in_buffer(position):
1215 cursor.clearSelection()
1215 cursor.clearSelection()
1216 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1216 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1217 QtGui.QTextCursor.KeepAnchor)
1217 QtGui.QTextCursor.KeepAnchor)
1218 if not cursor.hasSelection():
1218 if not cursor.hasSelection():
1219 # Line deletion (remove continuation prompt)
1219 # Line deletion (remove continuation prompt)
1220 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1220 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1221 QtGui.QTextCursor.KeepAnchor)
1221 QtGui.QTextCursor.KeepAnchor)
1222 cursor.movePosition(QtGui.QTextCursor.Right,
1222 cursor.movePosition(QtGui.QTextCursor.Right,
1223 QtGui.QTextCursor.KeepAnchor,
1223 QtGui.QTextCursor.KeepAnchor,
1224 len(self._continuation_prompt))
1224 len(self._continuation_prompt))
1225 self._kill_ring.kill_cursor(cursor)
1225 self._kill_ring.kill_cursor(cursor)
1226 self._set_cursor(cursor)
1226 self._set_cursor(cursor)
1227 intercepted = True
1227 intercepted = True
1228
1228
1229 elif key == QtCore.Qt.Key_L:
1229 elif key == QtCore.Qt.Key_L:
1230 self.prompt_to_top()
1230 self.prompt_to_top()
1231 intercepted = True
1231 intercepted = True
1232
1232
1233 elif key == QtCore.Qt.Key_O:
1233 elif key == QtCore.Qt.Key_O:
1234 if self._page_control and self._page_control.isVisible():
1234 if self._page_control and self._page_control.isVisible():
1235 self._page_control.setFocus()
1235 self._page_control.setFocus()
1236 intercepted = True
1236 intercepted = True
1237
1237
1238 elif key == QtCore.Qt.Key_U:
1238 elif key == QtCore.Qt.Key_U:
1239 if self._in_buffer(position):
1239 if self._in_buffer(position):
1240 cursor.clearSelection()
1240 cursor.clearSelection()
1241 start_line = cursor.blockNumber()
1241 start_line = cursor.blockNumber()
1242 if start_line == self._get_prompt_cursor().blockNumber():
1242 if start_line == self._get_prompt_cursor().blockNumber():
1243 offset = len(self._prompt)
1243 offset = len(self._prompt)
1244 else:
1244 else:
1245 offset = len(self._continuation_prompt)
1245 offset = len(self._continuation_prompt)
1246 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1246 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1247 QtGui.QTextCursor.KeepAnchor)
1247 QtGui.QTextCursor.KeepAnchor)
1248 cursor.movePosition(QtGui.QTextCursor.Right,
1248 cursor.movePosition(QtGui.QTextCursor.Right,
1249 QtGui.QTextCursor.KeepAnchor, offset)
1249 QtGui.QTextCursor.KeepAnchor, offset)
1250 self._kill_ring.kill_cursor(cursor)
1250 self._kill_ring.kill_cursor(cursor)
1251 self._set_cursor(cursor)
1251 self._set_cursor(cursor)
1252 intercepted = True
1252 intercepted = True
1253
1253
1254 elif key == QtCore.Qt.Key_Y:
1254 elif key == QtCore.Qt.Key_Y:
1255 self._keep_cursor_in_buffer()
1255 self._keep_cursor_in_buffer()
1256 self._kill_ring.yank()
1256 self._kill_ring.yank()
1257 intercepted = True
1257 intercepted = True
1258
1258
1259 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1259 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1260 if key == QtCore.Qt.Key_Backspace:
1260 if key == QtCore.Qt.Key_Backspace:
1261 cursor = self._get_word_start_cursor(position)
1261 cursor = self._get_word_start_cursor(position)
1262 else: # key == QtCore.Qt.Key_Delete
1262 else: # key == QtCore.Qt.Key_Delete
1263 cursor = self._get_word_end_cursor(position)
1263 cursor = self._get_word_end_cursor(position)
1264 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1264 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1265 self._kill_ring.kill_cursor(cursor)
1265 self._kill_ring.kill_cursor(cursor)
1266 intercepted = True
1266 intercepted = True
1267
1267
1268 elif key == QtCore.Qt.Key_D:
1268 elif key == QtCore.Qt.Key_D:
1269 if len(self.input_buffer) == 0:
1269 if len(self.input_buffer) == 0:
1270 self.exit_requested.emit(self)
1270 self.exit_requested.emit(self)
1271 else:
1271 else:
1272 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1272 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1273 QtCore.Qt.Key_Delete,
1273 QtCore.Qt.Key_Delete,
1274 QtCore.Qt.NoModifier)
1274 QtCore.Qt.NoModifier)
1275 QtGui.qApp.sendEvent(self._control, new_event)
1275 QtGui.qApp.sendEvent(self._control, new_event)
1276 intercepted = True
1276 intercepted = True
1277
1277
1278 #------ Alt modifier ---------------------------------------------------
1278 #------ Alt modifier ---------------------------------------------------
1279
1279
1280 elif alt_down:
1280 elif alt_down:
1281 if key == QtCore.Qt.Key_B:
1281 if key == QtCore.Qt.Key_B:
1282 self._set_cursor(self._get_word_start_cursor(position))
1282 self._set_cursor(self._get_word_start_cursor(position))
1283 intercepted = True
1283 intercepted = True
1284
1284
1285 elif key == QtCore.Qt.Key_F:
1285 elif key == QtCore.Qt.Key_F:
1286 self._set_cursor(self._get_word_end_cursor(position))
1286 self._set_cursor(self._get_word_end_cursor(position))
1287 intercepted = True
1287 intercepted = True
1288
1288
1289 elif key == QtCore.Qt.Key_Y:
1289 elif key == QtCore.Qt.Key_Y:
1290 self._kill_ring.rotate()
1290 self._kill_ring.rotate()
1291 intercepted = True
1291 intercepted = True
1292
1292
1293 elif key == QtCore.Qt.Key_Backspace:
1293 elif key == QtCore.Qt.Key_Backspace:
1294 cursor = self._get_word_start_cursor(position)
1294 cursor = self._get_word_start_cursor(position)
1295 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1295 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1296 self._kill_ring.kill_cursor(cursor)
1296 self._kill_ring.kill_cursor(cursor)
1297 intercepted = True
1297 intercepted = True
1298
1298
1299 elif key == QtCore.Qt.Key_D:
1299 elif key == QtCore.Qt.Key_D:
1300 cursor = self._get_word_end_cursor(position)
1300 cursor = self._get_word_end_cursor(position)
1301 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1301 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1302 self._kill_ring.kill_cursor(cursor)
1302 self._kill_ring.kill_cursor(cursor)
1303 intercepted = True
1303 intercepted = True
1304
1304
1305 elif key == QtCore.Qt.Key_Delete:
1305 elif key == QtCore.Qt.Key_Delete:
1306 intercepted = True
1306 intercepted = True
1307
1307
1308 elif key == QtCore.Qt.Key_Greater:
1308 elif key == QtCore.Qt.Key_Greater:
1309 self._control.moveCursor(QtGui.QTextCursor.End)
1309 self._control.moveCursor(QtGui.QTextCursor.End)
1310 intercepted = True
1310 intercepted = True
1311
1311
1312 elif key == QtCore.Qt.Key_Less:
1312 elif key == QtCore.Qt.Key_Less:
1313 self._control.setTextCursor(self._get_prompt_cursor())
1313 self._control.setTextCursor(self._get_prompt_cursor())
1314 intercepted = True
1314 intercepted = True
1315
1315
1316 #------ No modifiers ---------------------------------------------------
1316 #------ No modifiers ---------------------------------------------------
1317
1317
1318 else:
1318 else:
1319 if shift_down:
1319 if shift_down:
1320 anchormode = QtGui.QTextCursor.KeepAnchor
1320 anchormode = QtGui.QTextCursor.KeepAnchor
1321 else:
1321 else:
1322 anchormode = QtGui.QTextCursor.MoveAnchor
1322 anchormode = QtGui.QTextCursor.MoveAnchor
1323
1323
1324 if key == QtCore.Qt.Key_Escape:
1324 if key == QtCore.Qt.Key_Escape:
1325 self._keyboard_quit()
1325 self._keyboard_quit()
1326 intercepted = True
1326 intercepted = True
1327
1327
1328 elif key == QtCore.Qt.Key_Up:
1328 elif key == QtCore.Qt.Key_Up:
1329 if self._reading or not self._up_pressed(shift_down):
1329 if self._reading or not self._up_pressed(shift_down):
1330 intercepted = True
1330 intercepted = True
1331 else:
1331 else:
1332 prompt_line = self._get_prompt_cursor().blockNumber()
1332 prompt_line = self._get_prompt_cursor().blockNumber()
1333 intercepted = cursor.blockNumber() <= prompt_line
1333 intercepted = cursor.blockNumber() <= prompt_line
1334
1334
1335 elif key == QtCore.Qt.Key_Down:
1335 elif key == QtCore.Qt.Key_Down:
1336 if self._reading or not self._down_pressed(shift_down):
1336 if self._reading or not self._down_pressed(shift_down):
1337 intercepted = True
1337 intercepted = True
1338 else:
1338 else:
1339 end_line = self._get_end_cursor().blockNumber()
1339 end_line = self._get_end_cursor().blockNumber()
1340 intercepted = cursor.blockNumber() == end_line
1340 intercepted = cursor.blockNumber() == end_line
1341
1341
1342 elif key == QtCore.Qt.Key_Tab:
1342 elif key == QtCore.Qt.Key_Tab:
1343 if not self._reading:
1343 if not self._reading:
1344 if self._tab_pressed():
1344 if self._tab_pressed():
1345 # real tab-key, insert four spaces
1345 # real tab-key, insert four spaces
1346 cursor.insertText(' '*4)
1346 cursor.insertText(' '*4)
1347 intercepted = True
1347 intercepted = True
1348
1348
1349 elif key == QtCore.Qt.Key_Left:
1349 elif key == QtCore.Qt.Key_Left:
1350
1350
1351 # Move to the previous line
1351 # Move to the previous line
1352 line, col = cursor.blockNumber(), cursor.columnNumber()
1352 line, col = cursor.blockNumber(), cursor.columnNumber()
1353 if line > self._get_prompt_cursor().blockNumber() and \
1353 if line > self._get_prompt_cursor().blockNumber() and \
1354 col == len(self._continuation_prompt):
1354 col == len(self._continuation_prompt):
1355 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1355 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1356 mode=anchormode)
1356 mode=anchormode)
1357 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1357 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1358 mode=anchormode)
1358 mode=anchormode)
1359 intercepted = True
1359 intercepted = True
1360
1360
1361 # Regular left movement
1361 # Regular left movement
1362 else:
1362 else:
1363 intercepted = not self._in_buffer(position - 1)
1363 intercepted = not self._in_buffer(position - 1)
1364
1364
1365 elif key == QtCore.Qt.Key_Right:
1365 elif key == QtCore.Qt.Key_Right:
1366 original_block_number = cursor.blockNumber()
1366 original_block_number = cursor.blockNumber()
1367 cursor.movePosition(QtGui.QTextCursor.Right,
1367 cursor.movePosition(QtGui.QTextCursor.Right,
1368 mode=anchormode)
1368 mode=anchormode)
1369 if cursor.blockNumber() != original_block_number:
1369 if cursor.blockNumber() != original_block_number:
1370 cursor.movePosition(QtGui.QTextCursor.Right,
1370 cursor.movePosition(QtGui.QTextCursor.Right,
1371 n=len(self._continuation_prompt),
1371 n=len(self._continuation_prompt),
1372 mode=anchormode)
1372 mode=anchormode)
1373 self._set_cursor(cursor)
1373 self._set_cursor(cursor)
1374 intercepted = True
1374 intercepted = True
1375
1375
1376 elif key == QtCore.Qt.Key_Home:
1376 elif key == QtCore.Qt.Key_Home:
1377 start_line = cursor.blockNumber()
1377 start_line = cursor.blockNumber()
1378 if start_line == self._get_prompt_cursor().blockNumber():
1378 if start_line == self._get_prompt_cursor().blockNumber():
1379 start_pos = self._prompt_pos
1379 start_pos = self._prompt_pos
1380 else:
1380 else:
1381 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1381 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1382 QtGui.QTextCursor.KeepAnchor)
1382 QtGui.QTextCursor.KeepAnchor)
1383 start_pos = cursor.position()
1383 start_pos = cursor.position()
1384 start_pos += len(self._continuation_prompt)
1384 start_pos += len(self._continuation_prompt)
1385 cursor.setPosition(position)
1385 cursor.setPosition(position)
1386 if shift_down and self._in_buffer(position):
1386 if shift_down and self._in_buffer(position):
1387 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1387 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1388 else:
1388 else:
1389 cursor.setPosition(start_pos)
1389 cursor.setPosition(start_pos)
1390 self._set_cursor(cursor)
1390 self._set_cursor(cursor)
1391 intercepted = True
1391 intercepted = True
1392
1392
1393 elif key == QtCore.Qt.Key_Backspace:
1393 elif key == QtCore.Qt.Key_Backspace:
1394
1394
1395 # Line deletion (remove continuation prompt)
1395 # Line deletion (remove continuation prompt)
1396 line, col = cursor.blockNumber(), cursor.columnNumber()
1396 line, col = cursor.blockNumber(), cursor.columnNumber()
1397 if not self._reading and \
1397 if not self._reading and \
1398 col == len(self._continuation_prompt) and \
1398 col == len(self._continuation_prompt) and \
1399 line > self._get_prompt_cursor().blockNumber():
1399 line > self._get_prompt_cursor().blockNumber():
1400 cursor.beginEditBlock()
1400 cursor.beginEditBlock()
1401 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1401 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1402 QtGui.QTextCursor.KeepAnchor)
1402 QtGui.QTextCursor.KeepAnchor)
1403 cursor.removeSelectedText()
1403 cursor.removeSelectedText()
1404 cursor.deletePreviousChar()
1404 cursor.deletePreviousChar()
1405 cursor.endEditBlock()
1405 cursor.endEditBlock()
1406 intercepted = True
1406 intercepted = True
1407
1407
1408 # Regular backwards deletion
1408 # Regular backwards deletion
1409 else:
1409 else:
1410 anchor = cursor.anchor()
1410 anchor = cursor.anchor()
1411 if anchor == position:
1411 if anchor == position:
1412 intercepted = not self._in_buffer(position - 1)
1412 intercepted = not self._in_buffer(position - 1)
1413 else:
1413 else:
1414 intercepted = not self._in_buffer(min(anchor, position))
1414 intercepted = not self._in_buffer(min(anchor, position))
1415
1415
1416 elif key == QtCore.Qt.Key_Delete:
1416 elif key == QtCore.Qt.Key_Delete:
1417
1417
1418 # Line deletion (remove continuation prompt)
1418 # Line deletion (remove continuation prompt)
1419 if not self._reading and self._in_buffer(position) and \
1419 if not self._reading and self._in_buffer(position) and \
1420 cursor.atBlockEnd() and not cursor.hasSelection():
1420 cursor.atBlockEnd() and not cursor.hasSelection():
1421 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1421 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1422 QtGui.QTextCursor.KeepAnchor)
1422 QtGui.QTextCursor.KeepAnchor)
1423 cursor.movePosition(QtGui.QTextCursor.Right,
1423 cursor.movePosition(QtGui.QTextCursor.Right,
1424 QtGui.QTextCursor.KeepAnchor,
1424 QtGui.QTextCursor.KeepAnchor,
1425 len(self._continuation_prompt))
1425 len(self._continuation_prompt))
1426 cursor.removeSelectedText()
1426 cursor.removeSelectedText()
1427 intercepted = True
1427 intercepted = True
1428
1428
1429 # Regular forwards deletion:
1429 # Regular forwards deletion:
1430 else:
1430 else:
1431 anchor = cursor.anchor()
1431 anchor = cursor.anchor()
1432 intercepted = (not self._in_buffer(anchor) or
1432 intercepted = (not self._in_buffer(anchor) or
1433 not self._in_buffer(position))
1433 not self._in_buffer(position))
1434
1434
1435 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1435 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1436 # using the keyboard in any part of the buffer. Also, permit scrolling
1436 # using the keyboard in any part of the buffer. Also, permit scrolling
1437 # with Page Up/Down keys. Finally, if we're executing, don't move the
1437 # with Page Up/Down keys. Finally, if we're executing, don't move the
1438 # cursor (if even this made sense, we can't guarantee that the prompt
1438 # cursor (if even this made sense, we can't guarantee that the prompt
1439 # position is still valid due to text truncation).
1439 # position is still valid due to text truncation).
1440 if not (self._control_key_down(event.modifiers(), include_command=True)
1440 if not (self._control_key_down(event.modifiers(), include_command=True)
1441 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1441 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1442 or (self._executing and not self._reading)):
1442 or (self._executing and not self._reading)):
1443 self._keep_cursor_in_buffer()
1443 self._keep_cursor_in_buffer()
1444
1444
1445 return intercepted
1445 return intercepted
1446
1446
1447 def _event_filter_page_keypress(self, event):
1447 def _event_filter_page_keypress(self, event):
1448 """ Filter key events for the paging widget to create console-like
1448 """ Filter key events for the paging widget to create console-like
1449 interface.
1449 interface.
1450 """
1450 """
1451 key = event.key()
1451 key = event.key()
1452 ctrl_down = self._control_key_down(event.modifiers())
1452 ctrl_down = self._control_key_down(event.modifiers())
1453 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1453 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1454
1454
1455 if ctrl_down:
1455 if ctrl_down:
1456 if key == QtCore.Qt.Key_O:
1456 if key == QtCore.Qt.Key_O:
1457 self._control.setFocus()
1457 self._control.setFocus()
1458 intercept = True
1458 intercept = True
1459
1459
1460 elif alt_down:
1460 elif alt_down:
1461 if key == QtCore.Qt.Key_Greater:
1461 if key == QtCore.Qt.Key_Greater:
1462 self._page_control.moveCursor(QtGui.QTextCursor.End)
1462 self._page_control.moveCursor(QtGui.QTextCursor.End)
1463 intercepted = True
1463 intercepted = True
1464
1464
1465 elif key == QtCore.Qt.Key_Less:
1465 elif key == QtCore.Qt.Key_Less:
1466 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1466 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1467 intercepted = True
1467 intercepted = True
1468
1468
1469 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1469 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1470 if self._splitter:
1470 if self._splitter:
1471 self._page_control.hide()
1471 self._page_control.hide()
1472 self._control.setFocus()
1472 self._control.setFocus()
1473 else:
1473 else:
1474 self.layout().setCurrentWidget(self._control)
1474 self.layout().setCurrentWidget(self._control)
1475 return True
1475 return True
1476
1476
1477 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1477 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1478 QtCore.Qt.Key_Tab):
1478 QtCore.Qt.Key_Tab):
1479 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1479 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1480 QtCore.Qt.Key_PageDown,
1480 QtCore.Qt.Key_PageDown,
1481 QtCore.Qt.NoModifier)
1481 QtCore.Qt.NoModifier)
1482 QtGui.qApp.sendEvent(self._page_control, new_event)
1482 QtGui.qApp.sendEvent(self._page_control, new_event)
1483 return True
1483 return True
1484
1484
1485 elif key == QtCore.Qt.Key_Backspace:
1485 elif key == QtCore.Qt.Key_Backspace:
1486 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1486 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1487 QtCore.Qt.Key_PageUp,
1487 QtCore.Qt.Key_PageUp,
1488 QtCore.Qt.NoModifier)
1488 QtCore.Qt.NoModifier)
1489 QtGui.qApp.sendEvent(self._page_control, new_event)
1489 QtGui.qApp.sendEvent(self._page_control, new_event)
1490 return True
1490 return True
1491
1491
1492 return False
1492 return False
1493
1493
1494 def _on_flush_pending_stream_timer(self):
1494 def _on_flush_pending_stream_timer(self):
1495 """ Flush the pending stream output and change the
1495 """ Flush the pending stream output and change the
1496 prompt position appropriately.
1496 prompt position appropriately.
1497 """
1497 """
1498 cursor = self._control.textCursor()
1498 cursor = self._control.textCursor()
1499 cursor.movePosition(QtGui.QTextCursor.End)
1499 cursor.movePosition(QtGui.QTextCursor.End)
1500 pos = cursor.position()
1500 pos = cursor.position()
1501 self._flush_pending_stream()
1501 self._flush_pending_stream()
1502 cursor.movePosition(QtGui.QTextCursor.End)
1502 cursor.movePosition(QtGui.QTextCursor.End)
1503 diff = cursor.position() - pos
1503 diff = cursor.position() - pos
1504 if diff > 0:
1504 if diff > 0:
1505 self._prompt_pos += diff
1505 self._prompt_pos += diff
1506 self._append_before_prompt_pos += diff
1506 self._append_before_prompt_pos += diff
1507
1507
1508 def _flush_pending_stream(self):
1508 def _flush_pending_stream(self):
1509 """ Flush out pending text into the widget. """
1509 """ Flush out pending text into the widget. """
1510 text = self._pending_insert_text
1510 text = self._pending_insert_text
1511 self._pending_insert_text = []
1511 self._pending_insert_text = []
1512 buffer_size = self._control.document().maximumBlockCount()
1512 buffer_size = self._control.document().maximumBlockCount()
1513 if buffer_size > 0:
1513 if buffer_size > 0:
1514 text = self._get_last_lines_from_list(text, buffer_size)
1514 text = self._get_last_lines_from_list(text, buffer_size)
1515 text = ''.join(text)
1515 text = ''.join(text)
1516 t = time.time()
1516 t = time.time()
1517 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1517 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1518 # Set the flush interval to equal the maximum time to update text.
1518 # Set the flush interval to equal the maximum time to update text.
1519 self._pending_text_flush_interval.setInterval(max(100,
1519 self._pending_text_flush_interval.setInterval(max(100,
1520 (time.time()-t)*1000))
1520 (time.time()-t)*1000))
1521
1521
1522 def _format_as_columns(self, items, separator=' '):
1522 def _format_as_columns(self, items, separator=' '):
1523 """ Transform a list of strings into a single string with columns.
1523 """ Transform a list of strings into a single string with columns.
1524
1524
1525 Parameters
1525 Parameters
1526 ----------
1526 ----------
1527 items : sequence of strings
1527 items : sequence of strings
1528 The strings to process.
1528 The strings to process.
1529
1529
1530 separator : str, optional [default is two spaces]
1530 separator : str, optional [default is two spaces]
1531 The string that separates columns.
1531 The string that separates columns.
1532
1532
1533 Returns
1533 Returns
1534 -------
1534 -------
1535 The formatted string.
1535 The formatted string.
1536 """
1536 """
1537 # Calculate the number of characters available.
1537 # Calculate the number of characters available.
1538 width = self._control.viewport().width()
1538 width = self._control.viewport().width()
1539 char_width = QtGui.QFontMetrics(self.font).width(' ')
1539 char_width = QtGui.QFontMetrics(self.font).width(' ')
1540 displaywidth = max(10, (width / char_width) - 1)
1540 displaywidth = max(10, (width / char_width) - 1)
1541
1541
1542 return columnize(items, separator, displaywidth)
1542 return columnize(items, separator, displaywidth)
1543
1543
1544 def _get_block_plain_text(self, block):
1544 def _get_block_plain_text(self, block):
1545 """ Given a QTextBlock, return its unformatted text.
1545 """ Given a QTextBlock, return its unformatted text.
1546 """
1546 """
1547 cursor = QtGui.QTextCursor(block)
1547 cursor = QtGui.QTextCursor(block)
1548 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1548 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1549 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1549 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1550 QtGui.QTextCursor.KeepAnchor)
1550 QtGui.QTextCursor.KeepAnchor)
1551 return cursor.selection().toPlainText()
1551 return cursor.selection().toPlainText()
1552
1552
1553 def _get_cursor(self):
1553 def _get_cursor(self):
1554 """ Convenience method that returns a cursor for the current position.
1554 """ Convenience method that returns a cursor for the current position.
1555 """
1555 """
1556 return self._control.textCursor()
1556 return self._control.textCursor()
1557
1557
1558 def _get_end_cursor(self):
1558 def _get_end_cursor(self):
1559 """ Convenience method that returns a cursor for the last character.
1559 """ Convenience method that returns a cursor for the last character.
1560 """
1560 """
1561 cursor = self._control.textCursor()
1561 cursor = self._control.textCursor()
1562 cursor.movePosition(QtGui.QTextCursor.End)
1562 cursor.movePosition(QtGui.QTextCursor.End)
1563 return cursor
1563 return cursor
1564
1564
1565 def _get_input_buffer_cursor_column(self):
1565 def _get_input_buffer_cursor_column(self):
1566 """ Returns the column of the cursor in the input buffer, excluding the
1566 """ Returns the column of the cursor in the input buffer, excluding the
1567 contribution by the prompt, or -1 if there is no such column.
1567 contribution by the prompt, or -1 if there is no such column.
1568 """
1568 """
1569 prompt = self._get_input_buffer_cursor_prompt()
1569 prompt = self._get_input_buffer_cursor_prompt()
1570 if prompt is None:
1570 if prompt is None:
1571 return -1
1571 return -1
1572 else:
1572 else:
1573 cursor = self._control.textCursor()
1573 cursor = self._control.textCursor()
1574 return cursor.columnNumber() - len(prompt)
1574 return cursor.columnNumber() - len(prompt)
1575
1575
1576 def _get_input_buffer_cursor_line(self):
1576 def _get_input_buffer_cursor_line(self):
1577 """ Returns the text of the line of the input buffer that contains the
1577 """ Returns the text of the line of the input buffer that contains the
1578 cursor, or None if there is no such line.
1578 cursor, or None if there is no such line.
1579 """
1579 """
1580 prompt = self._get_input_buffer_cursor_prompt()
1580 prompt = self._get_input_buffer_cursor_prompt()
1581 if prompt is None:
1581 if prompt is None:
1582 return None
1582 return None
1583 else:
1583 else:
1584 cursor = self._control.textCursor()
1584 cursor = self._control.textCursor()
1585 text = self._get_block_plain_text(cursor.block())
1585 text = self._get_block_plain_text(cursor.block())
1586 return text[len(prompt):]
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 def _get_input_buffer_cursor_prompt(self):
1597 def _get_input_buffer_cursor_prompt(self):
1589 """ Returns the (plain text) prompt for line of the input buffer that
1598 """ Returns the (plain text) prompt for line of the input buffer that
1590 contains the cursor, or None if there is no such line.
1599 contains the cursor, or None if there is no such line.
1591 """
1600 """
1592 if self._executing:
1601 if self._executing:
1593 return None
1602 return None
1594 cursor = self._control.textCursor()
1603 cursor = self._control.textCursor()
1595 if cursor.position() >= self._prompt_pos:
1604 if cursor.position() >= self._prompt_pos:
1596 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1605 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1597 return self._prompt
1606 return self._prompt
1598 else:
1607 else:
1599 return self._continuation_prompt
1608 return self._continuation_prompt
1600 else:
1609 else:
1601 return None
1610 return None
1602
1611
1603 def _get_last_lines(self, text, num_lines, return_count=False):
1612 def _get_last_lines(self, text, num_lines, return_count=False):
1604 """ Return last specified number of lines of text (like `tail -n`).
1613 """ Return last specified number of lines of text (like `tail -n`).
1605 If return_count is True, returns a tuple of clipped text and the
1614 If return_count is True, returns a tuple of clipped text and the
1606 number of lines in the clipped text.
1615 number of lines in the clipped text.
1607 """
1616 """
1608 pos = len(text)
1617 pos = len(text)
1609 if pos < num_lines:
1618 if pos < num_lines:
1610 if return_count:
1619 if return_count:
1611 return text, text.count('\n') if return_count else text
1620 return text, text.count('\n') if return_count else text
1612 else:
1621 else:
1613 return text
1622 return text
1614 i = 0
1623 i = 0
1615 while i < num_lines:
1624 while i < num_lines:
1616 pos = text.rfind('\n', None, pos)
1625 pos = text.rfind('\n', None, pos)
1617 if pos == -1:
1626 if pos == -1:
1618 pos = None
1627 pos = None
1619 break
1628 break
1620 i += 1
1629 i += 1
1621 if return_count:
1630 if return_count:
1622 return text[pos:], i
1631 return text[pos:], i
1623 else:
1632 else:
1624 return text[pos:]
1633 return text[pos:]
1625
1634
1626 def _get_last_lines_from_list(self, text_list, num_lines):
1635 def _get_last_lines_from_list(self, text_list, num_lines):
1627 """ Return the list of text clipped to last specified lines.
1636 """ Return the list of text clipped to last specified lines.
1628 """
1637 """
1629 ret = []
1638 ret = []
1630 lines_pending = num_lines
1639 lines_pending = num_lines
1631 for text in reversed(text_list):
1640 for text in reversed(text_list):
1632 text, lines_added = self._get_last_lines(text, lines_pending,
1641 text, lines_added = self._get_last_lines(text, lines_pending,
1633 return_count=True)
1642 return_count=True)
1634 ret.append(text)
1643 ret.append(text)
1635 lines_pending -= lines_added
1644 lines_pending -= lines_added
1636 if lines_pending <= 0:
1645 if lines_pending <= 0:
1637 break
1646 break
1638 return ret[::-1]
1647 return ret[::-1]
1639
1648
1640 def _get_prompt_cursor(self):
1649 def _get_prompt_cursor(self):
1641 """ Convenience method that returns a cursor for the prompt position.
1650 """ Convenience method that returns a cursor for the prompt position.
1642 """
1651 """
1643 cursor = self._control.textCursor()
1652 cursor = self._control.textCursor()
1644 cursor.setPosition(self._prompt_pos)
1653 cursor.setPosition(self._prompt_pos)
1645 return cursor
1654 return cursor
1646
1655
1647 def _get_selection_cursor(self, start, end):
1656 def _get_selection_cursor(self, start, end):
1648 """ Convenience method that returns a cursor with text selected between
1657 """ Convenience method that returns a cursor with text selected between
1649 the positions 'start' and 'end'.
1658 the positions 'start' and 'end'.
1650 """
1659 """
1651 cursor = self._control.textCursor()
1660 cursor = self._control.textCursor()
1652 cursor.setPosition(start)
1661 cursor.setPosition(start)
1653 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1662 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1654 return cursor
1663 return cursor
1655
1664
1656 def _get_word_start_cursor(self, position):
1665 def _get_word_start_cursor(self, position):
1657 """ Find the start of the word to the left the given position. If a
1666 """ Find the start of the word to the left the given position. If a
1658 sequence of non-word characters precedes the first word, skip over
1667 sequence of non-word characters precedes the first word, skip over
1659 them. (This emulates the behavior of bash, emacs, etc.)
1668 them. (This emulates the behavior of bash, emacs, etc.)
1660 """
1669 """
1661 document = self._control.document()
1670 document = self._control.document()
1662 position -= 1
1671 position -= 1
1663 while position >= self._prompt_pos and \
1672 while position >= self._prompt_pos and \
1664 not is_letter_or_number(document.characterAt(position)):
1673 not is_letter_or_number(document.characterAt(position)):
1665 position -= 1
1674 position -= 1
1666 while position >= self._prompt_pos and \
1675 while position >= self._prompt_pos and \
1667 is_letter_or_number(document.characterAt(position)):
1676 is_letter_or_number(document.characterAt(position)):
1668 position -= 1
1677 position -= 1
1669 cursor = self._control.textCursor()
1678 cursor = self._control.textCursor()
1670 cursor.setPosition(position + 1)
1679 cursor.setPosition(position + 1)
1671 return cursor
1680 return cursor
1672
1681
1673 def _get_word_end_cursor(self, position):
1682 def _get_word_end_cursor(self, position):
1674 """ Find the end of the word to the right the given position. If a
1683 """ Find the end of the word to the right the given position. If a
1675 sequence of non-word characters precedes the first word, skip over
1684 sequence of non-word characters precedes the first word, skip over
1676 them. (This emulates the behavior of bash, emacs, etc.)
1685 them. (This emulates the behavior of bash, emacs, etc.)
1677 """
1686 """
1678 document = self._control.document()
1687 document = self._control.document()
1679 end = self._get_end_cursor().position()
1688 end = self._get_end_cursor().position()
1680 while position < end and \
1689 while position < end and \
1681 not is_letter_or_number(document.characterAt(position)):
1690 not is_letter_or_number(document.characterAt(position)):
1682 position += 1
1691 position += 1
1683 while position < end and \
1692 while position < end and \
1684 is_letter_or_number(document.characterAt(position)):
1693 is_letter_or_number(document.characterAt(position)):
1685 position += 1
1694 position += 1
1686 cursor = self._control.textCursor()
1695 cursor = self._control.textCursor()
1687 cursor.setPosition(position)
1696 cursor.setPosition(position)
1688 return cursor
1697 return cursor
1689
1698
1690 def _insert_continuation_prompt(self, cursor):
1699 def _insert_continuation_prompt(self, cursor):
1691 """ Inserts new continuation prompt using the specified cursor.
1700 """ Inserts new continuation prompt using the specified cursor.
1692 """
1701 """
1693 if self._continuation_prompt_html is None:
1702 if self._continuation_prompt_html is None:
1694 self._insert_plain_text(cursor, self._continuation_prompt)
1703 self._insert_plain_text(cursor, self._continuation_prompt)
1695 else:
1704 else:
1696 self._continuation_prompt = self._insert_html_fetching_plain_text(
1705 self._continuation_prompt = self._insert_html_fetching_plain_text(
1697 cursor, self._continuation_prompt_html)
1706 cursor, self._continuation_prompt_html)
1698
1707
1699 def _insert_block(self, cursor, block_format=None):
1708 def _insert_block(self, cursor, block_format=None):
1700 """ Inserts an empty QTextBlock using the specified cursor.
1709 """ Inserts an empty QTextBlock using the specified cursor.
1701 """
1710 """
1702 if block_format is None:
1711 if block_format is None:
1703 block_format = QtGui.QTextBlockFormat()
1712 block_format = QtGui.QTextBlockFormat()
1704 cursor.insertBlock(block_format)
1713 cursor.insertBlock(block_format)
1705
1714
1706 def _insert_html(self, cursor, html):
1715 def _insert_html(self, cursor, html):
1707 """ Inserts HTML using the specified cursor in such a way that future
1716 """ Inserts HTML using the specified cursor in such a way that future
1708 formatting is unaffected.
1717 formatting is unaffected.
1709 """
1718 """
1710 cursor.beginEditBlock()
1719 cursor.beginEditBlock()
1711 cursor.insertHtml(html)
1720 cursor.insertHtml(html)
1712
1721
1713 # After inserting HTML, the text document "remembers" it's in "html
1722 # After inserting HTML, the text document "remembers" it's in "html
1714 # mode", which means that subsequent calls adding plain text will result
1723 # mode", which means that subsequent calls adding plain text will result
1715 # in unwanted formatting, lost tab characters, etc. The following code
1724 # in unwanted formatting, lost tab characters, etc. The following code
1716 # hacks around this behavior, which I consider to be a bug in Qt, by
1725 # hacks around this behavior, which I consider to be a bug in Qt, by
1717 # (crudely) resetting the document's style state.
1726 # (crudely) resetting the document's style state.
1718 cursor.movePosition(QtGui.QTextCursor.Left,
1727 cursor.movePosition(QtGui.QTextCursor.Left,
1719 QtGui.QTextCursor.KeepAnchor)
1728 QtGui.QTextCursor.KeepAnchor)
1720 if cursor.selection().toPlainText() == ' ':
1729 if cursor.selection().toPlainText() == ' ':
1721 cursor.removeSelectedText()
1730 cursor.removeSelectedText()
1722 else:
1731 else:
1723 cursor.movePosition(QtGui.QTextCursor.Right)
1732 cursor.movePosition(QtGui.QTextCursor.Right)
1724 cursor.insertText(' ', QtGui.QTextCharFormat())
1733 cursor.insertText(' ', QtGui.QTextCharFormat())
1725 cursor.endEditBlock()
1734 cursor.endEditBlock()
1726
1735
1727 def _insert_html_fetching_plain_text(self, cursor, html):
1736 def _insert_html_fetching_plain_text(self, cursor, html):
1728 """ Inserts HTML using the specified cursor, then returns its plain text
1737 """ Inserts HTML using the specified cursor, then returns its plain text
1729 version.
1738 version.
1730 """
1739 """
1731 cursor.beginEditBlock()
1740 cursor.beginEditBlock()
1732 cursor.removeSelectedText()
1741 cursor.removeSelectedText()
1733
1742
1734 start = cursor.position()
1743 start = cursor.position()
1735 self._insert_html(cursor, html)
1744 self._insert_html(cursor, html)
1736 end = cursor.position()
1745 end = cursor.position()
1737 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1746 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1738 text = cursor.selection().toPlainText()
1747 text = cursor.selection().toPlainText()
1739
1748
1740 cursor.setPosition(end)
1749 cursor.setPosition(end)
1741 cursor.endEditBlock()
1750 cursor.endEditBlock()
1742 return text
1751 return text
1743
1752
1744 def _insert_plain_text(self, cursor, text, flush=False):
1753 def _insert_plain_text(self, cursor, text, flush=False):
1745 """ Inserts plain text using the specified cursor, processing ANSI codes
1754 """ Inserts plain text using the specified cursor, processing ANSI codes
1746 if enabled.
1755 if enabled.
1747 """
1756 """
1748 # maximumBlockCount() can be different from self.buffer_size in
1757 # maximumBlockCount() can be different from self.buffer_size in
1749 # case input prompt is active.
1758 # case input prompt is active.
1750 buffer_size = self._control.document().maximumBlockCount()
1759 buffer_size = self._control.document().maximumBlockCount()
1751
1760
1752 if self._executing and not flush and \
1761 if self._executing and not flush and \
1753 self._pending_text_flush_interval.isActive():
1762 self._pending_text_flush_interval.isActive():
1754 self._pending_insert_text.append(text)
1763 self._pending_insert_text.append(text)
1755 if buffer_size > 0:
1764 if buffer_size > 0:
1756 self._pending_insert_text = self._get_last_lines_from_list(
1765 self._pending_insert_text = self._get_last_lines_from_list(
1757 self._pending_insert_text, buffer_size)
1766 self._pending_insert_text, buffer_size)
1758 return
1767 return
1759
1768
1760 if self._executing and not self._pending_text_flush_interval.isActive():
1769 if self._executing and not self._pending_text_flush_interval.isActive():
1761 self._pending_text_flush_interval.start()
1770 self._pending_text_flush_interval.start()
1762
1771
1763 # Clip the text to last `buffer_size` lines.
1772 # Clip the text to last `buffer_size` lines.
1764 if buffer_size > 0:
1773 if buffer_size > 0:
1765 text = self._get_last_lines(text, buffer_size)
1774 text = self._get_last_lines(text, buffer_size)
1766
1775
1767 cursor.beginEditBlock()
1776 cursor.beginEditBlock()
1768 if self.ansi_codes:
1777 if self.ansi_codes:
1769 for substring in self._ansi_processor.split_string(text):
1778 for substring in self._ansi_processor.split_string(text):
1770 for act in self._ansi_processor.actions:
1779 for act in self._ansi_processor.actions:
1771
1780
1772 # Unlike real terminal emulators, we don't distinguish
1781 # Unlike real terminal emulators, we don't distinguish
1773 # between the screen and the scrollback buffer. A screen
1782 # between the screen and the scrollback buffer. A screen
1774 # erase request clears everything.
1783 # erase request clears everything.
1775 if act.action == 'erase' and act.area == 'screen':
1784 if act.action == 'erase' and act.area == 'screen':
1776 cursor.select(QtGui.QTextCursor.Document)
1785 cursor.select(QtGui.QTextCursor.Document)
1777 cursor.removeSelectedText()
1786 cursor.removeSelectedText()
1778
1787
1779 # Simulate a form feed by scrolling just past the last line.
1788 # Simulate a form feed by scrolling just past the last line.
1780 elif act.action == 'scroll' and act.unit == 'page':
1789 elif act.action == 'scroll' and act.unit == 'page':
1781 cursor.insertText('\n')
1790 cursor.insertText('\n')
1782 cursor.endEditBlock()
1791 cursor.endEditBlock()
1783 self._set_top_cursor(cursor)
1792 self._set_top_cursor(cursor)
1784 cursor.joinPreviousEditBlock()
1793 cursor.joinPreviousEditBlock()
1785 cursor.deletePreviousChar()
1794 cursor.deletePreviousChar()
1786
1795
1787 elif act.action == 'carriage-return':
1796 elif act.action == 'carriage-return':
1788 cursor.movePosition(
1797 cursor.movePosition(
1789 cursor.StartOfLine, cursor.KeepAnchor)
1798 cursor.StartOfLine, cursor.KeepAnchor)
1790
1799
1791 elif act.action == 'beep':
1800 elif act.action == 'beep':
1792 QtGui.qApp.beep()
1801 QtGui.qApp.beep()
1793
1802
1794 elif act.action == 'backspace':
1803 elif act.action == 'backspace':
1795 if not cursor.atBlockStart():
1804 if not cursor.atBlockStart():
1796 cursor.movePosition(
1805 cursor.movePosition(
1797 cursor.PreviousCharacter, cursor.KeepAnchor)
1806 cursor.PreviousCharacter, cursor.KeepAnchor)
1798
1807
1799 elif act.action == 'newline':
1808 elif act.action == 'newline':
1800 cursor.movePosition(cursor.EndOfLine)
1809 cursor.movePosition(cursor.EndOfLine)
1801
1810
1802 format = self._ansi_processor.get_format()
1811 format = self._ansi_processor.get_format()
1803
1812
1804 selection = cursor.selectedText()
1813 selection = cursor.selectedText()
1805 if len(selection) == 0:
1814 if len(selection) == 0:
1806 cursor.insertText(substring, format)
1815 cursor.insertText(substring, format)
1807 elif substring is not None:
1816 elif substring is not None:
1808 # BS and CR are treated as a change in print
1817 # BS and CR are treated as a change in print
1809 # position, rather than a backwards character
1818 # position, rather than a backwards character
1810 # deletion for output equivalence with (I)Python
1819 # deletion for output equivalence with (I)Python
1811 # terminal.
1820 # terminal.
1812 if len(substring) >= len(selection):
1821 if len(substring) >= len(selection):
1813 cursor.insertText(substring, format)
1822 cursor.insertText(substring, format)
1814 else:
1823 else:
1815 old_text = selection[len(substring):]
1824 old_text = selection[len(substring):]
1816 cursor.insertText(substring + old_text, format)
1825 cursor.insertText(substring + old_text, format)
1817 cursor.movePosition(cursor.PreviousCharacter,
1826 cursor.movePosition(cursor.PreviousCharacter,
1818 cursor.KeepAnchor, len(old_text))
1827 cursor.KeepAnchor, len(old_text))
1819 else:
1828 else:
1820 cursor.insertText(text)
1829 cursor.insertText(text)
1821 cursor.endEditBlock()
1830 cursor.endEditBlock()
1822
1831
1823 def _insert_plain_text_into_buffer(self, cursor, text):
1832 def _insert_plain_text_into_buffer(self, cursor, text):
1824 """ Inserts text into the input buffer using the specified cursor (which
1833 """ Inserts text into the input buffer using the specified cursor (which
1825 must be in the input buffer), ensuring that continuation prompts are
1834 must be in the input buffer), ensuring that continuation prompts are
1826 inserted as necessary.
1835 inserted as necessary.
1827 """
1836 """
1828 lines = text.splitlines(True)
1837 lines = text.splitlines(True)
1829 if lines:
1838 if lines:
1830 cursor.beginEditBlock()
1839 cursor.beginEditBlock()
1831 cursor.insertText(lines[0])
1840 cursor.insertText(lines[0])
1832 for line in lines[1:]:
1841 for line in lines[1:]:
1833 if self._continuation_prompt_html is None:
1842 if self._continuation_prompt_html is None:
1834 cursor.insertText(self._continuation_prompt)
1843 cursor.insertText(self._continuation_prompt)
1835 else:
1844 else:
1836 self._continuation_prompt = \
1845 self._continuation_prompt = \
1837 self._insert_html_fetching_plain_text(
1846 self._insert_html_fetching_plain_text(
1838 cursor, self._continuation_prompt_html)
1847 cursor, self._continuation_prompt_html)
1839 cursor.insertText(line)
1848 cursor.insertText(line)
1840 cursor.endEditBlock()
1849 cursor.endEditBlock()
1841
1850
1842 def _in_buffer(self, position=None):
1851 def _in_buffer(self, position=None):
1843 """ Returns whether the current cursor (or, if specified, a position) is
1852 """ Returns whether the current cursor (or, if specified, a position) is
1844 inside the editing region.
1853 inside the editing region.
1845 """
1854 """
1846 cursor = self._control.textCursor()
1855 cursor = self._control.textCursor()
1847 if position is None:
1856 if position is None:
1848 position = cursor.position()
1857 position = cursor.position()
1849 else:
1858 else:
1850 cursor.setPosition(position)
1859 cursor.setPosition(position)
1851 line = cursor.blockNumber()
1860 line = cursor.blockNumber()
1852 prompt_line = self._get_prompt_cursor().blockNumber()
1861 prompt_line = self._get_prompt_cursor().blockNumber()
1853 if line == prompt_line:
1862 if line == prompt_line:
1854 return position >= self._prompt_pos
1863 return position >= self._prompt_pos
1855 elif line > prompt_line:
1864 elif line > prompt_line:
1856 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1865 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1857 prompt_pos = cursor.position() + len(self._continuation_prompt)
1866 prompt_pos = cursor.position() + len(self._continuation_prompt)
1858 return position >= prompt_pos
1867 return position >= prompt_pos
1859 return False
1868 return False
1860
1869
1861 def _keep_cursor_in_buffer(self):
1870 def _keep_cursor_in_buffer(self):
1862 """ Ensures that the cursor is inside the editing region. Returns
1871 """ Ensures that the cursor is inside the editing region. Returns
1863 whether the cursor was moved.
1872 whether the cursor was moved.
1864 """
1873 """
1865 moved = not self._in_buffer()
1874 moved = not self._in_buffer()
1866 if moved:
1875 if moved:
1867 cursor = self._control.textCursor()
1876 cursor = self._control.textCursor()
1868 cursor.movePosition(QtGui.QTextCursor.End)
1877 cursor.movePosition(QtGui.QTextCursor.End)
1869 self._control.setTextCursor(cursor)
1878 self._control.setTextCursor(cursor)
1870 return moved
1879 return moved
1871
1880
1872 def _keyboard_quit(self):
1881 def _keyboard_quit(self):
1873 """ Cancels the current editing task ala Ctrl-G in Emacs.
1882 """ Cancels the current editing task ala Ctrl-G in Emacs.
1874 """
1883 """
1875 if self._temp_buffer_filled :
1884 if self._temp_buffer_filled :
1876 self._cancel_completion()
1885 self._cancel_completion()
1877 self._clear_temporary_buffer()
1886 self._clear_temporary_buffer()
1878 else:
1887 else:
1879 self.input_buffer = ''
1888 self.input_buffer = ''
1880
1889
1881 def _page(self, text, html=False):
1890 def _page(self, text, html=False):
1882 """ Displays text using the pager if it exceeds the height of the
1891 """ Displays text using the pager if it exceeds the height of the
1883 viewport.
1892 viewport.
1884
1893
1885 Parameters
1894 Parameters
1886 ----------
1895 ----------
1887 html : bool, optional (default False)
1896 html : bool, optional (default False)
1888 If set, the text will be interpreted as HTML instead of plain text.
1897 If set, the text will be interpreted as HTML instead of plain text.
1889 """
1898 """
1890 line_height = QtGui.QFontMetrics(self.font).height()
1899 line_height = QtGui.QFontMetrics(self.font).height()
1891 minlines = self._control.viewport().height() / line_height
1900 minlines = self._control.viewport().height() / line_height
1892 if self.paging != 'none' and \
1901 if self.paging != 'none' and \
1893 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1902 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1894 if self.paging == 'custom':
1903 if self.paging == 'custom':
1895 self.custom_page_requested.emit(text)
1904 self.custom_page_requested.emit(text)
1896 else:
1905 else:
1897 self._page_control.clear()
1906 self._page_control.clear()
1898 cursor = self._page_control.textCursor()
1907 cursor = self._page_control.textCursor()
1899 if html:
1908 if html:
1900 self._insert_html(cursor, text)
1909 self._insert_html(cursor, text)
1901 else:
1910 else:
1902 self._insert_plain_text(cursor, text)
1911 self._insert_plain_text(cursor, text)
1903 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1912 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1904
1913
1905 self._page_control.viewport().resize(self._control.size())
1914 self._page_control.viewport().resize(self._control.size())
1906 if self._splitter:
1915 if self._splitter:
1907 self._page_control.show()
1916 self._page_control.show()
1908 self._page_control.setFocus()
1917 self._page_control.setFocus()
1909 else:
1918 else:
1910 self.layout().setCurrentWidget(self._page_control)
1919 self.layout().setCurrentWidget(self._page_control)
1911 elif html:
1920 elif html:
1912 self._append_html(text)
1921 self._append_html(text)
1913 else:
1922 else:
1914 self._append_plain_text(text)
1923 self._append_plain_text(text)
1915
1924
1916 def _set_paging(self, paging):
1925 def _set_paging(self, paging):
1917 """
1926 """
1918 Change the pager to `paging` style.
1927 Change the pager to `paging` style.
1919
1928
1920 Parameters
1929 Parameters
1921 ----------
1930 ----------
1922 paging : string
1931 paging : string
1923 Either "hsplit", "vsplit", or "inside"
1932 Either "hsplit", "vsplit", or "inside"
1924 """
1933 """
1925 if self._splitter is None:
1934 if self._splitter is None:
1926 raise NotImplementedError("""can only switch if --paging=hsplit or
1935 raise NotImplementedError("""can only switch if --paging=hsplit or
1927 --paging=vsplit is used.""")
1936 --paging=vsplit is used.""")
1928 if paging == 'hsplit':
1937 if paging == 'hsplit':
1929 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1938 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1930 elif paging == 'vsplit':
1939 elif paging == 'vsplit':
1931 self._splitter.setOrientation(QtCore.Qt.Vertical)
1940 self._splitter.setOrientation(QtCore.Qt.Vertical)
1932 elif paging == 'inside':
1941 elif paging == 'inside':
1933 raise NotImplementedError("""switching to 'inside' paging not
1942 raise NotImplementedError("""switching to 'inside' paging not
1934 supported yet.""")
1943 supported yet.""")
1935 else:
1944 else:
1936 raise ValueError("unknown paging method '%s'" % paging)
1945 raise ValueError("unknown paging method '%s'" % paging)
1937 self.paging = paging
1946 self.paging = paging
1938
1947
1939 def _prompt_finished(self):
1948 def _prompt_finished(self):
1940 """ Called immediately after a prompt is finished, i.e. when some input
1949 """ Called immediately after a prompt is finished, i.e. when some input
1941 will be processed and a new prompt displayed.
1950 will be processed and a new prompt displayed.
1942 """
1951 """
1943 self._control.setReadOnly(True)
1952 self._control.setReadOnly(True)
1944 self._prompt_finished_hook()
1953 self._prompt_finished_hook()
1945
1954
1946 def _prompt_started(self):
1955 def _prompt_started(self):
1947 """ Called immediately after a new prompt is displayed.
1956 """ Called immediately after a new prompt is displayed.
1948 """
1957 """
1949 # Temporarily disable the maximum block count to permit undo/redo and
1958 # Temporarily disable the maximum block count to permit undo/redo and
1950 # to ensure that the prompt position does not change due to truncation.
1959 # to ensure that the prompt position does not change due to truncation.
1951 self._control.document().setMaximumBlockCount(0)
1960 self._control.document().setMaximumBlockCount(0)
1952 self._control.setUndoRedoEnabled(True)
1961 self._control.setUndoRedoEnabled(True)
1953
1962
1954 # Work around bug in QPlainTextEdit: input method is not re-enabled
1963 # Work around bug in QPlainTextEdit: input method is not re-enabled
1955 # when read-only is disabled.
1964 # when read-only is disabled.
1956 self._control.setReadOnly(False)
1965 self._control.setReadOnly(False)
1957 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1966 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1958
1967
1959 if not self._reading:
1968 if not self._reading:
1960 self._executing = False
1969 self._executing = False
1961 self._prompt_started_hook()
1970 self._prompt_started_hook()
1962
1971
1963 # If the input buffer has changed while executing, load it.
1972 # If the input buffer has changed while executing, load it.
1964 if self._input_buffer_pending:
1973 if self._input_buffer_pending:
1965 self.input_buffer = self._input_buffer_pending
1974 self.input_buffer = self._input_buffer_pending
1966 self._input_buffer_pending = ''
1975 self._input_buffer_pending = ''
1967
1976
1968 self._control.moveCursor(QtGui.QTextCursor.End)
1977 self._control.moveCursor(QtGui.QTextCursor.End)
1969
1978
1970 def _readline(self, prompt='', callback=None):
1979 def _readline(self, prompt='', callback=None):
1971 """ Reads one line of input from the user.
1980 """ Reads one line of input from the user.
1972
1981
1973 Parameters
1982 Parameters
1974 ----------
1983 ----------
1975 prompt : str, optional
1984 prompt : str, optional
1976 The prompt to print before reading the line.
1985 The prompt to print before reading the line.
1977
1986
1978 callback : callable, optional
1987 callback : callable, optional
1979 A callback to execute with the read line. If not specified, input is
1988 A callback to execute with the read line. If not specified, input is
1980 read *synchronously* and this method does not return until it has
1989 read *synchronously* and this method does not return until it has
1981 been read.
1990 been read.
1982
1991
1983 Returns
1992 Returns
1984 -------
1993 -------
1985 If a callback is specified, returns nothing. Otherwise, returns the
1994 If a callback is specified, returns nothing. Otherwise, returns the
1986 input string with the trailing newline stripped.
1995 input string with the trailing newline stripped.
1987 """
1996 """
1988 if self._reading:
1997 if self._reading:
1989 raise RuntimeError('Cannot read a line. Widget is already reading.')
1998 raise RuntimeError('Cannot read a line. Widget is already reading.')
1990
1999
1991 if not callback and not self.isVisible():
2000 if not callback and not self.isVisible():
1992 # If the user cannot see the widget, this function cannot return.
2001 # If the user cannot see the widget, this function cannot return.
1993 raise RuntimeError('Cannot synchronously read a line if the widget '
2002 raise RuntimeError('Cannot synchronously read a line if the widget '
1994 'is not visible!')
2003 'is not visible!')
1995
2004
1996 self._reading = True
2005 self._reading = True
1997 self._show_prompt(prompt, newline=False)
2006 self._show_prompt(prompt, newline=False)
1998
2007
1999 if callback is None:
2008 if callback is None:
2000 self._reading_callback = None
2009 self._reading_callback = None
2001 while self._reading:
2010 while self._reading:
2002 QtCore.QCoreApplication.processEvents()
2011 QtCore.QCoreApplication.processEvents()
2003 return self._get_input_buffer(force=True).rstrip('\n')
2012 return self._get_input_buffer(force=True).rstrip('\n')
2004
2013
2005 else:
2014 else:
2006 self._reading_callback = lambda: \
2015 self._reading_callback = lambda: \
2007 callback(self._get_input_buffer(force=True).rstrip('\n'))
2016 callback(self._get_input_buffer(force=True).rstrip('\n'))
2008
2017
2009 def _set_continuation_prompt(self, prompt, html=False):
2018 def _set_continuation_prompt(self, prompt, html=False):
2010 """ Sets the continuation prompt.
2019 """ Sets the continuation prompt.
2011
2020
2012 Parameters
2021 Parameters
2013 ----------
2022 ----------
2014 prompt : str
2023 prompt : str
2015 The prompt to show when more input is needed.
2024 The prompt to show when more input is needed.
2016
2025
2017 html : bool, optional (default False)
2026 html : bool, optional (default False)
2018 If set, the prompt will be inserted as formatted HTML. Otherwise,
2027 If set, the prompt will be inserted as formatted HTML. Otherwise,
2019 the prompt will be treated as plain text, though ANSI color codes
2028 the prompt will be treated as plain text, though ANSI color codes
2020 will be handled.
2029 will be handled.
2021 """
2030 """
2022 if html:
2031 if html:
2023 self._continuation_prompt_html = prompt
2032 self._continuation_prompt_html = prompt
2024 else:
2033 else:
2025 self._continuation_prompt = prompt
2034 self._continuation_prompt = prompt
2026 self._continuation_prompt_html = None
2035 self._continuation_prompt_html = None
2027
2036
2028 def _set_cursor(self, cursor):
2037 def _set_cursor(self, cursor):
2029 """ Convenience method to set the current cursor.
2038 """ Convenience method to set the current cursor.
2030 """
2039 """
2031 self._control.setTextCursor(cursor)
2040 self._control.setTextCursor(cursor)
2032
2041
2033 def _set_top_cursor(self, cursor):
2042 def _set_top_cursor(self, cursor):
2034 """ Scrolls the viewport so that the specified cursor is at the top.
2043 """ Scrolls the viewport so that the specified cursor is at the top.
2035 """
2044 """
2036 scrollbar = self._control.verticalScrollBar()
2045 scrollbar = self._control.verticalScrollBar()
2037 scrollbar.setValue(scrollbar.maximum())
2046 scrollbar.setValue(scrollbar.maximum())
2038 original_cursor = self._control.textCursor()
2047 original_cursor = self._control.textCursor()
2039 self._control.setTextCursor(cursor)
2048 self._control.setTextCursor(cursor)
2040 self._control.ensureCursorVisible()
2049 self._control.ensureCursorVisible()
2041 self._control.setTextCursor(original_cursor)
2050 self._control.setTextCursor(original_cursor)
2042
2051
2043 def _show_prompt(self, prompt=None, html=False, newline=True):
2052 def _show_prompt(self, prompt=None, html=False, newline=True):
2044 """ Writes a new prompt at the end of the buffer.
2053 """ Writes a new prompt at the end of the buffer.
2045
2054
2046 Parameters
2055 Parameters
2047 ----------
2056 ----------
2048 prompt : str, optional
2057 prompt : str, optional
2049 The prompt to show. If not specified, the previous prompt is used.
2058 The prompt to show. If not specified, the previous prompt is used.
2050
2059
2051 html : bool, optional (default False)
2060 html : bool, optional (default False)
2052 Only relevant when a prompt is specified. If set, the prompt will
2061 Only relevant when a prompt is specified. If set, the prompt will
2053 be inserted as formatted HTML. Otherwise, the prompt will be treated
2062 be inserted as formatted HTML. Otherwise, the prompt will be treated
2054 as plain text, though ANSI color codes will be handled.
2063 as plain text, though ANSI color codes will be handled.
2055
2064
2056 newline : bool, optional (default True)
2065 newline : bool, optional (default True)
2057 If set, a new line will be written before showing the prompt if
2066 If set, a new line will be written before showing the prompt if
2058 there is not already a newline at the end of the buffer.
2067 there is not already a newline at the end of the buffer.
2059 """
2068 """
2060 # Save the current end position to support _append*(before_prompt=True).
2069 # Save the current end position to support _append*(before_prompt=True).
2061 self._flush_pending_stream()
2070 self._flush_pending_stream()
2062 cursor = self._get_end_cursor()
2071 cursor = self._get_end_cursor()
2063 self._append_before_prompt_pos = cursor.position()
2072 self._append_before_prompt_pos = cursor.position()
2064
2073
2065 # Insert a preliminary newline, if necessary.
2074 # Insert a preliminary newline, if necessary.
2066 if newline and cursor.position() > 0:
2075 if newline and cursor.position() > 0:
2067 cursor.movePosition(QtGui.QTextCursor.Left,
2076 cursor.movePosition(QtGui.QTextCursor.Left,
2068 QtGui.QTextCursor.KeepAnchor)
2077 QtGui.QTextCursor.KeepAnchor)
2069 if cursor.selection().toPlainText() != '\n':
2078 if cursor.selection().toPlainText() != '\n':
2070 self._append_block()
2079 self._append_block()
2071 self._append_before_prompt_pos += 1
2080 self._append_before_prompt_pos += 1
2072
2081
2073 # Write the prompt.
2082 # Write the prompt.
2074 self._append_plain_text(self._prompt_sep)
2083 self._append_plain_text(self._prompt_sep)
2075 if prompt is None:
2084 if prompt is None:
2076 if self._prompt_html is None:
2085 if self._prompt_html is None:
2077 self._append_plain_text(self._prompt)
2086 self._append_plain_text(self._prompt)
2078 else:
2087 else:
2079 self._append_html(self._prompt_html)
2088 self._append_html(self._prompt_html)
2080 else:
2089 else:
2081 if html:
2090 if html:
2082 self._prompt = self._append_html_fetching_plain_text(prompt)
2091 self._prompt = self._append_html_fetching_plain_text(prompt)
2083 self._prompt_html = prompt
2092 self._prompt_html = prompt
2084 else:
2093 else:
2085 self._append_plain_text(prompt)
2094 self._append_plain_text(prompt)
2086 self._prompt = prompt
2095 self._prompt = prompt
2087 self._prompt_html = None
2096 self._prompt_html = None
2088
2097
2089 self._prompt_pos = self._get_end_cursor().position()
2098 self._prompt_pos = self._get_end_cursor().position()
2090 self._prompt_started()
2099 self._prompt_started()
2091
2100
2092 #------ Signal handlers ----------------------------------------------------
2101 #------ Signal handlers ----------------------------------------------------
2093
2102
2094 def _adjust_scrollbars(self):
2103 def _adjust_scrollbars(self):
2095 """ Expands the vertical scrollbar beyond the range set by Qt.
2104 """ Expands the vertical scrollbar beyond the range set by Qt.
2096 """
2105 """
2097 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2106 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2098 # and qtextedit.cpp.
2107 # and qtextedit.cpp.
2099 document = self._control.document()
2108 document = self._control.document()
2100 scrollbar = self._control.verticalScrollBar()
2109 scrollbar = self._control.verticalScrollBar()
2101 viewport_height = self._control.viewport().height()
2110 viewport_height = self._control.viewport().height()
2102 if isinstance(self._control, QtGui.QPlainTextEdit):
2111 if isinstance(self._control, QtGui.QPlainTextEdit):
2103 maximum = max(0, document.lineCount() - 1)
2112 maximum = max(0, document.lineCount() - 1)
2104 step = viewport_height / self._control.fontMetrics().lineSpacing()
2113 step = viewport_height / self._control.fontMetrics().lineSpacing()
2105 else:
2114 else:
2106 # QTextEdit does not do line-based layout and blocks will not in
2115 # QTextEdit does not do line-based layout and blocks will not in
2107 # general have the same height. Therefore it does not make sense to
2116 # general have the same height. Therefore it does not make sense to
2108 # attempt to scroll in line height increments.
2117 # attempt to scroll in line height increments.
2109 maximum = document.size().height()
2118 maximum = document.size().height()
2110 step = viewport_height
2119 step = viewport_height
2111 diff = maximum - scrollbar.maximum()
2120 diff = maximum - scrollbar.maximum()
2112 scrollbar.setRange(0, maximum)
2121 scrollbar.setRange(0, maximum)
2113 scrollbar.setPageStep(step)
2122 scrollbar.setPageStep(step)
2114
2123
2115 # Compensate for undesirable scrolling that occurs automatically due to
2124 # Compensate for undesirable scrolling that occurs automatically due to
2116 # maximumBlockCount() text truncation.
2125 # maximumBlockCount() text truncation.
2117 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2126 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2118 scrollbar.setValue(scrollbar.value() + diff)
2127 scrollbar.setValue(scrollbar.value() + diff)
2119
2128
2120 def _custom_context_menu_requested(self, pos):
2129 def _custom_context_menu_requested(self, pos):
2121 """ Shows a context menu at the given QPoint (in widget coordinates).
2130 """ Shows a context menu at the given QPoint (in widget coordinates).
2122 """
2131 """
2123 menu = self._context_menu_make(pos)
2132 menu = self._context_menu_make(pos)
2124 menu.exec_(self._control.mapToGlobal(pos))
2133 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,838 +1,830 b''
1 """Frontend widget for the Qt Console"""
1 """Frontend widget for the Qt Console"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import print_function
6 from __future__ import print_function
7
7
8 from collections import namedtuple
8 from collections import namedtuple
9 import sys
9 import sys
10 import uuid
10 import uuid
11
11
12 from IPython.external import qt
12 from IPython.external import qt
13 from IPython.external.qt import QtCore, QtGui
13 from IPython.external.qt import QtCore, QtGui
14 from IPython.utils import py3compat
14 from IPython.utils import py3compat
15 from IPython.utils.importstring import import_item
15 from IPython.utils.importstring import import_item
16
16
17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 from IPython.core.inputtransformer import classic_prompt
18 from IPython.core.inputtransformer import classic_prompt
19 from IPython.core.oinspect import call_tip
19 from IPython.core.oinspect import call_tip
20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 from .bracket_matcher import BracketMatcher
22 from .bracket_matcher import BracketMatcher
23 from .call_tip_widget import CallTipWidget
23 from .call_tip_widget import CallTipWidget
24 from .completion_lexer import CompletionLexer
24 from .completion_lexer import CompletionLexer
25 from .history_console_widget import HistoryConsoleWidget
25 from .history_console_widget import HistoryConsoleWidget
26 from .pygments_highlighter import PygmentsHighlighter
26 from .pygments_highlighter import PygmentsHighlighter
27
27
28
28
29 class FrontendHighlighter(PygmentsHighlighter):
29 class FrontendHighlighter(PygmentsHighlighter):
30 """ A PygmentsHighlighter that understands and ignores prompts.
30 """ A PygmentsHighlighter that understands and ignores prompts.
31 """
31 """
32
32
33 def __init__(self, frontend, lexer=None):
33 def __init__(self, frontend, lexer=None):
34 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
35 self._current_offset = 0
35 self._current_offset = 0
36 self._frontend = frontend
36 self._frontend = frontend
37 self.highlighting_on = False
37 self.highlighting_on = False
38
38
39 def highlightBlock(self, string):
39 def highlightBlock(self, string):
40 """ Highlight a block of text. Reimplemented to highlight selectively.
40 """ Highlight a block of text. Reimplemented to highlight selectively.
41 """
41 """
42 if not self.highlighting_on:
42 if not self.highlighting_on:
43 return
43 return
44
44
45 # The input to this function is a unicode string that may contain
45 # The input to this function is a unicode string that may contain
46 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 # paragraph break characters, non-breaking spaces, etc. Here we acquire
47 # the string as plain text so we can compare it.
47 # the string as plain text so we can compare it.
48 current_block = self.currentBlock()
48 current_block = self.currentBlock()
49 string = self._frontend._get_block_plain_text(current_block)
49 string = self._frontend._get_block_plain_text(current_block)
50
50
51 # Decide whether to check for the regular or continuation prompt.
51 # Decide whether to check for the regular or continuation prompt.
52 if current_block.contains(self._frontend._prompt_pos):
52 if current_block.contains(self._frontend._prompt_pos):
53 prompt = self._frontend._prompt
53 prompt = self._frontend._prompt
54 else:
54 else:
55 prompt = self._frontend._continuation_prompt
55 prompt = self._frontend._continuation_prompt
56
56
57 # Only highlight if we can identify a prompt, but make sure not to
57 # Only highlight if we can identify a prompt, but make sure not to
58 # highlight the prompt.
58 # highlight the prompt.
59 if string.startswith(prompt):
59 if string.startswith(prompt):
60 self._current_offset = len(prompt)
60 self._current_offset = len(prompt)
61 string = string[len(prompt):]
61 string = string[len(prompt):]
62 super(FrontendHighlighter, self).highlightBlock(string)
62 super(FrontendHighlighter, self).highlightBlock(string)
63
63
64 def rehighlightBlock(self, block):
64 def rehighlightBlock(self, block):
65 """ Reimplemented to temporarily enable highlighting if disabled.
65 """ Reimplemented to temporarily enable highlighting if disabled.
66 """
66 """
67 old = self.highlighting_on
67 old = self.highlighting_on
68 self.highlighting_on = True
68 self.highlighting_on = True
69 super(FrontendHighlighter, self).rehighlightBlock(block)
69 super(FrontendHighlighter, self).rehighlightBlock(block)
70 self.highlighting_on = old
70 self.highlighting_on = old
71
71
72 def setFormat(self, start, count, format):
72 def setFormat(self, start, count, format):
73 """ Reimplemented to highlight selectively.
73 """ Reimplemented to highlight selectively.
74 """
74 """
75 start += self._current_offset
75 start += self._current_offset
76 super(FrontendHighlighter, self).setFormat(start, count, format)
76 super(FrontendHighlighter, self).setFormat(start, count, format)
77
77
78
78
79 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
80 """ A Qt frontend for a generic Python kernel.
80 """ A Qt frontend for a generic Python kernel.
81 """
81 """
82
82
83 # The text to show when the kernel is (re)started.
83 # The text to show when the kernel is (re)started.
84 banner = Unicode(config=True)
84 banner = Unicode(config=True)
85
85
86 # An option and corresponding signal for overriding the default kernel
86 # An option and corresponding signal for overriding the default kernel
87 # interrupt behavior.
87 # interrupt behavior.
88 custom_interrupt = Bool(False)
88 custom_interrupt = Bool(False)
89 custom_interrupt_requested = QtCore.Signal()
89 custom_interrupt_requested = QtCore.Signal()
90
90
91 # An option and corresponding signals for overriding the default kernel
91 # An option and corresponding signals for overriding the default kernel
92 # restart behavior.
92 # restart behavior.
93 custom_restart = Bool(False)
93 custom_restart = Bool(False)
94 custom_restart_kernel_died = QtCore.Signal(float)
94 custom_restart_kernel_died = QtCore.Signal(float)
95 custom_restart_requested = QtCore.Signal()
95 custom_restart_requested = QtCore.Signal()
96
96
97 # Whether to automatically show calltips on open-parentheses.
97 # Whether to automatically show calltips on open-parentheses.
98 enable_calltips = Bool(True, config=True,
98 enable_calltips = Bool(True, config=True,
99 help="Whether to draw information calltips on open-parentheses.")
99 help="Whether to draw information calltips on open-parentheses.")
100
100
101 clear_on_kernel_restart = Bool(True, config=True,
101 clear_on_kernel_restart = Bool(True, config=True,
102 help="Whether to clear the console when the kernel is restarted")
102 help="Whether to clear the console when the kernel is restarted")
103
103
104 confirm_restart = Bool(True, config=True,
104 confirm_restart = Bool(True, config=True,
105 help="Whether to ask for user confirmation when restarting kernel")
105 help="Whether to ask for user confirmation when restarting kernel")
106
106
107 lexer_class = DottedObjectName(config=True,
107 lexer_class = DottedObjectName(config=True,
108 help="The pygments lexer class to use."
108 help="The pygments lexer class to use."
109 )
109 )
110 def _lexer_class_changed(self, name, old, new):
110 def _lexer_class_changed(self, name, old, new):
111 lexer_class = import_item(new)
111 lexer_class = import_item(new)
112 self.lexer = lexer_class()
112 self.lexer = lexer_class()
113
113
114 def _lexer_class_default(self):
114 def _lexer_class_default(self):
115 if py3compat.PY3:
115 if py3compat.PY3:
116 return 'pygments.lexers.Python3Lexer'
116 return 'pygments.lexers.Python3Lexer'
117 else:
117 else:
118 return 'pygments.lexers.PythonLexer'
118 return 'pygments.lexers.PythonLexer'
119
119
120 lexer = Any()
120 lexer = Any()
121 def _lexer_default(self):
121 def _lexer_default(self):
122 lexer_class = import_item(self.lexer_class)
122 lexer_class = import_item(self.lexer_class)
123 return lexer_class()
123 return lexer_class()
124
124
125 # Emitted when a user visible 'execute_request' has been submitted to the
125 # Emitted when a user visible 'execute_request' has been submitted to the
126 # kernel from the FrontendWidget. Contains the code to be executed.
126 # kernel from the FrontendWidget. Contains the code to be executed.
127 executing = QtCore.Signal(object)
127 executing = QtCore.Signal(object)
128
128
129 # Emitted when a user-visible 'execute_reply' has been received from the
129 # Emitted when a user-visible 'execute_reply' has been received from the
130 # kernel and processed by the FrontendWidget. Contains the response message.
130 # kernel and processed by the FrontendWidget. Contains the response message.
131 executed = QtCore.Signal(object)
131 executed = QtCore.Signal(object)
132
132
133 # Emitted when an exit request has been received from the kernel.
133 # Emitted when an exit request has been received from the kernel.
134 exit_requested = QtCore.Signal(object)
134 exit_requested = QtCore.Signal(object)
135
135
136 # Protected class variables.
136 # Protected class variables.
137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 logical_line_transforms=[],
138 logical_line_transforms=[],
139 python_line_transforms=[],
139 python_line_transforms=[],
140 )
140 )
141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 _input_splitter_class = InputSplitter
144 _input_splitter_class = InputSplitter
145 _local_kernel = False
145 _local_kernel = False
146 _highlighter = Instance(FrontendHighlighter)
146 _highlighter = Instance(FrontendHighlighter)
147
147
148 #---------------------------------------------------------------------------
148 #---------------------------------------------------------------------------
149 # 'object' interface
149 # 'object' interface
150 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
151
151
152 def __init__(self, *args, **kw):
152 def __init__(self, *args, **kw):
153 super(FrontendWidget, self).__init__(*args, **kw)
153 super(FrontendWidget, self).__init__(*args, **kw)
154 # FIXME: remove this when PySide min version is updated past 1.0.7
154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 if qt.QT_API == qt.QT_API_PYSIDE:
156 if qt.QT_API == qt.QT_API_PYSIDE:
157 import PySide
157 import PySide
158 if PySide.__version_info__ < (1,0,7):
158 if PySide.__version_info__ < (1,0,7):
159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 self.enable_calltips = False
160 self.enable_calltips = False
161
161
162 # FrontendWidget protected variables.
162 # FrontendWidget protected variables.
163 self._bracket_matcher = BracketMatcher(self._control)
163 self._bracket_matcher = BracketMatcher(self._control)
164 self._call_tip_widget = CallTipWidget(self._control)
164 self._call_tip_widget = CallTipWidget(self._control)
165 self._completion_lexer = CompletionLexer(self.lexer)
165 self._completion_lexer = CompletionLexer(self.lexer)
166 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
166 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
167 self._hidden = False
167 self._hidden = False
168 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
168 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
169 self._input_splitter = self._input_splitter_class()
169 self._input_splitter = self._input_splitter_class()
170 self._kernel_manager = None
170 self._kernel_manager = None
171 self._kernel_client = None
171 self._kernel_client = None
172 self._request_info = {}
172 self._request_info = {}
173 self._request_info['execute'] = {};
173 self._request_info['execute'] = {};
174 self._callback_dict = {}
174 self._callback_dict = {}
175
175
176 # Configure the ConsoleWidget.
176 # Configure the ConsoleWidget.
177 self.tab_width = 4
177 self.tab_width = 4
178 self._set_continuation_prompt('... ')
178 self._set_continuation_prompt('... ')
179
179
180 # Configure the CallTipWidget.
180 # Configure the CallTipWidget.
181 self._call_tip_widget.setFont(self.font)
181 self._call_tip_widget.setFont(self.font)
182 self.font_changed.connect(self._call_tip_widget.setFont)
182 self.font_changed.connect(self._call_tip_widget.setFont)
183
183
184 # Configure actions.
184 # Configure actions.
185 action = self._copy_raw_action
185 action = self._copy_raw_action
186 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
186 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
187 action.setEnabled(False)
187 action.setEnabled(False)
188 action.setShortcut(QtGui.QKeySequence(key))
188 action.setShortcut(QtGui.QKeySequence(key))
189 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
189 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
190 action.triggered.connect(self.copy_raw)
190 action.triggered.connect(self.copy_raw)
191 self.copy_available.connect(action.setEnabled)
191 self.copy_available.connect(action.setEnabled)
192 self.addAction(action)
192 self.addAction(action)
193
193
194 # Connect signal handlers.
194 # Connect signal handlers.
195 document = self._control.document()
195 document = self._control.document()
196 document.contentsChange.connect(self._document_contents_change)
196 document.contentsChange.connect(self._document_contents_change)
197
197
198 # Set flag for whether we are connected via localhost.
198 # Set flag for whether we are connected via localhost.
199 self._local_kernel = kw.get('local_kernel',
199 self._local_kernel = kw.get('local_kernel',
200 FrontendWidget._local_kernel)
200 FrontendWidget._local_kernel)
201
201
202 # Whether or not a clear_output call is pending new output.
202 # Whether or not a clear_output call is pending new output.
203 self._pending_clearoutput = False
203 self._pending_clearoutput = False
204
204
205 #---------------------------------------------------------------------------
205 #---------------------------------------------------------------------------
206 # 'ConsoleWidget' public interface
206 # 'ConsoleWidget' public interface
207 #---------------------------------------------------------------------------
207 #---------------------------------------------------------------------------
208
208
209 def copy(self):
209 def copy(self):
210 """ Copy the currently selected text to the clipboard, removing prompts.
210 """ Copy the currently selected text to the clipboard, removing prompts.
211 """
211 """
212 if self._page_control is not None and self._page_control.hasFocus():
212 if self._page_control is not None and self._page_control.hasFocus():
213 self._page_control.copy()
213 self._page_control.copy()
214 elif self._control.hasFocus():
214 elif self._control.hasFocus():
215 text = self._control.textCursor().selection().toPlainText()
215 text = self._control.textCursor().selection().toPlainText()
216 if text:
216 if text:
217 text = self._prompt_transformer.transform_cell(text)
217 text = self._prompt_transformer.transform_cell(text)
218 QtGui.QApplication.clipboard().setText(text)
218 QtGui.QApplication.clipboard().setText(text)
219 else:
219 else:
220 self.log.debug("frontend widget : unknown copy target")
220 self.log.debug("frontend widget : unknown copy target")
221
221
222 #---------------------------------------------------------------------------
222 #---------------------------------------------------------------------------
223 # 'ConsoleWidget' abstract interface
223 # 'ConsoleWidget' abstract interface
224 #---------------------------------------------------------------------------
224 #---------------------------------------------------------------------------
225
225
226 def _is_complete(self, source, interactive):
226 def _is_complete(self, source, interactive):
227 """ Returns whether 'source' can be completely processed and a new
227 """ Returns whether 'source' can be completely processed and a new
228 prompt created. When triggered by an Enter/Return key press,
228 prompt created. When triggered by an Enter/Return key press,
229 'interactive' is True; otherwise, it is False.
229 'interactive' is True; otherwise, it is False.
230 """
230 """
231 self._input_splitter.reset()
231 self._input_splitter.reset()
232 try:
232 try:
233 complete = self._input_splitter.push(source)
233 complete = self._input_splitter.push(source)
234 except SyntaxError:
234 except SyntaxError:
235 return True
235 return True
236 if interactive:
236 if interactive:
237 complete = not self._input_splitter.push_accepts_more()
237 complete = not self._input_splitter.push_accepts_more()
238 return complete
238 return complete
239
239
240 def _execute(self, source, hidden):
240 def _execute(self, source, hidden):
241 """ Execute 'source'. If 'hidden', do not show any output.
241 """ Execute 'source'. If 'hidden', do not show any output.
242
242
243 See parent class :meth:`execute` docstring for full details.
243 See parent class :meth:`execute` docstring for full details.
244 """
244 """
245 msg_id = self.kernel_client.execute(source, hidden)
245 msg_id = self.kernel_client.execute(source, hidden)
246 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
246 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
247 self._hidden = hidden
247 self._hidden = hidden
248 if not hidden:
248 if not hidden:
249 self.executing.emit(source)
249 self.executing.emit(source)
250
250
251 def _prompt_started_hook(self):
251 def _prompt_started_hook(self):
252 """ Called immediately after a new prompt is displayed.
252 """ Called immediately after a new prompt is displayed.
253 """
253 """
254 if not self._reading:
254 if not self._reading:
255 self._highlighter.highlighting_on = True
255 self._highlighter.highlighting_on = True
256
256
257 def _prompt_finished_hook(self):
257 def _prompt_finished_hook(self):
258 """ Called immediately after a prompt is finished, i.e. when some input
258 """ Called immediately after a prompt is finished, i.e. when some input
259 will be processed and a new prompt displayed.
259 will be processed and a new prompt displayed.
260 """
260 """
261 # Flush all state from the input splitter so the next round of
261 # Flush all state from the input splitter so the next round of
262 # reading input starts with a clean buffer.
262 # reading input starts with a clean buffer.
263 self._input_splitter.reset()
263 self._input_splitter.reset()
264
264
265 if not self._reading:
265 if not self._reading:
266 self._highlighter.highlighting_on = False
266 self._highlighter.highlighting_on = False
267
267
268 def _tab_pressed(self):
268 def _tab_pressed(self):
269 """ Called when the tab key is pressed. Returns whether to continue
269 """ Called when the tab key is pressed. Returns whether to continue
270 processing the event.
270 processing the event.
271 """
271 """
272 # Perform tab completion if:
272 # Perform tab completion if:
273 # 1) The cursor is in the input buffer.
273 # 1) The cursor is in the input buffer.
274 # 2) There is a non-whitespace character before the cursor.
274 # 2) There is a non-whitespace character before the cursor.
275 text = self._get_input_buffer_cursor_line()
275 text = self._get_input_buffer_cursor_line()
276 if text is None:
276 if text is None:
277 return False
277 return False
278 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
278 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
279 if complete:
279 if complete:
280 self._complete()
280 self._complete()
281 return not complete
281 return not complete
282
282
283 #---------------------------------------------------------------------------
283 #---------------------------------------------------------------------------
284 # 'ConsoleWidget' protected interface
284 # 'ConsoleWidget' protected interface
285 #---------------------------------------------------------------------------
285 #---------------------------------------------------------------------------
286
286
287 def _context_menu_make(self, pos):
287 def _context_menu_make(self, pos):
288 """ Reimplemented to add an action for raw copy.
288 """ Reimplemented to add an action for raw copy.
289 """
289 """
290 menu = super(FrontendWidget, self)._context_menu_make(pos)
290 menu = super(FrontendWidget, self)._context_menu_make(pos)
291 for before_action in menu.actions():
291 for before_action in menu.actions():
292 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
292 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
293 QtGui.QKeySequence.ExactMatch:
293 QtGui.QKeySequence.ExactMatch:
294 menu.insertAction(before_action, self._copy_raw_action)
294 menu.insertAction(before_action, self._copy_raw_action)
295 break
295 break
296 return menu
296 return menu
297
297
298 def request_interrupt_kernel(self):
298 def request_interrupt_kernel(self):
299 if self._executing:
299 if self._executing:
300 self.interrupt_kernel()
300 self.interrupt_kernel()
301
301
302 def request_restart_kernel(self):
302 def request_restart_kernel(self):
303 message = 'Are you sure you want to restart the kernel?'
303 message = 'Are you sure you want to restart the kernel?'
304 self.restart_kernel(message, now=False)
304 self.restart_kernel(message, now=False)
305
305
306 def _event_filter_console_keypress(self, event):
306 def _event_filter_console_keypress(self, event):
307 """ Reimplemented for execution interruption and smart backspace.
307 """ Reimplemented for execution interruption and smart backspace.
308 """
308 """
309 key = event.key()
309 key = event.key()
310 if self._control_key_down(event.modifiers(), include_command=False):
310 if self._control_key_down(event.modifiers(), include_command=False):
311
311
312 if key == QtCore.Qt.Key_C and self._executing:
312 if key == QtCore.Qt.Key_C and self._executing:
313 self.request_interrupt_kernel()
313 self.request_interrupt_kernel()
314 return True
314 return True
315
315
316 elif key == QtCore.Qt.Key_Period:
316 elif key == QtCore.Qt.Key_Period:
317 self.request_restart_kernel()
317 self.request_restart_kernel()
318 return True
318 return True
319
319
320 elif not event.modifiers() & QtCore.Qt.AltModifier:
320 elif not event.modifiers() & QtCore.Qt.AltModifier:
321
321
322 # Smart backspace: remove four characters in one backspace if:
322 # Smart backspace: remove four characters in one backspace if:
323 # 1) everything left of the cursor is whitespace
323 # 1) everything left of the cursor is whitespace
324 # 2) the four characters immediately left of the cursor are spaces
324 # 2) the four characters immediately left of the cursor are spaces
325 if key == QtCore.Qt.Key_Backspace:
325 if key == QtCore.Qt.Key_Backspace:
326 col = self._get_input_buffer_cursor_column()
326 col = self._get_input_buffer_cursor_column()
327 cursor = self._control.textCursor()
327 cursor = self._control.textCursor()
328 if col > 3 and not cursor.hasSelection():
328 if col > 3 and not cursor.hasSelection():
329 text = self._get_input_buffer_cursor_line()[:col]
329 text = self._get_input_buffer_cursor_line()[:col]
330 if text.endswith(' ') and not text.strip():
330 if text.endswith(' ') and not text.strip():
331 cursor.movePosition(QtGui.QTextCursor.Left,
331 cursor.movePosition(QtGui.QTextCursor.Left,
332 QtGui.QTextCursor.KeepAnchor, 4)
332 QtGui.QTextCursor.KeepAnchor, 4)
333 cursor.removeSelectedText()
333 cursor.removeSelectedText()
334 return True
334 return True
335
335
336 return super(FrontendWidget, self)._event_filter_console_keypress(event)
336 return super(FrontendWidget, self)._event_filter_console_keypress(event)
337
337
338 def _insert_continuation_prompt(self, cursor):
338 def _insert_continuation_prompt(self, cursor):
339 """ Reimplemented for auto-indentation.
339 """ Reimplemented for auto-indentation.
340 """
340 """
341 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
341 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
342 cursor.insertText(' ' * self._input_splitter.indent_spaces)
342 cursor.insertText(' ' * self._input_splitter.indent_spaces)
343
343
344 #---------------------------------------------------------------------------
344 #---------------------------------------------------------------------------
345 # 'BaseFrontendMixin' abstract interface
345 # 'BaseFrontendMixin' abstract interface
346 #---------------------------------------------------------------------------
346 #---------------------------------------------------------------------------
347 def _handle_clear_output(self, msg):
347 def _handle_clear_output(self, msg):
348 """Handle clear output messages."""
348 """Handle clear output messages."""
349 if not self._hidden and self._is_from_this_session(msg):
349 if not self._hidden and self._is_from_this_session(msg):
350 wait = msg['content'].get('wait', True)
350 wait = msg['content'].get('wait', True)
351 if wait:
351 if wait:
352 self._pending_clearoutput = True
352 self._pending_clearoutput = True
353 else:
353 else:
354 self.clear_output()
354 self.clear_output()
355
355
356 def _handle_complete_reply(self, rep):
356 def _handle_complete_reply(self, rep):
357 """ Handle replies for tab completion.
357 """ Handle replies for tab completion.
358 """
358 """
359 self.log.debug("complete: %s", rep.get('content', ''))
359 self.log.debug("complete: %s", rep.get('content', ''))
360 cursor = self._get_cursor()
360 cursor = self._get_cursor()
361 info = self._request_info.get('complete')
361 info = self._request_info.get('complete')
362 if info and info.id == rep['parent_header']['msg_id'] and \
362 if info and info.id == rep['parent_header']['msg_id'] and \
363 info.pos == cursor.position():
363 info.pos == cursor.position():
364 text = '.'.join(self._get_context())
364 text = '.'.join(self._get_context())
365 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
365 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
366 self._complete_with_items(cursor, rep['content']['matches'])
366 self._complete_with_items(cursor, rep['content']['matches'])
367
367
368 def _silent_exec_callback(self, expr, callback):
368 def _silent_exec_callback(self, expr, callback):
369 """Silently execute `expr` in the kernel and call `callback` with reply
369 """Silently execute `expr` in the kernel and call `callback` with reply
370
370
371 the `expr` is evaluated silently in the kernel (without) output in
371 the `expr` is evaluated silently in the kernel (without) output in
372 the frontend. Call `callback` with the
372 the frontend. Call `callback` with the
373 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
373 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
374
374
375 Parameters
375 Parameters
376 ----------
376 ----------
377 expr : string
377 expr : string
378 valid string to be executed by the kernel.
378 valid string to be executed by the kernel.
379 callback : function
379 callback : function
380 function accepting one argument, as a string. The string will be
380 function accepting one argument, as a string. The string will be
381 the `repr` of the result of evaluating `expr`
381 the `repr` of the result of evaluating `expr`
382
382
383 The `callback` is called with the `repr()` of the result of `expr` as
383 The `callback` is called with the `repr()` of the result of `expr` as
384 first argument. To get the object, do `eval()` on the passed value.
384 first argument. To get the object, do `eval()` on the passed value.
385
385
386 See Also
386 See Also
387 --------
387 --------
388 _handle_exec_callback : private method, deal with calling callback with reply
388 _handle_exec_callback : private method, deal with calling callback with reply
389
389
390 """
390 """
391
391
392 # generate uuid, which would be used as an indication of whether or
392 # generate uuid, which would be used as an indication of whether or
393 # not the unique request originated from here (can use msg id ?)
393 # not the unique request originated from here (can use msg id ?)
394 local_uuid = str(uuid.uuid1())
394 local_uuid = str(uuid.uuid1())
395 msg_id = self.kernel_client.execute('',
395 msg_id = self.kernel_client.execute('',
396 silent=True, user_expressions={ local_uuid:expr })
396 silent=True, user_expressions={ local_uuid:expr })
397 self._callback_dict[local_uuid] = callback
397 self._callback_dict[local_uuid] = callback
398 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
398 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
399
399
400 def _handle_exec_callback(self, msg):
400 def _handle_exec_callback(self, msg):
401 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
401 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
402
402
403 Parameters
403 Parameters
404 ----------
404 ----------
405 msg : raw message send by the kernel containing an `user_expressions`
405 msg : raw message send by the kernel containing an `user_expressions`
406 and having a 'silent_exec_callback' kind.
406 and having a 'silent_exec_callback' kind.
407
407
408 Notes
408 Notes
409 -----
409 -----
410 This function will look for a `callback` associated with the
410 This function will look for a `callback` associated with the
411 corresponding message id. Association has been made by
411 corresponding message id. Association has been made by
412 `_silent_exec_callback`. `callback` is then called with the `repr()`
412 `_silent_exec_callback`. `callback` is then called with the `repr()`
413 of the value of corresponding `user_expressions` as argument.
413 of the value of corresponding `user_expressions` as argument.
414 `callback` is then removed from the known list so that any message
414 `callback` is then removed from the known list so that any message
415 coming again with the same id won't trigger it.
415 coming again with the same id won't trigger it.
416
416
417 """
417 """
418
418
419 user_exp = msg['content'].get('user_expressions')
419 user_exp = msg['content'].get('user_expressions')
420 if not user_exp:
420 if not user_exp:
421 return
421 return
422 for expression in user_exp:
422 for expression in user_exp:
423 if expression in self._callback_dict:
423 if expression in self._callback_dict:
424 self._callback_dict.pop(expression)(user_exp[expression])
424 self._callback_dict.pop(expression)(user_exp[expression])
425
425
426 def _handle_execute_reply(self, msg):
426 def _handle_execute_reply(self, msg):
427 """ Handles replies for code execution.
427 """ Handles replies for code execution.
428 """
428 """
429 self.log.debug("execute: %s", msg.get('content', ''))
429 self.log.debug("execute: %s", msg.get('content', ''))
430 msg_id = msg['parent_header']['msg_id']
430 msg_id = msg['parent_header']['msg_id']
431 info = self._request_info['execute'].get(msg_id)
431 info = self._request_info['execute'].get(msg_id)
432 # unset reading flag, because if execute finished, raw_input can't
432 # unset reading flag, because if execute finished, raw_input can't
433 # still be pending.
433 # still be pending.
434 self._reading = False
434 self._reading = False
435 if info and info.kind == 'user' and not self._hidden:
435 if info and info.kind == 'user' and not self._hidden:
436 # Make sure that all output from the SUB channel has been processed
436 # Make sure that all output from the SUB channel has been processed
437 # before writing a new prompt.
437 # before writing a new prompt.
438 self.kernel_client.iopub_channel.flush()
438 self.kernel_client.iopub_channel.flush()
439
439
440 # Reset the ANSI style information to prevent bad text in stdout
440 # Reset the ANSI style information to prevent bad text in stdout
441 # from messing up our colors. We're not a true terminal so we're
441 # from messing up our colors. We're not a true terminal so we're
442 # allowed to do this.
442 # allowed to do this.
443 if self.ansi_codes:
443 if self.ansi_codes:
444 self._ansi_processor.reset_sgr()
444 self._ansi_processor.reset_sgr()
445
445
446 content = msg['content']
446 content = msg['content']
447 status = content['status']
447 status = content['status']
448 if status == 'ok':
448 if status == 'ok':
449 self._process_execute_ok(msg)
449 self._process_execute_ok(msg)
450 elif status == 'error':
450 elif status == 'error':
451 self._process_execute_error(msg)
451 self._process_execute_error(msg)
452 elif status == 'aborted':
452 elif status == 'aborted':
453 self._process_execute_abort(msg)
453 self._process_execute_abort(msg)
454
454
455 self._show_interpreter_prompt_for_reply(msg)
455 self._show_interpreter_prompt_for_reply(msg)
456 self.executed.emit(msg)
456 self.executed.emit(msg)
457 self._request_info['execute'].pop(msg_id)
457 self._request_info['execute'].pop(msg_id)
458 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
458 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
459 self._handle_exec_callback(msg)
459 self._handle_exec_callback(msg)
460 self._request_info['execute'].pop(msg_id)
460 self._request_info['execute'].pop(msg_id)
461 else:
461 else:
462 super(FrontendWidget, self)._handle_execute_reply(msg)
462 super(FrontendWidget, self)._handle_execute_reply(msg)
463
463
464 def _handle_input_request(self, msg):
464 def _handle_input_request(self, msg):
465 """ Handle requests for raw_input.
465 """ Handle requests for raw_input.
466 """
466 """
467 self.log.debug("input: %s", msg.get('content', ''))
467 self.log.debug("input: %s", msg.get('content', ''))
468 if self._hidden:
468 if self._hidden:
469 raise RuntimeError('Request for raw input during hidden execution.')
469 raise RuntimeError('Request for raw input during hidden execution.')
470
470
471 # Make sure that all output from the SUB channel has been processed
471 # Make sure that all output from the SUB channel has been processed
472 # before entering readline mode.
472 # before entering readline mode.
473 self.kernel_client.iopub_channel.flush()
473 self.kernel_client.iopub_channel.flush()
474
474
475 def callback(line):
475 def callback(line):
476 self.kernel_client.stdin_channel.input(line)
476 self.kernel_client.stdin_channel.input(line)
477 if self._reading:
477 if self._reading:
478 self.log.debug("Got second input request, assuming first was interrupted.")
478 self.log.debug("Got second input request, assuming first was interrupted.")
479 self._reading = False
479 self._reading = False
480 self._readline(msg['content']['prompt'], callback=callback)
480 self._readline(msg['content']['prompt'], callback=callback)
481
481
482 def _kernel_restarted_message(self, died=True):
482 def _kernel_restarted_message(self, died=True):
483 msg = "Kernel died, restarting" if died else "Kernel restarting"
483 msg = "Kernel died, restarting" if died else "Kernel restarting"
484 self._append_html("<br>%s<hr><br>" % msg,
484 self._append_html("<br>%s<hr><br>" % msg,
485 before_prompt=False
485 before_prompt=False
486 )
486 )
487
487
488 def _handle_kernel_died(self, since_last_heartbeat):
488 def _handle_kernel_died(self, since_last_heartbeat):
489 """Handle the kernel's death (if we do not own the kernel).
489 """Handle the kernel's death (if we do not own the kernel).
490 """
490 """
491 self.log.warn("kernel died: %s", since_last_heartbeat)
491 self.log.warn("kernel died: %s", since_last_heartbeat)
492 if self.custom_restart:
492 if self.custom_restart:
493 self.custom_restart_kernel_died.emit(since_last_heartbeat)
493 self.custom_restart_kernel_died.emit(since_last_heartbeat)
494 else:
494 else:
495 self._kernel_restarted_message(died=True)
495 self._kernel_restarted_message(died=True)
496 self.reset()
496 self.reset()
497
497
498 def _handle_kernel_restarted(self, died=True):
498 def _handle_kernel_restarted(self, died=True):
499 """Notice that the autorestarter restarted the kernel.
499 """Notice that the autorestarter restarted the kernel.
500
500
501 There's nothing to do but show a message.
501 There's nothing to do but show a message.
502 """
502 """
503 self.log.warn("kernel restarted")
503 self.log.warn("kernel restarted")
504 self._kernel_restarted_message(died=died)
504 self._kernel_restarted_message(died=died)
505 self.reset()
505 self.reset()
506
506
507 def _handle_object_info_reply(self, rep):
507 def _handle_object_info_reply(self, rep):
508 """ Handle replies for call tips.
508 """ Handle replies for call tips.
509 """
509 """
510 self.log.debug("oinfo: %s", rep.get('content', ''))
510 self.log.debug("oinfo: %s", rep.get('content', ''))
511 cursor = self._get_cursor()
511 cursor = self._get_cursor()
512 info = self._request_info.get('call_tip')
512 info = self._request_info.get('call_tip')
513 if info and info.id == rep['parent_header']['msg_id'] and \
513 if info and info.id == rep['parent_header']['msg_id'] and \
514 info.pos == cursor.position():
514 info.pos == cursor.position():
515 # Get the information for a call tip. For now we format the call
515 # Get the information for a call tip. For now we format the call
516 # line as string, later we can pass False to format_call and
516 # line as string, later we can pass False to format_call and
517 # syntax-highlight it ourselves for nicer formatting in the
517 # syntax-highlight it ourselves for nicer formatting in the
518 # calltip.
518 # calltip.
519 content = rep['content']
519 content = rep['content']
520 # if this is from pykernel, 'docstring' will be the only key
520 # if this is from pykernel, 'docstring' will be the only key
521 if content.get('ismagic', False):
521 if content.get('ismagic', False):
522 # Don't generate a call-tip for magics. Ideally, we should
522 # Don't generate a call-tip for magics. Ideally, we should
523 # generate a tooltip, but not on ( like we do for actual
523 # generate a tooltip, but not on ( like we do for actual
524 # callables.
524 # callables.
525 call_info, doc = None, None
525 call_info, doc = None, None
526 else:
526 else:
527 call_info, doc = call_tip(content, format_call=True)
527 call_info, doc = call_tip(content, format_call=True)
528 if call_info or doc:
528 if call_info or doc:
529 self._call_tip_widget.show_call_info(call_info, doc)
529 self._call_tip_widget.show_call_info(call_info, doc)
530
530
531 def _handle_execute_result(self, msg):
531 def _handle_execute_result(self, msg):
532 """ Handle display hook output.
532 """ Handle display hook output.
533 """
533 """
534 self.log.debug("execute_result: %s", msg.get('content', ''))
534 self.log.debug("execute_result: %s", msg.get('content', ''))
535 if not self._hidden and self._is_from_this_session(msg):
535 if not self._hidden and self._is_from_this_session(msg):
536 self.flush_clearoutput()
536 self.flush_clearoutput()
537 text = msg['content']['data']
537 text = msg['content']['data']
538 self._append_plain_text(text + '\n', before_prompt=True)
538 self._append_plain_text(text + '\n', before_prompt=True)
539
539
540 def _handle_stream(self, msg):
540 def _handle_stream(self, msg):
541 """ Handle stdout, stderr, and stdin.
541 """ Handle stdout, stderr, and stdin.
542 """
542 """
543 self.log.debug("stream: %s", msg.get('content', ''))
543 self.log.debug("stream: %s", msg.get('content', ''))
544 if not self._hidden and self._is_from_this_session(msg):
544 if not self._hidden and self._is_from_this_session(msg):
545 self.flush_clearoutput()
545 self.flush_clearoutput()
546 self.append_stream(msg['content']['data'])
546 self.append_stream(msg['content']['data'])
547
547
548 def _handle_shutdown_reply(self, msg):
548 def _handle_shutdown_reply(self, msg):
549 """ Handle shutdown signal, only if from other console.
549 """ Handle shutdown signal, only if from other console.
550 """
550 """
551 self.log.warn("shutdown: %s", msg.get('content', ''))
551 self.log.warn("shutdown: %s", msg.get('content', ''))
552 restart = msg.get('content', {}).get('restart', False)
552 restart = msg.get('content', {}).get('restart', False)
553 if not self._hidden and not self._is_from_this_session(msg):
553 if not self._hidden and not self._is_from_this_session(msg):
554 # got shutdown reply, request came from session other than ours
554 # got shutdown reply, request came from session other than ours
555 if restart:
555 if restart:
556 # someone restarted the kernel, handle it
556 # someone restarted the kernel, handle it
557 self._handle_kernel_restarted(died=False)
557 self._handle_kernel_restarted(died=False)
558 else:
558 else:
559 # kernel was shutdown permanently
559 # kernel was shutdown permanently
560 # this triggers exit_requested if the kernel was local,
560 # this triggers exit_requested if the kernel was local,
561 # and a dialog if the kernel was remote,
561 # and a dialog if the kernel was remote,
562 # so we don't suddenly clear the qtconsole without asking.
562 # so we don't suddenly clear the qtconsole without asking.
563 if self._local_kernel:
563 if self._local_kernel:
564 self.exit_requested.emit(self)
564 self.exit_requested.emit(self)
565 else:
565 else:
566 title = self.window().windowTitle()
566 title = self.window().windowTitle()
567 reply = QtGui.QMessageBox.question(self, title,
567 reply = QtGui.QMessageBox.question(self, title,
568 "Kernel has been shutdown permanently. "
568 "Kernel has been shutdown permanently. "
569 "Close the Console?",
569 "Close the Console?",
570 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
570 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
571 if reply == QtGui.QMessageBox.Yes:
571 if reply == QtGui.QMessageBox.Yes:
572 self.exit_requested.emit(self)
572 self.exit_requested.emit(self)
573
573
574 def _handle_status(self, msg):
574 def _handle_status(self, msg):
575 """Handle status message"""
575 """Handle status message"""
576 # This is where a busy/idle indicator would be triggered,
576 # This is where a busy/idle indicator would be triggered,
577 # when we make one.
577 # when we make one.
578 state = msg['content'].get('execution_state', '')
578 state = msg['content'].get('execution_state', '')
579 if state == 'starting':
579 if state == 'starting':
580 # kernel started while we were running
580 # kernel started while we were running
581 if self._executing:
581 if self._executing:
582 self._handle_kernel_restarted(died=True)
582 self._handle_kernel_restarted(died=True)
583 elif state == 'idle':
583 elif state == 'idle':
584 pass
584 pass
585 elif state == 'busy':
585 elif state == 'busy':
586 pass
586 pass
587
587
588 def _started_channels(self):
588 def _started_channels(self):
589 """ Called when the KernelManager channels have started listening or
589 """ Called when the KernelManager channels have started listening or
590 when the frontend is assigned an already listening KernelManager.
590 when the frontend is assigned an already listening KernelManager.
591 """
591 """
592 self.reset(clear=True)
592 self.reset(clear=True)
593
593
594 #---------------------------------------------------------------------------
594 #---------------------------------------------------------------------------
595 # 'FrontendWidget' public interface
595 # 'FrontendWidget' public interface
596 #---------------------------------------------------------------------------
596 #---------------------------------------------------------------------------
597
597
598 def copy_raw(self):
598 def copy_raw(self):
599 """ Copy the currently selected text to the clipboard without attempting
599 """ Copy the currently selected text to the clipboard without attempting
600 to remove prompts or otherwise alter the text.
600 to remove prompts or otherwise alter the text.
601 """
601 """
602 self._control.copy()
602 self._control.copy()
603
603
604 def execute_file(self, path, hidden=False):
604 def execute_file(self, path, hidden=False):
605 """ Attempts to execute file with 'path'. If 'hidden', no output is
605 """ Attempts to execute file with 'path'. If 'hidden', no output is
606 shown.
606 shown.
607 """
607 """
608 self.execute('execfile(%r)' % path, hidden=hidden)
608 self.execute('execfile(%r)' % path, hidden=hidden)
609
609
610 def interrupt_kernel(self):
610 def interrupt_kernel(self):
611 """ Attempts to interrupt the running kernel.
611 """ Attempts to interrupt the running kernel.
612
612
613 Also unsets _reading flag, to avoid runtime errors
613 Also unsets _reading flag, to avoid runtime errors
614 if raw_input is called again.
614 if raw_input is called again.
615 """
615 """
616 if self.custom_interrupt:
616 if self.custom_interrupt:
617 self._reading = False
617 self._reading = False
618 self.custom_interrupt_requested.emit()
618 self.custom_interrupt_requested.emit()
619 elif self.kernel_manager:
619 elif self.kernel_manager:
620 self._reading = False
620 self._reading = False
621 self.kernel_manager.interrupt_kernel()
621 self.kernel_manager.interrupt_kernel()
622 else:
622 else:
623 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
623 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
624
624
625 def reset(self, clear=False):
625 def reset(self, clear=False):
626 """ Resets the widget to its initial state if ``clear`` parameter
626 """ Resets the widget to its initial state if ``clear`` parameter
627 is True, otherwise
627 is True, otherwise
628 prints a visual indication of the fact that the kernel restarted, but
628 prints a visual indication of the fact that the kernel restarted, but
629 does not clear the traces from previous usage of the kernel before it
629 does not clear the traces from previous usage of the kernel before it
630 was restarted. With ``clear=True``, it is similar to ``%clear``, but
630 was restarted. With ``clear=True``, it is similar to ``%clear``, but
631 also re-writes the banner and aborts execution if necessary.
631 also re-writes the banner and aborts execution if necessary.
632 """
632 """
633 if self._executing:
633 if self._executing:
634 self._executing = False
634 self._executing = False
635 self._request_info['execute'] = {}
635 self._request_info['execute'] = {}
636 self._reading = False
636 self._reading = False
637 self._highlighter.highlighting_on = False
637 self._highlighter.highlighting_on = False
638
638
639 if clear:
639 if clear:
640 self._control.clear()
640 self._control.clear()
641 self._append_plain_text(self.banner)
641 self._append_plain_text(self.banner)
642 # update output marker for stdout/stderr, so that startup
642 # update output marker for stdout/stderr, so that startup
643 # messages appear after banner:
643 # messages appear after banner:
644 self._append_before_prompt_pos = self._get_cursor().position()
644 self._append_before_prompt_pos = self._get_cursor().position()
645 self._show_interpreter_prompt()
645 self._show_interpreter_prompt()
646
646
647 def restart_kernel(self, message, now=False):
647 def restart_kernel(self, message, now=False):
648 """ Attempts to restart the running kernel.
648 """ Attempts to restart the running kernel.
649 """
649 """
650 # FIXME: now should be configurable via a checkbox in the dialog. Right
650 # FIXME: now should be configurable via a checkbox in the dialog. Right
651 # now at least the heartbeat path sets it to True and the manual restart
651 # now at least the heartbeat path sets it to True and the manual restart
652 # to False. But those should just be the pre-selected states of a
652 # to False. But those should just be the pre-selected states of a
653 # checkbox that the user could override if so desired. But I don't know
653 # checkbox that the user could override if so desired. But I don't know
654 # enough Qt to go implementing the checkbox now.
654 # enough Qt to go implementing the checkbox now.
655
655
656 if self.custom_restart:
656 if self.custom_restart:
657 self.custom_restart_requested.emit()
657 self.custom_restart_requested.emit()
658 return
658 return
659
659
660 if self.kernel_manager:
660 if self.kernel_manager:
661 # Pause the heart beat channel to prevent further warnings.
661 # Pause the heart beat channel to prevent further warnings.
662 self.kernel_client.hb_channel.pause()
662 self.kernel_client.hb_channel.pause()
663
663
664 # Prompt the user to restart the kernel. Un-pause the heartbeat if
664 # Prompt the user to restart the kernel. Un-pause the heartbeat if
665 # they decline. (If they accept, the heartbeat will be un-paused
665 # they decline. (If they accept, the heartbeat will be un-paused
666 # automatically when the kernel is restarted.)
666 # automatically when the kernel is restarted.)
667 if self.confirm_restart:
667 if self.confirm_restart:
668 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
668 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
669 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
669 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
670 message, buttons)
670 message, buttons)
671 do_restart = result == QtGui.QMessageBox.Yes
671 do_restart = result == QtGui.QMessageBox.Yes
672 else:
672 else:
673 # confirm_restart is False, so we don't need to ask user
673 # confirm_restart is False, so we don't need to ask user
674 # anything, just do the restart
674 # anything, just do the restart
675 do_restart = True
675 do_restart = True
676 if do_restart:
676 if do_restart:
677 try:
677 try:
678 self.kernel_manager.restart_kernel(now=now)
678 self.kernel_manager.restart_kernel(now=now)
679 except RuntimeError as e:
679 except RuntimeError as e:
680 self._append_plain_text(
680 self._append_plain_text(
681 'Error restarting kernel: %s\n' % e,
681 'Error restarting kernel: %s\n' % e,
682 before_prompt=True
682 before_prompt=True
683 )
683 )
684 else:
684 else:
685 self._append_html("<br>Restarting kernel...\n<hr><br>",
685 self._append_html("<br>Restarting kernel...\n<hr><br>",
686 before_prompt=True,
686 before_prompt=True,
687 )
687 )
688 else:
688 else:
689 self.kernel_client.hb_channel.unpause()
689 self.kernel_client.hb_channel.unpause()
690
690
691 else:
691 else:
692 self._append_plain_text(
692 self._append_plain_text(
693 'Cannot restart a Kernel I did not start\n',
693 'Cannot restart a Kernel I did not start\n',
694 before_prompt=True
694 before_prompt=True
695 )
695 )
696
696
697 def append_stream(self, text):
697 def append_stream(self, text):
698 """Appends text to the output stream."""
698 """Appends text to the output stream."""
699 # Most consoles treat tabs as being 8 space characters. Convert tabs
699 # Most consoles treat tabs as being 8 space characters. Convert tabs
700 # to spaces so that output looks as expected regardless of this
700 # to spaces so that output looks as expected regardless of this
701 # widget's tab width.
701 # widget's tab width.
702 text = text.expandtabs(8)
702 text = text.expandtabs(8)
703 self._append_plain_text(text, before_prompt=True)
703 self._append_plain_text(text, before_prompt=True)
704 self._control.moveCursor(QtGui.QTextCursor.End)
704 self._control.moveCursor(QtGui.QTextCursor.End)
705
705
706 def flush_clearoutput(self):
706 def flush_clearoutput(self):
707 """If a clearoutput is pending, execute it."""
707 """If a clearoutput is pending, execute it."""
708 if self._pending_clearoutput:
708 if self._pending_clearoutput:
709 self._pending_clearoutput = False
709 self._pending_clearoutput = False
710 self.clear_output()
710 self.clear_output()
711
711
712 def clear_output(self):
712 def clear_output(self):
713 """Clears the current line of output."""
713 """Clears the current line of output."""
714 cursor = self._control.textCursor()
714 cursor = self._control.textCursor()
715 cursor.beginEditBlock()
715 cursor.beginEditBlock()
716 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
716 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
717 cursor.insertText('')
717 cursor.insertText('')
718 cursor.endEditBlock()
718 cursor.endEditBlock()
719
719
720 #---------------------------------------------------------------------------
720 #---------------------------------------------------------------------------
721 # 'FrontendWidget' protected interface
721 # 'FrontendWidget' protected interface
722 #---------------------------------------------------------------------------
722 #---------------------------------------------------------------------------
723
723
724 def _call_tip(self):
724 def _call_tip(self):
725 """ Shows a call tip, if appropriate, at the current cursor location.
725 """ Shows a call tip, if appropriate, at the current cursor location.
726 """
726 """
727 # Decide if it makes sense to show a call tip
727 # Decide if it makes sense to show a call tip
728 if not self.enable_calltips:
728 if not self.enable_calltips:
729 return False
729 return False
730 cursor = self._get_cursor()
730 cursor_pos = self._get_input_buffer_cursor_pos()
731 cursor.movePosition(QtGui.QTextCursor.Left)
731 code = self.input_buffer
732 if cursor.document().characterAt(cursor.position()) != '(':
733 return False
734 context = self._get_context(cursor)
735 if not context:
736 return False
737
738 # Send the metadata request to the kernel
732 # Send the metadata request to the kernel
739 name = '.'.join(context)
733 msg_id = self.kernel_client.object_info(code, cursor_pos)
740 msg_id = self.kernel_client.object_info(name)
741 pos = self._get_cursor().position()
734 pos = self._get_cursor().position()
742 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
735 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
743 return True
736 return True
744
737
745 def _complete(self):
738 def _complete(self):
746 """ Performs completion at the current cursor location.
739 """ Performs completion at the current cursor location.
747 """
740 """
748 context = self._get_context()
741 context = self._get_context()
749 if context:
742 if context:
750 # Send the completion request to the kernel
743 # Send the completion request to the kernel
751 msg_id = self.kernel_client.complete(
744 msg_id = self.kernel_client.complete(
752 '.'.join(context), # text
745 code=self.input_buffer,
753 self._get_input_buffer_cursor_line(), # line
746 cursor_pos=self._get_input_buffer_cursor_pos(),
754 self._get_input_buffer_cursor_column(), # cursor_pos
747 )
755 self.input_buffer) # block
756 pos = self._get_cursor().position()
748 pos = self._get_cursor().position()
757 info = self._CompletionRequest(msg_id, pos)
749 info = self._CompletionRequest(msg_id, pos)
758 self._request_info['complete'] = info
750 self._request_info['complete'] = info
759
751
760 def _get_context(self, cursor=None):
752 def _get_context(self, cursor=None):
761 """ Gets the context for the specified cursor (or the current cursor
753 """ Gets the context for the specified cursor (or the current cursor
762 if none is specified).
754 if none is specified).
763 """
755 """
764 if cursor is None:
756 if cursor is None:
765 cursor = self._get_cursor()
757 cursor = self._get_cursor()
766 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
758 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
767 QtGui.QTextCursor.KeepAnchor)
759 QtGui.QTextCursor.KeepAnchor)
768 text = cursor.selection().toPlainText()
760 text = cursor.selection().toPlainText()
769 return self._completion_lexer.get_context(text)
761 return self._completion_lexer.get_context(text)
770
762
771 def _process_execute_abort(self, msg):
763 def _process_execute_abort(self, msg):
772 """ Process a reply for an aborted execution request.
764 """ Process a reply for an aborted execution request.
773 """
765 """
774 self._append_plain_text("ERROR: execution aborted\n")
766 self._append_plain_text("ERROR: execution aborted\n")
775
767
776 def _process_execute_error(self, msg):
768 def _process_execute_error(self, msg):
777 """ Process a reply for an execution request that resulted in an error.
769 """ Process a reply for an execution request that resulted in an error.
778 """
770 """
779 content = msg['content']
771 content = msg['content']
780 # If a SystemExit is passed along, this means exit() was called - also
772 # If a SystemExit is passed along, this means exit() was called - also
781 # all the ipython %exit magic syntax of '-k' to be used to keep
773 # all the ipython %exit magic syntax of '-k' to be used to keep
782 # the kernel running
774 # the kernel running
783 if content['ename']=='SystemExit':
775 if content['ename']=='SystemExit':
784 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
776 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
785 self._keep_kernel_on_exit = keepkernel
777 self._keep_kernel_on_exit = keepkernel
786 self.exit_requested.emit(self)
778 self.exit_requested.emit(self)
787 else:
779 else:
788 traceback = ''.join(content['traceback'])
780 traceback = ''.join(content['traceback'])
789 self._append_plain_text(traceback)
781 self._append_plain_text(traceback)
790
782
791 def _process_execute_ok(self, msg):
783 def _process_execute_ok(self, msg):
792 """ Process a reply for a successful execution request.
784 """ Process a reply for a successful execution request.
793 """
785 """
794 payload = msg['content']['payload']
786 payload = msg['content']['payload']
795 for item in payload:
787 for item in payload:
796 if not self._process_execute_payload(item):
788 if not self._process_execute_payload(item):
797 warning = 'Warning: received unknown payload of type %s'
789 warning = 'Warning: received unknown payload of type %s'
798 print(warning % repr(item['source']))
790 print(warning % repr(item['source']))
799
791
800 def _process_execute_payload(self, item):
792 def _process_execute_payload(self, item):
801 """ Process a single payload item from the list of payload items in an
793 """ Process a single payload item from the list of payload items in an
802 execution reply. Returns whether the payload was handled.
794 execution reply. Returns whether the payload was handled.
803 """
795 """
804 # The basic FrontendWidget doesn't handle payloads, as they are a
796 # The basic FrontendWidget doesn't handle payloads, as they are a
805 # mechanism for going beyond the standard Python interpreter model.
797 # mechanism for going beyond the standard Python interpreter model.
806 return False
798 return False
807
799
808 def _show_interpreter_prompt(self):
800 def _show_interpreter_prompt(self):
809 """ Shows a prompt for the interpreter.
801 """ Shows a prompt for the interpreter.
810 """
802 """
811 self._show_prompt('>>> ')
803 self._show_prompt('>>> ')
812
804
813 def _show_interpreter_prompt_for_reply(self, msg):
805 def _show_interpreter_prompt_for_reply(self, msg):
814 """ Shows a prompt for the interpreter given an 'execute_reply' message.
806 """ Shows a prompt for the interpreter given an 'execute_reply' message.
815 """
807 """
816 self._show_interpreter_prompt()
808 self._show_interpreter_prompt()
817
809
818 #------ Signal handlers ----------------------------------------------------
810 #------ Signal handlers ----------------------------------------------------
819
811
820 def _document_contents_change(self, position, removed, added):
812 def _document_contents_change(self, position, removed, added):
821 """ Called whenever the document's content changes. Display a call tip
813 """ Called whenever the document's content changes. Display a call tip
822 if appropriate.
814 if appropriate.
823 """
815 """
824 # Calculate where the cursor should be *after* the change:
816 # Calculate where the cursor should be *after* the change:
825 position += added
817 position += added
826
818
827 document = self._control.document()
819 document = self._control.document()
828 if position == self._get_cursor().position():
820 if position == self._get_cursor().position():
829 self._call_tip()
821 self._call_tip()
830
822
831 #------ Trait default initializers -----------------------------------------
823 #------ Trait default initializers -----------------------------------------
832
824
833 def _banner_default(self):
825 def _banner_default(self):
834 """ Returns the standard Python banner.
826 """ Returns the standard Python banner.
835 """
827 """
836 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
828 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
837 '"license" for more information.'
829 '"license" for more information.'
838 return banner % (sys.version, sys.platform)
830 return banner % (sys.version, sys.platform)
@@ -1,590 +1,572 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from collections import namedtuple
9 from collections import namedtuple
10 import os.path
10 import os.path
11 import re
11 import re
12 from subprocess import Popen
12 from subprocess import Popen
13 import sys
13 import sys
14 import time
14 import time
15 from textwrap import dedent
15 from textwrap import dedent
16
16
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 from IPython.core.inputsplitter import IPythonInputSplitter
19 from IPython.core.inputsplitter import IPythonInputSplitter
20 from IPython.core.inputtransformer import ipy_prompt
20 from IPython.core.inputtransformer import ipy_prompt
21 from IPython.utils.traitlets import Bool, Unicode
21 from IPython.utils.traitlets import Bool, Unicode
22 from .frontend_widget import FrontendWidget
22 from .frontend_widget import FrontendWidget
23 from . import styles
23 from . import styles
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Constants
26 # Constants
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28
28
29 # Default strings to build and display input and output prompts (and separators
29 # Default strings to build and display input and output prompts (and separators
30 # in between)
30 # in between)
31 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
31 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
32 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
32 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
33 default_input_sep = '\n'
33 default_input_sep = '\n'
34 default_output_sep = ''
34 default_output_sep = ''
35 default_output_sep2 = ''
35 default_output_sep2 = ''
36
36
37 # Base path for most payload sources.
37 # Base path for most payload sources.
38 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
38 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
39
39
40 if sys.platform.startswith('win'):
40 if sys.platform.startswith('win'):
41 default_editor = 'notepad'
41 default_editor = 'notepad'
42 else:
42 else:
43 default_editor = ''
43 default_editor = ''
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # IPythonWidget class
46 # IPythonWidget class
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 class IPythonWidget(FrontendWidget):
49 class IPythonWidget(FrontendWidget):
50 """ A FrontendWidget for an IPython kernel.
50 """ A FrontendWidget for an IPython kernel.
51 """
51 """
52
52
53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 # settings.
55 # settings.
56 custom_edit = Bool(False)
56 custom_edit = Bool(False)
57 custom_edit_requested = QtCore.Signal(object, object)
57 custom_edit_requested = QtCore.Signal(object, object)
58
58
59 editor = Unicode(default_editor, config=True,
59 editor = Unicode(default_editor, config=True,
60 help="""
60 help="""
61 A command for invoking a system text editor. If the string contains a
61 A command for invoking a system text editor. If the string contains a
62 {filename} format specifier, it will be used. Otherwise, the filename
62 {filename} format specifier, it will be used. Otherwise, the filename
63 will be appended to the end the command.
63 will be appended to the end the command.
64 """)
64 """)
65
65
66 editor_line = Unicode(config=True,
66 editor_line = Unicode(config=True,
67 help="""
67 help="""
68 The editor command to use when a specific line number is requested. The
68 The editor command to use when a specific line number is requested. The
69 string should contain two format specifiers: {line} and {filename}. If
69 string should contain two format specifiers: {line} and {filename}. If
70 this parameter is not specified, the line number option to the %edit
70 this parameter is not specified, the line number option to the %edit
71 magic will be ignored.
71 magic will be ignored.
72 """)
72 """)
73
73
74 style_sheet = Unicode(config=True,
74 style_sheet = Unicode(config=True,
75 help="""
75 help="""
76 A CSS stylesheet. The stylesheet can contain classes for:
76 A CSS stylesheet. The stylesheet can contain classes for:
77 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
77 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
78 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
78 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
79 3. IPython: .error, .in-prompt, .out-prompt, etc
79 3. IPython: .error, .in-prompt, .out-prompt, etc
80 """)
80 """)
81
81
82 syntax_style = Unicode(config=True,
82 syntax_style = Unicode(config=True,
83 help="""
83 help="""
84 If not empty, use this Pygments style for syntax highlighting.
84 If not empty, use this Pygments style for syntax highlighting.
85 Otherwise, the style sheet is queried for Pygments style
85 Otherwise, the style sheet is queried for Pygments style
86 information.
86 information.
87 """)
87 """)
88
88
89 # Prompts.
89 # Prompts.
90 in_prompt = Unicode(default_in_prompt, config=True)
90 in_prompt = Unicode(default_in_prompt, config=True)
91 out_prompt = Unicode(default_out_prompt, config=True)
91 out_prompt = Unicode(default_out_prompt, config=True)
92 input_sep = Unicode(default_input_sep, config=True)
92 input_sep = Unicode(default_input_sep, config=True)
93 output_sep = Unicode(default_output_sep, config=True)
93 output_sep = Unicode(default_output_sep, config=True)
94 output_sep2 = Unicode(default_output_sep2, config=True)
94 output_sep2 = Unicode(default_output_sep2, config=True)
95
95
96 # FrontendWidget protected class variables.
96 # FrontendWidget protected class variables.
97 _input_splitter_class = IPythonInputSplitter
97 _input_splitter_class = IPythonInputSplitter
98 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
98 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
99 logical_line_transforms=[],
99 logical_line_transforms=[],
100 python_line_transforms=[],
100 python_line_transforms=[],
101 )
101 )
102
102
103 # IPythonWidget protected class variables.
103 # IPythonWidget protected class variables.
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 _payload_source_edit = 'edit_magic'
105 _payload_source_edit = 'edit_magic'
106 _payload_source_exit = 'ask_exit'
106 _payload_source_exit = 'ask_exit'
107 _payload_source_next_input = 'set_next_input'
107 _payload_source_next_input = 'set_next_input'
108 _payload_source_page = 'page'
108 _payload_source_page = 'page'
109 _retrying_history_request = False
109 _retrying_history_request = False
110
110
111 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
112 # 'object' interface
112 # 'object' interface
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114
114
115 def __init__(self, *args, **kw):
115 def __init__(self, *args, **kw):
116 super(IPythonWidget, self).__init__(*args, **kw)
116 super(IPythonWidget, self).__init__(*args, **kw)
117
117
118 # IPythonWidget protected variables.
118 # IPythonWidget protected variables.
119 self._payload_handlers = {
119 self._payload_handlers = {
120 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_edit : self._handle_payload_edit,
121 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_exit : self._handle_payload_exit,
122 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_page : self._handle_payload_page,
123 self._payload_source_next_input : self._handle_payload_next_input }
123 self._payload_source_next_input : self._handle_payload_next_input }
124 self._previous_prompt_obj = None
124 self._previous_prompt_obj = None
125 self._keep_kernel_on_exit = None
125 self._keep_kernel_on_exit = None
126
126
127 # Initialize widget styling.
127 # Initialize widget styling.
128 if self.style_sheet:
128 if self.style_sheet:
129 self._style_sheet_changed()
129 self._style_sheet_changed()
130 self._syntax_style_changed()
130 self._syntax_style_changed()
131 else:
131 else:
132 self.set_default_style()
132 self.set_default_style()
133
133
134 self._guiref_loaded = False
134 self._guiref_loaded = False
135
135
136 #---------------------------------------------------------------------------
136 #---------------------------------------------------------------------------
137 # 'BaseFrontendMixin' abstract interface
137 # 'BaseFrontendMixin' abstract interface
138 #---------------------------------------------------------------------------
138 #---------------------------------------------------------------------------
139 def _handle_complete_reply(self, rep):
139 def _handle_complete_reply(self, rep):
140 """ Reimplemented to support IPython's improved completion machinery.
140 """ Reimplemented to support IPython's improved completion machinery.
141 """
141 """
142 self.log.debug("complete: %s", rep.get('content', ''))
142 self.log.debug("complete: %s", rep.get('content', ''))
143 cursor = self._get_cursor()
143 cursor = self._get_cursor()
144 info = self._request_info.get('complete')
144 info = self._request_info.get('complete')
145 if info and info.id == rep['parent_header']['msg_id'] and \
145 if info and info.id == rep['parent_header']['msg_id'] and \
146 info.pos == cursor.position():
146 info.pos == cursor.position():
147 matches = rep['content']['matches']
147 matches = rep['content']['matches']
148 text = rep['content']['matched_text']
148 text = rep['content']['matched_text']
149 offset = len(text)
149 offset = len(text)
150
150
151 # Clean up matches with period and path separators if the matched
151 # Clean up matches with period and path separators if the matched
152 # text has not been transformed. This is done by truncating all
152 # text has not been transformed. This is done by truncating all
153 # but the last component and then suitably decreasing the offset
153 # but the last component and then suitably decreasing the offset
154 # between the current cursor position and the start of completion.
154 # between the current cursor position and the start of completion.
155 if len(matches) > 1 and matches[0][:offset] == text:
155 if len(matches) > 1 and matches[0][:offset] == text:
156 parts = re.split(r'[./\\]', text)
156 parts = re.split(r'[./\\]', text)
157 sep_count = len(parts) - 1
157 sep_count = len(parts) - 1
158 if sep_count:
158 if sep_count:
159 chop_length = sum(map(len, parts[:sep_count])) + sep_count
159 chop_length = sum(map(len, parts[:sep_count])) + sep_count
160 matches = [ match[chop_length:] for match in matches ]
160 matches = [ match[chop_length:] for match in matches ]
161 offset -= chop_length
161 offset -= chop_length
162
162
163 # Move the cursor to the start of the match and complete.
163 # Move the cursor to the start of the match and complete.
164 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
164 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
165 self._complete_with_items(cursor, matches)
165 self._complete_with_items(cursor, matches)
166
166
167 def _handle_execute_reply(self, msg):
167 def _handle_execute_reply(self, msg):
168 """ Reimplemented to support prompt requests.
168 """ Reimplemented to support prompt requests.
169 """
169 """
170 msg_id = msg['parent_header'].get('msg_id')
170 msg_id = msg['parent_header'].get('msg_id')
171 info = self._request_info['execute'].get(msg_id)
171 info = self._request_info['execute'].get(msg_id)
172 if info and info.kind == 'prompt':
172 if info and info.kind == 'prompt':
173 content = msg['content']
173 content = msg['content']
174 if content['status'] == 'aborted':
174 if content['status'] == 'aborted':
175 self._show_interpreter_prompt()
175 self._show_interpreter_prompt()
176 else:
176 else:
177 number = content['execution_count'] + 1
177 number = content['execution_count'] + 1
178 self._show_interpreter_prompt(number)
178 self._show_interpreter_prompt(number)
179 self._request_info['execute'].pop(msg_id)
179 self._request_info['execute'].pop(msg_id)
180 else:
180 else:
181 super(IPythonWidget, self)._handle_execute_reply(msg)
181 super(IPythonWidget, self)._handle_execute_reply(msg)
182
182
183 def _handle_history_reply(self, msg):
183 def _handle_history_reply(self, msg):
184 """ Implemented to handle history tail replies, which are only supported
184 """ Implemented to handle history tail replies, which are only supported
185 by the IPython kernel.
185 by the IPython kernel.
186 """
186 """
187 content = msg['content']
187 content = msg['content']
188 if 'history' not in content:
188 if 'history' not in content:
189 self.log.error("History request failed: %r"%content)
189 self.log.error("History request failed: %r"%content)
190 if content.get('status', '') == 'aborted' and \
190 if content.get('status', '') == 'aborted' and \
191 not self._retrying_history_request:
191 not self._retrying_history_request:
192 # a *different* action caused this request to be aborted, so
192 # a *different* action caused this request to be aborted, so
193 # we should try again.
193 # we should try again.
194 self.log.error("Retrying aborted history request")
194 self.log.error("Retrying aborted history request")
195 # prevent multiple retries of aborted requests:
195 # prevent multiple retries of aborted requests:
196 self._retrying_history_request = True
196 self._retrying_history_request = True
197 # wait out the kernel's queue flush, which is currently timed at 0.1s
197 # wait out the kernel's queue flush, which is currently timed at 0.1s
198 time.sleep(0.25)
198 time.sleep(0.25)
199 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
199 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
200 else:
200 else:
201 self._retrying_history_request = False
201 self._retrying_history_request = False
202 return
202 return
203 # reset retry flag
203 # reset retry flag
204 self._retrying_history_request = False
204 self._retrying_history_request = False
205 history_items = content['history']
205 history_items = content['history']
206 self.log.debug("Received history reply with %i entries", len(history_items))
206 self.log.debug("Received history reply with %i entries", len(history_items))
207 items = []
207 items = []
208 last_cell = u""
208 last_cell = u""
209 for _, _, cell in history_items:
209 for _, _, cell in history_items:
210 cell = cell.rstrip()
210 cell = cell.rstrip()
211 if cell != last_cell:
211 if cell != last_cell:
212 items.append(cell)
212 items.append(cell)
213 last_cell = cell
213 last_cell = cell
214 self._set_history(items)
214 self._set_history(items)
215
215
216 def _handle_execute_result(self, msg):
216 def _handle_execute_result(self, msg):
217 """ Reimplemented for IPython-style "display hook".
217 """ Reimplemented for IPython-style "display hook".
218 """
218 """
219 self.log.debug("execute_result: %s", msg.get('content', ''))
219 self.log.debug("execute_result: %s", msg.get('content', ''))
220 if not self._hidden and self._is_from_this_session(msg):
220 if not self._hidden and self._is_from_this_session(msg):
221 self.flush_clearoutput()
221 self.flush_clearoutput()
222 content = msg['content']
222 content = msg['content']
223 prompt_number = content.get('execution_count', 0)
223 prompt_number = content.get('execution_count', 0)
224 data = content['data']
224 data = content['data']
225 if 'text/plain' in data:
225 if 'text/plain' in data:
226 self._append_plain_text(self.output_sep, True)
226 self._append_plain_text(self.output_sep, True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
228 text = data['text/plain']
228 text = data['text/plain']
229 # If the repr is multiline, make sure we start on a new line,
229 # If the repr is multiline, make sure we start on a new line,
230 # so that its lines are aligned.
230 # so that its lines are aligned.
231 if "\n" in text and not self.output_sep.endswith("\n"):
231 if "\n" in text and not self.output_sep.endswith("\n"):
232 self._append_plain_text('\n', True)
232 self._append_plain_text('\n', True)
233 self._append_plain_text(text + self.output_sep2, True)
233 self._append_plain_text(text + self.output_sep2, True)
234
234
235 def _handle_display_data(self, msg):
235 def _handle_display_data(self, msg):
236 """ The base handler for the ``display_data`` message.
236 """ The base handler for the ``display_data`` message.
237 """
237 """
238 self.log.debug("display: %s", msg.get('content', ''))
238 self.log.debug("display: %s", msg.get('content', ''))
239 # For now, we don't display data from other frontends, but we
239 # For now, we don't display data from other frontends, but we
240 # eventually will as this allows all frontends to monitor the display
240 # eventually will as this allows all frontends to monitor the display
241 # data. But we need to figure out how to handle this in the GUI.
241 # data. But we need to figure out how to handle this in the GUI.
242 if not self._hidden and self._is_from_this_session(msg):
242 if not self._hidden and self._is_from_this_session(msg):
243 self.flush_clearoutput()
243 self.flush_clearoutput()
244 source = msg['content']['source']
244 source = msg['content']['source']
245 data = msg['content']['data']
245 data = msg['content']['data']
246 metadata = msg['content']['metadata']
246 metadata = msg['content']['metadata']
247 # In the regular IPythonWidget, we simply print the plain text
247 # In the regular IPythonWidget, we simply print the plain text
248 # representation.
248 # representation.
249 if 'text/plain' in data:
249 if 'text/plain' in data:
250 text = data['text/plain']
250 text = data['text/plain']
251 self._append_plain_text(text, True)
251 self._append_plain_text(text, True)
252 # This newline seems to be needed for text and html output.
252 # This newline seems to be needed for text and html output.
253 self._append_plain_text(u'\n', True)
253 self._append_plain_text(u'\n', True)
254
254
255 def _handle_kernel_info_reply(self, rep):
255 def _handle_kernel_info_reply(self, rep):
256 """ Handle kernel info replies.
256 """ Handle kernel info replies.
257 """
257 """
258 if not self._guiref_loaded:
258 if not self._guiref_loaded:
259 if rep['content'].get('language') == 'python':
259 if rep['content'].get('language') == 'python':
260 self._load_guiref_magic()
260 self._load_guiref_magic()
261 self._guiref_loaded = True
261 self._guiref_loaded = True
262
262
263 def _started_channels(self):
263 def _started_channels(self):
264 """Reimplemented to make a history request and load %guiref."""
264 """Reimplemented to make a history request and load %guiref."""
265 super(IPythonWidget, self)._started_channels()
265 super(IPythonWidget, self)._started_channels()
266
266
267 # The reply will trigger %guiref load provided language=='python'
267 # The reply will trigger %guiref load provided language=='python'
268 self.kernel_client.kernel_info()
268 self.kernel_client.kernel_info()
269
269
270 self.kernel_client.shell_channel.history(hist_access_type='tail',
270 self.kernel_client.shell_channel.history(hist_access_type='tail',
271 n=1000)
271 n=1000)
272
272
273 def _started_kernel(self):
273 def _started_kernel(self):
274 """Load %guiref when the kernel starts (if channels are also started).
274 """Load %guiref when the kernel starts (if channels are also started).
275
275
276 Principally triggered by kernel restart.
276 Principally triggered by kernel restart.
277 """
277 """
278 if self.kernel_client.shell_channel is not None:
278 if self.kernel_client.shell_channel is not None:
279 self._load_guiref_magic()
279 self._load_guiref_magic()
280
280
281 def _load_guiref_magic(self):
281 def _load_guiref_magic(self):
282 """Load %guiref magic."""
282 """Load %guiref magic."""
283 self.kernel_client.shell_channel.execute('\n'.join([
283 self.kernel_client.shell_channel.execute('\n'.join([
284 "try:",
284 "try:",
285 " _usage",
285 " _usage",
286 "except:",
286 "except:",
287 " from IPython.core import usage as _usage",
287 " from IPython.core import usage as _usage",
288 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
288 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
289 " del _usage",
289 " del _usage",
290 ]), silent=True)
290 ]), silent=True)
291
291
292 #---------------------------------------------------------------------------
292 #---------------------------------------------------------------------------
293 # 'ConsoleWidget' public interface
293 # 'ConsoleWidget' public interface
294 #---------------------------------------------------------------------------
294 #---------------------------------------------------------------------------
295
295
296 #---------------------------------------------------------------------------
296 #---------------------------------------------------------------------------
297 # 'FrontendWidget' public interface
297 # 'FrontendWidget' public interface
298 #---------------------------------------------------------------------------
298 #---------------------------------------------------------------------------
299
299
300 def execute_file(self, path, hidden=False):
300 def execute_file(self, path, hidden=False):
301 """ Reimplemented to use the 'run' magic.
301 """ Reimplemented to use the 'run' magic.
302 """
302 """
303 # Use forward slashes on Windows to avoid escaping each separator.
303 # Use forward slashes on Windows to avoid escaping each separator.
304 if sys.platform == 'win32':
304 if sys.platform == 'win32':
305 path = os.path.normpath(path).replace('\\', '/')
305 path = os.path.normpath(path).replace('\\', '/')
306
306
307 # Perhaps we should not be using %run directly, but while we
307 # Perhaps we should not be using %run directly, but while we
308 # are, it is necessary to quote or escape filenames containing spaces
308 # are, it is necessary to quote or escape filenames containing spaces
309 # or quotes.
309 # or quotes.
310
310
311 # In earlier code here, to minimize escaping, we sometimes quoted the
311 # In earlier code here, to minimize escaping, we sometimes quoted the
312 # filename with single quotes. But to do this, this code must be
312 # filename with single quotes. But to do this, this code must be
313 # platform-aware, because run uses shlex rather than python string
313 # platform-aware, because run uses shlex rather than python string
314 # parsing, so that:
314 # parsing, so that:
315 # * In Win: single quotes can be used in the filename without quoting,
315 # * In Win: single quotes can be used in the filename without quoting,
316 # and we cannot use single quotes to quote the filename.
316 # and we cannot use single quotes to quote the filename.
317 # * In *nix: we can escape double quotes in a double quoted filename,
317 # * In *nix: we can escape double quotes in a double quoted filename,
318 # but can't escape single quotes in a single quoted filename.
318 # but can't escape single quotes in a single quoted filename.
319
319
320 # So to keep this code non-platform-specific and simple, we now only
320 # So to keep this code non-platform-specific and simple, we now only
321 # use double quotes to quote filenames, and escape when needed:
321 # use double quotes to quote filenames, and escape when needed:
322 if ' ' in path or "'" in path or '"' in path:
322 if ' ' in path or "'" in path or '"' in path:
323 path = '"%s"' % path.replace('"', '\\"')
323 path = '"%s"' % path.replace('"', '\\"')
324 self.execute('%%run %s' % path, hidden=hidden)
324 self.execute('%%run %s' % path, hidden=hidden)
325
325
326 #---------------------------------------------------------------------------
326 #---------------------------------------------------------------------------
327 # 'FrontendWidget' protected interface
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 def _process_execute_error(self, msg):
330 def _process_execute_error(self, msg):
349 """ Reimplemented for IPython-style traceback formatting.
331 """ Reimplemented for IPython-style traceback formatting.
350 """
332 """
351 content = msg['content']
333 content = msg['content']
352 traceback = '\n'.join(content['traceback']) + '\n'
334 traceback = '\n'.join(content['traceback']) + '\n'
353 if False:
335 if False:
354 # FIXME: For now, tracebacks come as plain text, so we can't use
336 # FIXME: For now, tracebacks come as plain text, so we can't use
355 # the html renderer yet. Once we refactor ultratb to produce
337 # the html renderer yet. Once we refactor ultratb to produce
356 # properly styled tracebacks, this branch should be the default
338 # properly styled tracebacks, this branch should be the default
357 traceback = traceback.replace(' ', '&nbsp;')
339 traceback = traceback.replace(' ', '&nbsp;')
358 traceback = traceback.replace('\n', '<br/>')
340 traceback = traceback.replace('\n', '<br/>')
359
341
360 ename = content['ename']
342 ename = content['ename']
361 ename_styled = '<span class="error">%s</span>' % ename
343 ename_styled = '<span class="error">%s</span>' % ename
362 traceback = traceback.replace(ename, ename_styled)
344 traceback = traceback.replace(ename, ename_styled)
363
345
364 self._append_html(traceback)
346 self._append_html(traceback)
365 else:
347 else:
366 # This is the fallback for now, using plain text with ansi escapes
348 # This is the fallback for now, using plain text with ansi escapes
367 self._append_plain_text(traceback)
349 self._append_plain_text(traceback)
368
350
369 def _process_execute_payload(self, item):
351 def _process_execute_payload(self, item):
370 """ Reimplemented to dispatch payloads to handler methods.
352 """ Reimplemented to dispatch payloads to handler methods.
371 """
353 """
372 handler = self._payload_handlers.get(item['source'])
354 handler = self._payload_handlers.get(item['source'])
373 if handler is None:
355 if handler is None:
374 # We have no handler for this type of payload, simply ignore it
356 # We have no handler for this type of payload, simply ignore it
375 return False
357 return False
376 else:
358 else:
377 handler(item)
359 handler(item)
378 return True
360 return True
379
361
380 def _show_interpreter_prompt(self, number=None):
362 def _show_interpreter_prompt(self, number=None):
381 """ Reimplemented for IPython-style prompts.
363 """ Reimplemented for IPython-style prompts.
382 """
364 """
383 # If a number was not specified, make a prompt number request.
365 # If a number was not specified, make a prompt number request.
384 if number is None:
366 if number is None:
385 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
367 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
386 info = self._ExecutionRequest(msg_id, 'prompt')
368 info = self._ExecutionRequest(msg_id, 'prompt')
387 self._request_info['execute'][msg_id] = info
369 self._request_info['execute'][msg_id] = info
388 return
370 return
389
371
390 # Show a new prompt and save information about it so that it can be
372 # Show a new prompt and save information about it so that it can be
391 # updated later if the prompt number turns out to be wrong.
373 # updated later if the prompt number turns out to be wrong.
392 self._prompt_sep = self.input_sep
374 self._prompt_sep = self.input_sep
393 self._show_prompt(self._make_in_prompt(number), html=True)
375 self._show_prompt(self._make_in_prompt(number), html=True)
394 block = self._control.document().lastBlock()
376 block = self._control.document().lastBlock()
395 length = len(self._prompt)
377 length = len(self._prompt)
396 self._previous_prompt_obj = self._PromptBlock(block, length, number)
378 self._previous_prompt_obj = self._PromptBlock(block, length, number)
397
379
398 # Update continuation prompt to reflect (possibly) new prompt length.
380 # Update continuation prompt to reflect (possibly) new prompt length.
399 self._set_continuation_prompt(
381 self._set_continuation_prompt(
400 self._make_continuation_prompt(self._prompt), html=True)
382 self._make_continuation_prompt(self._prompt), html=True)
401
383
402 def _show_interpreter_prompt_for_reply(self, msg):
384 def _show_interpreter_prompt_for_reply(self, msg):
403 """ Reimplemented for IPython-style prompts.
385 """ Reimplemented for IPython-style prompts.
404 """
386 """
405 # Update the old prompt number if necessary.
387 # Update the old prompt number if necessary.
406 content = msg['content']
388 content = msg['content']
407 # abort replies do not have any keys:
389 # abort replies do not have any keys:
408 if content['status'] == 'aborted':
390 if content['status'] == 'aborted':
409 if self._previous_prompt_obj:
391 if self._previous_prompt_obj:
410 previous_prompt_number = self._previous_prompt_obj.number
392 previous_prompt_number = self._previous_prompt_obj.number
411 else:
393 else:
412 previous_prompt_number = 0
394 previous_prompt_number = 0
413 else:
395 else:
414 previous_prompt_number = content['execution_count']
396 previous_prompt_number = content['execution_count']
415 if self._previous_prompt_obj and \
397 if self._previous_prompt_obj and \
416 self._previous_prompt_obj.number != previous_prompt_number:
398 self._previous_prompt_obj.number != previous_prompt_number:
417 block = self._previous_prompt_obj.block
399 block = self._previous_prompt_obj.block
418
400
419 # Make sure the prompt block has not been erased.
401 # Make sure the prompt block has not been erased.
420 if block.isValid() and block.text():
402 if block.isValid() and block.text():
421
403
422 # Remove the old prompt and insert a new prompt.
404 # Remove the old prompt and insert a new prompt.
423 cursor = QtGui.QTextCursor(block)
405 cursor = QtGui.QTextCursor(block)
424 cursor.movePosition(QtGui.QTextCursor.Right,
406 cursor.movePosition(QtGui.QTextCursor.Right,
425 QtGui.QTextCursor.KeepAnchor,
407 QtGui.QTextCursor.KeepAnchor,
426 self._previous_prompt_obj.length)
408 self._previous_prompt_obj.length)
427 prompt = self._make_in_prompt(previous_prompt_number)
409 prompt = self._make_in_prompt(previous_prompt_number)
428 self._prompt = self._insert_html_fetching_plain_text(
410 self._prompt = self._insert_html_fetching_plain_text(
429 cursor, prompt)
411 cursor, prompt)
430
412
431 # When the HTML is inserted, Qt blows away the syntax
413 # When the HTML is inserted, Qt blows away the syntax
432 # highlighting for the line, so we need to rehighlight it.
414 # highlighting for the line, so we need to rehighlight it.
433 self._highlighter.rehighlightBlock(cursor.block())
415 self._highlighter.rehighlightBlock(cursor.block())
434
416
435 self._previous_prompt_obj = None
417 self._previous_prompt_obj = None
436
418
437 # Show a new prompt with the kernel's estimated prompt number.
419 # Show a new prompt with the kernel's estimated prompt number.
438 self._show_interpreter_prompt(previous_prompt_number + 1)
420 self._show_interpreter_prompt(previous_prompt_number + 1)
439
421
440 #---------------------------------------------------------------------------
422 #---------------------------------------------------------------------------
441 # 'IPythonWidget' interface
423 # 'IPythonWidget' interface
442 #---------------------------------------------------------------------------
424 #---------------------------------------------------------------------------
443
425
444 def set_default_style(self, colors='lightbg'):
426 def set_default_style(self, colors='lightbg'):
445 """ Sets the widget style to the class defaults.
427 """ Sets the widget style to the class defaults.
446
428
447 Parameters
429 Parameters
448 ----------
430 ----------
449 colors : str, optional (default lightbg)
431 colors : str, optional (default lightbg)
450 Whether to use the default IPython light background or dark
432 Whether to use the default IPython light background or dark
451 background or B&W style.
433 background or B&W style.
452 """
434 """
453 colors = colors.lower()
435 colors = colors.lower()
454 if colors=='lightbg':
436 if colors=='lightbg':
455 self.style_sheet = styles.default_light_style_sheet
437 self.style_sheet = styles.default_light_style_sheet
456 self.syntax_style = styles.default_light_syntax_style
438 self.syntax_style = styles.default_light_syntax_style
457 elif colors=='linux':
439 elif colors=='linux':
458 self.style_sheet = styles.default_dark_style_sheet
440 self.style_sheet = styles.default_dark_style_sheet
459 self.syntax_style = styles.default_dark_syntax_style
441 self.syntax_style = styles.default_dark_syntax_style
460 elif colors=='nocolor':
442 elif colors=='nocolor':
461 self.style_sheet = styles.default_bw_style_sheet
443 self.style_sheet = styles.default_bw_style_sheet
462 self.syntax_style = styles.default_bw_syntax_style
444 self.syntax_style = styles.default_bw_syntax_style
463 else:
445 else:
464 raise KeyError("No such color scheme: %s"%colors)
446 raise KeyError("No such color scheme: %s"%colors)
465
447
466 #---------------------------------------------------------------------------
448 #---------------------------------------------------------------------------
467 # 'IPythonWidget' protected interface
449 # 'IPythonWidget' protected interface
468 #---------------------------------------------------------------------------
450 #---------------------------------------------------------------------------
469
451
470 def _edit(self, filename, line=None):
452 def _edit(self, filename, line=None):
471 """ Opens a Python script for editing.
453 """ Opens a Python script for editing.
472
454
473 Parameters
455 Parameters
474 ----------
456 ----------
475 filename : str
457 filename : str
476 A path to a local system file.
458 A path to a local system file.
477
459
478 line : int, optional
460 line : int, optional
479 A line of interest in the file.
461 A line of interest in the file.
480 """
462 """
481 if self.custom_edit:
463 if self.custom_edit:
482 self.custom_edit_requested.emit(filename, line)
464 self.custom_edit_requested.emit(filename, line)
483 elif not self.editor:
465 elif not self.editor:
484 self._append_plain_text('No default editor available.\n'
466 self._append_plain_text('No default editor available.\n'
485 'Specify a GUI text editor in the `IPythonWidget.editor` '
467 'Specify a GUI text editor in the `IPythonWidget.editor` '
486 'configurable to enable the %edit magic')
468 'configurable to enable the %edit magic')
487 else:
469 else:
488 try:
470 try:
489 filename = '"%s"' % filename
471 filename = '"%s"' % filename
490 if line and self.editor_line:
472 if line and self.editor_line:
491 command = self.editor_line.format(filename=filename,
473 command = self.editor_line.format(filename=filename,
492 line=line)
474 line=line)
493 else:
475 else:
494 try:
476 try:
495 command = self.editor.format()
477 command = self.editor.format()
496 except KeyError:
478 except KeyError:
497 command = self.editor.format(filename=filename)
479 command = self.editor.format(filename=filename)
498 else:
480 else:
499 command += ' ' + filename
481 command += ' ' + filename
500 except KeyError:
482 except KeyError:
501 self._append_plain_text('Invalid editor command.\n')
483 self._append_plain_text('Invalid editor command.\n')
502 else:
484 else:
503 try:
485 try:
504 Popen(command, shell=True)
486 Popen(command, shell=True)
505 except OSError:
487 except OSError:
506 msg = 'Opening editor with command "%s" failed.\n'
488 msg = 'Opening editor with command "%s" failed.\n'
507 self._append_plain_text(msg % command)
489 self._append_plain_text(msg % command)
508
490
509 def _make_in_prompt(self, number):
491 def _make_in_prompt(self, number):
510 """ Given a prompt number, returns an HTML In prompt.
492 """ Given a prompt number, returns an HTML In prompt.
511 """
493 """
512 try:
494 try:
513 body = self.in_prompt % number
495 body = self.in_prompt % number
514 except TypeError:
496 except TypeError:
515 # allow in_prompt to leave out number, e.g. '>>> '
497 # allow in_prompt to leave out number, e.g. '>>> '
516 body = self.in_prompt
498 body = self.in_prompt
517 return '<span class="in-prompt">%s</span>' % body
499 return '<span class="in-prompt">%s</span>' % body
518
500
519 def _make_continuation_prompt(self, prompt):
501 def _make_continuation_prompt(self, prompt):
520 """ Given a plain text version of an In prompt, returns an HTML
502 """ Given a plain text version of an In prompt, returns an HTML
521 continuation prompt.
503 continuation prompt.
522 """
504 """
523 end_chars = '...: '
505 end_chars = '...: '
524 space_count = len(prompt.lstrip('\n')) - len(end_chars)
506 space_count = len(prompt.lstrip('\n')) - len(end_chars)
525 body = '&nbsp;' * space_count + end_chars
507 body = '&nbsp;' * space_count + end_chars
526 return '<span class="in-prompt">%s</span>' % body
508 return '<span class="in-prompt">%s</span>' % body
527
509
528 def _make_out_prompt(self, number):
510 def _make_out_prompt(self, number):
529 """ Given a prompt number, returns an HTML Out prompt.
511 """ Given a prompt number, returns an HTML Out prompt.
530 """
512 """
531 body = self.out_prompt % number
513 body = self.out_prompt % number
532 return '<span class="out-prompt">%s</span>' % body
514 return '<span class="out-prompt">%s</span>' % body
533
515
534 #------ Payload handlers --------------------------------------------------
516 #------ Payload handlers --------------------------------------------------
535
517
536 # Payload handlers with a generic interface: each takes the opaque payload
518 # Payload handlers with a generic interface: each takes the opaque payload
537 # dict, unpacks it and calls the underlying functions with the necessary
519 # dict, unpacks it and calls the underlying functions with the necessary
538 # arguments.
520 # arguments.
539
521
540 def _handle_payload_edit(self, item):
522 def _handle_payload_edit(self, item):
541 self._edit(item['filename'], item['line_number'])
523 self._edit(item['filename'], item['line_number'])
542
524
543 def _handle_payload_exit(self, item):
525 def _handle_payload_exit(self, item):
544 self._keep_kernel_on_exit = item['keepkernel']
526 self._keep_kernel_on_exit = item['keepkernel']
545 self.exit_requested.emit(self)
527 self.exit_requested.emit(self)
546
528
547 def _handle_payload_next_input(self, item):
529 def _handle_payload_next_input(self, item):
548 self.input_buffer = item['text']
530 self.input_buffer = item['text']
549
531
550 def _handle_payload_page(self, item):
532 def _handle_payload_page(self, item):
551 # Since the plain text widget supports only a very small subset of HTML
533 # Since the plain text widget supports only a very small subset of HTML
552 # and we have no control over the HTML source, we only page HTML
534 # and we have no control over the HTML source, we only page HTML
553 # payloads in the rich text widget.
535 # payloads in the rich text widget.
554 if item['html'] and self.kind == 'rich':
536 if item['html'] and self.kind == 'rich':
555 self._page(item['html'], html=True)
537 self._page(item['html'], html=True)
556 else:
538 else:
557 self._page(item['text'], html=False)
539 self._page(item['text'], html=False)
558
540
559 #------ Trait change handlers --------------------------------------------
541 #------ Trait change handlers --------------------------------------------
560
542
561 def _style_sheet_changed(self):
543 def _style_sheet_changed(self):
562 """ Set the style sheets of the underlying widgets.
544 """ Set the style sheets of the underlying widgets.
563 """
545 """
564 self.setStyleSheet(self.style_sheet)
546 self.setStyleSheet(self.style_sheet)
565 if self._control is not None:
547 if self._control is not None:
566 self._control.document().setDefaultStyleSheet(self.style_sheet)
548 self._control.document().setDefaultStyleSheet(self.style_sheet)
567 bg_color = self._control.palette().window().color()
549 bg_color = self._control.palette().window().color()
568 self._ansi_processor.set_background_color(bg_color)
550 self._ansi_processor.set_background_color(bg_color)
569
551
570 if self._page_control is not None:
552 if self._page_control is not None:
571 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
553 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
572
554
573
555
574
556
575 def _syntax_style_changed(self):
557 def _syntax_style_changed(self):
576 """ Set the style for the syntax highlighter.
558 """ Set the style for the syntax highlighter.
577 """
559 """
578 if self._highlighter is None:
560 if self._highlighter is None:
579 # ignore premature calls
561 # ignore premature calls
580 return
562 return
581 if self.syntax_style:
563 if self.syntax_style:
582 self._highlighter.set_style(self.syntax_style)
564 self._highlighter.set_style(self.syntax_style)
583 else:
565 else:
584 self._highlighter.set_style_sheet(self.style_sheet)
566 self._highlighter.set_style_sheet(self.style_sheet)
585
567
586 #------ Trait default initializers -----------------------------------------
568 #------ Trait default initializers -----------------------------------------
587
569
588 def _banner_default(self):
570 def _banner_default(self):
589 from IPython.core.usage import default_gui_banner
571 from IPython.core.usage import default_gui_banner
590 return default_gui_banner
572 return default_gui_banner
@@ -1,58 +1,63 b''
1 """Adapt readline completer interface to make ZMQ request.
2 """
3 # -*- coding: utf-8 -*-
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 try:
7 try:
5 from queue import Empty # Py 3
8 from queue import Empty # Py 3
6 except ImportError:
9 except ImportError:
7 from Queue import Empty # Py 2
10 from Queue import Empty # Py 2
8
11
9 from IPython.config import Configurable
12 from IPython.config import Configurable
10 from IPython.core.completer import IPCompleter
13 from IPython.core.completer import IPCompleter
11 from IPython.utils.traitlets import Float
14 from IPython.utils.traitlets import Float
12 import IPython.utils.rlineimpl as readline
15 import IPython.utils.rlineimpl as readline
13
16
14 class ZMQCompleter(IPCompleter):
17 class ZMQCompleter(IPCompleter):
15 """Client-side completion machinery.
18 """Client-side completion machinery.
16
19
17 How it works: self.complete will be called multiple times, with
20 How it works: self.complete will be called multiple times, with
18 state=0,1,2,... When state=0 it should compute ALL the completion matches,
21 state=0,1,2,... When state=0 it should compute ALL the completion matches,
19 and then return them for each value of state."""
22 and then return them for each value of state."""
20
23
21 timeout = Float(5.0, config=True, help='timeout before completion abort')
24 timeout = Float(5.0, config=True, help='timeout before completion abort')
22
25
23 def __init__(self, shell, client, config=None):
26 def __init__(self, shell, client, config=None):
24 super(ZMQCompleter,self).__init__(config=config)
27 super(ZMQCompleter,self).__init__(config=config)
25
28
26 self.shell = shell
29 self.shell = shell
27 self.client = client
30 self.client = client
28 self.matches = []
31 self.matches = []
29
32
30 def complete_request(self,text):
33 def complete_request(self, text):
31 line = readline.get_line_buffer()
34 line = readline.get_line_buffer()
32 cursor_pos = readline.get_endidx()
35 cursor_pos = readline.get_endidx()
33
36
34 # send completion request to kernel
37 # send completion request to kernel
35 # Give the kernel up to 0.5s to respond
38 # Give the kernel up to 0.5s to respond
36 msg_id = self.client.shell_channel.complete(text=text, line=line,
39 msg_id = self.client.shell_channel.complete(
37 cursor_pos=cursor_pos)
40 code=line,
41 cursor_pos=cursor_pos,
42 )
38
43
39 msg = self.client.shell_channel.get_msg(timeout=self.timeout)
44 msg = self.client.shell_channel.get_msg(timeout=self.timeout)
40 if msg['parent_header']['msg_id'] == msg_id:
45 if msg['parent_header']['msg_id'] == msg_id:
41 return msg["content"]["matches"]
46 return msg["content"]["matches"]
42 return []
47 return []
43
48
44 def rlcomplete(self, text, state):
49 def rlcomplete(self, text, state):
45 if state == 0:
50 if state == 0:
46 try:
51 try:
47 self.matches = self.complete_request(text)
52 self.matches = self.complete_request(text)
48 except Empty:
53 except Empty:
49 #print('WARNING: Kernel timeout on tab completion.')
54 #print('WARNING: Kernel timeout on tab completion.')
50 pass
55 pass
51
56
52 try:
57 try:
53 return self.matches[state]
58 return self.matches[state]
54 except IndexError:
59 except IndexError:
55 return None
60 return None
56
61
57 def complete(self, text, line, cursor_pos=None):
62 def complete(self, text, line, cursor_pos=None):
58 return self.rlcomplete(text, 0)
63 return self.rlcomplete(text, 0)
@@ -1,56 +1,63 b''
1 """Tests for tokenutil"""
1 """Tests for tokenutil"""
2 # Copyright (c) IPython Development Team.
2 # Copyright (c) IPython Development Team.
3 # Distributed under the terms of the Modified BSD License.
3 # Distributed under the terms of the Modified BSD License.
4
4
5 import nose.tools as nt
5 import nose.tools as nt
6
6
7 from IPython.utils.tokenutil import token_at_cursor
7 from IPython.utils.tokenutil import token_at_cursor
8
8
9 def expect_token(expected, cell, column, line=0):
9 def expect_token(expected, cell, cursor_pos):
10 token = token_at_cursor(cell, column, line)
10 token = token_at_cursor(cell, cursor_pos)
11
11 offset = 0
12 lines = cell.splitlines()
12 for line in cell.splitlines():
13 line_with_cursor = '%s|%s' % (lines[line][:column], lines[line][column:])
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 line
19 line
15 nt.assert_equal(token, expected,
20 nt.assert_equal(token, expected,
16 "Excpected %r, got %r in: %s" % (
21 "Excpected %r, got %r in: %s" % (
17 expected, token, line_with_cursor)
22 expected, token, line_with_cursor)
18 )
23 )
19
24
20 def test_simple():
25 def test_simple():
21 cell = "foo"
26 cell = "foo"
22 for i in range(len(cell)):
27 for i in range(len(cell)):
23 expect_token("foo", cell, i)
28 expect_token("foo", cell, i)
24
29
25 def test_function():
30 def test_function():
26 cell = "foo(a=5, b='10')"
31 cell = "foo(a=5, b='10')"
27 expected = 'foo'
32 expected = 'foo'
28 for i in (6,7,8,10,11,12):
33 for i in (6,7,8,10,11,12):
29 expect_token("foo", cell, i)
34 expect_token("foo", cell, i)
30
35
31 def test_multiline():
36 def test_multiline():
32 cell = '\n'.join([
37 cell = '\n'.join([
33 'a = 5',
38 'a = 5',
34 'b = hello("string", there)'
39 'b = hello("string", there)'
35 ])
40 ])
36 expected = 'hello'
41 expected = 'hello'
37 for i in range(4,9):
42 start = cell.index(expected)
38 expect_token(expected, cell, i, 1)
43 for i in range(start, start + len(expected)):
44 expect_token(expected, cell, i)
39 expected = 'there'
45 expected = 'there'
40 for i in range(21,27):
46 start = cell.index(expected)
41 expect_token(expected, cell, i, 1)
47 for i in range(start, start + len(expected)):
48 expect_token(expected, cell, i)
42
49
43 def test_attrs():
50 def test_attrs():
44 cell = "foo(a=obj.attr.subattr)"
51 cell = "foo(a=obj.attr.subattr)"
45 expected = 'obj'
52 expected = 'obj'
46 idx = cell.find('obj')
53 idx = cell.find('obj')
47 for i in range(idx, idx + 3):
54 for i in range(idx, idx + 3):
48 expect_token(expected, cell, i)
55 expect_token(expected, cell, i)
49 idx = idx + 4
56 idx = idx + 4
50 expected = 'obj.attr'
57 expected = 'obj.attr'
51 for i in range(idx, idx + 4):
58 for i in range(idx, idx + 4):
52 expect_token(expected, cell, i)
59 expect_token(expected, cell, i)
53 idx = idx + 5
60 idx = idx + 5
54 expected = 'obj.attr.subattr'
61 expected = 'obj.attr.subattr'
55 for i in range(idx, len(cell)):
62 for i in range(idx, len(cell)):
56 expect_token(expected, cell, i)
63 expect_token(expected, cell, i)
@@ -1,80 +1,78 b''
1 """Token-related utilities"""
1 """Token-related utilities"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import absolute_import, print_function
6 from __future__ import absolute_import, print_function
7
7
8 from collections import namedtuple
8 from collections import namedtuple
9 from io import StringIO
9 from io import StringIO
10 from keyword import iskeyword
10 from keyword import iskeyword
11
11
12 from . import tokenize2
12 from . import tokenize2
13 from .py3compat import cast_unicode_py2
13 from .py3compat import cast_unicode_py2
14
14
15 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
15 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
16
16
17 def generate_tokens(readline):
17 def generate_tokens(readline):
18 """wrap generate_tokens to catch EOF errors"""
18 """wrap generate_tokens to catch EOF errors"""
19 try:
19 try:
20 for token in tokenize2.generate_tokens(readline):
20 for token in tokenize2.generate_tokens(readline):
21 yield token
21 yield token
22 except tokenize2.TokenError:
22 except tokenize2.TokenError:
23 # catch EOF error
23 # catch EOF error
24 return
24 return
25
25
26 def token_at_cursor(cell, column, line=0):
26 def token_at_cursor(cell, cursor_pos=0):
27 """Get the token at a given cursor
27 """Get the token at a given cursor
28
28
29 Used for introspection.
29 Used for introspection.
30
30
31 Parameters
31 Parameters
32 ----------
32 ----------
33
33
34 cell : unicode
34 cell : unicode
35 A block of Python code
35 A block of Python code
36 column : int
36 cursor_pos : int
37 The column of the cursor offset, where the token should be found
37 The location of the cursor in the block 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)
40 """
38 """
41 cell = cast_unicode_py2(cell)
39 cell = cast_unicode_py2(cell)
42 names = []
40 names = []
43 tokens = []
41 tokens = []
44 current_line = 0
42 offset = 0
45 for tup in generate_tokens(StringIO(cell).readline):
43 for tup in generate_tokens(StringIO(cell).readline):
46
44
47 tok = Token(*tup)
45 tok = Token(*tup)
48
46
49 # token, text, start, end, line = tup
47 # token, text, start, end, line = tup
50 start_col = tok.start[1]
48 start_col = tok.start[1]
51 end_col = tok.end[1]
49 end_col = tok.end[1]
52 if line == current_line and start_col > column:
50 if offset + start_col > cursor_pos:
53 # current token starts after the cursor,
51 # current token starts after the cursor,
54 # don't consume it
52 # don't consume it
55 break
53 break
56
54
57 if tok.token == tokenize2.NAME and not iskeyword(tok.text):
55 if tok.token == tokenize2.NAME and not iskeyword(tok.text):
58 if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.':
56 if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.':
59 names[-1] = "%s.%s" % (names[-1], tok.text)
57 names[-1] = "%s.%s" % (names[-1], tok.text)
60 else:
58 else:
61 names.append(tok.text)
59 names.append(tok.text)
62 elif tok.token == tokenize2.OP:
60 elif tok.token == tokenize2.OP:
63 if tok.text == '=' and names:
61 if tok.text == '=' and names:
64 # don't inspect the lhs of an assignment
62 # don't inspect the lhs of an assignment
65 names.pop(-1)
63 names.pop(-1)
66
64
67 if line == current_line and end_col > column:
65 if offset + end_col > cursor_pos:
68 # we found the cursor, stop reading
66 # we found the cursor, stop reading
69 break
67 break
70
68
71 tokens.append(tok)
69 tokens.append(tok)
72 if tok.token == tokenize2.NEWLINE:
70 if tok.token == tokenize2.NEWLINE:
73 current_line += 1
71 offset += len(tok.line)
74
72
75 if names:
73 if names:
76 return names[-1]
74 return names[-1]
77 else:
75 else:
78 return ''
76 return ''
79
77
80
78
General Comments 0
You need to be logged in to leave comments. Login now