##// END OF EJS Templates
code updates per review of PR #454
MinRK -
Show More
@@ -1,514 +1,524 b''
1 """A simple configuration system.
1 """A simple configuration system.
2
2
3 Authors
3 Authors
4 -------
4 -------
5 * Brian Granger
5 * Brian Granger
6 * Fernando Perez
6 * Fernando Perez
7 * Min RK
7 * Min RK
8 """
8 """
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2008-2011 The IPython Development Team
11 # Copyright (C) 2008-2011 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 import __builtin__
21 import __builtin__
22 import re
22 import re
23 import sys
23 import sys
24
24
25 from IPython.external import argparse
25 from IPython.external import argparse
26 from IPython.utils.path import filefind
26 from IPython.utils.path import filefind
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Exceptions
29 # Exceptions
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class ConfigError(Exception):
33 class ConfigError(Exception):
34 pass
34 pass
35
35
36
36
37 class ConfigLoaderError(ConfigError):
37 class ConfigLoaderError(ConfigError):
38 pass
38 pass
39
39
40 class ArgumentError(ConfigLoaderError):
40 class ArgumentError(ConfigLoaderError):
41 pass
41 pass
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Argparse fix
44 # Argparse fix
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 # Unfortunately argparse by default prints help messages to stderr instead of
47 # Unfortunately argparse by default prints help messages to stderr instead of
48 # stdout. This makes it annoying to capture long help screens at the command
48 # stdout. This makes it annoying to capture long help screens at the command
49 # line, since one must know how to pipe stderr, which many users don't know how
49 # line, since one must know how to pipe stderr, which many users don't know how
50 # to do. So we override the print_help method with one that defaults to
50 # to do. So we override the print_help method with one that defaults to
51 # stdout and use our class instead.
51 # stdout and use our class instead.
52
52
53 class ArgumentParser(argparse.ArgumentParser):
53 class ArgumentParser(argparse.ArgumentParser):
54 """Simple argparse subclass that prints help to stdout by default."""
54 """Simple argparse subclass that prints help to stdout by default."""
55
55
56 def print_help(self, file=None):
56 def print_help(self, file=None):
57 if file is None:
57 if file is None:
58 file = sys.stdout
58 file = sys.stdout
59 return super(ArgumentParser, self).print_help(file)
59 return super(ArgumentParser, self).print_help(file)
60
60
61 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
61 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
62
62
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64 # Config class for holding config information
64 # Config class for holding config information
65 #-----------------------------------------------------------------------------
65 #-----------------------------------------------------------------------------
66
66
67
67
68 class Config(dict):
68 class Config(dict):
69 """An attribute based dict that can do smart merges."""
69 """An attribute based dict that can do smart merges."""
70
70
71 def __init__(self, *args, **kwds):
71 def __init__(self, *args, **kwds):
72 dict.__init__(self, *args, **kwds)
72 dict.__init__(self, *args, **kwds)
73 # This sets self.__dict__ = self, but it has to be done this way
73 # This sets self.__dict__ = self, but it has to be done this way
74 # because we are also overriding __setattr__.
74 # because we are also overriding __setattr__.
75 dict.__setattr__(self, '__dict__', self)
75 dict.__setattr__(self, '__dict__', self)
76
76
77 def _merge(self, other):
77 def _merge(self, other):
78 to_update = {}
78 to_update = {}
79 for k, v in other.iteritems():
79 for k, v in other.iteritems():
80 if not self.has_key(k):
80 if not self.has_key(k):
81 to_update[k] = v
81 to_update[k] = v
82 else: # I have this key
82 else: # I have this key
83 if isinstance(v, Config):
83 if isinstance(v, Config):
84 # Recursively merge common sub Configs
84 # Recursively merge common sub Configs
85 self[k]._merge(v)
85 self[k]._merge(v)
86 else:
86 else:
87 # Plain updates for non-Configs
87 # Plain updates for non-Configs
88 to_update[k] = v
88 to_update[k] = v
89
89
90 self.update(to_update)
90 self.update(to_update)
91
91
92 def _is_section_key(self, key):
92 def _is_section_key(self, key):
93 if key[0].upper()==key[0] and not key.startswith('_'):
93 if key[0].upper()==key[0] and not key.startswith('_'):
94 return True
94 return True
95 else:
95 else:
96 return False
96 return False
97
97
98 def __contains__(self, key):
98 def __contains__(self, key):
99 if self._is_section_key(key):
99 if self._is_section_key(key):
100 return True
100 return True
101 else:
101 else:
102 return super(Config, self).__contains__(key)
102 return super(Config, self).__contains__(key)
103 # .has_key is deprecated for dictionaries.
103 # .has_key is deprecated for dictionaries.
104 has_key = __contains__
104 has_key = __contains__
105
105
106 def _has_section(self, key):
106 def _has_section(self, key):
107 if self._is_section_key(key):
107 if self._is_section_key(key):
108 if super(Config, self).__contains__(key):
108 if super(Config, self).__contains__(key):
109 return True
109 return True
110 return False
110 return False
111
111
112 def copy(self):
112 def copy(self):
113 return type(self)(dict.copy(self))
113 return type(self)(dict.copy(self))
114
114
115 def __copy__(self):
115 def __copy__(self):
116 return self.copy()
116 return self.copy()
117
117
118 def __deepcopy__(self, memo):
118 def __deepcopy__(self, memo):
119 import copy
119 import copy
120 return type(self)(copy.deepcopy(self.items()))
120 return type(self)(copy.deepcopy(self.items()))
121
121
122 def __getitem__(self, key):
122 def __getitem__(self, key):
123 # We cannot use directly self._is_section_key, because it triggers
123 # We cannot use directly self._is_section_key, because it triggers
124 # infinite recursion on top of PyPy. Instead, we manually fish the
124 # infinite recursion on top of PyPy. Instead, we manually fish the
125 # bound method.
125 # bound method.
126 is_section_key = self.__class__._is_section_key.__get__(self)
126 is_section_key = self.__class__._is_section_key.__get__(self)
127
127
128 # Because we use this for an exec namespace, we need to delegate
128 # Because we use this for an exec namespace, we need to delegate
129 # the lookup of names in __builtin__ to itself. This means
129 # the lookup of names in __builtin__ to itself. This means
130 # that you can't have section or attribute names that are
130 # that you can't have section or attribute names that are
131 # builtins.
131 # builtins.
132 try:
132 try:
133 return getattr(__builtin__, key)
133 return getattr(__builtin__, key)
134 except AttributeError:
134 except AttributeError:
135 pass
135 pass
136 if is_section_key(key):
136 if is_section_key(key):
137 try:
137 try:
138 return dict.__getitem__(self, key)
138 return dict.__getitem__(self, key)
139 except KeyError:
139 except KeyError:
140 c = Config()
140 c = Config()
141 dict.__setitem__(self, key, c)
141 dict.__setitem__(self, key, c)
142 return c
142 return c
143 else:
143 else:
144 return dict.__getitem__(self, key)
144 return dict.__getitem__(self, key)
145
145
146 def __setitem__(self, key, value):
146 def __setitem__(self, key, value):
147 # Don't allow names in __builtin__ to be modified.
147 # Don't allow names in __builtin__ to be modified.
148 if hasattr(__builtin__, key):
148 if hasattr(__builtin__, key):
149 raise ConfigError('Config variable names cannot have the same name '
149 raise ConfigError('Config variable names cannot have the same name '
150 'as a Python builtin: %s' % key)
150 'as a Python builtin: %s' % key)
151 if self._is_section_key(key):
151 if self._is_section_key(key):
152 if not isinstance(value, Config):
152 if not isinstance(value, Config):
153 raise ValueError('values whose keys begin with an uppercase '
153 raise ValueError('values whose keys begin with an uppercase '
154 'char must be Config instances: %r, %r' % (key, value))
154 'char must be Config instances: %r, %r' % (key, value))
155 else:
155 else:
156 dict.__setitem__(self, key, value)
156 dict.__setitem__(self, key, value)
157
157
158 def __getattr__(self, key):
158 def __getattr__(self, key):
159 try:
159 try:
160 return self.__getitem__(key)
160 return self.__getitem__(key)
161 except KeyError, e:
161 except KeyError, e:
162 raise AttributeError(e)
162 raise AttributeError(e)
163
163
164 def __setattr__(self, key, value):
164 def __setattr__(self, key, value):
165 try:
165 try:
166 self.__setitem__(key, value)
166 self.__setitem__(key, value)
167 except KeyError, e:
167 except KeyError, e:
168 raise AttributeError(e)
168 raise AttributeError(e)
169
169
170 def __delattr__(self, key):
170 def __delattr__(self, key):
171 try:
171 try:
172 dict.__delitem__(self, key)
172 dict.__delitem__(self, key)
173 except KeyError, e:
173 except KeyError, e:
174 raise AttributeError(e)
174 raise AttributeError(e)
175
175
176
176
177 #-----------------------------------------------------------------------------
177 #-----------------------------------------------------------------------------
178 # Config loading classes
178 # Config loading classes
179 #-----------------------------------------------------------------------------
179 #-----------------------------------------------------------------------------
180
180
181
181
182 class ConfigLoader(object):
182 class ConfigLoader(object):
183 """A object for loading configurations from just about anywhere.
183 """A object for loading configurations from just about anywhere.
184
184
185 The resulting configuration is packaged as a :class:`Struct`.
185 The resulting configuration is packaged as a :class:`Struct`.
186
186
187 Notes
187 Notes
188 -----
188 -----
189 A :class:`ConfigLoader` does one thing: load a config from a source
189 A :class:`ConfigLoader` does one thing: load a config from a source
190 (file, command line arguments) and returns the data as a :class:`Struct`.
190 (file, command line arguments) and returns the data as a :class:`Struct`.
191 There are lots of things that :class:`ConfigLoader` does not do. It does
191 There are lots of things that :class:`ConfigLoader` does not do. It does
192 not implement complex logic for finding config files. It does not handle
192 not implement complex logic for finding config files. It does not handle
193 default values or merge multiple configs. These things need to be
193 default values or merge multiple configs. These things need to be
194 handled elsewhere.
194 handled elsewhere.
195 """
195 """
196
196
197 def __init__(self):
197 def __init__(self):
198 """A base class for config loaders.
198 """A base class for config loaders.
199
199
200 Examples
200 Examples
201 --------
201 --------
202
202
203 >>> cl = ConfigLoader()
203 >>> cl = ConfigLoader()
204 >>> config = cl.load_config()
204 >>> config = cl.load_config()
205 >>> config
205 >>> config
206 {}
206 {}
207 """
207 """
208 self.clear()
208 self.clear()
209
209
210 def clear(self):
210 def clear(self):
211 self.config = Config()
211 self.config = Config()
212
212
213 def load_config(self):
213 def load_config(self):
214 """Load a config from somewhere, return a :class:`Config` instance.
214 """Load a config from somewhere, return a :class:`Config` instance.
215
215
216 Usually, this will cause self.config to be set and then returned.
216 Usually, this will cause self.config to be set and then returned.
217 However, in most cases, :meth:`ConfigLoader.clear` should be called
217 However, in most cases, :meth:`ConfigLoader.clear` should be called
218 to erase any previous state.
218 to erase any previous state.
219 """
219 """
220 self.clear()
220 self.clear()
221 return self.config
221 return self.config
222
222
223
223
224 class FileConfigLoader(ConfigLoader):
224 class FileConfigLoader(ConfigLoader):
225 """A base class for file based configurations.
225 """A base class for file based configurations.
226
226
227 As we add more file based config loaders, the common logic should go
227 As we add more file based config loaders, the common logic should go
228 here.
228 here.
229 """
229 """
230 pass
230 pass
231
231
232
232
233 class PyFileConfigLoader(FileConfigLoader):
233 class PyFileConfigLoader(FileConfigLoader):
234 """A config loader for pure python files.
234 """A config loader for pure python files.
235
235
236 This calls execfile on a plain python file and looks for attributes
236 This calls execfile on a plain python file and looks for attributes
237 that are all caps. These attribute are added to the config Struct.
237 that are all caps. These attribute are added to the config Struct.
238 """
238 """
239
239
240 def __init__(self, filename, path=None):
240 def __init__(self, filename, path=None):
241 """Build a config loader for a filename and path.
241 """Build a config loader for a filename and path.
242
242
243 Parameters
243 Parameters
244 ----------
244 ----------
245 filename : str
245 filename : str
246 The file name of the config file.
246 The file name of the config file.
247 path : str, list, tuple
247 path : str, list, tuple
248 The path to search for the config file on, or a sequence of
248 The path to search for the config file on, or a sequence of
249 paths to try in order.
249 paths to try in order.
250 """
250 """
251 super(PyFileConfigLoader, self).__init__()
251 super(PyFileConfigLoader, self).__init__()
252 self.filename = filename
252 self.filename = filename
253 self.path = path
253 self.path = path
254 self.full_filename = ''
254 self.full_filename = ''
255 self.data = None
255 self.data = None
256
256
257 def load_config(self):
257 def load_config(self):
258 """Load the config from a file and return it as a Struct."""
258 """Load the config from a file and return it as a Struct."""
259 self.clear()
259 self.clear()
260 self._find_file()
260 self._find_file()
261 self._read_file_as_dict()
261 self._read_file_as_dict()
262 self._convert_to_config()
262 self._convert_to_config()
263 return self.config
263 return self.config
264
264
265 def _find_file(self):
265 def _find_file(self):
266 """Try to find the file by searching the paths."""
266 """Try to find the file by searching the paths."""
267 self.full_filename = filefind(self.filename, self.path)
267 self.full_filename = filefind(self.filename, self.path)
268
268
269 def _read_file_as_dict(self):
269 def _read_file_as_dict(self):
270 """Load the config file into self.config, with recursive loading."""
270 """Load the config file into self.config, with recursive loading."""
271 # This closure is made available in the namespace that is used
271 # This closure is made available in the namespace that is used
272 # to exec the config file. This allows users to call
272 # to exec the config file. This allows users to call
273 # load_subconfig('myconfig.py') to load config files recursively.
273 # load_subconfig('myconfig.py') to load config files recursively.
274 # It needs to be a closure because it has references to self.path
274 # It needs to be a closure because it has references to self.path
275 # and self.config. The sub-config is loaded with the same path
275 # and self.config. The sub-config is loaded with the same path
276 # as the parent, but it uses an empty config which is then merged
276 # as the parent, but it uses an empty config which is then merged
277 # with the parents.
277 # with the parents.
278 def load_subconfig(fname):
278 def load_subconfig(fname):
279 loader = PyFileConfigLoader(fname, self.path)
279 loader = PyFileConfigLoader(fname, self.path)
280 try:
280 try:
281 sub_config = loader.load_config()
281 sub_config = loader.load_config()
282 except IOError:
282 except IOError:
283 # Pass silently if the sub config is not there. This happens
283 # Pass silently if the sub config is not there. This happens
284 # when a user us using a profile, but not the default config.
284 # when a user us using a profile, but not the default config.
285 pass
285 pass
286 else:
286 else:
287 self.config._merge(sub_config)
287 self.config._merge(sub_config)
288
288
289 # Again, this needs to be a closure and should be used in config
289 # Again, this needs to be a closure and should be used in config
290 # files to get the config being loaded.
290 # files to get the config being loaded.
291 def get_config():
291 def get_config():
292 return self.config
292 return self.config
293
293
294 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
294 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
295 fs_encoding = sys.getfilesystemencoding() or 'ascii'
295 fs_encoding = sys.getfilesystemencoding() or 'ascii'
296 conf_filename = self.full_filename.encode(fs_encoding)
296 conf_filename = self.full_filename.encode(fs_encoding)
297 execfile(conf_filename, namespace)
297 execfile(conf_filename, namespace)
298
298
299 def _convert_to_config(self):
299 def _convert_to_config(self):
300 if self.data is None:
300 if self.data is None:
301 ConfigLoaderError('self.data does not exist')
301 ConfigLoaderError('self.data does not exist')
302
302
303
303
304 class CommandLineConfigLoader(ConfigLoader):
304 class CommandLineConfigLoader(ConfigLoader):
305 """A config loader for command line arguments.
305 """A config loader for command line arguments.
306
306
307 As we add more command line based loaders, the common logic should go
307 As we add more command line based loaders, the common logic should go
308 here.
308 here.
309 """
309 """
310
310
311 kv_pattern = re.compile(r'[A-Za-z]\w*(\.\w+)*\=.*')
311 kv_pattern = re.compile(r'[A-Za-z]\w*(\.\w+)*\=.*')
312 flag_pattern = re.compile(r'\-\-\w+(\-\w)*')
312 flag_pattern = re.compile(r'\-\-\w+(\-\w)*')
313
313
314 class KeyValueConfigLoader(CommandLineConfigLoader):
314 class KeyValueConfigLoader(CommandLineConfigLoader):
315 """A config loader that loads key value pairs from the command line.
315 """A config loader that loads key value pairs from the command line.
316
316
317 This allows command line options to be gives in the following form::
317 This allows command line options to be gives in the following form::
318
318
319 ipython Global.profile="foo" InteractiveShell.autocall=False
319 ipython Global.profile="foo" InteractiveShell.autocall=False
320 """
320 """
321
321
322 def __init__(self, argv=None, aliases=None, flags=None):
322 def __init__(self, argv=None, aliases=None, flags=None):
323 """Create a key value pair config loader.
323 """Create a key value pair config loader.
324
324
325 Parameters
325 Parameters
326 ----------
326 ----------
327 argv : list
327 argv : list
328 A list that has the form of sys.argv[1:] which has unicode
328 A list that has the form of sys.argv[1:] which has unicode
329 elements of the form u"key=value". If this is None (default),
329 elements of the form u"key=value". If this is None (default),
330 then sys.argv[1:] will be used.
330 then sys.argv[1:] will be used.
331 aliases : dict
331 aliases : dict
332 A dict of aliases for configurable traits.
332 A dict of aliases for configurable traits.
333 Keys are the short aliases, Values are the resolved trait.
333 Keys are the short aliases, Values are the resolved trait.
334 Of the form: `{'alias' : 'Configurable.trait'}`
334 Of the form: `{'alias' : 'Configurable.trait'}`
335 flags : dict
335 flags : dict
336 A dict of flags, keyed by str name. Vaues can be Config objects,
336 A dict of flags, keyed by str name. Vaues can be Config objects,
337 dicts, or "key=value" strings. If Config or dict, when the flag
337 dicts, or "key=value" strings. If Config or dict, when the flag
338 is triggered, The flag is loaded as `self.config.update(m)`.
338 is triggered, The flag is loaded as `self.config.update(m)`.
339
339
340 Returns
340 Returns
341 -------
341 -------
342 config : Config
342 config : Config
343 The resulting Config object.
343 The resulting Config object.
344
344
345 Examples
345 Examples
346 --------
346 --------
347
347
348 >>> from IPython.config.loader import KeyValueConfigLoader
348 >>> from IPython.config.loader import KeyValueConfigLoader
349 >>> cl = KeyValueConfigLoader()
349 >>> cl = KeyValueConfigLoader()
350 >>> cl.load_config(["foo='bar'","A.name='brian'","B.number=0"])
350 >>> cl.load_config(["foo='bar'","A.name='brian'","B.number=0"])
351 {'A': {'name': 'brian'}, 'B': {'number': 0}, 'foo': 'bar'}
351 {'A': {'name': 'brian'}, 'B': {'number': 0}, 'foo': 'bar'}
352 """
352 """
353 self.clear()
353 if argv is None:
354 if argv is None:
354 argv = sys.argv[1:]
355 argv = sys.argv[1:]
355 self.argv = argv
356 self.argv = argv
356 self.aliases = aliases or {}
357 self.aliases = aliases or {}
357 self.flags = flags or {}
358 self.flags = flags or {}
359
360
361 def clear(self):
362 super(KeyValueConfigLoader, self).clear()
363 self.extra_args = []
364
358
365
359 def load_config(self, argv=None, aliases=None, flags=None):
366 def load_config(self, argv=None, aliases=None, flags=None):
360 """Parse the configuration and generate the Config object.
367 """Parse the configuration and generate the Config object.
361
368
369 After loading, any arguments that are not key-value or
370 flags will be stored in self.extra_args - a list of
371 unparsed command-line arguments. This is used for
372 arguments such as input files or subcommands.
373
362 Parameters
374 Parameters
363 ----------
375 ----------
364 argv : list, optional
376 argv : list, optional
365 A list that has the form of sys.argv[1:] which has unicode
377 A list that has the form of sys.argv[1:] which has unicode
366 elements of the form u"key=value". If this is None (default),
378 elements of the form u"key=value". If this is None (default),
367 then self.argv will be used.
379 then self.argv will be used.
368 aliases : dict
380 aliases : dict
369 A dict of aliases for configurable traits.
381 A dict of aliases for configurable traits.
370 Keys are the short aliases, Values are the resolved trait.
382 Keys are the short aliases, Values are the resolved trait.
371 Of the form: `{'alias' : 'Configurable.trait'}`
383 Of the form: `{'alias' : 'Configurable.trait'}`
372 flags : dict
384 flags : dict
373 A dict of flags, keyed by str name. Values can be Config objects
385 A dict of flags, keyed by str name. Values can be Config objects
374 or dicts. When the flag is triggered, The config is loaded as
386 or dicts. When the flag is triggered, The config is loaded as
375 `self.config.update(cfg)`.
387 `self.config.update(cfg)`.
376 """
388 """
377 from IPython.config.configurable import Configurable
389 from IPython.config.configurable import Configurable
378
390
379 self.clear()
391 self.clear()
380 if argv is None:
392 if argv is None:
381 argv = self.argv
393 argv = self.argv
382 if aliases is None:
394 if aliases is None:
383 aliases = self.aliases
395 aliases = self.aliases
384 if flags is None:
396 if flags is None:
385 flags = self.flags
397 flags = self.flags
386
398
387 self.extra_args = []
388
389 for item in argv:
399 for item in argv:
390 if kv_pattern.match(item):
400 if kv_pattern.match(item):
391 lhs,rhs = item.split('=',1)
401 lhs,rhs = item.split('=',1)
392 # Substitute longnames for aliases.
402 # Substitute longnames for aliases.
393 if lhs in aliases:
403 if lhs in aliases:
394 lhs = aliases[lhs]
404 lhs = aliases[lhs]
395 exec_str = 'self.config.' + lhs + '=' + rhs
405 exec_str = 'self.config.' + lhs + '=' + rhs
396 try:
406 try:
397 # Try to see if regular Python syntax will work. This
407 # Try to see if regular Python syntax will work. This
398 # won't handle strings as the quote marks are removed
408 # won't handle strings as the quote marks are removed
399 # by the system shell.
409 # by the system shell.
400 exec exec_str in locals(), globals()
410 exec exec_str in locals(), globals()
401 except (NameError, SyntaxError):
411 except (NameError, SyntaxError):
402 # This case happens if the rhs is a string but without
412 # This case happens if the rhs is a string but without
403 # the quote marks. We add the quote marks and see if
413 # the quote marks. We add the quote marks and see if
404 # it succeeds. If it still fails, we let it raise.
414 # it succeeds. If it still fails, we let it raise.
405 exec_str = 'self.config.' + lhs + '="' + rhs + '"'
415 exec_str = 'self.config.' + lhs + '="' + rhs + '"'
406 exec exec_str in locals(), globals()
416 exec exec_str in locals(), globals()
407 elif flag_pattern.match(item):
417 elif flag_pattern.match(item):
408 # trim leading '--'
418 # trim leading '--'
409 m = item[2:]
419 m = item[2:]
410 cfg,_ = flags.get(m, (None,None))
420 cfg,_ = flags.get(m, (None,None))
411 if cfg is None:
421 if cfg is None:
412 raise ArgumentError("Unrecognized flag: %r"%item)
422 raise ArgumentError("Unrecognized flag: %r"%item)
413 elif isinstance(cfg, (dict, Config)):
423 elif isinstance(cfg, (dict, Config)):
414 # don't clobber whole config sections, update
424 # don't clobber whole config sections, update
415 # each section from config:
425 # each section from config:
416 for sec,c in cfg.iteritems():
426 for sec,c in cfg.iteritems():
417 self.config[sec].update(c)
427 self.config[sec].update(c)
418 else:
428 else:
419 raise ValueError("Invalid flag: %r"%flag)
429 raise ValueError("Invalid flag: %r"%flag)
420 elif item.startswith('-'):
430 elif item.startswith('-'):
421 # this shouldn't ever be valid
431 # this shouldn't ever be valid
422 raise ArgumentError("Invalid argument: %r"%item)
432 raise ArgumentError("Invalid argument: %r"%item)
423 else:
433 else:
424 # keep all args that aren't valid in a list,
434 # keep all args that aren't valid in a list,
425 # in case our parent knows what to do with them.
435 # in case our parent knows what to do with them.
426 self.extra_args.append(item)
436 self.extra_args.append(item)
427 return self.config
437 return self.config
428
438
429 class ArgParseConfigLoader(CommandLineConfigLoader):
439 class ArgParseConfigLoader(CommandLineConfigLoader):
430 """A loader that uses the argparse module to load from the command line."""
440 """A loader that uses the argparse module to load from the command line."""
431
441
432 def __init__(self, argv=None, *parser_args, **parser_kw):
442 def __init__(self, argv=None, *parser_args, **parser_kw):
433 """Create a config loader for use with argparse.
443 """Create a config loader for use with argparse.
434
444
435 Parameters
445 Parameters
436 ----------
446 ----------
437
447
438 argv : optional, list
448 argv : optional, list
439 If given, used to read command-line arguments from, otherwise
449 If given, used to read command-line arguments from, otherwise
440 sys.argv[1:] is used.
450 sys.argv[1:] is used.
441
451
442 parser_args : tuple
452 parser_args : tuple
443 A tuple of positional arguments that will be passed to the
453 A tuple of positional arguments that will be passed to the
444 constructor of :class:`argparse.ArgumentParser`.
454 constructor of :class:`argparse.ArgumentParser`.
445
455
446 parser_kw : dict
456 parser_kw : dict
447 A tuple of keyword arguments that will be passed to the
457 A tuple of keyword arguments that will be passed to the
448 constructor of :class:`argparse.ArgumentParser`.
458 constructor of :class:`argparse.ArgumentParser`.
449
459
450 Returns
460 Returns
451 -------
461 -------
452 config : Config
462 config : Config
453 The resulting Config object.
463 The resulting Config object.
454 """
464 """
455 super(CommandLineConfigLoader, self).__init__()
465 super(CommandLineConfigLoader, self).__init__()
456 if argv == None:
466 if argv == None:
457 argv = sys.argv[1:]
467 argv = sys.argv[1:]
458 self.argv = argv
468 self.argv = argv
459 self.parser_args = parser_args
469 self.parser_args = parser_args
460 self.version = parser_kw.pop("version", None)
470 self.version = parser_kw.pop("version", None)
461 kwargs = dict(argument_default=argparse.SUPPRESS)
471 kwargs = dict(argument_default=argparse.SUPPRESS)
462 kwargs.update(parser_kw)
472 kwargs.update(parser_kw)
463 self.parser_kw = kwargs
473 self.parser_kw = kwargs
464
474
465 def load_config(self, argv=None):
475 def load_config(self, argv=None):
466 """Parse command line arguments and return as a Config object.
476 """Parse command line arguments and return as a Config object.
467
477
468 Parameters
478 Parameters
469 ----------
479 ----------
470
480
471 args : optional, list
481 args : optional, list
472 If given, a list with the structure of sys.argv[1:] to parse
482 If given, a list with the structure of sys.argv[1:] to parse
473 arguments from. If not given, the instance's self.argv attribute
483 arguments from. If not given, the instance's self.argv attribute
474 (given at construction time) is used."""
484 (given at construction time) is used."""
475 self.clear()
485 self.clear()
476 if argv is None:
486 if argv is None:
477 argv = self.argv
487 argv = self.argv
478 self._create_parser()
488 self._create_parser()
479 self._parse_args(argv)
489 self._parse_args(argv)
480 self._convert_to_config()
490 self._convert_to_config()
481 return self.config
491 return self.config
482
492
483 def get_extra_args(self):
493 def get_extra_args(self):
484 if hasattr(self, 'extra_args'):
494 if hasattr(self, 'extra_args'):
485 return self.extra_args
495 return self.extra_args
486 else:
496 else:
487 return []
497 return []
488
498
489 def _create_parser(self):
499 def _create_parser(self):
490 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
500 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
491 self._add_arguments()
501 self._add_arguments()
492
502
493 def _add_arguments(self):
503 def _add_arguments(self):
494 raise NotImplementedError("subclasses must implement _add_arguments")
504 raise NotImplementedError("subclasses must implement _add_arguments")
495
505
496 def _parse_args(self, args):
506 def _parse_args(self, args):
497 """self.parser->self.parsed_data"""
507 """self.parser->self.parsed_data"""
498 # decode sys.argv to support unicode command-line options
508 # decode sys.argv to support unicode command-line options
499 uargs = []
509 uargs = []
500 for a in args:
510 for a in args:
501 if isinstance(a, str):
511 if isinstance(a, str):
502 # don't decode if we already got unicode
512 # don't decode if we already got unicode
503 a = a.decode(sys.stdin.encoding or
513 a = a.decode(sys.stdin.encoding or
504 sys.getdefaultencoding())
514 sys.getdefaultencoding())
505 uargs.append(a)
515 uargs.append(a)
506 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
516 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
507
517
508 def _convert_to_config(self):
518 def _convert_to_config(self):
509 """self.parsed_data->self.config"""
519 """self.parsed_data->self.config"""
510 for k, v in vars(self.parsed_data).iteritems():
520 for k, v in vars(self.parsed_data).iteritems():
511 exec_str = 'self.config.' + k + '= v'
521 exec_str = 'self.config.' + k + '= v'
512 exec exec_str in locals(), globals()
522 exec exec_str in locals(), globals()
513
523
514
524
@@ -1,243 +1,244 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 A mixin for :class:`~IPython.core.newapplication.Application` classes that
4 A mixin for :class:`~IPython.core.newapplication.Application` classes that
5 launch InteractiveShell instances, load extensions, etc.
5 launch InteractiveShell instances, load extensions, etc.
6
6
7 Authors
7 Authors
8 -------
8 -------
9
9
10 * Min Ragan-Kelley
10 * Min Ragan-Kelley
11 """
11 """
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Copyright (C) 2008-2011 The IPython Development Team
14 # Copyright (C) 2008-2011 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 from __future__ import absolute_import
24 from __future__ import absolute_import
25
25
26 import os
26 import os
27 import sys
27 import sys
28
28
29 from IPython.config.application import boolean_flag
29 from IPython.config.application import boolean_flag
30 from IPython.config.configurable import Configurable
30 from IPython.config.configurable import Configurable
31 from IPython.config.loader import Config
31 from IPython.utils.path import filefind
32 from IPython.utils.path import filefind
32 from IPython.utils.traitlets import Unicode, Instance, List
33 from IPython.utils.traitlets import Unicode, Instance, List
33
34
34 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
35 # Aliases and Flags
36 # Aliases and Flags
36 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
37
38
38 shell_flags = {}
39 shell_flags = {}
39
40
40 addflag = lambda *args: shell_flags.update(boolean_flag(*args))
41 addflag = lambda *args: shell_flags.update(boolean_flag(*args))
41 addflag('autoindent', 'InteractiveShell.autoindent',
42 addflag('autoindent', 'InteractiveShell.autoindent',
42 'Turn on autoindenting.', 'Turn off autoindenting.'
43 'Turn on autoindenting.', 'Turn off autoindenting.'
43 )
44 )
44 addflag('automagic', 'InteractiveShell.automagic',
45 addflag('automagic', 'InteractiveShell.automagic',
45 """Turn on the auto calling of magic commands. Type %%magic at the
46 """Turn on the auto calling of magic commands. Type %%magic at the
46 IPython prompt for more information.""",
47 IPython prompt for more information.""",
47 'Turn off the auto calling of magic commands.'
48 'Turn off the auto calling of magic commands.'
48 )
49 )
49 addflag('pdb', 'InteractiveShell.pdb',
50 addflag('pdb', 'InteractiveShell.pdb',
50 "Enable auto calling the pdb debugger after every exception.",
51 "Enable auto calling the pdb debugger after every exception.",
51 "Disable auto calling the pdb debugger after every exception."
52 "Disable auto calling the pdb debugger after every exception."
52 )
53 )
53 addflag('pprint', 'PlainTextFormatter.pprint',
54 addflag('pprint', 'PlainTextFormatter.pprint',
54 "Enable auto pretty printing of results.",
55 "Enable auto pretty printing of results.",
55 "Disable auto auto pretty printing of results."
56 "Disable auto auto pretty printing of results."
56 )
57 )
57 addflag('color-info', 'InteractiveShell.color_info',
58 addflag('color-info', 'InteractiveShell.color_info',
58 """IPython can display information about objects via a set of func-
59 """IPython can display information about objects via a set of func-
59 tions, and optionally can use colors for this, syntax highlighting
60 tions, and optionally can use colors for this, syntax highlighting
60 source code and various other elements. However, because this
61 source code and various other elements. However, because this
61 information is passed through a pager (like 'less') and many pagers get
62 information is passed through a pager (like 'less') and many pagers get
62 confused with color codes, this option is off by default. You can test
63 confused with color codes, this option is off by default. You can test
63 it and turn it on permanently in your ipython_config.py file if it
64 it and turn it on permanently in your ipython_config.py file if it
64 works for you. Test it and turn it on permanently if it works with
65 works for you. Test it and turn it on permanently if it works with
65 your system. The magic function %%color_info allows you to toggle this
66 your system. The magic function %%color_info allows you to toggle this
66 inter- actively for testing.""",
67 interactively for testing.""",
67 "Disable using colors for info related things."
68 "Disable using colors for info related things."
68 )
69 )
69 addflag('deep-reload', 'InteractiveShell.deep_reload',
70 addflag('deep-reload', 'InteractiveShell.deep_reload',
70 """Enable deep (recursive) reloading by default. IPython can use the
71 """Enable deep (recursive) reloading by default. IPython can use the
71 deep_reload module which reloads changes in modules recursively (it
72 deep_reload module which reloads changes in modules recursively (it
72 replaces the reload() function, so you don't need to change anything to
73 replaces the reload() function, so you don't need to change anything to
73 use it). deep_reload() forces a full reload of modules whose code may
74 use it). deep_reload() forces a full reload of modules whose code may
74 have changed, which the default reload() function does not. When
75 have changed, which the default reload() function does not. When
75 deep_reload is off, IPython will use the normal reload(), but
76 deep_reload is off, IPython will use the normal reload(), but
76 deep_reload will still be available as dreload(). This feature is off
77 deep_reload will still be available as dreload(). This feature is off
77 by default [which means that you have both normal reload() and
78 by default [which means that you have both normal reload() and
78 dreload()].""",
79 dreload()].""",
79 "Disable deep (recursive) reloading by default."
80 "Disable deep (recursive) reloading by default."
80 )
81 )
82 nosep_config = Config()
83 nosep_config.InteractiveShell.separate_in = ''
84 nosep_config.InteractiveShell.separate_out = ''
85 nosep_config.InteractiveShell.separate_out2 = ''
86
87 shell_flags['nosep']=(nosep_config, "Eliminate all spacing between prompts.")
88
81
89
82 # it's possible we don't want short aliases for *all* of these:
90 # it's possible we don't want short aliases for *all* of these:
83 shell_aliases = dict(
91 shell_aliases = dict(
84 autocall='InteractiveShell.autocall',
92 autocall='InteractiveShell.autocall',
85 cache_size='InteractiveShell.cache_size',
93 cache_size='InteractiveShell.cache_size',
86 colors='InteractiveShell.colors',
94 colors='InteractiveShell.colors',
87 logfile='InteractiveShell.logfile',
95 logfile='InteractiveShell.logfile',
88 log_append='InteractiveShell.logappend',
96 log_append='InteractiveShell.logappend',
89 pi1='InteractiveShell.prompt_in1',
90 pi2='InteractiveShell.prompt_in1',
91 po='InteractiveShell.prompt_out',
92 si='InteractiveShell.separate_in',
93 so='InteractiveShell.separate_out',
94 so2='InteractiveShell.separate_out2',
95 xmode='InteractiveShell.xmode',
96 c='InteractiveShellApp.code_to_run',
97 c='InteractiveShellApp.code_to_run',
97 ext='InteractiveShellApp.extra_extension',
98 ext='InteractiveShellApp.extra_extension',
98 )
99 )
99
100
100 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
101 # Main classes and functions
102 # Main classes and functions
102 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
103
104
104 class InteractiveShellApp(Configurable):
105 class InteractiveShellApp(Configurable):
105 """A Mixin for applications that start InteractiveShell instances.
106 """A Mixin for applications that start InteractiveShell instances.
106
107
107 Provides configurables for loading extensions and executing files
108 Provides configurables for loading extensions and executing files
108 as part of configuring a Shell environment.
109 as part of configuring a Shell environment.
109
110
110 Provides init_extensions() and init_code() methods, to be called
111 Provides init_extensions() and init_code() methods, to be called
111 after init_shell(), which must be implemented by subclasses.
112 after init_shell(), which must be implemented by subclasses.
112 """
113 """
113 extensions = List(Unicode, config=True,
114 extensions = List(Unicode, config=True,
114 help="A list of dotted module names of IPython extensions to load."
115 help="A list of dotted module names of IPython extensions to load."
115 )
116 )
116 extra_extension = Unicode('', config=True,
117 extra_extension = Unicode('', config=True,
117 help="dotted module name of an IPython extension to load."
118 help="dotted module name of an IPython extension to load."
118 )
119 )
119 def _extra_extension_changed(self, name, old, new):
120 def _extra_extension_changed(self, name, old, new):
120 if new:
121 if new:
121 # add to self.extensions
122 # add to self.extensions
122 self.extensions.append(new)
123 self.extensions.append(new)
123
124
124 exec_files = List(Unicode, config=True,
125 exec_files = List(Unicode, config=True,
125 help="""List of files to run at IPython startup."""
126 help="""List of files to run at IPython startup."""
126 )
127 )
127 file_to_run = Unicode('', config=True,
128 file_to_run = Unicode('', config=True,
128 help="""A file to be run""")
129 help="""A file to be run""")
129
130
130 exec_lines = List(Unicode, config=True,
131 exec_lines = List(Unicode, config=True,
131 help="""lines of code to run at IPython startup."""
132 help="""lines of code to run at IPython startup."""
132 )
133 )
133 code_to_run = Unicode('', config=True,
134 code_to_run = Unicode('', config=True,
134 help="Execute the given command string."
135 help="Execute the given command string."
135 )
136 )
136 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
137 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
137
138
138 def init_shell(self):
139 def init_shell(self):
139 raise NotImplementedError("Override in subclasses")
140 raise NotImplementedError("Override in subclasses")
140
141
141 def init_extensions(self):
142 def init_extensions(self):
142 """Load all IPython extensions in IPythonApp.extensions.
143 """Load all IPython extensions in IPythonApp.extensions.
143
144
144 This uses the :meth:`ExtensionManager.load_extensions` to load all
145 This uses the :meth:`ExtensionManager.load_extensions` to load all
145 the extensions listed in ``self.extensions``.
146 the extensions listed in ``self.extensions``.
146 """
147 """
147 if not self.extensions:
148 if not self.extensions:
148 return
149 return
149 try:
150 try:
150 self.log.debug("Loading IPython extensions...")
151 self.log.debug("Loading IPython extensions...")
151 extensions = self.extensions
152 extensions = self.extensions
152 for ext in extensions:
153 for ext in extensions:
153 try:
154 try:
154 self.log.info("Loading IPython extension: %s" % ext)
155 self.log.info("Loading IPython extension: %s" % ext)
155 self.shell.extension_manager.load_extension(ext)
156 self.shell.extension_manager.load_extension(ext)
156 except:
157 except:
157 self.log.warn("Error in loading extension: %s" % ext)
158 self.log.warn("Error in loading extension: %s" % ext)
158 self.shell.showtraceback()
159 self.shell.showtraceback()
159 except:
160 except:
160 self.log.warn("Unknown error in loading extensions:")
161 self.log.warn("Unknown error in loading extensions:")
161 self.shell.showtraceback()
162 self.shell.showtraceback()
162
163
163 def init_code(self):
164 def init_code(self):
164 """run the pre-flight code, specified via exec_lines"""
165 """run the pre-flight code, specified via exec_lines"""
165 self._run_exec_lines()
166 self._run_exec_lines()
166 self._run_exec_files()
167 self._run_exec_files()
167 self._run_cmd_line_code()
168 self._run_cmd_line_code()
168
169
169 def _run_exec_lines(self):
170 def _run_exec_lines(self):
170 """Run lines of code in IPythonApp.exec_lines in the user's namespace."""
171 """Run lines of code in IPythonApp.exec_lines in the user's namespace."""
171 if not self.exec_lines:
172 if not self.exec_lines:
172 return
173 return
173 try:
174 try:
174 self.log.debug("Running code from IPythonApp.exec_lines...")
175 self.log.debug("Running code from IPythonApp.exec_lines...")
175 for line in self.exec_lines:
176 for line in self.exec_lines:
176 try:
177 try:
177 self.log.info("Running code in user namespace: %s" %
178 self.log.info("Running code in user namespace: %s" %
178 line)
179 line)
179 self.shell.run_cell(line, store_history=False)
180 self.shell.run_cell(line, store_history=False)
180 except:
181 except:
181 self.log.warn("Error in executing line in user "
182 self.log.warn("Error in executing line in user "
182 "namespace: %s" % line)
183 "namespace: %s" % line)
183 self.shell.showtraceback()
184 self.shell.showtraceback()
184 except:
185 except:
185 self.log.warn("Unknown error in handling IPythonApp.exec_lines:")
186 self.log.warn("Unknown error in handling IPythonApp.exec_lines:")
186 self.shell.showtraceback()
187 self.shell.showtraceback()
187
188
188 def _exec_file(self, fname):
189 def _exec_file(self, fname):
189 full_filename = filefind(fname, [u'.', self.ipython_dir])
190 full_filename = filefind(fname, [u'.', self.ipython_dir])
190 if os.path.isfile(full_filename):
191 if os.path.isfile(full_filename):
191 if full_filename.endswith(u'.py'):
192 if full_filename.endswith(u'.py'):
192 self.log.info("Running file in user namespace: %s" %
193 self.log.info("Running file in user namespace: %s" %
193 full_filename)
194 full_filename)
194 # Ensure that __file__ is always defined to match Python behavior
195 # Ensure that __file__ is always defined to match Python behavior
195 self.shell.user_ns['__file__'] = fname
196 self.shell.user_ns['__file__'] = fname
196 try:
197 try:
197 self.shell.safe_execfile(full_filename, self.shell.user_ns)
198 self.shell.safe_execfile(full_filename, self.shell.user_ns)
198 finally:
199 finally:
199 del self.shell.user_ns['__file__']
200 del self.shell.user_ns['__file__']
200 elif full_filename.endswith('.ipy'):
201 elif full_filename.endswith('.ipy'):
201 self.log.info("Running file in user namespace: %s" %
202 self.log.info("Running file in user namespace: %s" %
202 full_filename)
203 full_filename)
203 self.shell.safe_execfile_ipy(full_filename)
204 self.shell.safe_execfile_ipy(full_filename)
204 else:
205 else:
205 self.log.warn("File does not have a .py or .ipy extension: <%s>"
206 self.log.warn("File does not have a .py or .ipy extension: <%s>"
206 % full_filename)
207 % full_filename)
207
208
208 def _run_exec_files(self):
209 def _run_exec_files(self):
209 """Run files from IPythonApp.exec_files"""
210 """Run files from IPythonApp.exec_files"""
210 if not self.exec_files:
211 if not self.exec_files:
211 return
212 return
212
213
213 self.log.debug("Running files in IPythonApp.exec_files...")
214 self.log.debug("Running files in IPythonApp.exec_files...")
214 try:
215 try:
215 for fname in self.exec_files:
216 for fname in self.exec_files:
216 self._exec_file(fname)
217 self._exec_file(fname)
217 except:
218 except:
218 self.log.warn("Unknown error in handling IPythonApp.exec_files:")
219 self.log.warn("Unknown error in handling IPythonApp.exec_files:")
219 self.shell.showtraceback()
220 self.shell.showtraceback()
220
221
221 def _run_cmd_line_code(self):
222 def _run_cmd_line_code(self):
222 """Run code or file specified at the command-line"""
223 """Run code or file specified at the command-line"""
223 if self.code_to_run:
224 if self.code_to_run:
224 line = self.code_to_run
225 line = self.code_to_run
225 try:
226 try:
226 self.log.info("Running code given at command line (c=): %s" %
227 self.log.info("Running code given at command line (c=): %s" %
227 line)
228 line)
228 self.shell.run_cell(line, store_history=False)
229 self.shell.run_cell(line, store_history=False)
229 except:
230 except:
230 self.log.warn("Error in executing line in user namespace: %s" %
231 self.log.warn("Error in executing line in user namespace: %s" %
231 line)
232 line)
232 self.shell.showtraceback()
233 self.shell.showtraceback()
233
234
234 # Like Python itself, ignore the second if the first of these is present
235 # Like Python itself, ignore the second if the first of these is present
235 elif self.file_to_run:
236 elif self.file_to_run:
236 fname = self.file_to_run
237 fname = self.file_to_run
237 try:
238 try:
238 self._exec_file(fname)
239 self._exec_file(fname)
239 except:
240 except:
240 self.log.warn("Error in executing file in user namespace: %s" %
241 self.log.warn("Error in executing file in user namespace: %s" %
241 fname)
242 fname)
242 self.shell.showtraceback()
243 self.shell.showtraceback()
243
244
@@ -1,1796 +1,1797 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
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import Configurable
19 from IPython.config.configurable import Configurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
22 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
23 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
24 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
25 from kill_ring import QtKillRing
25 from kill_ring import QtKillRing
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Functions
28 # Functions
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 def is_letter_or_number(char):
31 def is_letter_or_number(char):
32 """ Returns whether the specified unicode character is a letter or a number.
32 """ Returns whether the specified unicode character is a letter or a number.
33 """
33 """
34 cat = category(char)
34 cat = category(char)
35 return cat.startswith('L') or cat.startswith('N')
35 return cat.startswith('L') or cat.startswith('N')
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Classes
38 # Classes
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40
40
41 class ConsoleWidget(Configurable, QtGui.QWidget):
41 class ConsoleWidget(Configurable, QtGui.QWidget):
42 """ An abstract base class for console-type widgets. This class has
42 """ An abstract base class for console-type widgets. This class has
43 functionality for:
43 functionality for:
44
44
45 * Maintaining a prompt and editing region
45 * Maintaining a prompt and editing region
46 * Providing the traditional Unix-style console keyboard shortcuts
46 * Providing the traditional Unix-style console keyboard shortcuts
47 * Performing tab completion
47 * Performing tab completion
48 * Paging text
48 * Paging text
49 * Handling ANSI escape codes
49 * Handling ANSI escape codes
50
50
51 ConsoleWidget also provides a number of utility methods that will be
51 ConsoleWidget also provides a number of utility methods that will be
52 convenient to implementors of a console-style widget.
52 convenient to implementors of a console-style widget.
53 """
53 """
54 __metaclass__ = MetaQObjectHasTraits
54 __metaclass__ = MetaQObjectHasTraits
55
55
56 #------ Configuration ------------------------------------------------------
56 #------ Configuration ------------------------------------------------------
57
57
58 ansi_codes = Bool(True, config=True,
58 ansi_codes = Bool(True, config=True,
59 help="Whether to process ANSI escape codes."
59 help="Whether to process ANSI escape codes."
60 )
60 )
61 buffer_size = Int(500, config=True,
61 buffer_size = Int(500, config=True,
62 help="""
62 help="""
63 The maximum number of lines of text before truncation. Specifying a
63 The maximum number of lines of text before truncation. Specifying a
64 non-positive number disables text truncation (not recommended).
64 non-positive number disables text truncation (not recommended).
65 """
65 """
66 )
66 )
67 gui_completion = Bool(False, config=True,
67 gui_completion = Bool(False, config=True,
68 help="Use a list widget instead of plain text output for tab completion."
68 help="Use a list widget instead of plain text output for tab completion."
69 )
69 )
70 # NOTE: this value can only be specified during initialization.
70 # NOTE: this value can only be specified during initialization.
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
72 help="""
72 help="""
73 The type of underlying text widget to use. Valid values are 'plain', which
73 The type of underlying text widget to use. Valid values are 'plain', which
74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
75 """
75 """
76 )
76 )
77 # NOTE: this value can only be specified during initialization.
77 # NOTE: this value can only be specified during initialization.
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 default_value='inside', config=True,
79 default_value='inside', config=True,
80 help="""
80 help="""
81 The type of paging to use. Valid values are:
81 The type of paging to use. Valid values are:
82
82 'inside' : The widget pages like a traditional terminal.
83 'inside' : The widget pages like a traditional terminal.
83 'hsplit' : When paging is requested, the widget is split
84 'hsplit' : When paging is requested, the widget is split
84 : horizontally. The top pane contains the console, and the
85 horizontally. The top pane contains the console, and the
85 : bottom pane contains the paged text.
86 bottom pane contains the paged text.
86 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
87 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
87 'custom' : No action is taken by the widget beyond emitting a
88 'custom' : No action is taken by the widget beyond emitting a
88 : 'custom_page_requested(str)' signal.
89 'custom_page_requested(str)' signal.
89 'none' : The text is written directly to the console.
90 'none' : The text is written directly to the console.
90 """)
91 """)
91
92
92 font_family = Unicode(config=True,
93 font_family = Unicode(config=True,
93 help="""The font family to use for the console.
94 help="""The font family to use for the console.
94 On OSX this defaults to Monaco, on Windows the default is
95 On OSX this defaults to Monaco, on Windows the default is
95 Consolas with fallback of Courier, and on other platforms
96 Consolas with fallback of Courier, and on other platforms
96 the default is Monospace.
97 the default is Monospace.
97 """)
98 """)
98 def _font_family_default(self):
99 def _font_family_default(self):
99 if sys.platform == 'win32':
100 if sys.platform == 'win32':
100 # Consolas ships with Vista/Win7, fallback to Courier if needed
101 # Consolas ships with Vista/Win7, fallback to Courier if needed
101 return 'Consolas'
102 return 'Consolas'
102 elif sys.platform == 'darwin':
103 elif sys.platform == 'darwin':
103 # OSX always has Monaco, no need for a fallback
104 # OSX always has Monaco, no need for a fallback
104 return 'Monaco'
105 return 'Monaco'
105 else:
106 else:
106 # Monospace should always exist, no need for a fallback
107 # Monospace should always exist, no need for a fallback
107 return 'Monospace'
108 return 'Monospace'
108
109
109 font_size = Int(config=True,
110 font_size = Int(config=True,
110 help="""The font size. If unconfigured, Qt will be entrusted
111 help="""The font size. If unconfigured, Qt will be entrusted
111 with the size of the font.
112 with the size of the font.
112 """)
113 """)
113
114
114 # Whether to override ShortcutEvents for the keybindings defined by this
115 # Whether to override ShortcutEvents for the keybindings defined by this
115 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
116 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
116 # priority (when it has focus) over, e.g., window-level menu shortcuts.
117 # priority (when it has focus) over, e.g., window-level menu shortcuts.
117 override_shortcuts = Bool(False)
118 override_shortcuts = Bool(False)
118
119
119 #------ Signals ------------------------------------------------------------
120 #------ Signals ------------------------------------------------------------
120
121
121 # Signals that indicate ConsoleWidget state.
122 # Signals that indicate ConsoleWidget state.
122 copy_available = QtCore.Signal(bool)
123 copy_available = QtCore.Signal(bool)
123 redo_available = QtCore.Signal(bool)
124 redo_available = QtCore.Signal(bool)
124 undo_available = QtCore.Signal(bool)
125 undo_available = QtCore.Signal(bool)
125
126
126 # Signal emitted when paging is needed and the paging style has been
127 # Signal emitted when paging is needed and the paging style has been
127 # specified as 'custom'.
128 # specified as 'custom'.
128 custom_page_requested = QtCore.Signal(object)
129 custom_page_requested = QtCore.Signal(object)
129
130
130 # Signal emitted when the font is changed.
131 # Signal emitted when the font is changed.
131 font_changed = QtCore.Signal(QtGui.QFont)
132 font_changed = QtCore.Signal(QtGui.QFont)
132
133
133 #------ Protected class variables ------------------------------------------
134 #------ Protected class variables ------------------------------------------
134
135
135 # When the control key is down, these keys are mapped.
136 # When the control key is down, these keys are mapped.
136 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
137 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
137 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
138 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
138 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
139 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
139 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
140 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
140 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
141 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
141 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
142 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
142 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
143 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
143 if not sys.platform == 'darwin':
144 if not sys.platform == 'darwin':
144 # On OS X, Ctrl-E already does the right thing, whereas End moves the
145 # On OS X, Ctrl-E already does the right thing, whereas End moves the
145 # cursor to the bottom of the buffer.
146 # cursor to the bottom of the buffer.
146 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
147 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
147
148
148 # The shortcuts defined by this widget. We need to keep track of these to
149 # The shortcuts defined by this widget. We need to keep track of these to
149 # support 'override_shortcuts' above.
150 # support 'override_shortcuts' above.
150 _shortcuts = set(_ctrl_down_remap.keys() +
151 _shortcuts = set(_ctrl_down_remap.keys() +
151 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
152 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
152 QtCore.Qt.Key_V ])
153 QtCore.Qt.Key_V ])
153
154
154 #---------------------------------------------------------------------------
155 #---------------------------------------------------------------------------
155 # 'QObject' interface
156 # 'QObject' interface
156 #---------------------------------------------------------------------------
157 #---------------------------------------------------------------------------
157
158
158 def __init__(self, parent=None, **kw):
159 def __init__(self, parent=None, **kw):
159 """ Create a ConsoleWidget.
160 """ Create a ConsoleWidget.
160
161
161 Parameters:
162 Parameters:
162 -----------
163 -----------
163 parent : QWidget, optional [default None]
164 parent : QWidget, optional [default None]
164 The parent for this widget.
165 The parent for this widget.
165 """
166 """
166 QtGui.QWidget.__init__(self, parent)
167 QtGui.QWidget.__init__(self, parent)
167 Configurable.__init__(self, **kw)
168 Configurable.__init__(self, **kw)
168
169
169 # Create the layout and underlying text widget.
170 # Create the layout and underlying text widget.
170 layout = QtGui.QStackedLayout(self)
171 layout = QtGui.QStackedLayout(self)
171 layout.setContentsMargins(0, 0, 0, 0)
172 layout.setContentsMargins(0, 0, 0, 0)
172 self._control = self._create_control()
173 self._control = self._create_control()
173 self._page_control = None
174 self._page_control = None
174 self._splitter = None
175 self._splitter = None
175 if self.paging in ('hsplit', 'vsplit'):
176 if self.paging in ('hsplit', 'vsplit'):
176 self._splitter = QtGui.QSplitter()
177 self._splitter = QtGui.QSplitter()
177 if self.paging == 'hsplit':
178 if self.paging == 'hsplit':
178 self._splitter.setOrientation(QtCore.Qt.Horizontal)
179 self._splitter.setOrientation(QtCore.Qt.Horizontal)
179 else:
180 else:
180 self._splitter.setOrientation(QtCore.Qt.Vertical)
181 self._splitter.setOrientation(QtCore.Qt.Vertical)
181 self._splitter.addWidget(self._control)
182 self._splitter.addWidget(self._control)
182 layout.addWidget(self._splitter)
183 layout.addWidget(self._splitter)
183 else:
184 else:
184 layout.addWidget(self._control)
185 layout.addWidget(self._control)
185
186
186 # Create the paging widget, if necessary.
187 # Create the paging widget, if necessary.
187 if self.paging in ('inside', 'hsplit', 'vsplit'):
188 if self.paging in ('inside', 'hsplit', 'vsplit'):
188 self._page_control = self._create_page_control()
189 self._page_control = self._create_page_control()
189 if self._splitter:
190 if self._splitter:
190 self._page_control.hide()
191 self._page_control.hide()
191 self._splitter.addWidget(self._page_control)
192 self._splitter.addWidget(self._page_control)
192 else:
193 else:
193 layout.addWidget(self._page_control)
194 layout.addWidget(self._page_control)
194
195
195 # Initialize protected variables. Some variables contain useful state
196 # Initialize protected variables. Some variables contain useful state
196 # information for subclasses; they should be considered read-only.
197 # information for subclasses; they should be considered read-only.
197 self._ansi_processor = QtAnsiCodeProcessor()
198 self._ansi_processor = QtAnsiCodeProcessor()
198 self._completion_widget = CompletionWidget(self._control)
199 self._completion_widget = CompletionWidget(self._control)
199 self._continuation_prompt = '> '
200 self._continuation_prompt = '> '
200 self._continuation_prompt_html = None
201 self._continuation_prompt_html = None
201 self._executing = False
202 self._executing = False
202 self._filter_drag = False
203 self._filter_drag = False
203 self._filter_resize = False
204 self._filter_resize = False
204 self._html_exporter = HtmlExporter(self._control)
205 self._html_exporter = HtmlExporter(self._control)
205 self._input_buffer_executing = ''
206 self._input_buffer_executing = ''
206 self._input_buffer_pending = ''
207 self._input_buffer_pending = ''
207 self._kill_ring = QtKillRing(self._control)
208 self._kill_ring = QtKillRing(self._control)
208 self._prompt = ''
209 self._prompt = ''
209 self._prompt_html = None
210 self._prompt_html = None
210 self._prompt_pos = 0
211 self._prompt_pos = 0
211 self._prompt_sep = ''
212 self._prompt_sep = ''
212 self._reading = False
213 self._reading = False
213 self._reading_callback = None
214 self._reading_callback = None
214 self._tab_width = 8
215 self._tab_width = 8
215 self._text_completing_pos = 0
216 self._text_completing_pos = 0
216
217
217 # Set a monospaced font.
218 # Set a monospaced font.
218 self.reset_font()
219 self.reset_font()
219
220
220 # Configure actions.
221 # Configure actions.
221 action = QtGui.QAction('Print', None)
222 action = QtGui.QAction('Print', None)
222 action.setEnabled(True)
223 action.setEnabled(True)
223 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
224 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
224 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
225 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
225 # Only override the default if there is a collision.
226 # Only override the default if there is a collision.
226 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
227 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
227 printkey = "Ctrl+Shift+P"
228 printkey = "Ctrl+Shift+P"
228 action.setShortcut(printkey)
229 action.setShortcut(printkey)
229 action.triggered.connect(self.print_)
230 action.triggered.connect(self.print_)
230 self.addAction(action)
231 self.addAction(action)
231 self._print_action = action
232 self._print_action = action
232
233
233 action = QtGui.QAction('Save as HTML/XML', None)
234 action = QtGui.QAction('Save as HTML/XML', None)
234 action.setShortcut(QtGui.QKeySequence.Save)
235 action.setShortcut(QtGui.QKeySequence.Save)
235 action.triggered.connect(self.export_html)
236 action.triggered.connect(self.export_html)
236 self.addAction(action)
237 self.addAction(action)
237 self._export_action = action
238 self._export_action = action
238
239
239 action = QtGui.QAction('Select All', None)
240 action = QtGui.QAction('Select All', None)
240 action.setEnabled(True)
241 action.setEnabled(True)
241 action.setShortcut(QtGui.QKeySequence.SelectAll)
242 action.setShortcut(QtGui.QKeySequence.SelectAll)
242 action.triggered.connect(self.select_all)
243 action.triggered.connect(self.select_all)
243 self.addAction(action)
244 self.addAction(action)
244 self._select_all_action = action
245 self._select_all_action = action
245
246
246 def eventFilter(self, obj, event):
247 def eventFilter(self, obj, event):
247 """ Reimplemented to ensure a console-like behavior in the underlying
248 """ Reimplemented to ensure a console-like behavior in the underlying
248 text widgets.
249 text widgets.
249 """
250 """
250 etype = event.type()
251 etype = event.type()
251 if etype == QtCore.QEvent.KeyPress:
252 if etype == QtCore.QEvent.KeyPress:
252
253
253 # Re-map keys for all filtered widgets.
254 # Re-map keys for all filtered widgets.
254 key = event.key()
255 key = event.key()
255 if self._control_key_down(event.modifiers()) and \
256 if self._control_key_down(event.modifiers()) and \
256 key in self._ctrl_down_remap:
257 key in self._ctrl_down_remap:
257 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
258 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
258 self._ctrl_down_remap[key],
259 self._ctrl_down_remap[key],
259 QtCore.Qt.NoModifier)
260 QtCore.Qt.NoModifier)
260 QtGui.qApp.sendEvent(obj, new_event)
261 QtGui.qApp.sendEvent(obj, new_event)
261 return True
262 return True
262
263
263 elif obj == self._control:
264 elif obj == self._control:
264 return self._event_filter_console_keypress(event)
265 return self._event_filter_console_keypress(event)
265
266
266 elif obj == self._page_control:
267 elif obj == self._page_control:
267 return self._event_filter_page_keypress(event)
268 return self._event_filter_page_keypress(event)
268
269
269 # Make middle-click paste safe.
270 # Make middle-click paste safe.
270 elif etype == QtCore.QEvent.MouseButtonRelease and \
271 elif etype == QtCore.QEvent.MouseButtonRelease and \
271 event.button() == QtCore.Qt.MidButton and \
272 event.button() == QtCore.Qt.MidButton and \
272 obj == self._control.viewport():
273 obj == self._control.viewport():
273 cursor = self._control.cursorForPosition(event.pos())
274 cursor = self._control.cursorForPosition(event.pos())
274 self._control.setTextCursor(cursor)
275 self._control.setTextCursor(cursor)
275 self.paste(QtGui.QClipboard.Selection)
276 self.paste(QtGui.QClipboard.Selection)
276 return True
277 return True
277
278
278 # Manually adjust the scrollbars *after* a resize event is dispatched.
279 # Manually adjust the scrollbars *after* a resize event is dispatched.
279 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
280 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
280 self._filter_resize = True
281 self._filter_resize = True
281 QtGui.qApp.sendEvent(obj, event)
282 QtGui.qApp.sendEvent(obj, event)
282 self._adjust_scrollbars()
283 self._adjust_scrollbars()
283 self._filter_resize = False
284 self._filter_resize = False
284 return True
285 return True
285
286
286 # Override shortcuts for all filtered widgets.
287 # Override shortcuts for all filtered widgets.
287 elif etype == QtCore.QEvent.ShortcutOverride and \
288 elif etype == QtCore.QEvent.ShortcutOverride and \
288 self.override_shortcuts and \
289 self.override_shortcuts and \
289 self._control_key_down(event.modifiers()) and \
290 self._control_key_down(event.modifiers()) and \
290 event.key() in self._shortcuts:
291 event.key() in self._shortcuts:
291 event.accept()
292 event.accept()
292
293
293 # Ensure that drags are safe. The problem is that the drag starting
294 # Ensure that drags are safe. The problem is that the drag starting
294 # logic, which determines whether the drag is a Copy or Move, is locked
295 # logic, which determines whether the drag is a Copy or Move, is locked
295 # down in QTextControl. If the widget is editable, which it must be if
296 # down in QTextControl. If the widget is editable, which it must be if
296 # we're not executing, the drag will be a Move. The following hack
297 # we're not executing, the drag will be a Move. The following hack
297 # prevents QTextControl from deleting the text by clearing the selection
298 # prevents QTextControl from deleting the text by clearing the selection
298 # when a drag leave event originating from this widget is dispatched.
299 # when a drag leave event originating from this widget is dispatched.
299 # The fact that we have to clear the user's selection is unfortunate,
300 # The fact that we have to clear the user's selection is unfortunate,
300 # but the alternative--trying to prevent Qt from using its hardwired
301 # but the alternative--trying to prevent Qt from using its hardwired
301 # drag logic and writing our own--is worse.
302 # drag logic and writing our own--is worse.
302 elif etype == QtCore.QEvent.DragEnter and \
303 elif etype == QtCore.QEvent.DragEnter and \
303 obj == self._control.viewport() and \
304 obj == self._control.viewport() and \
304 event.source() == self._control.viewport():
305 event.source() == self._control.viewport():
305 self._filter_drag = True
306 self._filter_drag = True
306 elif etype == QtCore.QEvent.DragLeave and \
307 elif etype == QtCore.QEvent.DragLeave and \
307 obj == self._control.viewport() and \
308 obj == self._control.viewport() and \
308 self._filter_drag:
309 self._filter_drag:
309 cursor = self._control.textCursor()
310 cursor = self._control.textCursor()
310 cursor.clearSelection()
311 cursor.clearSelection()
311 self._control.setTextCursor(cursor)
312 self._control.setTextCursor(cursor)
312 self._filter_drag = False
313 self._filter_drag = False
313
314
314 # Ensure that drops are safe.
315 # Ensure that drops are safe.
315 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
316 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
316 cursor = self._control.cursorForPosition(event.pos())
317 cursor = self._control.cursorForPosition(event.pos())
317 if self._in_buffer(cursor.position()):
318 if self._in_buffer(cursor.position()):
318 text = event.mimeData().text()
319 text = event.mimeData().text()
319 self._insert_plain_text_into_buffer(cursor, text)
320 self._insert_plain_text_into_buffer(cursor, text)
320
321
321 # Qt is expecting to get something here--drag and drop occurs in its
322 # Qt is expecting to get something here--drag and drop occurs in its
322 # own event loop. Send a DragLeave event to end it.
323 # own event loop. Send a DragLeave event to end it.
323 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
324 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
324 return True
325 return True
325
326
326 return super(ConsoleWidget, self).eventFilter(obj, event)
327 return super(ConsoleWidget, self).eventFilter(obj, event)
327
328
328 #---------------------------------------------------------------------------
329 #---------------------------------------------------------------------------
329 # 'QWidget' interface
330 # 'QWidget' interface
330 #---------------------------------------------------------------------------
331 #---------------------------------------------------------------------------
331
332
332 def sizeHint(self):
333 def sizeHint(self):
333 """ Reimplemented to suggest a size that is 80 characters wide and
334 """ Reimplemented to suggest a size that is 80 characters wide and
334 25 lines high.
335 25 lines high.
335 """
336 """
336 font_metrics = QtGui.QFontMetrics(self.font)
337 font_metrics = QtGui.QFontMetrics(self.font)
337 margin = (self._control.frameWidth() +
338 margin = (self._control.frameWidth() +
338 self._control.document().documentMargin()) * 2
339 self._control.document().documentMargin()) * 2
339 style = self.style()
340 style = self.style()
340 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
341 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
341
342
342 # Note 1: Despite my best efforts to take the various margins into
343 # Note 1: Despite my best efforts to take the various margins into
343 # account, the width is still coming out a bit too small, so we include
344 # account, the width is still coming out a bit too small, so we include
344 # a fudge factor of one character here.
345 # a fudge factor of one character here.
345 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
346 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
346 # to a Qt bug on certain Mac OS systems where it returns 0.
347 # to a Qt bug on certain Mac OS systems where it returns 0.
347 width = font_metrics.width(' ') * 81 + margin
348 width = font_metrics.width(' ') * 81 + margin
348 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
349 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
349 if self.paging == 'hsplit':
350 if self.paging == 'hsplit':
350 width = width * 2 + splitwidth
351 width = width * 2 + splitwidth
351
352
352 height = font_metrics.height() * 25 + margin
353 height = font_metrics.height() * 25 + margin
353 if self.paging == 'vsplit':
354 if self.paging == 'vsplit':
354 height = height * 2 + splitwidth
355 height = height * 2 + splitwidth
355
356
356 return QtCore.QSize(width, height)
357 return QtCore.QSize(width, height)
357
358
358 #---------------------------------------------------------------------------
359 #---------------------------------------------------------------------------
359 # 'ConsoleWidget' public interface
360 # 'ConsoleWidget' public interface
360 #---------------------------------------------------------------------------
361 #---------------------------------------------------------------------------
361
362
362 def can_copy(self):
363 def can_copy(self):
363 """ Returns whether text can be copied to the clipboard.
364 """ Returns whether text can be copied to the clipboard.
364 """
365 """
365 return self._control.textCursor().hasSelection()
366 return self._control.textCursor().hasSelection()
366
367
367 def can_cut(self):
368 def can_cut(self):
368 """ Returns whether text can be cut to the clipboard.
369 """ Returns whether text can be cut to the clipboard.
369 """
370 """
370 cursor = self._control.textCursor()
371 cursor = self._control.textCursor()
371 return (cursor.hasSelection() and
372 return (cursor.hasSelection() and
372 self._in_buffer(cursor.anchor()) and
373 self._in_buffer(cursor.anchor()) and
373 self._in_buffer(cursor.position()))
374 self._in_buffer(cursor.position()))
374
375
375 def can_paste(self):
376 def can_paste(self):
376 """ Returns whether text can be pasted from the clipboard.
377 """ Returns whether text can be pasted from the clipboard.
377 """
378 """
378 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
379 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
379 return bool(QtGui.QApplication.clipboard().text())
380 return bool(QtGui.QApplication.clipboard().text())
380 return False
381 return False
381
382
382 def clear(self, keep_input=True):
383 def clear(self, keep_input=True):
383 """ Clear the console.
384 """ Clear the console.
384
385
385 Parameters:
386 Parameters:
386 -----------
387 -----------
387 keep_input : bool, optional (default True)
388 keep_input : bool, optional (default True)
388 If set, restores the old input buffer if a new prompt is written.
389 If set, restores the old input buffer if a new prompt is written.
389 """
390 """
390 if self._executing:
391 if self._executing:
391 self._control.clear()
392 self._control.clear()
392 else:
393 else:
393 if keep_input:
394 if keep_input:
394 input_buffer = self.input_buffer
395 input_buffer = self.input_buffer
395 self._control.clear()
396 self._control.clear()
396 self._show_prompt()
397 self._show_prompt()
397 if keep_input:
398 if keep_input:
398 self.input_buffer = input_buffer
399 self.input_buffer = input_buffer
399
400
400 def copy(self):
401 def copy(self):
401 """ Copy the currently selected text to the clipboard.
402 """ Copy the currently selected text to the clipboard.
402 """
403 """
403 self._control.copy()
404 self._control.copy()
404
405
405 def cut(self):
406 def cut(self):
406 """ Copy the currently selected text to the clipboard and delete it
407 """ Copy the currently selected text to the clipboard and delete it
407 if it's inside the input buffer.
408 if it's inside the input buffer.
408 """
409 """
409 self.copy()
410 self.copy()
410 if self.can_cut():
411 if self.can_cut():
411 self._control.textCursor().removeSelectedText()
412 self._control.textCursor().removeSelectedText()
412
413
413 def execute(self, source=None, hidden=False, interactive=False):
414 def execute(self, source=None, hidden=False, interactive=False):
414 """ Executes source or the input buffer, possibly prompting for more
415 """ Executes source or the input buffer, possibly prompting for more
415 input.
416 input.
416
417
417 Parameters:
418 Parameters:
418 -----------
419 -----------
419 source : str, optional
420 source : str, optional
420
421
421 The source to execute. If not specified, the input buffer will be
422 The source to execute. If not specified, the input buffer will be
422 used. If specified and 'hidden' is False, the input buffer will be
423 used. If specified and 'hidden' is False, the input buffer will be
423 replaced with the source before execution.
424 replaced with the source before execution.
424
425
425 hidden : bool, optional (default False)
426 hidden : bool, optional (default False)
426
427
427 If set, no output will be shown and the prompt will not be modified.
428 If set, no output will be shown and the prompt will not be modified.
428 In other words, it will be completely invisible to the user that
429 In other words, it will be completely invisible to the user that
429 an execution has occurred.
430 an execution has occurred.
430
431
431 interactive : bool, optional (default False)
432 interactive : bool, optional (default False)
432
433
433 Whether the console is to treat the source as having been manually
434 Whether the console is to treat the source as having been manually
434 entered by the user. The effect of this parameter depends on the
435 entered by the user. The effect of this parameter depends on the
435 subclass implementation.
436 subclass implementation.
436
437
437 Raises:
438 Raises:
438 -------
439 -------
439 RuntimeError
440 RuntimeError
440 If incomplete input is given and 'hidden' is True. In this case,
441 If incomplete input is given and 'hidden' is True. In this case,
441 it is not possible to prompt for more input.
442 it is not possible to prompt for more input.
442
443
443 Returns:
444 Returns:
444 --------
445 --------
445 A boolean indicating whether the source was executed.
446 A boolean indicating whether the source was executed.
446 """
447 """
447 # WARNING: The order in which things happen here is very particular, in
448 # WARNING: The order in which things happen here is very particular, in
448 # large part because our syntax highlighting is fragile. If you change
449 # large part because our syntax highlighting is fragile. If you change
449 # something, test carefully!
450 # something, test carefully!
450
451
451 # Decide what to execute.
452 # Decide what to execute.
452 if source is None:
453 if source is None:
453 source = self.input_buffer
454 source = self.input_buffer
454 if not hidden:
455 if not hidden:
455 # A newline is appended later, but it should be considered part
456 # A newline is appended later, but it should be considered part
456 # of the input buffer.
457 # of the input buffer.
457 source += '\n'
458 source += '\n'
458 elif not hidden:
459 elif not hidden:
459 self.input_buffer = source
460 self.input_buffer = source
460
461
461 # Execute the source or show a continuation prompt if it is incomplete.
462 # Execute the source or show a continuation prompt if it is incomplete.
462 complete = self._is_complete(source, interactive)
463 complete = self._is_complete(source, interactive)
463 if hidden:
464 if hidden:
464 if complete:
465 if complete:
465 self._execute(source, hidden)
466 self._execute(source, hidden)
466 else:
467 else:
467 error = 'Incomplete noninteractive input: "%s"'
468 error = 'Incomplete noninteractive input: "%s"'
468 raise RuntimeError(error % source)
469 raise RuntimeError(error % source)
469 else:
470 else:
470 if complete:
471 if complete:
471 self._append_plain_text('\n')
472 self._append_plain_text('\n')
472 self._input_buffer_executing = self.input_buffer
473 self._input_buffer_executing = self.input_buffer
473 self._executing = True
474 self._executing = True
474 self._prompt_finished()
475 self._prompt_finished()
475
476
476 # The maximum block count is only in effect during execution.
477 # The maximum block count is only in effect during execution.
477 # This ensures that _prompt_pos does not become invalid due to
478 # This ensures that _prompt_pos does not become invalid due to
478 # text truncation.
479 # text truncation.
479 self._control.document().setMaximumBlockCount(self.buffer_size)
480 self._control.document().setMaximumBlockCount(self.buffer_size)
480
481
481 # Setting a positive maximum block count will automatically
482 # Setting a positive maximum block count will automatically
482 # disable the undo/redo history, but just to be safe:
483 # disable the undo/redo history, but just to be safe:
483 self._control.setUndoRedoEnabled(False)
484 self._control.setUndoRedoEnabled(False)
484
485
485 # Perform actual execution.
486 # Perform actual execution.
486 self._execute(source, hidden)
487 self._execute(source, hidden)
487
488
488 else:
489 else:
489 # Do this inside an edit block so continuation prompts are
490 # Do this inside an edit block so continuation prompts are
490 # removed seamlessly via undo/redo.
491 # removed seamlessly via undo/redo.
491 cursor = self._get_end_cursor()
492 cursor = self._get_end_cursor()
492 cursor.beginEditBlock()
493 cursor.beginEditBlock()
493 cursor.insertText('\n')
494 cursor.insertText('\n')
494 self._insert_continuation_prompt(cursor)
495 self._insert_continuation_prompt(cursor)
495 cursor.endEditBlock()
496 cursor.endEditBlock()
496
497
497 # Do not do this inside the edit block. It works as expected
498 # Do not do this inside the edit block. It works as expected
498 # when using a QPlainTextEdit control, but does not have an
499 # when using a QPlainTextEdit control, but does not have an
499 # effect when using a QTextEdit. I believe this is a Qt bug.
500 # effect when using a QTextEdit. I believe this is a Qt bug.
500 self._control.moveCursor(QtGui.QTextCursor.End)
501 self._control.moveCursor(QtGui.QTextCursor.End)
501
502
502 return complete
503 return complete
503
504
504 def export_html(self):
505 def export_html(self):
505 """ Shows a dialog to export HTML/XML in various formats.
506 """ Shows a dialog to export HTML/XML in various formats.
506 """
507 """
507 self._html_exporter.export()
508 self._html_exporter.export()
508
509
509 def _get_input_buffer(self):
510 def _get_input_buffer(self):
510 """ The text that the user has entered entered at the current prompt.
511 """ The text that the user has entered entered at the current prompt.
511
512
512 If the console is currently executing, the text that is executing will
513 If the console is currently executing, the text that is executing will
513 always be returned.
514 always be returned.
514 """
515 """
515 # If we're executing, the input buffer may not even exist anymore due to
516 # If we're executing, the input buffer may not even exist anymore due to
516 # the limit imposed by 'buffer_size'. Therefore, we store it.
517 # the limit imposed by 'buffer_size'. Therefore, we store it.
517 if self._executing:
518 if self._executing:
518 return self._input_buffer_executing
519 return self._input_buffer_executing
519
520
520 cursor = self._get_end_cursor()
521 cursor = self._get_end_cursor()
521 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
522 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
522 input_buffer = cursor.selection().toPlainText()
523 input_buffer = cursor.selection().toPlainText()
523
524
524 # Strip out continuation prompts.
525 # Strip out continuation prompts.
525 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
526 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
526
527
527 def _set_input_buffer(self, string):
528 def _set_input_buffer(self, string):
528 """ Sets the text in the input buffer.
529 """ Sets the text in the input buffer.
529
530
530 If the console is currently executing, this call has no *immediate*
531 If the console is currently executing, this call has no *immediate*
531 effect. When the execution is finished, the input buffer will be updated
532 effect. When the execution is finished, the input buffer will be updated
532 appropriately.
533 appropriately.
533 """
534 """
534 # If we're executing, store the text for later.
535 # If we're executing, store the text for later.
535 if self._executing:
536 if self._executing:
536 self._input_buffer_pending = string
537 self._input_buffer_pending = string
537 return
538 return
538
539
539 # Remove old text.
540 # Remove old text.
540 cursor = self._get_end_cursor()
541 cursor = self._get_end_cursor()
541 cursor.beginEditBlock()
542 cursor.beginEditBlock()
542 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
543 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
543 cursor.removeSelectedText()
544 cursor.removeSelectedText()
544
545
545 # Insert new text with continuation prompts.
546 # Insert new text with continuation prompts.
546 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
547 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
547 cursor.endEditBlock()
548 cursor.endEditBlock()
548 self._control.moveCursor(QtGui.QTextCursor.End)
549 self._control.moveCursor(QtGui.QTextCursor.End)
549
550
550 input_buffer = property(_get_input_buffer, _set_input_buffer)
551 input_buffer = property(_get_input_buffer, _set_input_buffer)
551
552
552 def _get_font(self):
553 def _get_font(self):
553 """ The base font being used by the ConsoleWidget.
554 """ The base font being used by the ConsoleWidget.
554 """
555 """
555 return self._control.document().defaultFont()
556 return self._control.document().defaultFont()
556
557
557 def _set_font(self, font):
558 def _set_font(self, font):
558 """ Sets the base font for the ConsoleWidget to the specified QFont.
559 """ Sets the base font for the ConsoleWidget to the specified QFont.
559 """
560 """
560 font_metrics = QtGui.QFontMetrics(font)
561 font_metrics = QtGui.QFontMetrics(font)
561 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
562 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
562
563
563 self._completion_widget.setFont(font)
564 self._completion_widget.setFont(font)
564 self._control.document().setDefaultFont(font)
565 self._control.document().setDefaultFont(font)
565 if self._page_control:
566 if self._page_control:
566 self._page_control.document().setDefaultFont(font)
567 self._page_control.document().setDefaultFont(font)
567
568
568 self.font_changed.emit(font)
569 self.font_changed.emit(font)
569
570
570 font = property(_get_font, _set_font)
571 font = property(_get_font, _set_font)
571
572
572 def paste(self, mode=QtGui.QClipboard.Clipboard):
573 def paste(self, mode=QtGui.QClipboard.Clipboard):
573 """ Paste the contents of the clipboard into the input region.
574 """ Paste the contents of the clipboard into the input region.
574
575
575 Parameters:
576 Parameters:
576 -----------
577 -----------
577 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
578 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
578
579
579 Controls which part of the system clipboard is used. This can be
580 Controls which part of the system clipboard is used. This can be
580 used to access the selection clipboard in X11 and the Find buffer
581 used to access the selection clipboard in X11 and the Find buffer
581 in Mac OS. By default, the regular clipboard is used.
582 in Mac OS. By default, the regular clipboard is used.
582 """
583 """
583 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
584 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
584 # Make sure the paste is safe.
585 # Make sure the paste is safe.
585 self._keep_cursor_in_buffer()
586 self._keep_cursor_in_buffer()
586 cursor = self._control.textCursor()
587 cursor = self._control.textCursor()
587
588
588 # Remove any trailing newline, which confuses the GUI and forces the
589 # Remove any trailing newline, which confuses the GUI and forces the
589 # user to backspace.
590 # user to backspace.
590 text = QtGui.QApplication.clipboard().text(mode).rstrip()
591 text = QtGui.QApplication.clipboard().text(mode).rstrip()
591 self._insert_plain_text_into_buffer(cursor, dedent(text))
592 self._insert_plain_text_into_buffer(cursor, dedent(text))
592
593
593 def print_(self, printer = None):
594 def print_(self, printer = None):
594 """ Print the contents of the ConsoleWidget to the specified QPrinter.
595 """ Print the contents of the ConsoleWidget to the specified QPrinter.
595 """
596 """
596 if (not printer):
597 if (not printer):
597 printer = QtGui.QPrinter()
598 printer = QtGui.QPrinter()
598 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
599 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
599 return
600 return
600 self._control.print_(printer)
601 self._control.print_(printer)
601
602
602 def prompt_to_top(self):
603 def prompt_to_top(self):
603 """ Moves the prompt to the top of the viewport.
604 """ Moves the prompt to the top of the viewport.
604 """
605 """
605 if not self._executing:
606 if not self._executing:
606 prompt_cursor = self._get_prompt_cursor()
607 prompt_cursor = self._get_prompt_cursor()
607 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
608 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
608 self._set_cursor(prompt_cursor)
609 self._set_cursor(prompt_cursor)
609 self._set_top_cursor(prompt_cursor)
610 self._set_top_cursor(prompt_cursor)
610
611
611 def redo(self):
612 def redo(self):
612 """ Redo the last operation. If there is no operation to redo, nothing
613 """ Redo the last operation. If there is no operation to redo, nothing
613 happens.
614 happens.
614 """
615 """
615 self._control.redo()
616 self._control.redo()
616
617
617 def reset_font(self):
618 def reset_font(self):
618 """ Sets the font to the default fixed-width font for this platform.
619 """ Sets the font to the default fixed-width font for this platform.
619 """
620 """
620 if sys.platform == 'win32':
621 if sys.platform == 'win32':
621 # Consolas ships with Vista/Win7, fallback to Courier if needed
622 # Consolas ships with Vista/Win7, fallback to Courier if needed
622 fallback = 'Courier'
623 fallback = 'Courier'
623 elif sys.platform == 'darwin':
624 elif sys.platform == 'darwin':
624 # OSX always has Monaco
625 # OSX always has Monaco
625 fallback = 'Monaco'
626 fallback = 'Monaco'
626 else:
627 else:
627 # Monospace should always exist
628 # Monospace should always exist
628 fallback = 'Monospace'
629 fallback = 'Monospace'
629 font = get_font(self.font_family, fallback)
630 font = get_font(self.font_family, fallback)
630 if self.font_size:
631 if self.font_size:
631 font.setPointSize(self.font_size)
632 font.setPointSize(self.font_size)
632 else:
633 else:
633 font.setPointSize(QtGui.qApp.font().pointSize())
634 font.setPointSize(QtGui.qApp.font().pointSize())
634 font.setStyleHint(QtGui.QFont.TypeWriter)
635 font.setStyleHint(QtGui.QFont.TypeWriter)
635 self._set_font(font)
636 self._set_font(font)
636
637
637 def change_font_size(self, delta):
638 def change_font_size(self, delta):
638 """Change the font size by the specified amount (in points).
639 """Change the font size by the specified amount (in points).
639 """
640 """
640 font = self.font
641 font = self.font
641 size = max(font.pointSize() + delta, 1) # minimum 1 point
642 size = max(font.pointSize() + delta, 1) # minimum 1 point
642 font.setPointSize(size)
643 font.setPointSize(size)
643 self._set_font(font)
644 self._set_font(font)
644
645
645 def select_all(self):
646 def select_all(self):
646 """ Selects all the text in the buffer.
647 """ Selects all the text in the buffer.
647 """
648 """
648 self._control.selectAll()
649 self._control.selectAll()
649
650
650 def _get_tab_width(self):
651 def _get_tab_width(self):
651 """ The width (in terms of space characters) for tab characters.
652 """ The width (in terms of space characters) for tab characters.
652 """
653 """
653 return self._tab_width
654 return self._tab_width
654
655
655 def _set_tab_width(self, tab_width):
656 def _set_tab_width(self, tab_width):
656 """ Sets the width (in terms of space characters) for tab characters.
657 """ Sets the width (in terms of space characters) for tab characters.
657 """
658 """
658 font_metrics = QtGui.QFontMetrics(self.font)
659 font_metrics = QtGui.QFontMetrics(self.font)
659 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
660 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
660
661
661 self._tab_width = tab_width
662 self._tab_width = tab_width
662
663
663 tab_width = property(_get_tab_width, _set_tab_width)
664 tab_width = property(_get_tab_width, _set_tab_width)
664
665
665 def undo(self):
666 def undo(self):
666 """ Undo the last operation. If there is no operation to undo, nothing
667 """ Undo the last operation. If there is no operation to undo, nothing
667 happens.
668 happens.
668 """
669 """
669 self._control.undo()
670 self._control.undo()
670
671
671 #---------------------------------------------------------------------------
672 #---------------------------------------------------------------------------
672 # 'ConsoleWidget' abstract interface
673 # 'ConsoleWidget' abstract interface
673 #---------------------------------------------------------------------------
674 #---------------------------------------------------------------------------
674
675
675 def _is_complete(self, source, interactive):
676 def _is_complete(self, source, interactive):
676 """ Returns whether 'source' can be executed. When triggered by an
677 """ Returns whether 'source' can be executed. When triggered by an
677 Enter/Return key press, 'interactive' is True; otherwise, it is
678 Enter/Return key press, 'interactive' is True; otherwise, it is
678 False.
679 False.
679 """
680 """
680 raise NotImplementedError
681 raise NotImplementedError
681
682
682 def _execute(self, source, hidden):
683 def _execute(self, source, hidden):
683 """ Execute 'source'. If 'hidden', do not show any output.
684 """ Execute 'source'. If 'hidden', do not show any output.
684 """
685 """
685 raise NotImplementedError
686 raise NotImplementedError
686
687
687 def _prompt_started_hook(self):
688 def _prompt_started_hook(self):
688 """ Called immediately after a new prompt is displayed.
689 """ Called immediately after a new prompt is displayed.
689 """
690 """
690 pass
691 pass
691
692
692 def _prompt_finished_hook(self):
693 def _prompt_finished_hook(self):
693 """ Called immediately after a prompt is finished, i.e. when some input
694 """ Called immediately after a prompt is finished, i.e. when some input
694 will be processed and a new prompt displayed.
695 will be processed and a new prompt displayed.
695 """
696 """
696 pass
697 pass
697
698
698 def _up_pressed(self, shift_modifier):
699 def _up_pressed(self, shift_modifier):
699 """ Called when the up key is pressed. Returns whether to continue
700 """ Called when the up key is pressed. Returns whether to continue
700 processing the event.
701 processing the event.
701 """
702 """
702 return True
703 return True
703
704
704 def _down_pressed(self, shift_modifier):
705 def _down_pressed(self, shift_modifier):
705 """ Called when the down key is pressed. Returns whether to continue
706 """ Called when the down key is pressed. Returns whether to continue
706 processing the event.
707 processing the event.
707 """
708 """
708 return True
709 return True
709
710
710 def _tab_pressed(self):
711 def _tab_pressed(self):
711 """ Called when the tab key is pressed. Returns whether to continue
712 """ Called when the tab key is pressed. Returns whether to continue
712 processing the event.
713 processing the event.
713 """
714 """
714 return False
715 return False
715
716
716 #--------------------------------------------------------------------------
717 #--------------------------------------------------------------------------
717 # 'ConsoleWidget' protected interface
718 # 'ConsoleWidget' protected interface
718 #--------------------------------------------------------------------------
719 #--------------------------------------------------------------------------
719
720
720 def _append_html(self, html):
721 def _append_html(self, html):
721 """ Appends html at the end of the console buffer.
722 """ Appends html at the end of the console buffer.
722 """
723 """
723 cursor = self._get_end_cursor()
724 cursor = self._get_end_cursor()
724 self._insert_html(cursor, html)
725 self._insert_html(cursor, html)
725
726
726 def _append_html_fetching_plain_text(self, html):
727 def _append_html_fetching_plain_text(self, html):
727 """ Appends 'html', then returns the plain text version of it.
728 """ Appends 'html', then returns the plain text version of it.
728 """
729 """
729 cursor = self._get_end_cursor()
730 cursor = self._get_end_cursor()
730 return self._insert_html_fetching_plain_text(cursor, html)
731 return self._insert_html_fetching_plain_text(cursor, html)
731
732
732 def _append_plain_text(self, text):
733 def _append_plain_text(self, text):
733 """ Appends plain text at the end of the console buffer, processing
734 """ Appends plain text at the end of the console buffer, processing
734 ANSI codes if enabled.
735 ANSI codes if enabled.
735 """
736 """
736 cursor = self._get_end_cursor()
737 cursor = self._get_end_cursor()
737 self._insert_plain_text(cursor, text)
738 self._insert_plain_text(cursor, text)
738
739
739 def _append_plain_text_keeping_prompt(self, text):
740 def _append_plain_text_keeping_prompt(self, text):
740 """ Writes 'text' after the current prompt, then restores the old prompt
741 """ Writes 'text' after the current prompt, then restores the old prompt
741 with its old input buffer.
742 with its old input buffer.
742 """
743 """
743 input_buffer = self.input_buffer
744 input_buffer = self.input_buffer
744 self._append_plain_text('\n')
745 self._append_plain_text('\n')
745 self._prompt_finished()
746 self._prompt_finished()
746
747
747 self._append_plain_text(text)
748 self._append_plain_text(text)
748 self._show_prompt()
749 self._show_prompt()
749 self.input_buffer = input_buffer
750 self.input_buffer = input_buffer
750
751
751 def _cancel_text_completion(self):
752 def _cancel_text_completion(self):
752 """ If text completion is progress, cancel it.
753 """ If text completion is progress, cancel it.
753 """
754 """
754 if self._text_completing_pos:
755 if self._text_completing_pos:
755 self._clear_temporary_buffer()
756 self._clear_temporary_buffer()
756 self._text_completing_pos = 0
757 self._text_completing_pos = 0
757
758
758 def _clear_temporary_buffer(self):
759 def _clear_temporary_buffer(self):
759 """ Clears the "temporary text" buffer, i.e. all the text following
760 """ Clears the "temporary text" buffer, i.e. all the text following
760 the prompt region.
761 the prompt region.
761 """
762 """
762 # Select and remove all text below the input buffer.
763 # Select and remove all text below the input buffer.
763 cursor = self._get_prompt_cursor()
764 cursor = self._get_prompt_cursor()
764 prompt = self._continuation_prompt.lstrip()
765 prompt = self._continuation_prompt.lstrip()
765 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
766 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
766 temp_cursor = QtGui.QTextCursor(cursor)
767 temp_cursor = QtGui.QTextCursor(cursor)
767 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
768 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
768 text = temp_cursor.selection().toPlainText().lstrip()
769 text = temp_cursor.selection().toPlainText().lstrip()
769 if not text.startswith(prompt):
770 if not text.startswith(prompt):
770 break
771 break
771 else:
772 else:
772 # We've reached the end of the input buffer and no text follows.
773 # We've reached the end of the input buffer and no text follows.
773 return
774 return
774 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
775 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
775 cursor.movePosition(QtGui.QTextCursor.End,
776 cursor.movePosition(QtGui.QTextCursor.End,
776 QtGui.QTextCursor.KeepAnchor)
777 QtGui.QTextCursor.KeepAnchor)
777 cursor.removeSelectedText()
778 cursor.removeSelectedText()
778
779
779 # After doing this, we have no choice but to clear the undo/redo
780 # After doing this, we have no choice but to clear the undo/redo
780 # history. Otherwise, the text is not "temporary" at all, because it
781 # history. Otherwise, the text is not "temporary" at all, because it
781 # can be recalled with undo/redo. Unfortunately, Qt does not expose
782 # can be recalled with undo/redo. Unfortunately, Qt does not expose
782 # fine-grained control to the undo/redo system.
783 # fine-grained control to the undo/redo system.
783 if self._control.isUndoRedoEnabled():
784 if self._control.isUndoRedoEnabled():
784 self._control.setUndoRedoEnabled(False)
785 self._control.setUndoRedoEnabled(False)
785 self._control.setUndoRedoEnabled(True)
786 self._control.setUndoRedoEnabled(True)
786
787
787 def _complete_with_items(self, cursor, items):
788 def _complete_with_items(self, cursor, items):
788 """ Performs completion with 'items' at the specified cursor location.
789 """ Performs completion with 'items' at the specified cursor location.
789 """
790 """
790 self._cancel_text_completion()
791 self._cancel_text_completion()
791
792
792 if len(items) == 1:
793 if len(items) == 1:
793 cursor.setPosition(self._control.textCursor().position(),
794 cursor.setPosition(self._control.textCursor().position(),
794 QtGui.QTextCursor.KeepAnchor)
795 QtGui.QTextCursor.KeepAnchor)
795 cursor.insertText(items[0])
796 cursor.insertText(items[0])
796
797
797 elif len(items) > 1:
798 elif len(items) > 1:
798 current_pos = self._control.textCursor().position()
799 current_pos = self._control.textCursor().position()
799 prefix = commonprefix(items)
800 prefix = commonprefix(items)
800 if prefix:
801 if prefix:
801 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
802 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
802 cursor.insertText(prefix)
803 cursor.insertText(prefix)
803 current_pos = cursor.position()
804 current_pos = cursor.position()
804
805
805 if self.gui_completion:
806 if self.gui_completion:
806 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
807 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
807 self._completion_widget.show_items(cursor, items)
808 self._completion_widget.show_items(cursor, items)
808 else:
809 else:
809 cursor.beginEditBlock()
810 cursor.beginEditBlock()
810 self._append_plain_text('\n')
811 self._append_plain_text('\n')
811 self._page(self._format_as_columns(items))
812 self._page(self._format_as_columns(items))
812 cursor.endEditBlock()
813 cursor.endEditBlock()
813
814
814 cursor.setPosition(current_pos)
815 cursor.setPosition(current_pos)
815 self._control.moveCursor(QtGui.QTextCursor.End)
816 self._control.moveCursor(QtGui.QTextCursor.End)
816 self._control.setTextCursor(cursor)
817 self._control.setTextCursor(cursor)
817 self._text_completing_pos = current_pos
818 self._text_completing_pos = current_pos
818
819
819 def _context_menu_make(self, pos):
820 def _context_menu_make(self, pos):
820 """ Creates a context menu for the given QPoint (in widget coordinates).
821 """ Creates a context menu for the given QPoint (in widget coordinates).
821 """
822 """
822 menu = QtGui.QMenu(self)
823 menu = QtGui.QMenu(self)
823
824
824 cut_action = menu.addAction('Cut', self.cut)
825 cut_action = menu.addAction('Cut', self.cut)
825 cut_action.setEnabled(self.can_cut())
826 cut_action.setEnabled(self.can_cut())
826 cut_action.setShortcut(QtGui.QKeySequence.Cut)
827 cut_action.setShortcut(QtGui.QKeySequence.Cut)
827
828
828 copy_action = menu.addAction('Copy', self.copy)
829 copy_action = menu.addAction('Copy', self.copy)
829 copy_action.setEnabled(self.can_copy())
830 copy_action.setEnabled(self.can_copy())
830 copy_action.setShortcut(QtGui.QKeySequence.Copy)
831 copy_action.setShortcut(QtGui.QKeySequence.Copy)
831
832
832 paste_action = menu.addAction('Paste', self.paste)
833 paste_action = menu.addAction('Paste', self.paste)
833 paste_action.setEnabled(self.can_paste())
834 paste_action.setEnabled(self.can_paste())
834 paste_action.setShortcut(QtGui.QKeySequence.Paste)
835 paste_action.setShortcut(QtGui.QKeySequence.Paste)
835
836
836 menu.addSeparator()
837 menu.addSeparator()
837 menu.addAction(self._select_all_action)
838 menu.addAction(self._select_all_action)
838
839
839 menu.addSeparator()
840 menu.addSeparator()
840 menu.addAction(self._export_action)
841 menu.addAction(self._export_action)
841 menu.addAction(self._print_action)
842 menu.addAction(self._print_action)
842
843
843 return menu
844 return menu
844
845
845 def _control_key_down(self, modifiers, include_command=False):
846 def _control_key_down(self, modifiers, include_command=False):
846 """ Given a KeyboardModifiers flags object, return whether the Control
847 """ Given a KeyboardModifiers flags object, return whether the Control
847 key is down.
848 key is down.
848
849
849 Parameters:
850 Parameters:
850 -----------
851 -----------
851 include_command : bool, optional (default True)
852 include_command : bool, optional (default True)
852 Whether to treat the Command key as a (mutually exclusive) synonym
853 Whether to treat the Command key as a (mutually exclusive) synonym
853 for Control when in Mac OS.
854 for Control when in Mac OS.
854 """
855 """
855 # Note that on Mac OS, ControlModifier corresponds to the Command key
856 # Note that on Mac OS, ControlModifier corresponds to the Command key
856 # while MetaModifier corresponds to the Control key.
857 # while MetaModifier corresponds to the Control key.
857 if sys.platform == 'darwin':
858 if sys.platform == 'darwin':
858 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
859 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
859 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
860 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
860 else:
861 else:
861 return bool(modifiers & QtCore.Qt.ControlModifier)
862 return bool(modifiers & QtCore.Qt.ControlModifier)
862
863
863 def _create_control(self):
864 def _create_control(self):
864 """ Creates and connects the underlying text widget.
865 """ Creates and connects the underlying text widget.
865 """
866 """
866 # Create the underlying control.
867 # Create the underlying control.
867 if self.kind == 'plain':
868 if self.kind == 'plain':
868 control = QtGui.QPlainTextEdit()
869 control = QtGui.QPlainTextEdit()
869 elif self.kind == 'rich':
870 elif self.kind == 'rich':
870 control = QtGui.QTextEdit()
871 control = QtGui.QTextEdit()
871 control.setAcceptRichText(False)
872 control.setAcceptRichText(False)
872
873
873 # Install event filters. The filter on the viewport is needed for
874 # Install event filters. The filter on the viewport is needed for
874 # mouse events and drag events.
875 # mouse events and drag events.
875 control.installEventFilter(self)
876 control.installEventFilter(self)
876 control.viewport().installEventFilter(self)
877 control.viewport().installEventFilter(self)
877
878
878 # Connect signals.
879 # Connect signals.
879 control.cursorPositionChanged.connect(self._cursor_position_changed)
880 control.cursorPositionChanged.connect(self._cursor_position_changed)
880 control.customContextMenuRequested.connect(
881 control.customContextMenuRequested.connect(
881 self._custom_context_menu_requested)
882 self._custom_context_menu_requested)
882 control.copyAvailable.connect(self.copy_available)
883 control.copyAvailable.connect(self.copy_available)
883 control.redoAvailable.connect(self.redo_available)
884 control.redoAvailable.connect(self.redo_available)
884 control.undoAvailable.connect(self.undo_available)
885 control.undoAvailable.connect(self.undo_available)
885
886
886 # Hijack the document size change signal to prevent Qt from adjusting
887 # Hijack the document size change signal to prevent Qt from adjusting
887 # the viewport's scrollbar. We are relying on an implementation detail
888 # the viewport's scrollbar. We are relying on an implementation detail
888 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
889 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
889 # this functionality we cannot create a nice terminal interface.
890 # this functionality we cannot create a nice terminal interface.
890 layout = control.document().documentLayout()
891 layout = control.document().documentLayout()
891 layout.documentSizeChanged.disconnect()
892 layout.documentSizeChanged.disconnect()
892 layout.documentSizeChanged.connect(self._adjust_scrollbars)
893 layout.documentSizeChanged.connect(self._adjust_scrollbars)
893
894
894 # Configure the control.
895 # Configure the control.
895 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
896 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
896 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
897 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
897 control.setReadOnly(True)
898 control.setReadOnly(True)
898 control.setUndoRedoEnabled(False)
899 control.setUndoRedoEnabled(False)
899 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
900 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
900 return control
901 return control
901
902
902 def _create_page_control(self):
903 def _create_page_control(self):
903 """ Creates and connects the underlying paging widget.
904 """ Creates and connects the underlying paging widget.
904 """
905 """
905 if self.kind == 'plain':
906 if self.kind == 'plain':
906 control = QtGui.QPlainTextEdit()
907 control = QtGui.QPlainTextEdit()
907 elif self.kind == 'rich':
908 elif self.kind == 'rich':
908 control = QtGui.QTextEdit()
909 control = QtGui.QTextEdit()
909 control.installEventFilter(self)
910 control.installEventFilter(self)
910 control.setReadOnly(True)
911 control.setReadOnly(True)
911 control.setUndoRedoEnabled(False)
912 control.setUndoRedoEnabled(False)
912 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
913 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
913 return control
914 return control
914
915
915 def _event_filter_console_keypress(self, event):
916 def _event_filter_console_keypress(self, event):
916 """ Filter key events for the underlying text widget to create a
917 """ Filter key events for the underlying text widget to create a
917 console-like interface.
918 console-like interface.
918 """
919 """
919 intercepted = False
920 intercepted = False
920 cursor = self._control.textCursor()
921 cursor = self._control.textCursor()
921 position = cursor.position()
922 position = cursor.position()
922 key = event.key()
923 key = event.key()
923 ctrl_down = self._control_key_down(event.modifiers())
924 ctrl_down = self._control_key_down(event.modifiers())
924 alt_down = event.modifiers() & QtCore.Qt.AltModifier
925 alt_down = event.modifiers() & QtCore.Qt.AltModifier
925 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
926 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
926
927
927 #------ Special sequences ----------------------------------------------
928 #------ Special sequences ----------------------------------------------
928
929
929 if event.matches(QtGui.QKeySequence.Copy):
930 if event.matches(QtGui.QKeySequence.Copy):
930 self.copy()
931 self.copy()
931 intercepted = True
932 intercepted = True
932
933
933 elif event.matches(QtGui.QKeySequence.Cut):
934 elif event.matches(QtGui.QKeySequence.Cut):
934 self.cut()
935 self.cut()
935 intercepted = True
936 intercepted = True
936
937
937 elif event.matches(QtGui.QKeySequence.Paste):
938 elif event.matches(QtGui.QKeySequence.Paste):
938 self.paste()
939 self.paste()
939 intercepted = True
940 intercepted = True
940
941
941 #------ Special modifier logic -----------------------------------------
942 #------ Special modifier logic -----------------------------------------
942
943
943 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
944 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
944 intercepted = True
945 intercepted = True
945
946
946 # Special handling when tab completing in text mode.
947 # Special handling when tab completing in text mode.
947 self._cancel_text_completion()
948 self._cancel_text_completion()
948
949
949 if self._in_buffer(position):
950 if self._in_buffer(position):
950 if self._reading:
951 if self._reading:
951 self._append_plain_text('\n')
952 self._append_plain_text('\n')
952 self._reading = False
953 self._reading = False
953 if self._reading_callback:
954 if self._reading_callback:
954 self._reading_callback()
955 self._reading_callback()
955
956
956 # If the input buffer is a single line or there is only
957 # If the input buffer is a single line or there is only
957 # whitespace after the cursor, execute. Otherwise, split the
958 # whitespace after the cursor, execute. Otherwise, split the
958 # line with a continuation prompt.
959 # line with a continuation prompt.
959 elif not self._executing:
960 elif not self._executing:
960 cursor.movePosition(QtGui.QTextCursor.End,
961 cursor.movePosition(QtGui.QTextCursor.End,
961 QtGui.QTextCursor.KeepAnchor)
962 QtGui.QTextCursor.KeepAnchor)
962 at_end = len(cursor.selectedText().strip()) == 0
963 at_end = len(cursor.selectedText().strip()) == 0
963 single_line = (self._get_end_cursor().blockNumber() ==
964 single_line = (self._get_end_cursor().blockNumber() ==
964 self._get_prompt_cursor().blockNumber())
965 self._get_prompt_cursor().blockNumber())
965 if (at_end or shift_down or single_line) and not ctrl_down:
966 if (at_end or shift_down or single_line) and not ctrl_down:
966 self.execute(interactive = not shift_down)
967 self.execute(interactive = not shift_down)
967 else:
968 else:
968 # Do this inside an edit block for clean undo/redo.
969 # Do this inside an edit block for clean undo/redo.
969 cursor.beginEditBlock()
970 cursor.beginEditBlock()
970 cursor.setPosition(position)
971 cursor.setPosition(position)
971 cursor.insertText('\n')
972 cursor.insertText('\n')
972 self._insert_continuation_prompt(cursor)
973 self._insert_continuation_prompt(cursor)
973 cursor.endEditBlock()
974 cursor.endEditBlock()
974
975
975 # Ensure that the whole input buffer is visible.
976 # Ensure that the whole input buffer is visible.
976 # FIXME: This will not be usable if the input buffer is
977 # FIXME: This will not be usable if the input buffer is
977 # taller than the console widget.
978 # taller than the console widget.
978 self._control.moveCursor(QtGui.QTextCursor.End)
979 self._control.moveCursor(QtGui.QTextCursor.End)
979 self._control.setTextCursor(cursor)
980 self._control.setTextCursor(cursor)
980
981
981 #------ Control/Cmd modifier -------------------------------------------
982 #------ Control/Cmd modifier -------------------------------------------
982
983
983 elif ctrl_down:
984 elif ctrl_down:
984 if key == QtCore.Qt.Key_G:
985 if key == QtCore.Qt.Key_G:
985 self._keyboard_quit()
986 self._keyboard_quit()
986 intercepted = True
987 intercepted = True
987
988
988 elif key == QtCore.Qt.Key_K:
989 elif key == QtCore.Qt.Key_K:
989 if self._in_buffer(position):
990 if self._in_buffer(position):
990 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
991 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
991 QtGui.QTextCursor.KeepAnchor)
992 QtGui.QTextCursor.KeepAnchor)
992 if not cursor.hasSelection():
993 if not cursor.hasSelection():
993 # Line deletion (remove continuation prompt)
994 # Line deletion (remove continuation prompt)
994 cursor.movePosition(QtGui.QTextCursor.NextBlock,
995 cursor.movePosition(QtGui.QTextCursor.NextBlock,
995 QtGui.QTextCursor.KeepAnchor)
996 QtGui.QTextCursor.KeepAnchor)
996 cursor.movePosition(QtGui.QTextCursor.Right,
997 cursor.movePosition(QtGui.QTextCursor.Right,
997 QtGui.QTextCursor.KeepAnchor,
998 QtGui.QTextCursor.KeepAnchor,
998 len(self._continuation_prompt))
999 len(self._continuation_prompt))
999 self._kill_ring.kill_cursor(cursor)
1000 self._kill_ring.kill_cursor(cursor)
1000 intercepted = True
1001 intercepted = True
1001
1002
1002 elif key == QtCore.Qt.Key_L:
1003 elif key == QtCore.Qt.Key_L:
1003 self.prompt_to_top()
1004 self.prompt_to_top()
1004 intercepted = True
1005 intercepted = True
1005
1006
1006 elif key == QtCore.Qt.Key_O:
1007 elif key == QtCore.Qt.Key_O:
1007 if self._page_control and self._page_control.isVisible():
1008 if self._page_control and self._page_control.isVisible():
1008 self._page_control.setFocus()
1009 self._page_control.setFocus()
1009 intercepted = True
1010 intercepted = True
1010
1011
1011 elif key == QtCore.Qt.Key_U:
1012 elif key == QtCore.Qt.Key_U:
1012 if self._in_buffer(position):
1013 if self._in_buffer(position):
1013 start_line = cursor.blockNumber()
1014 start_line = cursor.blockNumber()
1014 if start_line == self._get_prompt_cursor().blockNumber():
1015 if start_line == self._get_prompt_cursor().blockNumber():
1015 offset = len(self._prompt)
1016 offset = len(self._prompt)
1016 else:
1017 else:
1017 offset = len(self._continuation_prompt)
1018 offset = len(self._continuation_prompt)
1018 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1019 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1019 QtGui.QTextCursor.KeepAnchor)
1020 QtGui.QTextCursor.KeepAnchor)
1020 cursor.movePosition(QtGui.QTextCursor.Right,
1021 cursor.movePosition(QtGui.QTextCursor.Right,
1021 QtGui.QTextCursor.KeepAnchor, offset)
1022 QtGui.QTextCursor.KeepAnchor, offset)
1022 self._kill_ring.kill_cursor(cursor)
1023 self._kill_ring.kill_cursor(cursor)
1023 intercepted = True
1024 intercepted = True
1024
1025
1025 elif key == QtCore.Qt.Key_Y:
1026 elif key == QtCore.Qt.Key_Y:
1026 self._keep_cursor_in_buffer()
1027 self._keep_cursor_in_buffer()
1027 self._kill_ring.yank()
1028 self._kill_ring.yank()
1028 intercepted = True
1029 intercepted = True
1029
1030
1030 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1031 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1031 intercepted = True
1032 intercepted = True
1032
1033
1033 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1034 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1034 self.change_font_size(1)
1035 self.change_font_size(1)
1035 intercepted = True
1036 intercepted = True
1036
1037
1037 elif key == QtCore.Qt.Key_Minus:
1038 elif key == QtCore.Qt.Key_Minus:
1038 self.change_font_size(-1)
1039 self.change_font_size(-1)
1039 intercepted = True
1040 intercepted = True
1040
1041
1041 elif key == QtCore.Qt.Key_0:
1042 elif key == QtCore.Qt.Key_0:
1042 self.reset_font()
1043 self.reset_font()
1043 intercepted = True
1044 intercepted = True
1044
1045
1045 #------ Alt modifier ---------------------------------------------------
1046 #------ Alt modifier ---------------------------------------------------
1046
1047
1047 elif alt_down:
1048 elif alt_down:
1048 if key == QtCore.Qt.Key_B:
1049 if key == QtCore.Qt.Key_B:
1049 self._set_cursor(self._get_word_start_cursor(position))
1050 self._set_cursor(self._get_word_start_cursor(position))
1050 intercepted = True
1051 intercepted = True
1051
1052
1052 elif key == QtCore.Qt.Key_F:
1053 elif key == QtCore.Qt.Key_F:
1053 self._set_cursor(self._get_word_end_cursor(position))
1054 self._set_cursor(self._get_word_end_cursor(position))
1054 intercepted = True
1055 intercepted = True
1055
1056
1056 elif key == QtCore.Qt.Key_Y:
1057 elif key == QtCore.Qt.Key_Y:
1057 self._kill_ring.rotate()
1058 self._kill_ring.rotate()
1058 intercepted = True
1059 intercepted = True
1059
1060
1060 elif key == QtCore.Qt.Key_Backspace:
1061 elif key == QtCore.Qt.Key_Backspace:
1061 cursor = self._get_word_start_cursor(position)
1062 cursor = self._get_word_start_cursor(position)
1062 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1063 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1063 self._kill_ring.kill_cursor(cursor)
1064 self._kill_ring.kill_cursor(cursor)
1064 intercepted = True
1065 intercepted = True
1065
1066
1066 elif key == QtCore.Qt.Key_D:
1067 elif key == QtCore.Qt.Key_D:
1067 cursor = self._get_word_end_cursor(position)
1068 cursor = self._get_word_end_cursor(position)
1068 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1069 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1069 self._kill_ring.kill_cursor(cursor)
1070 self._kill_ring.kill_cursor(cursor)
1070 intercepted = True
1071 intercepted = True
1071
1072
1072 elif key == QtCore.Qt.Key_Delete:
1073 elif key == QtCore.Qt.Key_Delete:
1073 intercepted = True
1074 intercepted = True
1074
1075
1075 elif key == QtCore.Qt.Key_Greater:
1076 elif key == QtCore.Qt.Key_Greater:
1076 self._control.moveCursor(QtGui.QTextCursor.End)
1077 self._control.moveCursor(QtGui.QTextCursor.End)
1077 intercepted = True
1078 intercepted = True
1078
1079
1079 elif key == QtCore.Qt.Key_Less:
1080 elif key == QtCore.Qt.Key_Less:
1080 self._control.setTextCursor(self._get_prompt_cursor())
1081 self._control.setTextCursor(self._get_prompt_cursor())
1081 intercepted = True
1082 intercepted = True
1082
1083
1083 #------ No modifiers ---------------------------------------------------
1084 #------ No modifiers ---------------------------------------------------
1084
1085
1085 else:
1086 else:
1086 if shift_down:
1087 if shift_down:
1087 anchormode = QtGui.QTextCursor.KeepAnchor
1088 anchormode = QtGui.QTextCursor.KeepAnchor
1088 else:
1089 else:
1089 anchormode = QtGui.QTextCursor.MoveAnchor
1090 anchormode = QtGui.QTextCursor.MoveAnchor
1090
1091
1091 if key == QtCore.Qt.Key_Escape:
1092 if key == QtCore.Qt.Key_Escape:
1092 self._keyboard_quit()
1093 self._keyboard_quit()
1093 intercepted = True
1094 intercepted = True
1094
1095
1095 elif key == QtCore.Qt.Key_Up:
1096 elif key == QtCore.Qt.Key_Up:
1096 if self._reading or not self._up_pressed(shift_down):
1097 if self._reading or not self._up_pressed(shift_down):
1097 intercepted = True
1098 intercepted = True
1098 else:
1099 else:
1099 prompt_line = self._get_prompt_cursor().blockNumber()
1100 prompt_line = self._get_prompt_cursor().blockNumber()
1100 intercepted = cursor.blockNumber() <= prompt_line
1101 intercepted = cursor.blockNumber() <= prompt_line
1101
1102
1102 elif key == QtCore.Qt.Key_Down:
1103 elif key == QtCore.Qt.Key_Down:
1103 if self._reading or not self._down_pressed(shift_down):
1104 if self._reading or not self._down_pressed(shift_down):
1104 intercepted = True
1105 intercepted = True
1105 else:
1106 else:
1106 end_line = self._get_end_cursor().blockNumber()
1107 end_line = self._get_end_cursor().blockNumber()
1107 intercepted = cursor.blockNumber() == end_line
1108 intercepted = cursor.blockNumber() == end_line
1108
1109
1109 elif key == QtCore.Qt.Key_Tab:
1110 elif key == QtCore.Qt.Key_Tab:
1110 if not self._reading:
1111 if not self._reading:
1111 intercepted = not self._tab_pressed()
1112 intercepted = not self._tab_pressed()
1112
1113
1113 elif key == QtCore.Qt.Key_Left:
1114 elif key == QtCore.Qt.Key_Left:
1114
1115
1115 # Move to the previous line
1116 # Move to the previous line
1116 line, col = cursor.blockNumber(), cursor.columnNumber()
1117 line, col = cursor.blockNumber(), cursor.columnNumber()
1117 if line > self._get_prompt_cursor().blockNumber() and \
1118 if line > self._get_prompt_cursor().blockNumber() and \
1118 col == len(self._continuation_prompt):
1119 col == len(self._continuation_prompt):
1119 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1120 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1120 mode=anchormode)
1121 mode=anchormode)
1121 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1122 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1122 mode=anchormode)
1123 mode=anchormode)
1123 intercepted = True
1124 intercepted = True
1124
1125
1125 # Regular left movement
1126 # Regular left movement
1126 else:
1127 else:
1127 intercepted = not self._in_buffer(position - 1)
1128 intercepted = not self._in_buffer(position - 1)
1128
1129
1129 elif key == QtCore.Qt.Key_Right:
1130 elif key == QtCore.Qt.Key_Right:
1130 original_block_number = cursor.blockNumber()
1131 original_block_number = cursor.blockNumber()
1131 cursor.movePosition(QtGui.QTextCursor.Right,
1132 cursor.movePosition(QtGui.QTextCursor.Right,
1132 mode=anchormode)
1133 mode=anchormode)
1133 if cursor.blockNumber() != original_block_number:
1134 if cursor.blockNumber() != original_block_number:
1134 cursor.movePosition(QtGui.QTextCursor.Right,
1135 cursor.movePosition(QtGui.QTextCursor.Right,
1135 n=len(self._continuation_prompt),
1136 n=len(self._continuation_prompt),
1136 mode=anchormode)
1137 mode=anchormode)
1137 self._set_cursor(cursor)
1138 self._set_cursor(cursor)
1138 intercepted = True
1139 intercepted = True
1139
1140
1140 elif key == QtCore.Qt.Key_Home:
1141 elif key == QtCore.Qt.Key_Home:
1141 start_line = cursor.blockNumber()
1142 start_line = cursor.blockNumber()
1142 if start_line == self._get_prompt_cursor().blockNumber():
1143 if start_line == self._get_prompt_cursor().blockNumber():
1143 start_pos = self._prompt_pos
1144 start_pos = self._prompt_pos
1144 else:
1145 else:
1145 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1146 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1146 QtGui.QTextCursor.KeepAnchor)
1147 QtGui.QTextCursor.KeepAnchor)
1147 start_pos = cursor.position()
1148 start_pos = cursor.position()
1148 start_pos += len(self._continuation_prompt)
1149 start_pos += len(self._continuation_prompt)
1149 cursor.setPosition(position)
1150 cursor.setPosition(position)
1150 if shift_down and self._in_buffer(position):
1151 if shift_down and self._in_buffer(position):
1151 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1152 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1152 else:
1153 else:
1153 cursor.setPosition(start_pos)
1154 cursor.setPosition(start_pos)
1154 self._set_cursor(cursor)
1155 self._set_cursor(cursor)
1155 intercepted = True
1156 intercepted = True
1156
1157
1157 elif key == QtCore.Qt.Key_Backspace:
1158 elif key == QtCore.Qt.Key_Backspace:
1158
1159
1159 # Line deletion (remove continuation prompt)
1160 # Line deletion (remove continuation prompt)
1160 line, col = cursor.blockNumber(), cursor.columnNumber()
1161 line, col = cursor.blockNumber(), cursor.columnNumber()
1161 if not self._reading and \
1162 if not self._reading and \
1162 col == len(self._continuation_prompt) and \
1163 col == len(self._continuation_prompt) and \
1163 line > self._get_prompt_cursor().blockNumber():
1164 line > self._get_prompt_cursor().blockNumber():
1164 cursor.beginEditBlock()
1165 cursor.beginEditBlock()
1165 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1166 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1166 QtGui.QTextCursor.KeepAnchor)
1167 QtGui.QTextCursor.KeepAnchor)
1167 cursor.removeSelectedText()
1168 cursor.removeSelectedText()
1168 cursor.deletePreviousChar()
1169 cursor.deletePreviousChar()
1169 cursor.endEditBlock()
1170 cursor.endEditBlock()
1170 intercepted = True
1171 intercepted = True
1171
1172
1172 # Regular backwards deletion
1173 # Regular backwards deletion
1173 else:
1174 else:
1174 anchor = cursor.anchor()
1175 anchor = cursor.anchor()
1175 if anchor == position:
1176 if anchor == position:
1176 intercepted = not self._in_buffer(position - 1)
1177 intercepted = not self._in_buffer(position - 1)
1177 else:
1178 else:
1178 intercepted = not self._in_buffer(min(anchor, position))
1179 intercepted = not self._in_buffer(min(anchor, position))
1179
1180
1180 elif key == QtCore.Qt.Key_Delete:
1181 elif key == QtCore.Qt.Key_Delete:
1181
1182
1182 # Line deletion (remove continuation prompt)
1183 # Line deletion (remove continuation prompt)
1183 if not self._reading and self._in_buffer(position) and \
1184 if not self._reading and self._in_buffer(position) and \
1184 cursor.atBlockEnd() and not cursor.hasSelection():
1185 cursor.atBlockEnd() and not cursor.hasSelection():
1185 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1186 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1186 QtGui.QTextCursor.KeepAnchor)
1187 QtGui.QTextCursor.KeepAnchor)
1187 cursor.movePosition(QtGui.QTextCursor.Right,
1188 cursor.movePosition(QtGui.QTextCursor.Right,
1188 QtGui.QTextCursor.KeepAnchor,
1189 QtGui.QTextCursor.KeepAnchor,
1189 len(self._continuation_prompt))
1190 len(self._continuation_prompt))
1190 cursor.removeSelectedText()
1191 cursor.removeSelectedText()
1191 intercepted = True
1192 intercepted = True
1192
1193
1193 # Regular forwards deletion:
1194 # Regular forwards deletion:
1194 else:
1195 else:
1195 anchor = cursor.anchor()
1196 anchor = cursor.anchor()
1196 intercepted = (not self._in_buffer(anchor) or
1197 intercepted = (not self._in_buffer(anchor) or
1197 not self._in_buffer(position))
1198 not self._in_buffer(position))
1198
1199
1199 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1200 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1200 # using the keyboard in any part of the buffer.
1201 # using the keyboard in any part of the buffer.
1201 if not self._control_key_down(event.modifiers(), include_command=True):
1202 if not self._control_key_down(event.modifiers(), include_command=True):
1202 self._keep_cursor_in_buffer()
1203 self._keep_cursor_in_buffer()
1203
1204
1204 return intercepted
1205 return intercepted
1205
1206
1206 def _event_filter_page_keypress(self, event):
1207 def _event_filter_page_keypress(self, event):
1207 """ Filter key events for the paging widget to create console-like
1208 """ Filter key events for the paging widget to create console-like
1208 interface.
1209 interface.
1209 """
1210 """
1210 key = event.key()
1211 key = event.key()
1211 ctrl_down = self._control_key_down(event.modifiers())
1212 ctrl_down = self._control_key_down(event.modifiers())
1212 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1213 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1213
1214
1214 if ctrl_down:
1215 if ctrl_down:
1215 if key == QtCore.Qt.Key_O:
1216 if key == QtCore.Qt.Key_O:
1216 self._control.setFocus()
1217 self._control.setFocus()
1217 intercept = True
1218 intercept = True
1218
1219
1219 elif alt_down:
1220 elif alt_down:
1220 if key == QtCore.Qt.Key_Greater:
1221 if key == QtCore.Qt.Key_Greater:
1221 self._page_control.moveCursor(QtGui.QTextCursor.End)
1222 self._page_control.moveCursor(QtGui.QTextCursor.End)
1222 intercepted = True
1223 intercepted = True
1223
1224
1224 elif key == QtCore.Qt.Key_Less:
1225 elif key == QtCore.Qt.Key_Less:
1225 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1226 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1226 intercepted = True
1227 intercepted = True
1227
1228
1228 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1229 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1229 if self._splitter:
1230 if self._splitter:
1230 self._page_control.hide()
1231 self._page_control.hide()
1231 else:
1232 else:
1232 self.layout().setCurrentWidget(self._control)
1233 self.layout().setCurrentWidget(self._control)
1233 return True
1234 return True
1234
1235
1235 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1236 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1236 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1237 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1237 QtCore.Qt.Key_PageDown,
1238 QtCore.Qt.Key_PageDown,
1238 QtCore.Qt.NoModifier)
1239 QtCore.Qt.NoModifier)
1239 QtGui.qApp.sendEvent(self._page_control, new_event)
1240 QtGui.qApp.sendEvent(self._page_control, new_event)
1240 return True
1241 return True
1241
1242
1242 elif key == QtCore.Qt.Key_Backspace:
1243 elif key == QtCore.Qt.Key_Backspace:
1243 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1244 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1244 QtCore.Qt.Key_PageUp,
1245 QtCore.Qt.Key_PageUp,
1245 QtCore.Qt.NoModifier)
1246 QtCore.Qt.NoModifier)
1246 QtGui.qApp.sendEvent(self._page_control, new_event)
1247 QtGui.qApp.sendEvent(self._page_control, new_event)
1247 return True
1248 return True
1248
1249
1249 return False
1250 return False
1250
1251
1251 def _format_as_columns(self, items, separator=' '):
1252 def _format_as_columns(self, items, separator=' '):
1252 """ Transform a list of strings into a single string with columns.
1253 """ Transform a list of strings into a single string with columns.
1253
1254
1254 Parameters
1255 Parameters
1255 ----------
1256 ----------
1256 items : sequence of strings
1257 items : sequence of strings
1257 The strings to process.
1258 The strings to process.
1258
1259
1259 separator : str, optional [default is two spaces]
1260 separator : str, optional [default is two spaces]
1260 The string that separates columns.
1261 The string that separates columns.
1261
1262
1262 Returns
1263 Returns
1263 -------
1264 -------
1264 The formatted string.
1265 The formatted string.
1265 """
1266 """
1266 # Note: this code is adapted from columnize 0.3.2.
1267 # Note: this code is adapted from columnize 0.3.2.
1267 # See http://code.google.com/p/pycolumnize/
1268 # See http://code.google.com/p/pycolumnize/
1268
1269
1269 # Calculate the number of characters available.
1270 # Calculate the number of characters available.
1270 width = self._control.viewport().width()
1271 width = self._control.viewport().width()
1271 char_width = QtGui.QFontMetrics(self.font).width(' ')
1272 char_width = QtGui.QFontMetrics(self.font).width(' ')
1272 displaywidth = max(10, (width / char_width) - 1)
1273 displaywidth = max(10, (width / char_width) - 1)
1273
1274
1274 # Some degenerate cases.
1275 # Some degenerate cases.
1275 size = len(items)
1276 size = len(items)
1276 if size == 0:
1277 if size == 0:
1277 return '\n'
1278 return '\n'
1278 elif size == 1:
1279 elif size == 1:
1279 return '%s\n' % items[0]
1280 return '%s\n' % items[0]
1280
1281
1281 # Try every row count from 1 upwards
1282 # Try every row count from 1 upwards
1282 array_index = lambda nrows, row, col: nrows*col + row
1283 array_index = lambda nrows, row, col: nrows*col + row
1283 for nrows in range(1, size):
1284 for nrows in range(1, size):
1284 ncols = (size + nrows - 1) // nrows
1285 ncols = (size + nrows - 1) // nrows
1285 colwidths = []
1286 colwidths = []
1286 totwidth = -len(separator)
1287 totwidth = -len(separator)
1287 for col in range(ncols):
1288 for col in range(ncols):
1288 # Get max column width for this column
1289 # Get max column width for this column
1289 colwidth = 0
1290 colwidth = 0
1290 for row in range(nrows):
1291 for row in range(nrows):
1291 i = array_index(nrows, row, col)
1292 i = array_index(nrows, row, col)
1292 if i >= size: break
1293 if i >= size: break
1293 x = items[i]
1294 x = items[i]
1294 colwidth = max(colwidth, len(x))
1295 colwidth = max(colwidth, len(x))
1295 colwidths.append(colwidth)
1296 colwidths.append(colwidth)
1296 totwidth += colwidth + len(separator)
1297 totwidth += colwidth + len(separator)
1297 if totwidth > displaywidth:
1298 if totwidth > displaywidth:
1298 break
1299 break
1299 if totwidth <= displaywidth:
1300 if totwidth <= displaywidth:
1300 break
1301 break
1301
1302
1302 # The smallest number of rows computed and the max widths for each
1303 # The smallest number of rows computed and the max widths for each
1303 # column has been obtained. Now we just have to format each of the rows.
1304 # column has been obtained. Now we just have to format each of the rows.
1304 string = ''
1305 string = ''
1305 for row in range(nrows):
1306 for row in range(nrows):
1306 texts = []
1307 texts = []
1307 for col in range(ncols):
1308 for col in range(ncols):
1308 i = row + nrows*col
1309 i = row + nrows*col
1309 if i >= size:
1310 if i >= size:
1310 texts.append('')
1311 texts.append('')
1311 else:
1312 else:
1312 texts.append(items[i])
1313 texts.append(items[i])
1313 while texts and not texts[-1]:
1314 while texts and not texts[-1]:
1314 del texts[-1]
1315 del texts[-1]
1315 for col in range(len(texts)):
1316 for col in range(len(texts)):
1316 texts[col] = texts[col].ljust(colwidths[col])
1317 texts[col] = texts[col].ljust(colwidths[col])
1317 string += '%s\n' % separator.join(texts)
1318 string += '%s\n' % separator.join(texts)
1318 return string
1319 return string
1319
1320
1320 def _get_block_plain_text(self, block):
1321 def _get_block_plain_text(self, block):
1321 """ Given a QTextBlock, return its unformatted text.
1322 """ Given a QTextBlock, return its unformatted text.
1322 """
1323 """
1323 cursor = QtGui.QTextCursor(block)
1324 cursor = QtGui.QTextCursor(block)
1324 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1325 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1325 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1326 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1326 QtGui.QTextCursor.KeepAnchor)
1327 QtGui.QTextCursor.KeepAnchor)
1327 return cursor.selection().toPlainText()
1328 return cursor.selection().toPlainText()
1328
1329
1329 def _get_cursor(self):
1330 def _get_cursor(self):
1330 """ Convenience method that returns a cursor for the current position.
1331 """ Convenience method that returns a cursor for the current position.
1331 """
1332 """
1332 return self._control.textCursor()
1333 return self._control.textCursor()
1333
1334
1334 def _get_end_cursor(self):
1335 def _get_end_cursor(self):
1335 """ Convenience method that returns a cursor for the last character.
1336 """ Convenience method that returns a cursor for the last character.
1336 """
1337 """
1337 cursor = self._control.textCursor()
1338 cursor = self._control.textCursor()
1338 cursor.movePosition(QtGui.QTextCursor.End)
1339 cursor.movePosition(QtGui.QTextCursor.End)
1339 return cursor
1340 return cursor
1340
1341
1341 def _get_input_buffer_cursor_column(self):
1342 def _get_input_buffer_cursor_column(self):
1342 """ Returns the column of the cursor in the input buffer, excluding the
1343 """ Returns the column of the cursor in the input buffer, excluding the
1343 contribution by the prompt, or -1 if there is no such column.
1344 contribution by the prompt, or -1 if there is no such column.
1344 """
1345 """
1345 prompt = self._get_input_buffer_cursor_prompt()
1346 prompt = self._get_input_buffer_cursor_prompt()
1346 if prompt is None:
1347 if prompt is None:
1347 return -1
1348 return -1
1348 else:
1349 else:
1349 cursor = self._control.textCursor()
1350 cursor = self._control.textCursor()
1350 return cursor.columnNumber() - len(prompt)
1351 return cursor.columnNumber() - len(prompt)
1351
1352
1352 def _get_input_buffer_cursor_line(self):
1353 def _get_input_buffer_cursor_line(self):
1353 """ Returns the text of the line of the input buffer that contains the
1354 """ Returns the text of the line of the input buffer that contains the
1354 cursor, or None if there is no such line.
1355 cursor, or None if there is no such line.
1355 """
1356 """
1356 prompt = self._get_input_buffer_cursor_prompt()
1357 prompt = self._get_input_buffer_cursor_prompt()
1357 if prompt is None:
1358 if prompt is None:
1358 return None
1359 return None
1359 else:
1360 else:
1360 cursor = self._control.textCursor()
1361 cursor = self._control.textCursor()
1361 text = self._get_block_plain_text(cursor.block())
1362 text = self._get_block_plain_text(cursor.block())
1362 return text[len(prompt):]
1363 return text[len(prompt):]
1363
1364
1364 def _get_input_buffer_cursor_prompt(self):
1365 def _get_input_buffer_cursor_prompt(self):
1365 """ Returns the (plain text) prompt for line of the input buffer that
1366 """ Returns the (plain text) prompt for line of the input buffer that
1366 contains the cursor, or None if there is no such line.
1367 contains the cursor, or None if there is no such line.
1367 """
1368 """
1368 if self._executing:
1369 if self._executing:
1369 return None
1370 return None
1370 cursor = self._control.textCursor()
1371 cursor = self._control.textCursor()
1371 if cursor.position() >= self._prompt_pos:
1372 if cursor.position() >= self._prompt_pos:
1372 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1373 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1373 return self._prompt
1374 return self._prompt
1374 else:
1375 else:
1375 return self._continuation_prompt
1376 return self._continuation_prompt
1376 else:
1377 else:
1377 return None
1378 return None
1378
1379
1379 def _get_prompt_cursor(self):
1380 def _get_prompt_cursor(self):
1380 """ Convenience method that returns a cursor for the prompt position.
1381 """ Convenience method that returns a cursor for the prompt position.
1381 """
1382 """
1382 cursor = self._control.textCursor()
1383 cursor = self._control.textCursor()
1383 cursor.setPosition(self._prompt_pos)
1384 cursor.setPosition(self._prompt_pos)
1384 return cursor
1385 return cursor
1385
1386
1386 def _get_selection_cursor(self, start, end):
1387 def _get_selection_cursor(self, start, end):
1387 """ Convenience method that returns a cursor with text selected between
1388 """ Convenience method that returns a cursor with text selected between
1388 the positions 'start' and 'end'.
1389 the positions 'start' and 'end'.
1389 """
1390 """
1390 cursor = self._control.textCursor()
1391 cursor = self._control.textCursor()
1391 cursor.setPosition(start)
1392 cursor.setPosition(start)
1392 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1393 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1393 return cursor
1394 return cursor
1394
1395
1395 def _get_word_start_cursor(self, position):
1396 def _get_word_start_cursor(self, position):
1396 """ Find the start of the word to the left the given position. If a
1397 """ Find the start of the word to the left the given position. If a
1397 sequence of non-word characters precedes the first word, skip over
1398 sequence of non-word characters precedes the first word, skip over
1398 them. (This emulates the behavior of bash, emacs, etc.)
1399 them. (This emulates the behavior of bash, emacs, etc.)
1399 """
1400 """
1400 document = self._control.document()
1401 document = self._control.document()
1401 position -= 1
1402 position -= 1
1402 while position >= self._prompt_pos and \
1403 while position >= self._prompt_pos and \
1403 not is_letter_or_number(document.characterAt(position)):
1404 not is_letter_or_number(document.characterAt(position)):
1404 position -= 1
1405 position -= 1
1405 while position >= self._prompt_pos and \
1406 while position >= self._prompt_pos and \
1406 is_letter_or_number(document.characterAt(position)):
1407 is_letter_or_number(document.characterAt(position)):
1407 position -= 1
1408 position -= 1
1408 cursor = self._control.textCursor()
1409 cursor = self._control.textCursor()
1409 cursor.setPosition(position + 1)
1410 cursor.setPosition(position + 1)
1410 return cursor
1411 return cursor
1411
1412
1412 def _get_word_end_cursor(self, position):
1413 def _get_word_end_cursor(self, position):
1413 """ Find the end of the word to the right the given position. If a
1414 """ Find the end of the word to the right the given position. If a
1414 sequence of non-word characters precedes the first word, skip over
1415 sequence of non-word characters precedes the first word, skip over
1415 them. (This emulates the behavior of bash, emacs, etc.)
1416 them. (This emulates the behavior of bash, emacs, etc.)
1416 """
1417 """
1417 document = self._control.document()
1418 document = self._control.document()
1418 end = self._get_end_cursor().position()
1419 end = self._get_end_cursor().position()
1419 while position < end and \
1420 while position < end and \
1420 not is_letter_or_number(document.characterAt(position)):
1421 not is_letter_or_number(document.characterAt(position)):
1421 position += 1
1422 position += 1
1422 while position < end and \
1423 while position < end and \
1423 is_letter_or_number(document.characterAt(position)):
1424 is_letter_or_number(document.characterAt(position)):
1424 position += 1
1425 position += 1
1425 cursor = self._control.textCursor()
1426 cursor = self._control.textCursor()
1426 cursor.setPosition(position)
1427 cursor.setPosition(position)
1427 return cursor
1428 return cursor
1428
1429
1429 def _insert_continuation_prompt(self, cursor):
1430 def _insert_continuation_prompt(self, cursor):
1430 """ Inserts new continuation prompt using the specified cursor.
1431 """ Inserts new continuation prompt using the specified cursor.
1431 """
1432 """
1432 if self._continuation_prompt_html is None:
1433 if self._continuation_prompt_html is None:
1433 self._insert_plain_text(cursor, self._continuation_prompt)
1434 self._insert_plain_text(cursor, self._continuation_prompt)
1434 else:
1435 else:
1435 self._continuation_prompt = self._insert_html_fetching_plain_text(
1436 self._continuation_prompt = self._insert_html_fetching_plain_text(
1436 cursor, self._continuation_prompt_html)
1437 cursor, self._continuation_prompt_html)
1437
1438
1438 def _insert_html(self, cursor, html):
1439 def _insert_html(self, cursor, html):
1439 """ Inserts HTML using the specified cursor in such a way that future
1440 """ Inserts HTML using the specified cursor in such a way that future
1440 formatting is unaffected.
1441 formatting is unaffected.
1441 """
1442 """
1442 cursor.beginEditBlock()
1443 cursor.beginEditBlock()
1443 cursor.insertHtml(html)
1444 cursor.insertHtml(html)
1444
1445
1445 # After inserting HTML, the text document "remembers" it's in "html
1446 # After inserting HTML, the text document "remembers" it's in "html
1446 # mode", which means that subsequent calls adding plain text will result
1447 # mode", which means that subsequent calls adding plain text will result
1447 # in unwanted formatting, lost tab characters, etc. The following code
1448 # in unwanted formatting, lost tab characters, etc. The following code
1448 # hacks around this behavior, which I consider to be a bug in Qt, by
1449 # hacks around this behavior, which I consider to be a bug in Qt, by
1449 # (crudely) resetting the document's style state.
1450 # (crudely) resetting the document's style state.
1450 cursor.movePosition(QtGui.QTextCursor.Left,
1451 cursor.movePosition(QtGui.QTextCursor.Left,
1451 QtGui.QTextCursor.KeepAnchor)
1452 QtGui.QTextCursor.KeepAnchor)
1452 if cursor.selection().toPlainText() == ' ':
1453 if cursor.selection().toPlainText() == ' ':
1453 cursor.removeSelectedText()
1454 cursor.removeSelectedText()
1454 else:
1455 else:
1455 cursor.movePosition(QtGui.QTextCursor.Right)
1456 cursor.movePosition(QtGui.QTextCursor.Right)
1456 cursor.insertText(' ', QtGui.QTextCharFormat())
1457 cursor.insertText(' ', QtGui.QTextCharFormat())
1457 cursor.endEditBlock()
1458 cursor.endEditBlock()
1458
1459
1459 def _insert_html_fetching_plain_text(self, cursor, html):
1460 def _insert_html_fetching_plain_text(self, cursor, html):
1460 """ Inserts HTML using the specified cursor, then returns its plain text
1461 """ Inserts HTML using the specified cursor, then returns its plain text
1461 version.
1462 version.
1462 """
1463 """
1463 cursor.beginEditBlock()
1464 cursor.beginEditBlock()
1464 cursor.removeSelectedText()
1465 cursor.removeSelectedText()
1465
1466
1466 start = cursor.position()
1467 start = cursor.position()
1467 self._insert_html(cursor, html)
1468 self._insert_html(cursor, html)
1468 end = cursor.position()
1469 end = cursor.position()
1469 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1470 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1470 text = cursor.selection().toPlainText()
1471 text = cursor.selection().toPlainText()
1471
1472
1472 cursor.setPosition(end)
1473 cursor.setPosition(end)
1473 cursor.endEditBlock()
1474 cursor.endEditBlock()
1474 return text
1475 return text
1475
1476
1476 def _insert_plain_text(self, cursor, text):
1477 def _insert_plain_text(self, cursor, text):
1477 """ Inserts plain text using the specified cursor, processing ANSI codes
1478 """ Inserts plain text using the specified cursor, processing ANSI codes
1478 if enabled.
1479 if enabled.
1479 """
1480 """
1480 cursor.beginEditBlock()
1481 cursor.beginEditBlock()
1481 if self.ansi_codes:
1482 if self.ansi_codes:
1482 for substring in self._ansi_processor.split_string(text):
1483 for substring in self._ansi_processor.split_string(text):
1483 for act in self._ansi_processor.actions:
1484 for act in self._ansi_processor.actions:
1484
1485
1485 # Unlike real terminal emulators, we don't distinguish
1486 # Unlike real terminal emulators, we don't distinguish
1486 # between the screen and the scrollback buffer. A screen
1487 # between the screen and the scrollback buffer. A screen
1487 # erase request clears everything.
1488 # erase request clears everything.
1488 if act.action == 'erase' and act.area == 'screen':
1489 if act.action == 'erase' and act.area == 'screen':
1489 cursor.select(QtGui.QTextCursor.Document)
1490 cursor.select(QtGui.QTextCursor.Document)
1490 cursor.removeSelectedText()
1491 cursor.removeSelectedText()
1491
1492
1492 # Simulate a form feed by scrolling just past the last line.
1493 # Simulate a form feed by scrolling just past the last line.
1493 elif act.action == 'scroll' and act.unit == 'page':
1494 elif act.action == 'scroll' and act.unit == 'page':
1494 cursor.insertText('\n')
1495 cursor.insertText('\n')
1495 cursor.endEditBlock()
1496 cursor.endEditBlock()
1496 self._set_top_cursor(cursor)
1497 self._set_top_cursor(cursor)
1497 cursor.joinPreviousEditBlock()
1498 cursor.joinPreviousEditBlock()
1498 cursor.deletePreviousChar()
1499 cursor.deletePreviousChar()
1499
1500
1500 format = self._ansi_processor.get_format()
1501 format = self._ansi_processor.get_format()
1501 cursor.insertText(substring, format)
1502 cursor.insertText(substring, format)
1502 else:
1503 else:
1503 cursor.insertText(text)
1504 cursor.insertText(text)
1504 cursor.endEditBlock()
1505 cursor.endEditBlock()
1505
1506
1506 def _insert_plain_text_into_buffer(self, cursor, text):
1507 def _insert_plain_text_into_buffer(self, cursor, text):
1507 """ Inserts text into the input buffer using the specified cursor (which
1508 """ Inserts text into the input buffer using the specified cursor (which
1508 must be in the input buffer), ensuring that continuation prompts are
1509 must be in the input buffer), ensuring that continuation prompts are
1509 inserted as necessary.
1510 inserted as necessary.
1510 """
1511 """
1511 lines = text.splitlines(True)
1512 lines = text.splitlines(True)
1512 if lines:
1513 if lines:
1513 cursor.beginEditBlock()
1514 cursor.beginEditBlock()
1514 cursor.insertText(lines[0])
1515 cursor.insertText(lines[0])
1515 for line in lines[1:]:
1516 for line in lines[1:]:
1516 if self._continuation_prompt_html is None:
1517 if self._continuation_prompt_html is None:
1517 cursor.insertText(self._continuation_prompt)
1518 cursor.insertText(self._continuation_prompt)
1518 else:
1519 else:
1519 self._continuation_prompt = \
1520 self._continuation_prompt = \
1520 self._insert_html_fetching_plain_text(
1521 self._insert_html_fetching_plain_text(
1521 cursor, self._continuation_prompt_html)
1522 cursor, self._continuation_prompt_html)
1522 cursor.insertText(line)
1523 cursor.insertText(line)
1523 cursor.endEditBlock()
1524 cursor.endEditBlock()
1524
1525
1525 def _in_buffer(self, position=None):
1526 def _in_buffer(self, position=None):
1526 """ Returns whether the current cursor (or, if specified, a position) is
1527 """ Returns whether the current cursor (or, if specified, a position) is
1527 inside the editing region.
1528 inside the editing region.
1528 """
1529 """
1529 cursor = self._control.textCursor()
1530 cursor = self._control.textCursor()
1530 if position is None:
1531 if position is None:
1531 position = cursor.position()
1532 position = cursor.position()
1532 else:
1533 else:
1533 cursor.setPosition(position)
1534 cursor.setPosition(position)
1534 line = cursor.blockNumber()
1535 line = cursor.blockNumber()
1535 prompt_line = self._get_prompt_cursor().blockNumber()
1536 prompt_line = self._get_prompt_cursor().blockNumber()
1536 if line == prompt_line:
1537 if line == prompt_line:
1537 return position >= self._prompt_pos
1538 return position >= self._prompt_pos
1538 elif line > prompt_line:
1539 elif line > prompt_line:
1539 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1540 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1540 prompt_pos = cursor.position() + len(self._continuation_prompt)
1541 prompt_pos = cursor.position() + len(self._continuation_prompt)
1541 return position >= prompt_pos
1542 return position >= prompt_pos
1542 return False
1543 return False
1543
1544
1544 def _keep_cursor_in_buffer(self):
1545 def _keep_cursor_in_buffer(self):
1545 """ Ensures that the cursor is inside the editing region. Returns
1546 """ Ensures that the cursor is inside the editing region. Returns
1546 whether the cursor was moved.
1547 whether the cursor was moved.
1547 """
1548 """
1548 moved = not self._in_buffer()
1549 moved = not self._in_buffer()
1549 if moved:
1550 if moved:
1550 cursor = self._control.textCursor()
1551 cursor = self._control.textCursor()
1551 cursor.movePosition(QtGui.QTextCursor.End)
1552 cursor.movePosition(QtGui.QTextCursor.End)
1552 self._control.setTextCursor(cursor)
1553 self._control.setTextCursor(cursor)
1553 return moved
1554 return moved
1554
1555
1555 def _keyboard_quit(self):
1556 def _keyboard_quit(self):
1556 """ Cancels the current editing task ala Ctrl-G in Emacs.
1557 """ Cancels the current editing task ala Ctrl-G in Emacs.
1557 """
1558 """
1558 if self._text_completing_pos:
1559 if self._text_completing_pos:
1559 self._cancel_text_completion()
1560 self._cancel_text_completion()
1560 else:
1561 else:
1561 self.input_buffer = ''
1562 self.input_buffer = ''
1562
1563
1563 def _page(self, text, html=False):
1564 def _page(self, text, html=False):
1564 """ Displays text using the pager if it exceeds the height of the
1565 """ Displays text using the pager if it exceeds the height of the
1565 viewport.
1566 viewport.
1566
1567
1567 Parameters:
1568 Parameters:
1568 -----------
1569 -----------
1569 html : bool, optional (default False)
1570 html : bool, optional (default False)
1570 If set, the text will be interpreted as HTML instead of plain text.
1571 If set, the text will be interpreted as HTML instead of plain text.
1571 """
1572 """
1572 line_height = QtGui.QFontMetrics(self.font).height()
1573 line_height = QtGui.QFontMetrics(self.font).height()
1573 minlines = self._control.viewport().height() / line_height
1574 minlines = self._control.viewport().height() / line_height
1574 if self.paging != 'none' and \
1575 if self.paging != 'none' and \
1575 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1576 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1576 if self.paging == 'custom':
1577 if self.paging == 'custom':
1577 self.custom_page_requested.emit(text)
1578 self.custom_page_requested.emit(text)
1578 else:
1579 else:
1579 self._page_control.clear()
1580 self._page_control.clear()
1580 cursor = self._page_control.textCursor()
1581 cursor = self._page_control.textCursor()
1581 if html:
1582 if html:
1582 self._insert_html(cursor, text)
1583 self._insert_html(cursor, text)
1583 else:
1584 else:
1584 self._insert_plain_text(cursor, text)
1585 self._insert_plain_text(cursor, text)
1585 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1586 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1586
1587
1587 self._page_control.viewport().resize(self._control.size())
1588 self._page_control.viewport().resize(self._control.size())
1588 if self._splitter:
1589 if self._splitter:
1589 self._page_control.show()
1590 self._page_control.show()
1590 self._page_control.setFocus()
1591 self._page_control.setFocus()
1591 else:
1592 else:
1592 self.layout().setCurrentWidget(self._page_control)
1593 self.layout().setCurrentWidget(self._page_control)
1593 elif html:
1594 elif html:
1594 self._append_plain_html(text)
1595 self._append_plain_html(text)
1595 else:
1596 else:
1596 self._append_plain_text(text)
1597 self._append_plain_text(text)
1597
1598
1598 def _prompt_finished(self):
1599 def _prompt_finished(self):
1599 """ Called immediately after a prompt is finished, i.e. when some input
1600 """ Called immediately after a prompt is finished, i.e. when some input
1600 will be processed and a new prompt displayed.
1601 will be processed and a new prompt displayed.
1601 """
1602 """
1602 self._control.setReadOnly(True)
1603 self._control.setReadOnly(True)
1603 self._prompt_finished_hook()
1604 self._prompt_finished_hook()
1604
1605
1605 def _prompt_started(self):
1606 def _prompt_started(self):
1606 """ Called immediately after a new prompt is displayed.
1607 """ Called immediately after a new prompt is displayed.
1607 """
1608 """
1608 # Temporarily disable the maximum block count to permit undo/redo and
1609 # Temporarily disable the maximum block count to permit undo/redo and
1609 # to ensure that the prompt position does not change due to truncation.
1610 # to ensure that the prompt position does not change due to truncation.
1610 self._control.document().setMaximumBlockCount(0)
1611 self._control.document().setMaximumBlockCount(0)
1611 self._control.setUndoRedoEnabled(True)
1612 self._control.setUndoRedoEnabled(True)
1612
1613
1613 # Work around bug in QPlainTextEdit: input method is not re-enabled
1614 # Work around bug in QPlainTextEdit: input method is not re-enabled
1614 # when read-only is disabled.
1615 # when read-only is disabled.
1615 self._control.setReadOnly(False)
1616 self._control.setReadOnly(False)
1616 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1617 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1617
1618
1618 self._executing = False
1619 self._executing = False
1619 self._prompt_started_hook()
1620 self._prompt_started_hook()
1620
1621
1621 # If the input buffer has changed while executing, load it.
1622 # If the input buffer has changed while executing, load it.
1622 if self._input_buffer_pending:
1623 if self._input_buffer_pending:
1623 self.input_buffer = self._input_buffer_pending
1624 self.input_buffer = self._input_buffer_pending
1624 self._input_buffer_pending = ''
1625 self._input_buffer_pending = ''
1625
1626
1626 self._control.moveCursor(QtGui.QTextCursor.End)
1627 self._control.moveCursor(QtGui.QTextCursor.End)
1627
1628
1628 def _readline(self, prompt='', callback=None):
1629 def _readline(self, prompt='', callback=None):
1629 """ Reads one line of input from the user.
1630 """ Reads one line of input from the user.
1630
1631
1631 Parameters
1632 Parameters
1632 ----------
1633 ----------
1633 prompt : str, optional
1634 prompt : str, optional
1634 The prompt to print before reading the line.
1635 The prompt to print before reading the line.
1635
1636
1636 callback : callable, optional
1637 callback : callable, optional
1637 A callback to execute with the read line. If not specified, input is
1638 A callback to execute with the read line. If not specified, input is
1638 read *synchronously* and this method does not return until it has
1639 read *synchronously* and this method does not return until it has
1639 been read.
1640 been read.
1640
1641
1641 Returns
1642 Returns
1642 -------
1643 -------
1643 If a callback is specified, returns nothing. Otherwise, returns the
1644 If a callback is specified, returns nothing. Otherwise, returns the
1644 input string with the trailing newline stripped.
1645 input string with the trailing newline stripped.
1645 """
1646 """
1646 if self._reading:
1647 if self._reading:
1647 raise RuntimeError('Cannot read a line. Widget is already reading.')
1648 raise RuntimeError('Cannot read a line. Widget is already reading.')
1648
1649
1649 if not callback and not self.isVisible():
1650 if not callback and not self.isVisible():
1650 # If the user cannot see the widget, this function cannot return.
1651 # If the user cannot see the widget, this function cannot return.
1651 raise RuntimeError('Cannot synchronously read a line if the widget '
1652 raise RuntimeError('Cannot synchronously read a line if the widget '
1652 'is not visible!')
1653 'is not visible!')
1653
1654
1654 self._reading = True
1655 self._reading = True
1655 self._show_prompt(prompt, newline=False)
1656 self._show_prompt(prompt, newline=False)
1656
1657
1657 if callback is None:
1658 if callback is None:
1658 self._reading_callback = None
1659 self._reading_callback = None
1659 while self._reading:
1660 while self._reading:
1660 QtCore.QCoreApplication.processEvents()
1661 QtCore.QCoreApplication.processEvents()
1661 return self.input_buffer.rstrip('\n')
1662 return self.input_buffer.rstrip('\n')
1662
1663
1663 else:
1664 else:
1664 self._reading_callback = lambda: \
1665 self._reading_callback = lambda: \
1665 callback(self.input_buffer.rstrip('\n'))
1666 callback(self.input_buffer.rstrip('\n'))
1666
1667
1667 def _set_continuation_prompt(self, prompt, html=False):
1668 def _set_continuation_prompt(self, prompt, html=False):
1668 """ Sets the continuation prompt.
1669 """ Sets the continuation prompt.
1669
1670
1670 Parameters
1671 Parameters
1671 ----------
1672 ----------
1672 prompt : str
1673 prompt : str
1673 The prompt to show when more input is needed.
1674 The prompt to show when more input is needed.
1674
1675
1675 html : bool, optional (default False)
1676 html : bool, optional (default False)
1676 If set, the prompt will be inserted as formatted HTML. Otherwise,
1677 If set, the prompt will be inserted as formatted HTML. Otherwise,
1677 the prompt will be treated as plain text, though ANSI color codes
1678 the prompt will be treated as plain text, though ANSI color codes
1678 will be handled.
1679 will be handled.
1679 """
1680 """
1680 if html:
1681 if html:
1681 self._continuation_prompt_html = prompt
1682 self._continuation_prompt_html = prompt
1682 else:
1683 else:
1683 self._continuation_prompt = prompt
1684 self._continuation_prompt = prompt
1684 self._continuation_prompt_html = None
1685 self._continuation_prompt_html = None
1685
1686
1686 def _set_cursor(self, cursor):
1687 def _set_cursor(self, cursor):
1687 """ Convenience method to set the current cursor.
1688 """ Convenience method to set the current cursor.
1688 """
1689 """
1689 self._control.setTextCursor(cursor)
1690 self._control.setTextCursor(cursor)
1690
1691
1691 def _set_top_cursor(self, cursor):
1692 def _set_top_cursor(self, cursor):
1692 """ Scrolls the viewport so that the specified cursor is at the top.
1693 """ Scrolls the viewport so that the specified cursor is at the top.
1693 """
1694 """
1694 scrollbar = self._control.verticalScrollBar()
1695 scrollbar = self._control.verticalScrollBar()
1695 scrollbar.setValue(scrollbar.maximum())
1696 scrollbar.setValue(scrollbar.maximum())
1696 original_cursor = self._control.textCursor()
1697 original_cursor = self._control.textCursor()
1697 self._control.setTextCursor(cursor)
1698 self._control.setTextCursor(cursor)
1698 self._control.ensureCursorVisible()
1699 self._control.ensureCursorVisible()
1699 self._control.setTextCursor(original_cursor)
1700 self._control.setTextCursor(original_cursor)
1700
1701
1701 def _show_prompt(self, prompt=None, html=False, newline=True):
1702 def _show_prompt(self, prompt=None, html=False, newline=True):
1702 """ Writes a new prompt at the end of the buffer.
1703 """ Writes a new prompt at the end of the buffer.
1703
1704
1704 Parameters
1705 Parameters
1705 ----------
1706 ----------
1706 prompt : str, optional
1707 prompt : str, optional
1707 The prompt to show. If not specified, the previous prompt is used.
1708 The prompt to show. If not specified, the previous prompt is used.
1708
1709
1709 html : bool, optional (default False)
1710 html : bool, optional (default False)
1710 Only relevant when a prompt is specified. If set, the prompt will
1711 Only relevant when a prompt is specified. If set, the prompt will
1711 be inserted as formatted HTML. Otherwise, the prompt will be treated
1712 be inserted as formatted HTML. Otherwise, the prompt will be treated
1712 as plain text, though ANSI color codes will be handled.
1713 as plain text, though ANSI color codes will be handled.
1713
1714
1714 newline : bool, optional (default True)
1715 newline : bool, optional (default True)
1715 If set, a new line will be written before showing the prompt if
1716 If set, a new line will be written before showing the prompt if
1716 there is not already a newline at the end of the buffer.
1717 there is not already a newline at the end of the buffer.
1717 """
1718 """
1718 # Insert a preliminary newline, if necessary.
1719 # Insert a preliminary newline, if necessary.
1719 if newline:
1720 if newline:
1720 cursor = self._get_end_cursor()
1721 cursor = self._get_end_cursor()
1721 if cursor.position() > 0:
1722 if cursor.position() > 0:
1722 cursor.movePosition(QtGui.QTextCursor.Left,
1723 cursor.movePosition(QtGui.QTextCursor.Left,
1723 QtGui.QTextCursor.KeepAnchor)
1724 QtGui.QTextCursor.KeepAnchor)
1724 if cursor.selection().toPlainText() != '\n':
1725 if cursor.selection().toPlainText() != '\n':
1725 self._append_plain_text('\n')
1726 self._append_plain_text('\n')
1726
1727
1727 # Write the prompt.
1728 # Write the prompt.
1728 self._append_plain_text(self._prompt_sep)
1729 self._append_plain_text(self._prompt_sep)
1729 if prompt is None:
1730 if prompt is None:
1730 if self._prompt_html is None:
1731 if self._prompt_html is None:
1731 self._append_plain_text(self._prompt)
1732 self._append_plain_text(self._prompt)
1732 else:
1733 else:
1733 self._append_html(self._prompt_html)
1734 self._append_html(self._prompt_html)
1734 else:
1735 else:
1735 if html:
1736 if html:
1736 self._prompt = self._append_html_fetching_plain_text(prompt)
1737 self._prompt = self._append_html_fetching_plain_text(prompt)
1737 self._prompt_html = prompt
1738 self._prompt_html = prompt
1738 else:
1739 else:
1739 self._append_plain_text(prompt)
1740 self._append_plain_text(prompt)
1740 self._prompt = prompt
1741 self._prompt = prompt
1741 self._prompt_html = None
1742 self._prompt_html = None
1742
1743
1743 self._prompt_pos = self._get_end_cursor().position()
1744 self._prompt_pos = self._get_end_cursor().position()
1744 self._prompt_started()
1745 self._prompt_started()
1745
1746
1746 #------ Signal handlers ----------------------------------------------------
1747 #------ Signal handlers ----------------------------------------------------
1747
1748
1748 def _adjust_scrollbars(self):
1749 def _adjust_scrollbars(self):
1749 """ Expands the vertical scrollbar beyond the range set by Qt.
1750 """ Expands the vertical scrollbar beyond the range set by Qt.
1750 """
1751 """
1751 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1752 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1752 # and qtextedit.cpp.
1753 # and qtextedit.cpp.
1753 document = self._control.document()
1754 document = self._control.document()
1754 scrollbar = self._control.verticalScrollBar()
1755 scrollbar = self._control.verticalScrollBar()
1755 viewport_height = self._control.viewport().height()
1756 viewport_height = self._control.viewport().height()
1756 if isinstance(self._control, QtGui.QPlainTextEdit):
1757 if isinstance(self._control, QtGui.QPlainTextEdit):
1757 maximum = max(0, document.lineCount() - 1)
1758 maximum = max(0, document.lineCount() - 1)
1758 step = viewport_height / self._control.fontMetrics().lineSpacing()
1759 step = viewport_height / self._control.fontMetrics().lineSpacing()
1759 else:
1760 else:
1760 # QTextEdit does not do line-based layout and blocks will not in
1761 # QTextEdit does not do line-based layout and blocks will not in
1761 # general have the same height. Therefore it does not make sense to
1762 # general have the same height. Therefore it does not make sense to
1762 # attempt to scroll in line height increments.
1763 # attempt to scroll in line height increments.
1763 maximum = document.size().height()
1764 maximum = document.size().height()
1764 step = viewport_height
1765 step = viewport_height
1765 diff = maximum - scrollbar.maximum()
1766 diff = maximum - scrollbar.maximum()
1766 scrollbar.setRange(0, maximum)
1767 scrollbar.setRange(0, maximum)
1767 scrollbar.setPageStep(step)
1768 scrollbar.setPageStep(step)
1768
1769
1769 # Compensate for undesirable scrolling that occurs automatically due to
1770 # Compensate for undesirable scrolling that occurs automatically due to
1770 # maximumBlockCount() text truncation.
1771 # maximumBlockCount() text truncation.
1771 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1772 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1772 scrollbar.setValue(scrollbar.value() + diff)
1773 scrollbar.setValue(scrollbar.value() + diff)
1773
1774
1774 def _cursor_position_changed(self):
1775 def _cursor_position_changed(self):
1775 """ Clears the temporary buffer based on the cursor position.
1776 """ Clears the temporary buffer based on the cursor position.
1776 """
1777 """
1777 if self._text_completing_pos:
1778 if self._text_completing_pos:
1778 document = self._control.document()
1779 document = self._control.document()
1779 if self._text_completing_pos < document.characterCount():
1780 if self._text_completing_pos < document.characterCount():
1780 cursor = self._control.textCursor()
1781 cursor = self._control.textCursor()
1781 pos = cursor.position()
1782 pos = cursor.position()
1782 text_cursor = self._control.textCursor()
1783 text_cursor = self._control.textCursor()
1783 text_cursor.setPosition(self._text_completing_pos)
1784 text_cursor.setPosition(self._text_completing_pos)
1784 if pos < self._text_completing_pos or \
1785 if pos < self._text_completing_pos or \
1785 cursor.blockNumber() > text_cursor.blockNumber():
1786 cursor.blockNumber() > text_cursor.blockNumber():
1786 self._clear_temporary_buffer()
1787 self._clear_temporary_buffer()
1787 self._text_completing_pos = 0
1788 self._text_completing_pos = 0
1788 else:
1789 else:
1789 self._clear_temporary_buffer()
1790 self._clear_temporary_buffer()
1790 self._text_completing_pos = 0
1791 self._text_completing_pos = 0
1791
1792
1792 def _custom_context_menu_requested(self, pos):
1793 def _custom_context_menu_requested(self, pos):
1793 """ Shows a context menu at the given QPoint (in widget coordinates).
1794 """ Shows a context menu at the given QPoint (in widget coordinates).
1794 """
1795 """
1795 menu = self._context_menu_make(pos)
1796 menu = self._context_menu_make(pos)
1796 menu.exec_(self._control.mapToGlobal(pos))
1797 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,404 +1,420 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
5
6 Authors:
7
8 * Evan Patterson
9 * Min RK
10 * Erik Tollerud
11 * Fernando Perez
12
2 """
13 """
3
14
4 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
5 # Imports
16 # Imports
6 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
7
18
8 # stdlib imports
19 # stdlib imports
9 import os
20 import os
10 import signal
21 import signal
11 import sys
22 import sys
12
23
13 # System library imports
24 # System library imports
14 from IPython.external.qt import QtGui
25 from IPython.external.qt import QtGui
15 from pygments.styles import get_all_styles
26 from pygments.styles import get_all_styles
16
27
17 # Local imports
28 # Local imports
18 from IPython.config.application import boolean_flag
29 from IPython.config.application import boolean_flag
19 from IPython.core.newapplication import ProfileDir, BaseIPythonApplication
30 from IPython.core.newapplication import ProfileDir, BaseIPythonApplication
20 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
31 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
21 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
32 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
22 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
33 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
23 from IPython.frontend.qt.console import styles
34 from IPython.frontend.qt.console import styles
24 from IPython.frontend.qt.kernelmanager import QtKernelManager
35 from IPython.frontend.qt.kernelmanager import QtKernelManager
25 from IPython.utils.traitlets import (
36 from IPython.utils.traitlets import (
26 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
37 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
27 )
38 )
28 from IPython.zmq.ipkernel import (
39 from IPython.zmq.ipkernel import (
29 flags as ipkernel_flags,
40 flags as ipkernel_flags,
30 aliases as ipkernel_aliases,
41 aliases as ipkernel_aliases,
31 IPKernelApp
42 IPKernelApp
32 )
43 )
33 from IPython.zmq.session import Session
44 from IPython.zmq.session import Session
34 from IPython.zmq.zmqshell import ZMQInteractiveShell
45 from IPython.zmq.zmqshell import ZMQInteractiveShell
35
46
36
47
37 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
38 # Network Constants
49 # Network Constants
39 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
40
51
41 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
52 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
42
53
43 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
44 # Classes
55 # Classes
45 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
46
57
47 class MainWindow(QtGui.QMainWindow):
58 class MainWindow(QtGui.QMainWindow):
48
59
49 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
50 # 'object' interface
61 # 'object' interface
51 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
52
63
53 def __init__(self, app, frontend, existing=False, may_close=True,
64 def __init__(self, app, frontend, existing=False, may_close=True,
54 confirm_exit=True):
65 confirm_exit=True):
55 """ Create a MainWindow for the specified FrontendWidget.
66 """ Create a MainWindow for the specified FrontendWidget.
56
67
57 The app is passed as an argument to allow for different
68 The app is passed as an argument to allow for different
58 closing behavior depending on whether we are the Kernel's parent.
69 closing behavior depending on whether we are the Kernel's parent.
59
70
60 If existing is True, then this Console does not own the Kernel.
71 If existing is True, then this Console does not own the Kernel.
61
72
62 If may_close is True, then this Console is permitted to close the kernel
73 If may_close is True, then this Console is permitted to close the kernel
63 """
74 """
64 super(MainWindow, self).__init__()
75 super(MainWindow, self).__init__()
65 self._app = app
76 self._app = app
66 self._frontend = frontend
77 self._frontend = frontend
67 self._existing = existing
78 self._existing = existing
68 if existing:
79 if existing:
69 self._may_close = may_close
80 self._may_close = may_close
70 else:
81 else:
71 self._may_close = True
82 self._may_close = True
72 self._frontend.exit_requested.connect(self.close)
83 self._frontend.exit_requested.connect(self.close)
73 self._confirm_exit = confirm_exit
84 self._confirm_exit = confirm_exit
74 self.setCentralWidget(frontend)
85 self.setCentralWidget(frontend)
75
86
76 #---------------------------------------------------------------------------
87 #---------------------------------------------------------------------------
77 # QWidget interface
88 # QWidget interface
78 #---------------------------------------------------------------------------
89 #---------------------------------------------------------------------------
79
90
80 def closeEvent(self, event):
91 def closeEvent(self, event):
81 """ Close the window and the kernel (if necessary).
92 """ Close the window and the kernel (if necessary).
82
93
83 This will prompt the user if they are finished with the kernel, and if
94 This will prompt the user if they are finished with the kernel, and if
84 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
95 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
85 it closes without prompt.
96 it closes without prompt.
86 """
97 """
87 keepkernel = None #Use the prompt by default
98 keepkernel = None #Use the prompt by default
88 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
99 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
89 keepkernel = self._frontend._keep_kernel_on_exit
100 keepkernel = self._frontend._keep_kernel_on_exit
90
101
91 kernel_manager = self._frontend.kernel_manager
102 kernel_manager = self._frontend.kernel_manager
92
103
93 if keepkernel is None and not self._confirm_exit:
104 if keepkernel is None and not self._confirm_exit:
94 # don't prompt, just terminate the kernel if we own it
105 # don't prompt, just terminate the kernel if we own it
95 # or leave it alone if we don't
106 # or leave it alone if we don't
96 keepkernel = not self._existing
107 keepkernel = not self._existing
97
108
98 if keepkernel is None: #show prompt
109 if keepkernel is None: #show prompt
99 if kernel_manager and kernel_manager.channels_running:
110 if kernel_manager and kernel_manager.channels_running:
100 title = self.window().windowTitle()
111 title = self.window().windowTitle()
101 cancel = QtGui.QMessageBox.Cancel
112 cancel = QtGui.QMessageBox.Cancel
102 okay = QtGui.QMessageBox.Ok
113 okay = QtGui.QMessageBox.Ok
103 if self._may_close:
114 if self._may_close:
104 msg = "You are closing this Console window."
115 msg = "You are closing this Console window."
105 info = "Would you like to quit the Kernel and all attached Consoles as well?"
116 info = "Would you like to quit the Kernel and all attached Consoles as well?"
106 justthis = QtGui.QPushButton("&No, just this Console", self)
117 justthis = QtGui.QPushButton("&No, just this Console", self)
107 justthis.setShortcut('N')
118 justthis.setShortcut('N')
108 closeall = QtGui.QPushButton("&Yes, quit everything", self)
119 closeall = QtGui.QPushButton("&Yes, quit everything", self)
109 closeall.setShortcut('Y')
120 closeall.setShortcut('Y')
110 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
121 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
111 title, msg)
122 title, msg)
112 box.setInformativeText(info)
123 box.setInformativeText(info)
113 box.addButton(cancel)
124 box.addButton(cancel)
114 box.addButton(justthis, QtGui.QMessageBox.NoRole)
125 box.addButton(justthis, QtGui.QMessageBox.NoRole)
115 box.addButton(closeall, QtGui.QMessageBox.YesRole)
126 box.addButton(closeall, QtGui.QMessageBox.YesRole)
116 box.setDefaultButton(closeall)
127 box.setDefaultButton(closeall)
117 box.setEscapeButton(cancel)
128 box.setEscapeButton(cancel)
118 reply = box.exec_()
129 reply = box.exec_()
119 if reply == 1: # close All
130 if reply == 1: # close All
120 kernel_manager.shutdown_kernel()
131 kernel_manager.shutdown_kernel()
121 #kernel_manager.stop_channels()
132 #kernel_manager.stop_channels()
122 event.accept()
133 event.accept()
123 elif reply == 0: # close Console
134 elif reply == 0: # close Console
124 if not self._existing:
135 if not self._existing:
125 # Have kernel: don't quit, just close the window
136 # Have kernel: don't quit, just close the window
126 self._app.setQuitOnLastWindowClosed(False)
137 self._app.setQuitOnLastWindowClosed(False)
127 self.deleteLater()
138 self.deleteLater()
128 event.accept()
139 event.accept()
129 else:
140 else:
130 event.ignore()
141 event.ignore()
131 else:
142 else:
132 reply = QtGui.QMessageBox.question(self, title,
143 reply = QtGui.QMessageBox.question(self, title,
133 "Are you sure you want to close this Console?"+
144 "Are you sure you want to close this Console?"+
134 "\nThe Kernel and other Consoles will remain active.",
145 "\nThe Kernel and other Consoles will remain active.",
135 okay|cancel,
146 okay|cancel,
136 defaultButton=okay
147 defaultButton=okay
137 )
148 )
138 if reply == okay:
149 if reply == okay:
139 event.accept()
150 event.accept()
140 else:
151 else:
141 event.ignore()
152 event.ignore()
142 elif keepkernel: #close console but leave kernel running (no prompt)
153 elif keepkernel: #close console but leave kernel running (no prompt)
143 if kernel_manager and kernel_manager.channels_running:
154 if kernel_manager and kernel_manager.channels_running:
144 if not self._existing:
155 if not self._existing:
145 # I have the kernel: don't quit, just close the window
156 # I have the kernel: don't quit, just close the window
146 self._app.setQuitOnLastWindowClosed(False)
157 self._app.setQuitOnLastWindowClosed(False)
147 event.accept()
158 event.accept()
148 else: #close console and kernel (no prompt)
159 else: #close console and kernel (no prompt)
149 if kernel_manager and kernel_manager.channels_running:
160 if kernel_manager and kernel_manager.channels_running:
150 kernel_manager.shutdown_kernel()
161 kernel_manager.shutdown_kernel()
151 event.accept()
162 event.accept()
152
163
153 #-----------------------------------------------------------------------------
164 #-----------------------------------------------------------------------------
154 # Aliases and Flags
165 # Aliases and Flags
155 #-----------------------------------------------------------------------------
166 #-----------------------------------------------------------------------------
156
167
157 flags = dict(ipkernel_flags)
168 flags = dict(ipkernel_flags)
158
169
159 flags.update({
170 flags.update({
160 'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}},
171 'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}},
161 "Connect to an existing kernel."),
172 "Connect to an existing kernel."),
162 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
173 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
163 "Use a pure Python kernel instead of an IPython kernel."),
174 "Use a pure Python kernel instead of an IPython kernel."),
164 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
175 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
165 "Disable rich text support."),
176 "Disable rich text support."),
166 })
177 })
167 flags.update(boolean_flag(
178 flags.update(boolean_flag(
168 'gui-completion', 'ConsoleWidget.gui_completion',
179 'gui-completion', 'ConsoleWidget.gui_completion',
169 "use a GUI widget for tab completion",
180 "use a GUI widget for tab completion",
170 "use plaintext output for completion"
181 "use plaintext output for completion"
171 ))
182 ))
172 flags.update(boolean_flag(
183 flags.update(boolean_flag(
173 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
184 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
174 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
185 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
175 to force a direct exit without any confirmation.
186 to force a direct exit without any confirmation.
176 """,
187 """,
177 """Don't prompt the user when exiting. This will terminate the kernel
188 """Don't prompt the user when exiting. This will terminate the kernel
178 if it is owned by the frontend, and leave it alive if it is external.
189 if it is owned by the frontend, and leave it alive if it is external.
179 """
190 """
180 ))
191 ))
181 # the flags that are specific to the frontend
192 # the flags that are specific to the frontend
182 # these must be scrubbed before being passed to the kernel,
193 # these must be scrubbed before being passed to the kernel,
183 # or it will raise an error on unrecognized flags
194 # or it will raise an error on unrecognized flags
184 qt_flags = ['existing', 'pure', 'plain', 'gui-completion', 'no-gui-completion',
195 qt_flags = ['existing', 'pure', 'plain', 'gui-completion', 'no-gui-completion',
185 'confirm-exit', 'no-confirm-exit']
196 'confirm-exit', 'no-confirm-exit']
186
197
187 aliases = dict(ipkernel_aliases)
198 aliases = dict(ipkernel_aliases)
188
199
189 aliases.update(dict(
200 aliases.update(dict(
190 hb = 'IPythonQtConsoleApp.hb_port',
201 hb = 'IPythonQtConsoleApp.hb_port',
191 shell = 'IPythonQtConsoleApp.shell_port',
202 shell = 'IPythonQtConsoleApp.shell_port',
192 iopub = 'IPythonQtConsoleApp.iopub_port',
203 iopub = 'IPythonQtConsoleApp.iopub_port',
193 stdin = 'IPythonQtConsoleApp.stdin_port',
204 stdin = 'IPythonQtConsoleApp.stdin_port',
194 ip = 'IPythonQtConsoleApp.ip',
205 ip = 'IPythonQtConsoleApp.ip',
195
206
196 plain = 'IPythonQtConsoleApp.plain',
207 plain = 'IPythonQtConsoleApp.plain',
197 pure = 'IPythonQtConsoleApp.pure',
208 pure = 'IPythonQtConsoleApp.pure',
198 gui_completion = 'ConsoleWidget.gui_completion',
209 gui_completion = 'ConsoleWidget.gui_completion',
199 style = 'IPythonWidget.syntax_style',
210 style = 'IPythonWidget.syntax_style',
200 stylesheet = 'IPythonQtConsoleApp.stylesheet',
211 stylesheet = 'IPythonQtConsoleApp.stylesheet',
201 colors = 'ZMQInteractiveShell.colors',
212 colors = 'ZMQInteractiveShell.colors',
202
213
203 editor = 'IPythonWidget.editor',
214 editor = 'IPythonWidget.editor',
204 pi = 'IPythonWidget.in_prompt',
205 po = 'IPythonWidget.out_prompt',
206 si = 'IPythonWidget.input_sep',
207 so = 'IPythonWidget.output_sep',
208 so2 = 'IPythonWidget.output_sep2',
209 ))
215 ))
210
216
211 #-----------------------------------------------------------------------------
217 #-----------------------------------------------------------------------------
212 # IPythonQtConsole
218 # IPythonQtConsole
213 #-----------------------------------------------------------------------------
219 #-----------------------------------------------------------------------------
214
215 class IPythonQtConsoleApp(BaseIPythonApplication):
220 class IPythonQtConsoleApp(BaseIPythonApplication):
216 name = 'ipython-qtconsole'
221 name = 'ipython-qtconsole'
217 default_config_file_name='ipython_config.py'
222 default_config_file_name='ipython_config.py'
223
224 description = """
225 The IPython QtConsole.
226
227 This launches a Console-style application using Qt. It is not a full
228 console, in that launched terminal subprocesses will not.
229
230 The QtConsole supports various extra features beyond the
231
232 """
233
218 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
234 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
219 flags = Dict(flags)
235 flags = Dict(flags)
220 aliases = Dict(aliases)
236 aliases = Dict(aliases)
221
237
222 kernel_argv = List(Unicode)
238 kernel_argv = List(Unicode)
223
239
224 # connection info:
240 # connection info:
225 ip = Unicode(LOCALHOST, config=True,
241 ip = Unicode(LOCALHOST, config=True,
226 help="""Set the kernel\'s IP address [default localhost].
242 help="""Set the kernel\'s IP address [default localhost].
227 If the IP address is something other than localhost, then
243 If the IP address is something other than localhost, then
228 Consoles on other machines will be able to connect
244 Consoles on other machines will be able to connect
229 to the Kernel, so be careful!"""
245 to the Kernel, so be careful!"""
230 )
246 )
231 hb_port = Int(0, config=True,
247 hb_port = Int(0, config=True,
232 help="set the heartbeat port [default: random]")
248 help="set the heartbeat port [default: random]")
233 shell_port = Int(0, config=True,
249 shell_port = Int(0, config=True,
234 help="set the shell (XREP) port [default: random]")
250 help="set the shell (XREP) port [default: random]")
235 iopub_port = Int(0, config=True,
251 iopub_port = Int(0, config=True,
236 help="set the iopub (PUB) port [default: random]")
252 help="set the iopub (PUB) port [default: random]")
237 stdin_port = Int(0, config=True,
253 stdin_port = Int(0, config=True,
238 help="set the stdin (XREQ) port [default: random]")
254 help="set the stdin (XREQ) port [default: random]")
239
255
240 existing = CBool(False, config=True,
256 existing = CBool(False, config=True,
241 help="Whether to connect to an already running Kernel.")
257 help="Whether to connect to an already running Kernel.")
242
258
243 stylesheet = Unicode('', config=True,
259 stylesheet = Unicode('', config=True,
244 help="path to a custom CSS stylesheet")
260 help="path to a custom CSS stylesheet")
245
261
246 pure = CBool(False, config=True,
262 pure = CBool(False, config=True,
247 help="Use a pure Python kernel instead of an IPython kernel.")
263 help="Use a pure Python kernel instead of an IPython kernel.")
248 plain = CBool(False, config=True,
264 plain = CBool(False, config=True,
249 help="Use a plaintext widget instead of rich text (plain can't print/save).")
265 help="Use a plaintext widget instead of rich text (plain can't print/save).")
250
266
251 def _pure_changed(self, name, old, new):
267 def _pure_changed(self, name, old, new):
252 kind = 'plain' if self.plain else 'rich'
268 kind = 'plain' if self.plain else 'rich'
253 self.config.ConsoleWidget.kind = kind
269 self.config.ConsoleWidget.kind = kind
254 if self.pure:
270 if self.pure:
255 self.widget_factory = FrontendWidget
271 self.widget_factory = FrontendWidget
256 elif self.plain:
272 elif self.plain:
257 self.widget_factory = IPythonWidget
273 self.widget_factory = IPythonWidget
258 else:
274 else:
259 self.widget_factory = RichIPythonWidget
275 self.widget_factory = RichIPythonWidget
260
276
261 _plain_changed = _pure_changed
277 _plain_changed = _pure_changed
262
278
263 confirm_exit = CBool(True, config=True,
279 confirm_exit = CBool(True, config=True,
264 help="""
280 help="""
265 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
281 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
266 to force a direct exit without any confirmation.""",
282 to force a direct exit without any confirmation.""",
267 )
283 )
268
284
269 # the factory for creating a widget
285 # the factory for creating a widget
270 widget_factory = Any(RichIPythonWidget)
286 widget_factory = Any(RichIPythonWidget)
271
287
272 def parse_command_line(self, argv=None):
288 def parse_command_line(self, argv=None):
273 super(IPythonQtConsoleApp, self).parse_command_line(argv)
289 super(IPythonQtConsoleApp, self).parse_command_line(argv)
274 if argv is None:
290 if argv is None:
275 argv = sys.argv[1:]
291 argv = sys.argv[1:]
276
292
277 self.kernel_argv = list(argv) # copy
293 self.kernel_argv = list(argv) # copy
278
294
279 # scrub frontend-specific flags
295 # scrub frontend-specific flags
280 for a in argv:
296 for a in argv:
281 if a.startswith('--') and a[2:] in qt_flags:
297 if a.startswith('--') and a[2:] in qt_flags:
282 self.kernel_argv.remove(a)
298 self.kernel_argv.remove(a)
283
299
284 def init_kernel_manager(self):
300 def init_kernel_manager(self):
285 # Don't let Qt or ZMQ swallow KeyboardInterupts.
301 # Don't let Qt or ZMQ swallow KeyboardInterupts.
286 signal.signal(signal.SIGINT, signal.SIG_DFL)
302 signal.signal(signal.SIGINT, signal.SIG_DFL)
287
303
288 # Create a KernelManager and start a kernel.
304 # Create a KernelManager and start a kernel.
289 self.kernel_manager = QtKernelManager(
305 self.kernel_manager = QtKernelManager(
290 shell_address=(self.ip, self.shell_port),
306 shell_address=(self.ip, self.shell_port),
291 sub_address=(self.ip, self.iopub_port),
307 sub_address=(self.ip, self.iopub_port),
292 stdin_address=(self.ip, self.stdin_port),
308 stdin_address=(self.ip, self.stdin_port),
293 hb_address=(self.ip, self.hb_port),
309 hb_address=(self.ip, self.hb_port),
294 config=self.config
310 config=self.config
295 )
311 )
296 # start the kernel
312 # start the kernel
297 if not self.existing:
313 if not self.existing:
298 kwargs = dict(ip=self.ip, ipython=not self.pure)
314 kwargs = dict(ip=self.ip, ipython=not self.pure)
299 kwargs['extra_arguments'] = self.kernel_argv
315 kwargs['extra_arguments'] = self.kernel_argv
300 self.kernel_manager.start_kernel(**kwargs)
316 self.kernel_manager.start_kernel(**kwargs)
301 self.kernel_manager.start_channels()
317 self.kernel_manager.start_channels()
302
318
303
319
304 def init_qt_elements(self):
320 def init_qt_elements(self):
305 # Create the widget.
321 # Create the widget.
306 self.app = QtGui.QApplication([])
322 self.app = QtGui.QApplication([])
307 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
323 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
308 self.widget = self.widget_factory(config=self.config,
324 self.widget = self.widget_factory(config=self.config,
309 local_kernel=local_kernel)
325 local_kernel=local_kernel)
310 self.widget.kernel_manager = self.kernel_manager
326 self.widget.kernel_manager = self.kernel_manager
311 self.window = MainWindow(self.app, self.widget, self.existing,
327 self.window = MainWindow(self.app, self.widget, self.existing,
312 may_close=local_kernel,
328 may_close=local_kernel,
313 confirm_exit=self.confirm_exit)
329 confirm_exit=self.confirm_exit)
314 self.window.setWindowTitle('Python' if self.pure else 'IPython')
330 self.window.setWindowTitle('Python' if self.pure else 'IPython')
315
331
316 def init_colors(self):
332 def init_colors(self):
317 """Configure the coloring of the widget"""
333 """Configure the coloring of the widget"""
318 # Note: This will be dramatically simplified when colors
334 # Note: This will be dramatically simplified when colors
319 # are removed from the backend.
335 # are removed from the backend.
320
336
321 if self.pure:
337 if self.pure:
322 # only IPythonWidget supports styling
338 # only IPythonWidget supports styling
323 return
339 return
324
340
325 # parse the colors arg down to current known labels
341 # parse the colors arg down to current known labels
326 try:
342 try:
327 colors = self.config.ZMQInteractiveShell.colors
343 colors = self.config.ZMQInteractiveShell.colors
328 except AttributeError:
344 except AttributeError:
329 colors = None
345 colors = None
330 try:
346 try:
331 style = self.config.IPythonWidget.colors
347 style = self.config.IPythonWidget.colors
332 except AttributeError:
348 except AttributeError:
333 style = None
349 style = None
334
350
335 # find the value for colors:
351 # find the value for colors:
336 if colors:
352 if colors:
337 colors=colors.lower()
353 colors=colors.lower()
338 if colors in ('lightbg', 'light'):
354 if colors in ('lightbg', 'light'):
339 colors='lightbg'
355 colors='lightbg'
340 elif colors in ('dark', 'linux'):
356 elif colors in ('dark', 'linux'):
341 colors='linux'
357 colors='linux'
342 else:
358 else:
343 colors='nocolor'
359 colors='nocolor'
344 elif style:
360 elif style:
345 if style=='bw':
361 if style=='bw':
346 colors='nocolor'
362 colors='nocolor'
347 elif styles.dark_style(style):
363 elif styles.dark_style(style):
348 colors='linux'
364 colors='linux'
349 else:
365 else:
350 colors='lightbg'
366 colors='lightbg'
351 else:
367 else:
352 colors=None
368 colors=None
353
369
354 # Configure the style.
370 # Configure the style.
355 widget = self.widget
371 widget = self.widget
356 if style:
372 if style:
357 widget.style_sheet = styles.sheet_from_template(style, colors)
373 widget.style_sheet = styles.sheet_from_template(style, colors)
358 widget.syntax_style = style
374 widget.syntax_style = style
359 widget._syntax_style_changed()
375 widget._syntax_style_changed()
360 widget._style_sheet_changed()
376 widget._style_sheet_changed()
361 elif colors:
377 elif colors:
362 # use a default style
378 # use a default style
363 widget.set_default_style(colors=colors)
379 widget.set_default_style(colors=colors)
364 else:
380 else:
365 # this is redundant for now, but allows the widget's
381 # this is redundant for now, but allows the widget's
366 # defaults to change
382 # defaults to change
367 widget.set_default_style()
383 widget.set_default_style()
368
384
369 if self.stylesheet:
385 if self.stylesheet:
370 # we got an expicit stylesheet
386 # we got an expicit stylesheet
371 if os.path.isfile(self.stylesheet):
387 if os.path.isfile(self.stylesheet):
372 with open(self.stylesheet) as f:
388 with open(self.stylesheet) as f:
373 sheet = f.read()
389 sheet = f.read()
374 widget.style_sheet = sheet
390 widget.style_sheet = sheet
375 widget._style_sheet_changed()
391 widget._style_sheet_changed()
376 else:
392 else:
377 raise IOError("Stylesheet %r not found."%self.stylesheet)
393 raise IOError("Stylesheet %r not found."%self.stylesheet)
378
394
379 def initialize(self, argv=None):
395 def initialize(self, argv=None):
380 super(IPythonQtConsoleApp, self).initialize(argv)
396 super(IPythonQtConsoleApp, self).initialize(argv)
381 self.init_kernel_manager()
397 self.init_kernel_manager()
382 self.init_qt_elements()
398 self.init_qt_elements()
383 self.init_colors()
399 self.init_colors()
384
400
385 def start(self):
401 def start(self):
386
402
387 # draw the window
403 # draw the window
388 self.window.show()
404 self.window.show()
389
405
390 # Start the application main loop.
406 # Start the application main loop.
391 self.app.exec_()
407 self.app.exec_()
392
408
393 #-----------------------------------------------------------------------------
409 #-----------------------------------------------------------------------------
394 # Main entry point
410 # Main entry point
395 #-----------------------------------------------------------------------------
411 #-----------------------------------------------------------------------------
396
412
397 def main():
413 def main():
398 app = IPythonQtConsoleApp()
414 app = IPythonQtConsoleApp()
399 app.initialize()
415 app.initialize()
400 app.start()
416 app.start()
401
417
402
418
403 if __name__ == '__main__':
419 if __name__ == '__main__':
404 main()
420 main()
@@ -1,365 +1,357 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # encoding: utf-8
2 # encoding: utf-8
3 """
3 """
4 The :class:`~IPython.core.newapplication.Application` object for the command
4 The :class:`~IPython.core.newapplication.Application` object for the command
5 line :command:`ipython` program.
5 line :command:`ipython` program.
6
6
7 Authors
7 Authors
8 -------
8 -------
9
9
10 * Brian Granger
10 * Brian Granger
11 * Fernando Perez
11 * Fernando Perez
12 * Min Ragan-Kelley
12 * Min Ragan-Kelley
13 """
13 """
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Copyright (C) 2008-2010 The IPython Development Team
16 # Copyright (C) 2008-2010 The IPython Development Team
17 #
17 #
18 # Distributed under the terms of the BSD License. The full license is in
18 # Distributed under the terms of the BSD License. The full license is in
19 # the file COPYING, distributed as part of this software.
19 # the file COPYING, distributed as part of this software.
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Imports
23 # Imports
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 from __future__ import absolute_import
26 from __future__ import absolute_import
27
27
28 import logging
28 import logging
29 import os
29 import os
30 import sys
30 import sys
31
31
32 from IPython.config.loader import (
32 from IPython.config.loader import (
33 Config, PyFileConfigLoader
33 Config, PyFileConfigLoader
34 )
34 )
35 from IPython.config.application import boolean_flag
35 from IPython.config.application import boolean_flag
36 from IPython.core import release
36 from IPython.core import release
37 from IPython.core import usage
37 from IPython.core import usage
38 from IPython.core.crashhandler import CrashHandler
38 from IPython.core.crashhandler import CrashHandler
39 from IPython.core.formatters import PlainTextFormatter
39 from IPython.core.formatters import PlainTextFormatter
40 from IPython.core.newapplication import (
40 from IPython.core.newapplication import (
41 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
41 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
42 )
42 )
43 from IPython.core.shellapp import (
43 from IPython.core.shellapp import (
44 InteractiveShellApp, shell_flags, shell_aliases
44 InteractiveShellApp, shell_flags, shell_aliases
45 )
45 )
46 from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
46 from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
47 from IPython.lib import inputhook
47 from IPython.lib import inputhook
48 from IPython.utils.path import get_ipython_dir, check_for_old_config
48 from IPython.utils.path import get_ipython_dir, check_for_old_config
49 from IPython.utils.traitlets import (
49 from IPython.utils.traitlets import (
50 Bool, Dict, CaselessStrEnum
50 Bool, Dict, CaselessStrEnum
51 )
51 )
52
52
53 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
54 # Globals, utilities and helpers
54 # Globals, utilities and helpers
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56
56
57 #: The default config file name for this application.
57 #: The default config file name for this application.
58 default_config_file_name = u'ipython_config.py'
58 default_config_file_name = u'ipython_config.py'
59
59
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Crash handler for this application
62 # Crash handler for this application
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 _message_template = """\
65 _message_template = """\
66 Oops, $self.app_name crashed. We do our best to make it stable, but...
66 Oops, $self.app_name crashed. We do our best to make it stable, but...
67
67
68 A crash report was automatically generated with the following information:
68 A crash report was automatically generated with the following information:
69 - A verbatim copy of the crash traceback.
69 - A verbatim copy of the crash traceback.
70 - A copy of your input history during this session.
70 - A copy of your input history during this session.
71 - Data on your current $self.app_name configuration.
71 - Data on your current $self.app_name configuration.
72
72
73 It was left in the file named:
73 It was left in the file named:
74 \t'$self.crash_report_fname'
74 \t'$self.crash_report_fname'
75 If you can email this file to the developers, the information in it will help
75 If you can email this file to the developers, the information in it will help
76 them in understanding and correcting the problem.
76 them in understanding and correcting the problem.
77
77
78 You can mail it to: $self.contact_name at $self.contact_email
78 You can mail it to: $self.contact_name at $self.contact_email
79 with the subject '$self.app_name Crash Report'.
79 with the subject '$self.app_name Crash Report'.
80
80
81 If you want to do it now, the following command will work (under Unix):
81 If you want to do it now, the following command will work (under Unix):
82 mail -s '$self.app_name Crash Report' $self.contact_email < $self.crash_report_fname
82 mail -s '$self.app_name Crash Report' $self.contact_email < $self.crash_report_fname
83
83
84 To ensure accurate tracking of this issue, please file a report about it at:
84 To ensure accurate tracking of this issue, please file a report about it at:
85 $self.bug_tracker
85 $self.bug_tracker
86 """
86 """
87
87
88 class IPAppCrashHandler(CrashHandler):
88 class IPAppCrashHandler(CrashHandler):
89 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
89 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
90
90
91 message_template = _message_template
91 message_template = _message_template
92
92
93 def __init__(self, app):
93 def __init__(self, app):
94 contact_name = release.authors['Fernando'][0]
94 contact_name = release.authors['Fernando'][0]
95 contact_email = release.authors['Fernando'][1]
95 contact_email = release.authors['Fernando'][1]
96 bug_tracker = 'http://github.com/ipython/ipython/issues'
96 bug_tracker = 'http://github.com/ipython/ipython/issues'
97 super(IPAppCrashHandler,self).__init__(
97 super(IPAppCrashHandler,self).__init__(
98 app, contact_name, contact_email, bug_tracker
98 app, contact_name, contact_email, bug_tracker
99 )
99 )
100
100
101 def make_report(self,traceback):
101 def make_report(self,traceback):
102 """Return a string containing a crash report."""
102 """Return a string containing a crash report."""
103
103
104 sec_sep = self.section_sep
104 sec_sep = self.section_sep
105 # Start with parent report
105 # Start with parent report
106 report = [super(IPAppCrashHandler, self).make_report(traceback)]
106 report = [super(IPAppCrashHandler, self).make_report(traceback)]
107 # Add interactive-specific info we may have
107 # Add interactive-specific info we may have
108 rpt_add = report.append
108 rpt_add = report.append
109 try:
109 try:
110 rpt_add(sec_sep+"History of session input:")
110 rpt_add(sec_sep+"History of session input:")
111 for line in self.app.shell.user_ns['_ih']:
111 for line in self.app.shell.user_ns['_ih']:
112 rpt_add(line)
112 rpt_add(line)
113 rpt_add('\n*** Last line of input (may not be in above history):\n')
113 rpt_add('\n*** Last line of input (may not be in above history):\n')
114 rpt_add(self.app.shell._last_input_line+'\n')
114 rpt_add(self.app.shell._last_input_line+'\n')
115 except:
115 except:
116 pass
116 pass
117
117
118 return ''.join(report)
118 return ''.join(report)
119
119
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121 # Aliases and Flags
121 # Aliases and Flags
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 flags = dict(base_flags)
123 flags = dict(base_flags)
124 flags.update(shell_flags)
124 flags.update(shell_flags)
125 addflag = lambda *args: flags.update(boolean_flag(*args))
125 addflag = lambda *args: flags.update(boolean_flag(*args))
126 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
126 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
127 'Turn on auto editing of files with syntax errors.',
127 'Turn on auto editing of files with syntax errors.',
128 'Turn off auto editing of files with syntax errors.'
128 'Turn off auto editing of files with syntax errors.'
129 )
129 )
130 addflag('banner', 'TerminalIPythonApp.display_banner',
130 addflag('banner', 'TerminalIPythonApp.display_banner',
131 "Display a banner upon starting IPython.",
131 "Display a banner upon starting IPython.",
132 "Don't display a banner upon starting IPython."
132 "Don't display a banner upon starting IPython."
133 )
133 )
134 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
134 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
135 """Set to confirm when you try to exit IPython with an EOF (Control-D
135 """Set to confirm when you try to exit IPython with an EOF (Control-D
136 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
136 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
137 you can force a direct exit without any confirmation.""",
137 you can force a direct exit without any confirmation.""",
138 "Don't prompt the user when exiting."
138 "Don't prompt the user when exiting."
139 )
139 )
140 addflag('term-title', 'TerminalInteractiveShell.term_title',
140 addflag('term-title', 'TerminalInteractiveShell.term_title',
141 "Enable auto setting the terminal title.",
141 "Enable auto setting the terminal title.",
142 "Disable auto setting the terminal title."
142 "Disable auto setting the terminal title."
143 )
143 )
144 classic_config = Config()
144 classic_config = Config()
145 classic_config.InteractiveShell.cache_size = 0
145 classic_config.InteractiveShell.cache_size = 0
146 classic_config.PlainTextFormatter.pprint = False
146 classic_config.PlainTextFormatter.pprint = False
147 classic_config.InteractiveShell.prompt_in1 = '>>> '
147 classic_config.InteractiveShell.prompt_in1 = '>>> '
148 classic_config.InteractiveShell.prompt_in2 = '... '
148 classic_config.InteractiveShell.prompt_in2 = '... '
149 classic_config.InteractiveShell.prompt_out = ''
149 classic_config.InteractiveShell.prompt_out = ''
150 classic_config.InteractiveShell.separate_in = ''
150 classic_config.InteractiveShell.separate_in = ''
151 classic_config.InteractiveShell.separate_out = ''
151 classic_config.InteractiveShell.separate_out = ''
152 classic_config.InteractiveShell.separate_out2 = ''
152 classic_config.InteractiveShell.separate_out2 = ''
153 classic_config.InteractiveShell.colors = 'NoColor'
153 classic_config.InteractiveShell.colors = 'NoColor'
154 classic_config.InteractiveShell.xmode = 'Plain'
154 classic_config.InteractiveShell.xmode = 'Plain'
155
155
156 flags['classic']=(
156 flags['classic']=(
157 classic_config,
157 classic_config,
158 "Gives IPython a similar feel to the classic Python prompt."
158 "Gives IPython a similar feel to the classic Python prompt."
159 )
159 )
160 # # log doesn't make so much sense this way anymore
160 # # log doesn't make so much sense this way anymore
161 # paa('--log','-l',
161 # paa('--log','-l',
162 # action='store_true', dest='InteractiveShell.logstart',
162 # action='store_true', dest='InteractiveShell.logstart',
163 # help="Start logging to the default log file (./ipython_log.py).")
163 # help="Start logging to the default log file (./ipython_log.py).")
164 #
164 #
165 # # quick is harder to implement
165 # # quick is harder to implement
166 flags['quick']=(
166 flags['quick']=(
167 {'TerminalIPythonApp' : {'quick' : True}},
167 {'TerminalIPythonApp' : {'quick' : True}},
168 "Enable quick startup with no config files."
168 "Enable quick startup with no config files."
169 )
169 )
170
170
171 nosep_config = Config()
172 nosep_config.InteractiveShell.separate_in = ''
173 nosep_config.InteractiveShell.separate_out = ''
174 nosep_config.InteractiveShell.separate_out2 = ''
175
176 flags['nosep']=(nosep_config, "Eliminate all spacing between prompts.")
177
178 flags['i'] = (
171 flags['i'] = (
179 {'TerminalIPythonApp' : {'force_interact' : True}},
172 {'TerminalIPythonApp' : {'force_interact' : True}},
180 "If running code from the command line, become interactive afterwards."
173 "If running code from the command line, become interactive afterwards."
181 )
174 )
182 flags['pylab'] = (
175 flags['pylab'] = (
183 {'TerminalIPythonApp' : {'pylab' : 'auto'}},
176 {'TerminalIPythonApp' : {'pylab' : 'auto'}},
184 """Pre-load matplotlib and numpy for interactive use with
177 """Pre-load matplotlib and numpy for interactive use with
185 the default matplotlib backend."""
178 the default matplotlib backend."""
186 )
179 )
187
180
188 aliases = dict(base_aliases)
181 aliases = dict(base_aliases)
189 aliases.update(shell_aliases)
182 aliases.update(shell_aliases)
190
183
191 # it's possible we don't want short aliases for *all* of these:
184 # it's possible we don't want short aliases for *all* of these:
192 aliases.update(dict(
185 aliases.update(dict(
193 editor='TerminalInteractiveShell.editor',
194 sl='TerminalInteractiveShell.screen_length',
195 gui='TerminalIPythonApp.gui',
186 gui='TerminalIPythonApp.gui',
196 pylab='TerminalIPythonApp.pylab',
187 pylab='TerminalIPythonApp.pylab',
197 ))
188 ))
198
189
199 #-----------------------------------------------------------------------------
190 #-----------------------------------------------------------------------------
200 # Main classes and functions
191 # Main classes and functions
201 #-----------------------------------------------------------------------------
192 #-----------------------------------------------------------------------------
202
193
203 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
194 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
204 name = u'ipython'
195 name = u'ipython'
205 description = usage.cl_usage
196 description = usage.cl_usage
206 # command_line_loader = IPAppConfigLoader
197 # command_line_loader = IPAppConfigLoader
207 default_config_file_name = default_config_file_name
198 default_config_file_name = default_config_file_name
208 crash_handler_class = IPAppCrashHandler
199 crash_handler_class = IPAppCrashHandler
209
200
210 flags = Dict(flags)
201 flags = Dict(flags)
211 aliases = Dict(aliases)
202 aliases = Dict(aliases)
212 classes = [InteractiveShellApp, TerminalInteractiveShell, ProfileDir, PlainTextFormatter]
203 classes = [InteractiveShellApp, TerminalInteractiveShell, ProfileDir, PlainTextFormatter]
213 subcommands = Dict(dict(
204 subcommands = Dict(dict(
214 qtconsole=('IPython.frontend.qt.console.ipythonqt.IPythonQtConsoleApp',
205 qtconsole=('IPython.frontend.qt.console.ipythonqt.IPythonQtConsoleApp',
215 """Launch the IPython QtConsole. Also launched as ipython-qtconsole"""
206 """Launch the IPython QtConsole. Also launched as ipython-qtconsole"""
216 )
207 )
217 ))
208 ))
218
209
219 # *do* autocreate requested profile
210 # *do* autocreate requested profile
220 auto_create=Bool(True)
211 auto_create=Bool(True)
221 copy_config_files=Bool(True)
212 copy_config_files=Bool(True)
222 # configurables
213 # configurables
223 ignore_old_config=Bool(False, config=True,
214 ignore_old_config=Bool(False, config=True,
224 help="Suppress warning messages about legacy config files"
215 help="Suppress warning messages about legacy config files"
225 )
216 )
226 quick = Bool(False, config=True,
217 quick = Bool(False, config=True,
227 help="""Start IPython quickly by skipping the loading of config files."""
218 help="""Start IPython quickly by skipping the loading of config files."""
228 )
219 )
229 def _quick_changed(self, name, old, new):
220 def _quick_changed(self, name, old, new):
230 if new:
221 if new:
231 self.load_config_file = lambda *a, **kw: None
222 self.load_config_file = lambda *a, **kw: None
232 self.ignore_old_config=True
223 self.ignore_old_config=True
233
224
234 gui = CaselessStrEnum(('qt','wx','gtk'), config=True,
225 gui = CaselessStrEnum(('qt','wx','gtk'), config=True,
235 help="Enable GUI event loop integration ('qt', 'wx', 'gtk')."
226 help="Enable GUI event loop integration ('qt', 'wx', 'gtk')."
236 )
227 )
237 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'auto'],
228 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'auto'],
238 config=True,
229 config=True,
239 help="""Pre-load matplotlib and numpy for interactive use,
230 help="""Pre-load matplotlib and numpy for interactive use,
240 selecting a particular matplotlib backend and loop integration.
231 selecting a particular matplotlib backend and loop integration.
241 """
232 """
242 )
233 )
243 display_banner = Bool(True, config=True,
234 display_banner = Bool(True, config=True,
244 help="Whether to display a banner upon starting IPython."
235 help="Whether to display a banner upon starting IPython."
245 )
236 )
246
237
247 # if there is code of files to run from the cmd line, don't interact
238 # if there is code of files to run from the cmd line, don't interact
248 # unless the --i flag (App.force_interact) is true.
239 # unless the --i flag (App.force_interact) is true.
249 force_interact = Bool(False, config=True,
240 force_interact = Bool(False, config=True,
250 help="""If a command or file is given via the command-line,
241 help="""If a command or file is given via the command-line,
251 e.g. 'ipython foo.py"""
242 e.g. 'ipython foo.py"""
252 )
243 )
253 def _force_interact_changed(self, name, old, new):
244 def _force_interact_changed(self, name, old, new):
254 if new:
245 if new:
255 self.interact = True
246 self.interact = True
256
247
257 def _file_to_run_changed(self, name, old, new):
248 def _file_to_run_changed(self, name, old, new):
258 if new and not self.force_interact:
249 if new and not self.force_interact:
259 self.interact = False
250 self.interact = False
260 _code_to_run_changed = _file_to_run_changed
251 _code_to_run_changed = _file_to_run_changed
261
252
262 # internal, not-configurable
253 # internal, not-configurable
263 interact=Bool(True)
254 interact=Bool(True)
264
255
265
256
266 def initialize(self, argv=None):
257 def initialize(self, argv=None):
267 """Do actions after construct, but before starting the app."""
258 """Do actions after construct, but before starting the app."""
268 super(TerminalIPythonApp, self).initialize(argv)
259 super(TerminalIPythonApp, self).initialize(argv)
269 if self.subapp is not None:
260 if self.subapp is not None:
270 # don't bother initializing further, starting subapp
261 # don't bother initializing further, starting subapp
271 return
262 return
272 if not self.ignore_old_config:
263 if not self.ignore_old_config:
273 check_for_old_config(self.ipython_dir)
264 check_for_old_config(self.ipython_dir)
274 # print self.extra_args
265 # print self.extra_args
275 if self.extra_args:
266 if self.extra_args:
276 self.file_to_run = self.extra_args[0]
267 self.file_to_run = self.extra_args[0]
277 # create the shell
268 # create the shell
278 self.init_shell()
269 self.init_shell()
279 # and draw the banner
270 # and draw the banner
280 self.init_banner()
271 self.init_banner()
281 # Now a variety of things that happen after the banner is printed.
272 # Now a variety of things that happen after the banner is printed.
282 self.init_gui_pylab()
273 self.init_gui_pylab()
283 self.init_extensions()
274 self.init_extensions()
284 self.init_code()
275 self.init_code()
285
276
286 def init_shell(self):
277 def init_shell(self):
287 """initialize the InteractiveShell instance"""
278 """initialize the InteractiveShell instance"""
288 # I am a little hesitant to put these into InteractiveShell itself.
279 # I am a little hesitant to put these into InteractiveShell itself.
289 # But that might be the place for them
280 # But that might be the place for them
290 sys.path.insert(0, '')
281 sys.path.insert(0, '')
291
282
292 # Create an InteractiveShell instance.
283 # Create an InteractiveShell instance.
293 # shell.display_banner should always be False for the terminal
284 # shell.display_banner should always be False for the terminal
294 # based app, because we call shell.show_banner() by hand below
285 # based app, because we call shell.show_banner() by hand below
295 # so the banner shows *before* all extension loading stuff.
286 # so the banner shows *before* all extension loading stuff.
296 self.shell = TerminalInteractiveShell.instance(config=self.config,
287 self.shell = TerminalInteractiveShell.instance(config=self.config,
297 display_banner=False, profile_dir=self.profile_dir,
288 display_banner=False, profile_dir=self.profile_dir,
298 ipython_dir=self.ipython_dir)
289 ipython_dir=self.ipython_dir)
299
290
300 def init_banner(self):
291 def init_banner(self):
301 """optionally display the banner"""
292 """optionally display the banner"""
302 if self.display_banner and self.interact:
293 if self.display_banner and self.interact:
303 self.shell.show_banner()
294 self.shell.show_banner()
304 # Make sure there is a space below the banner.
295 # Make sure there is a space below the banner.
305 if self.log_level <= logging.INFO: print
296 if self.log_level <= logging.INFO: print
306
297
307
298
308 def init_gui_pylab(self):
299 def init_gui_pylab(self):
309 """Enable GUI event loop integration, taking pylab into account."""
300 """Enable GUI event loop integration, taking pylab into account."""
310 gui = self.gui
301 gui = self.gui
311
302
312 # Using `pylab` will also require gui activation, though which toolkit
303 # Using `pylab` will also require gui activation, though which toolkit
313 # to use may be chosen automatically based on mpl configuration.
304 # to use may be chosen automatically based on mpl configuration.
314 if self.pylab:
305 if self.pylab:
315 activate = self.shell.enable_pylab
306 activate = self.shell.enable_pylab
316 if self.pylab == 'auto':
307 if self.pylab == 'auto':
317 gui = None
308 gui = None
318 else:
309 else:
319 gui = self.pylab
310 gui = self.pylab
320 else:
311 else:
321 # Enable only GUI integration, no pylab
312 # Enable only GUI integration, no pylab
322 activate = inputhook.enable_gui
313 activate = inputhook.enable_gui
323
314
324 if gui or self.pylab:
315 if gui or self.pylab:
325 try:
316 try:
326 self.log.info("Enabling GUI event loop integration, "
317 self.log.info("Enabling GUI event loop integration, "
327 "toolkit=%s, pylab=%s" % (gui, self.pylab) )
318 "toolkit=%s, pylab=%s" % (gui, self.pylab) )
328 activate(gui)
319 activate(gui)
329 except:
320 except:
330 self.log.warn("Error in enabling GUI event loop integration:")
321 self.log.warn("Error in enabling GUI event loop integration:")
331 self.shell.showtraceback()
322 self.shell.showtraceback()
332
323
333 def start(self):
324 def start(self):
334 if self.subapp is not None:
325 if self.subapp is not None:
335 return self.subapp.start()
326 return self.subapp.start()
336 # perform any prexec steps:
327 # perform any prexec steps:
337 if self.interact:
328 if self.interact:
338 self.log.debug("Starting IPython's mainloop...")
329 self.log.debug("Starting IPython's mainloop...")
339 self.shell.mainloop()
330 self.shell.mainloop()
340 else:
331 else:
341 self.log.debug("IPython not interactive...")
332 self.log.debug("IPython not interactive...")
342
333
343
334
344 def load_default_config(ipython_dir=None):
335 def load_default_config(ipython_dir=None):
345 """Load the default config file from the default ipython_dir.
336 """Load the default config file from the default ipython_dir.
346
337
347 This is useful for embedded shells.
338 This is useful for embedded shells.
348 """
339 """
349 if ipython_dir is None:
340 if ipython_dir is None:
350 ipython_dir = get_ipython_dir()
341 ipython_dir = get_ipython_dir()
351 profile_dir = os.path.join(ipython_dir, 'profile_default')
342 profile_dir = os.path.join(ipython_dir, 'profile_default')
352 cl = PyFileConfigLoader(default_config_file_name, profile_dir)
343 cl = PyFileConfigLoader(default_config_file_name, profile_dir)
353 config = cl.load_config()
344 config = cl.load_config()
354 return config
345 return config
355
346
356
347
357 def launch_new_instance():
348 def launch_new_instance():
358 """Create and run a full blown IPython instance"""
349 """Create and run a full blown IPython instance"""
359 app = TerminalIPythonApp.instance()
350 app = TerminalIPythonApp.instance()
360 app.initialize()
351 app.initialize()
361 app.start()
352 app.start()
362
353
363
354
364 if __name__ == '__main__':
355 if __name__ == '__main__':
365 launch_new_instance()
356 launch_new_instance()
357
@@ -1,73 +1,73 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """Utility for forwarding file read events over a zmq socket.
2 """Utility for forwarding file read events over a zmq socket.
3
3
4 This is necessary because select on Windows only supports
4 This is necessary because select on Windows only supports sockets, not FDs.
5
5
6 Authors:
6 Authors:
7
7
8 * MinRK
8 * MinRK
9
9
10 """
10 """
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Copyright (C) 2011 The IPython Development Team
13 # Copyright (C) 2011 The IPython Development Team
14 #
14 #
15 # Distributed under the terms of the BSD License. The full license is in
15 # Distributed under the terms of the BSD License. The full license is in
16 # the file COPYING, distributed as part of this software.
16 # the file COPYING, distributed as part of this software.
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 import uuid
23 import uuid
24 import zmq
24 import zmq
25
25
26 from threading import Thread
26 from threading import Thread
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Code
29 # Code
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 class ForwarderThread(Thread):
32 class ForwarderThread(Thread):
33 def __init__(self, sock, fd):
33 def __init__(self, sock, fd):
34 Thread.__init__(self)
34 Thread.__init__(self)
35 self.daemon=True
35 self.daemon=True
36 self.sock = sock
36 self.sock = sock
37 self.fd = fd
37 self.fd = fd
38
38
39 def run(self):
39 def run(self):
40 """loop through lines in self.fd, and send them over self.sock"""
40 """Loop through lines in self.fd, and send them over self.sock."""
41 line = self.fd.readline()
41 line = self.fd.readline()
42 # allow for files opened in unicode mode
42 # allow for files opened in unicode mode
43 if isinstance(line, unicode):
43 if isinstance(line, unicode):
44 send = self.sock.send_unicode
44 send = self.sock.send_unicode
45 else:
45 else:
46 send = self.sock.send
46 send = self.sock.send
47 while line:
47 while line:
48 send(line)
48 send(line)
49 line = self.fd.readline()
49 line = self.fd.readline()
50 # line == '' means EOF
50 # line == '' means EOF
51 self.fd.close()
51 self.fd.close()
52 self.sock.close()
52 self.sock.close()
53
53
54 def forward_read_events(fd, context=None):
54 def forward_read_events(fd, context=None):
55 """forward read events from an FD over a socket.
55 """Forward read events from an FD over a socket.
56
56
57 This method wraps a file in a socket pair, so it can
57 This method wraps a file in a socket pair, so it can
58 be polled for read events by select (specifically zmq.eventloop.ioloop)
58 be polled for read events by select (specifically zmq.eventloop.ioloop)
59 """
59 """
60 if context is None:
60 if context is None:
61 context = zmq.Context.instance()
61 context = zmq.Context.instance()
62 push = context.socket(zmq.PUSH)
62 push = context.socket(zmq.PUSH)
63 push.setsockopt(zmq.LINGER, -1)
63 push.setsockopt(zmq.LINGER, -1)
64 pull = context.socket(zmq.PULL)
64 pull = context.socket(zmq.PULL)
65 addr='inproc://%s'%uuid.uuid4()
65 addr='inproc://%s'%uuid.uuid4()
66 push.bind(addr)
66 push.bind(addr)
67 pull.connect(addr)
67 pull.connect(addr)
68 forwarder = ForwarderThread(push, fd)
68 forwarder = ForwarderThread(push, fd)
69 forwarder.start()
69 forwarder.start()
70 return pull
70 return pull
71
71
72
72
73 __all__ = ['forward_read_events'] No newline at end of file
73 __all__ = ['forward_read_events']
@@ -1,214 +1,215 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """An Application for launching a kernel
2 """An Application for launching a kernel
3
3
4 Authors
4 Authors
5 -------
5 -------
6 * MinRK
6 * MinRK
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING.txt, distributed as part of this software.
12 # the file COPYING.txt, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # Standard library imports.
19 # Standard library imports.
20 import os
20 import os
21 import sys
21 import sys
22
22
23 # System library imports.
23 # System library imports.
24 import zmq
24 import zmq
25
25
26 # IPython imports.
26 # IPython imports.
27 from IPython.core.ultratb import FormattedTB
27 from IPython.core.ultratb import FormattedTB
28 from IPython.core.newapplication import (
28 from IPython.core.newapplication import (
29 BaseIPythonApplication, base_flags, base_aliases
29 BaseIPythonApplication, base_flags, base_aliases
30 )
30 )
31 from IPython.utils import io
31 from IPython.utils import io
32 from IPython.utils.localinterfaces import LOCALHOST
32 from IPython.utils.localinterfaces import LOCALHOST
33 from IPython.utils.traitlets import Any, Instance, Dict, Unicode, Int, Bool
33 from IPython.utils.traitlets import Any, Instance, Dict, Unicode, Int, Bool
34 from IPython.utils.importstring import import_item
34 from IPython.utils.importstring import import_item
35 # local imports
35 # local imports
36 from IPython.zmq.heartbeat import Heartbeat
36 from IPython.zmq.heartbeat import Heartbeat
37 from IPython.zmq.parentpoller import ParentPollerUnix, ParentPollerWindows
37 from IPython.zmq.parentpoller import ParentPollerUnix, ParentPollerWindows
38 from IPython.zmq.session import Session
38 from IPython.zmq.session import Session
39
39
40
40
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42 # Flags and Aliases
42 # Flags and Aliases
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44
44
45 kernel_aliases = dict(base_aliases)
45 kernel_aliases = dict(base_aliases)
46 kernel_aliases.update({
46 kernel_aliases.update({
47 'ip' : 'KernelApp.ip',
47 'ip' : 'KernelApp.ip',
48 'hb' : 'KernelApp.hb_port',
48 'hb' : 'KernelApp.hb_port',
49 'shell' : 'KernelApp.shell_port',
49 'shell' : 'KernelApp.shell_port',
50 'iopub' : 'KernelApp.iopub_port',
50 'iopub' : 'KernelApp.iopub_port',
51 'stdin' : 'KernelApp.stdin_port',
51 'stdin' : 'KernelApp.stdin_port',
52 'parent': 'KernelApp.parent',
52 'parent': 'KernelApp.parent',
53 })
53 })
54 if sys.platform.startswith('win'):
54 if sys.platform.startswith('win'):
55 kernel_aliases['interrupt'] = 'KernelApp.interrupt'
55 kernel_aliases['interrupt'] = 'KernelApp.interrupt'
56
56
57 kernel_flags = dict(base_flags)
57 kernel_flags = dict(base_flags)
58 kernel_flags.update({
58 kernel_flags.update({
59 'no-stdout' : (
59 'no-stdout' : (
60 {'KernelApp' : {'no_stdout' : True}},
60 {'KernelApp' : {'no_stdout' : True}},
61 "redirect stdout to the null device"),
61 "redirect stdout to the null device"),
62 'no-stderr' : (
62 'no-stderr' : (
63 {'KernelApp' : {'no_stderr' : True}},
63 {'KernelApp' : {'no_stderr' : True}},
64 "redirect stderr to the null device"),
64 "redirect stderr to the null device"),
65 })
65 })
66
66
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Application class for starting a Kernel
69 # Application class for starting a Kernel
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 class KernelApp(BaseIPythonApplication):
72 class KernelApp(BaseIPythonApplication):
73 name='pykernel'
73 name='pykernel'
74 aliases = Dict(kernel_aliases)
74 aliases = Dict(kernel_aliases)
75 flags = Dict(kernel_flags)
75 flags = Dict(kernel_flags)
76 classes = [Session]
76 classes = [Session]
77 # the kernel class, as an importstring
77 # the kernel class, as an importstring
78 kernel_class = Unicode('IPython.zmq.pykernel.Kernel')
78 kernel_class = Unicode('IPython.zmq.pykernel.Kernel')
79 kernel = Any()
79 kernel = Any()
80 poller = Any() # don't restrict this even though current pollers are all Threads
80 poller = Any() # don't restrict this even though current pollers are all Threads
81 heartbeat = Instance(Heartbeat)
81 heartbeat = Instance(Heartbeat)
82 session = Instance('IPython.zmq.session.Session')
82 session = Instance('IPython.zmq.session.Session')
83 ports = Dict()
83 ports = Dict()
84
84
85 # connection info:
85 # connection info:
86 ip = Unicode(LOCALHOST, config=True,
86 ip = Unicode(LOCALHOST, config=True,
87 help="Set the IP or interface on which the kernel will listen.")
87 help="Set the IP or interface on which the kernel will listen.")
88 hb_port = Int(0, config=True, help="set the heartbeat port [default: random]")
88 hb_port = Int(0, config=True, help="set the heartbeat port [default: random]")
89 shell_port = Int(0, config=True, help="set the shell (XREP) port [default: random]")
89 shell_port = Int(0, config=True, help="set the shell (XREP) port [default: random]")
90 iopub_port = Int(0, config=True, help="set the iopub (PUB) port [default: random]")
90 iopub_port = Int(0, config=True, help="set the iopub (PUB) port [default: random]")
91 stdin_port = Int(0, config=True, help="set the stdin (XREQ) port [default: random]")
91 stdin_port = Int(0, config=True, help="set the stdin (XREQ) port [default: random]")
92
92
93 # streams, etc.
93 # streams, etc.
94 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
94 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
95 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
95 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
96 outstream_class = Unicode('IPython.zmq.iostream.OutStream', config=True,
96 outstream_class = Unicode('IPython.zmq.iostream.OutStream', config=True,
97 help="The importstring for the OutStream factory")
97 help="The importstring for the OutStream factory")
98 displayhook_class = Unicode('IPython.zmq.displayhook.DisplayHook', config=True,
98 displayhook_class = Unicode('IPython.zmq.displayhook.DisplayHook', config=True,
99 help="The importstring for the DisplayHook factory")
99 help="The importstring for the DisplayHook factory")
100
100
101 # polling
101 # polling
102 parent = Int(0, config=True,
102 parent = Int(0, config=True,
103 help="""kill this process if its parent dies. On Windows, the argument
103 help="""kill this process if its parent dies. On Windows, the argument
104 specifies the HANDLE of the parent process, otherwise it is simply boolean.
104 specifies the HANDLE of the parent process, otherwise it is simply boolean.
105 """)
105 """)
106 interrupt = Int(0, config=True,
106 interrupt = Int(0, config=True,
107 help="""ONLY USED ON WINDOWS
107 help="""ONLY USED ON WINDOWS
108 Interrupt this process when the parent is signalled.
108 Interrupt this process when the parent is signalled.
109 """)
109 """)
110
110
111 def init_crash_handler(self):
111 def init_crash_handler(self):
112 # Install minimal exception handling
112 # Install minimal exception handling
113 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
113 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
114 ostream=sys.__stdout__)
114 ostream=sys.__stdout__)
115
115
116 def init_poller(self):
116 def init_poller(self):
117 if sys.platform == 'win32':
117 if sys.platform == 'win32':
118 if self.interrupt or self.parent:
118 if self.interrupt or self.parent:
119 self.poller = ParentPollerWindows(self.interrupt, self.parent)
119 self.poller = ParentPollerWindows(self.interrupt, self.parent)
120 elif self.parent:
120 elif self.parent:
121 self.poller = ParentPollerUnix()
121 self.poller = ParentPollerUnix()
122
122
123 def _bind_socket(self, s, port):
123 def _bind_socket(self, s, port):
124 iface = 'tcp://%s' % self.ip
124 iface = 'tcp://%s' % self.ip
125 if port <= 0:
125 if port <= 0:
126 port = s.bind_to_random_port(iface)
126 port = s.bind_to_random_port(iface)
127 else:
127 else:
128 s.bind(iface + ':%i'%port)
128 s.bind(iface + ':%i'%port)
129 return port
129 return port
130
130
131 def init_sockets(self):
131 def init_sockets(self):
132 # Create a context, a session, and the kernel sockets.
132 # Create a context, a session, and the kernel sockets.
133 io.raw_print("Starting the kernel at pid:", os.getpid())
133 io.raw_print("Starting the kernel at pid:", os.getpid())
134 context = zmq.Context.instance()
134 context = zmq.Context.instance()
135 # Uncomment this to try closing the context.
135 # Uncomment this to try closing the context.
136 # atexit.register(context.term)
136 # atexit.register(context.term)
137
137
138 self.shell_socket = context.socket(zmq.XREP)
138 self.shell_socket = context.socket(zmq.XREP)
139 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
139 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
140 self.log.debug("shell XREP Channel on port: %i"%self.shell_port)
140 self.log.debug("shell XREP Channel on port: %i"%self.shell_port)
141
141
142 self.iopub_socket = context.socket(zmq.PUB)
142 self.iopub_socket = context.socket(zmq.PUB)
143 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
143 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
144 self.log.debug("iopub PUB Channel on port: %i"%self.iopub_port)
144 self.log.debug("iopub PUB Channel on port: %i"%self.iopub_port)
145
145
146 self.stdin_socket = context.socket(zmq.XREQ)
146 self.stdin_socket = context.socket(zmq.XREQ)
147 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
147 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
148 self.log.debug("stdin XREQ Channel on port: %i"%self.stdin_port)
148 self.log.debug("stdin XREQ Channel on port: %i"%self.stdin_port)
149
149
150 self.heartbeat = Heartbeat(context, (self.ip, self.hb_port))
150 self.heartbeat = Heartbeat(context, (self.ip, self.hb_port))
151 self.hb_port = self.heartbeat.port
151 self.hb_port = self.heartbeat.port
152 self.log.debug("Heartbeat REP Channel on port: %i"%self.hb_port)
152 self.log.debug("Heartbeat REP Channel on port: %i"%self.hb_port)
153
153
154 # Helper to make it easier to connect to an existing kernel, until we have
154 # Helper to make it easier to connect to an existing kernel, until we have
155 # single-port connection negotiation fully implemented.
155 # single-port connection negotiation fully implemented.
156 self.log.info("To connect another client to this kernel, use:")
156 self.log.info("To connect another client to this kernel, use:")
157 self.log.info("--external shell={0} iopub={1} stdin={2} hb={3}".format(
157 self.log.info("--external shell={0} iopub={1} stdin={2} hb={3}".format(
158 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port))
158 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port))
159
159
160
160
161 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
161 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
162 stdin=self.stdin_port, hb=self.hb_port)
162 stdin=self.stdin_port, hb=self.hb_port)
163
163
164 def init_session(self):
164 def init_session(self):
165 """create our session object"""
165 """create our session object"""
166 self.session = Session(config=self.config, username=u'kernel')
166 self.session = Session(config=self.config, username=u'kernel')
167
167
168 def init_io(self):
168 def init_io(self):
169 """redirects stdout/stderr, and installs a display hook"""
169 """redirects stdout/stderr, and installs a display hook"""
170 # Re-direct stdout/stderr, if necessary.
170 # Re-direct stdout/stderr, if necessary.
171 if self.no_stdout or self.no_stderr:
171 if self.no_stdout or self.no_stderr:
172 blackhole = file(os.devnull, 'w')
172 blackhole = file(os.devnull, 'w')
173 if self.no_stdout:
173 if self.no_stdout:
174 sys.stdout = sys.__stdout__ = blackhole
174 sys.stdout = sys.__stdout__ = blackhole
175 if self.no_stderr:
175 if self.no_stderr:
176 sys.stderr = sys.__stderr__ = blackhole
176 sys.stderr = sys.__stderr__ = blackhole
177
177
178 # Redirect input streams and set a display hook.
178 # Redirect input streams and set a display hook.
179
179
180 if self.outstream_class:
180 if self.outstream_class:
181 outstream_factory = import_item(str(self.outstream_class))
181 outstream_factory = import_item(str(self.outstream_class))
182 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
182 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
183 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
183 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
184 if self.displayhook_class:
184 if self.displayhook_class:
185 displayhook_factory = import_item(str(self.displayhook_class))
185 displayhook_factory = import_item(str(self.displayhook_class))
186 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
186 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
187
187
188 def init_kernel(self):
188 def init_kernel(self):
189 """Create the Kernel object itself"""
189 """Create the Kernel object itself"""
190 kernel_factory = import_item(str(self.kernel_class))
190 kernel_factory = import_item(str(self.kernel_class))
191 self.kernel = kernel_factory(config=self.config, session=self.session,
191 self.kernel = kernel_factory(config=self.config, session=self.session,
192 shell_socket=self.shell_socket,
192 shell_socket=self.shell_socket,
193 iopub_socket=self.iopub_socket,
193 iopub_socket=self.iopub_socket,
194 stdin_socket=self.stdin_socket,
194 stdin_socket=self.stdin_socket,
195 log=self.log
195 log=self.log
196 )
196 )
197 self.kernel.record_ports(self.ports)
197 self.kernel.record_ports(self.ports)
198
198
199 def initialize(self, argv=None):
199 def initialize(self, argv=None):
200 super(KernelApp, self).initialize(argv)
200 super(KernelApp, self).initialize(argv)
201 self.init_session()
201 self.init_session()
202 self.init_poller()
202 self.init_poller()
203 self.init_sockets()
203 self.init_sockets()
204 self.init_io()
204 self.init_io()
205 self.init_kernel()
205 self.init_kernel()
206
206
207 def start(self):
207 def start(self):
208 self.heartbeat.start()
208 self.heartbeat.start()
209 if self.poller is not None:
209 if self.poller is not None:
210 self.poller.start()
210 self.poller.start()
211 try:
211 try:
212 self.kernel.start()
212 self.kernel.start()
213 except KeyboardInterrupt:
213 except KeyboardInterrupt:
214 pass
214 pass
215
General Comments 0
You need to be logged in to leave comments. Login now