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