##// END OF EJS Templates
refactoring common code out of qtconsole
Paul Ivanov -
Show More
@@ -0,0 +1,366 b''
1 """ A minimal application base mixin for all ZMQ based IPython frontends.
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. This is a
5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
6
7 Authors:
8
9 * Evan Patterson
10 * Min RK
11 * Erik Tollerud
12 * Fernando Perez
13 * Bussonnier Matthias
14 * Thomas Kluyver
15 * Paul Ivanov
16
17 """
18
19 #-----------------------------------------------------------------------------
20 # Imports
21 #-----------------------------------------------------------------------------
22
23 # stdlib imports
24 import json
25 import os
26 import signal
27 import sys
28 import uuid
29
30
31 # Local imports
32 from IPython.config.application import boolean_flag
33 from IPython.config.configurable import Configurable
34 from IPython.core.profiledir import ProfileDir
35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
37 from IPython.utils.path import filefind
38 from IPython.utils.py3compat import str_to_bytes
39 from IPython.utils.traitlets import (
40 Dict, List, Unicode, CUnicode, Int, CBool, Any
41 )
42 from IPython.zmq.ipkernel import (
43 flags as ipkernel_flags,
44 aliases as ipkernel_aliases,
45 IPKernelApp
46 )
47 from IPython.zmq.session import Session, default_secure
48 from IPython.zmq.zmqshell import ZMQInteractiveShell
49
50 #-----------------------------------------------------------------------------
51 # Network Constants
52 #-----------------------------------------------------------------------------
53
54 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
55
56 #-----------------------------------------------------------------------------
57 # Globals
58 #-----------------------------------------------------------------------------
59
60
61 #-----------------------------------------------------------------------------
62 # Aliases and Flags
63 #-----------------------------------------------------------------------------
64
65 flags = dict(ipkernel_flags)
66 app_flags = {
67 'existing' : ({'IPythonMixinConsoleApp' : {'existing' : 'kernel*.json'}},
68 "Connect to an existing kernel. If no argument specified, guess most recent"),
69 'pure' : ({'IPythonMixinConsoleApp' : {'pure' : True}},
70 "Use a pure Python kernel instead of an IPython kernel."),
71 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
72 "Disable rich text support."),
73 }
74 app_flags.update(boolean_flag(
75 'confirm-exit', 'IPythonMixinConsoleApp.confirm_exit',
76 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
77 to force a direct exit without any confirmation.
78 """,
79 """Don't prompt the user when exiting. This will terminate the kernel
80 if it is owned by the frontend, and leave it alive if it is external.
81 """
82 ))
83 flags.update(app_flags)
84
85 aliases = dict(ipkernel_aliases)
86
87 app_aliases = dict(
88 hb = 'IPythonMixinConsoleApp.hb_port',
89 shell = 'IPythonMixinConsoleApp.shell_port',
90 iopub = 'IPythonMixinConsoleApp.iopub_port',
91 stdin = 'IPythonMixinConsoleApp.stdin_port',
92 ip = 'IPythonMixinConsoleApp.ip',
93 existing = 'IPythonMixinConsoleApp.existing',
94 f = 'IPythonMixinConsoleApp.connection_file',
95
96
97 ssh = 'IPythonMixinConsoleApp.sshserver',
98 )
99 aliases.update(app_aliases)
100
101 #-----------------------------------------------------------------------------
102 # Classes
103 #-----------------------------------------------------------------------------
104
105 #-----------------------------------------------------------------------------
106 # IPythonMixinConsole
107 #-----------------------------------------------------------------------------
108
109
110 class IPythonMixinConsoleApp(Configurable):
111 name = 'ipython-console-mixin'
112 default_config_file_name='ipython_config.py'
113
114 description = """
115 The IPython Mixin Console.
116
117 This class contains the common portions of console client (QtConsole,
118 ZMQ-based terminal console, etc). It is not a full console, in that
119 launched terminal subprocesses will not be able to accept input.
120
121 The Console using this mixing supports various extra features beyond
122 the single-process Terminal IPython shell, such as connecting to
123 existing kernel, via:
124
125 ipython <appname> --existing
126
127 as well as tunnel via SSH
128
129 """
130
131 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
132 flags = Dict(flags)
133 aliases = Dict(aliases)
134 kernel_manager_class = BlockingKernelManager
135
136 kernel_argv = List(Unicode)
137
138 # create requested profiles by default, if they don't exist:
139 auto_create = CBool(True)
140 # connection info:
141 ip = Unicode(LOCALHOST, config=True,
142 help="""Set the kernel\'s IP address [default localhost].
143 If the IP address is something other than localhost, then
144 Consoles on other machines will be able to connect
145 to the Kernel, so be careful!"""
146 )
147
148 sshserver = Unicode('', config=True,
149 help="""The SSH server to use to connect to the kernel.""")
150 sshkey = Unicode('', config=True,
151 help="""Path to the ssh key to use for logging in to the ssh server.""")
152
153 hb_port = Int(0, config=True,
154 help="set the heartbeat port [default: random]")
155 shell_port = Int(0, config=True,
156 help="set the shell (XREP) port [default: random]")
157 iopub_port = Int(0, config=True,
158 help="set the iopub (PUB) port [default: random]")
159 stdin_port = Int(0, config=True,
160 help="set the stdin (XREQ) port [default: random]")
161 connection_file = Unicode('', config=True,
162 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
163
164 This file will contain the IP, ports, and authentication key needed to connect
165 clients to this kernel. By default, this file will be created in the security-dir
166 of the current profile, but can be specified by absolute path.
167 """)
168 def _connection_file_default(self):
169 return 'kernel-%i.json' % os.getpid()
170
171 existing = CUnicode('', config=True,
172 help="""Connect to an already running kernel""")
173
174 confirm_exit = CBool(True, config=True,
175 help="""
176 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
177 to force a direct exit without any confirmation.""",
178 )
179
180
181 def parse_command_line(self, argv=None):
182 super(PythonBaseConsoleApp, self).parse_command_line(argv)
183 # make this stuff after this a function, in case the super stuff goes
184 # away. Also, Min notes that this functionality should be moved to a
185 # generic library of kernel stuff
186 if argv is None:
187 argv = sys.argv[1:]
188 self.kernel_argv = list(argv) # copy
189 # kernel should inherit default config file from frontend
190 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
191 # Scrub frontend-specific flags
192 swallow_next = False
193 was_flag = False
194 # copy again, in case some aliases have the same name as a flag
195 # argv = list(self.kernel_argv)
196 for a in argv:
197 if swallow_next:
198 swallow_next = False
199 # last arg was an alias, remove the next one
200 # *unless* the last alias has a no-arg flag version, in which
201 # case, don't swallow the next arg if it's also a flag:
202 if not (was_flag and a.startswith('-')):
203 self.kernel_argv.remove(a)
204 continue
205 if a.startswith('-'):
206 split = a.lstrip('-').split('=')
207 alias = split[0]
208 if alias in qt_aliases:
209 self.kernel_argv.remove(a)
210 if len(split) == 1:
211 # alias passed with arg via space
212 swallow_next = True
213 # could have been a flag that matches an alias, e.g. `existing`
214 # in which case, we might not swallow the next arg
215 was_flag = alias in qt_flags
216 elif alias in qt_flags:
217 # strip flag, but don't swallow next, as flags don't take args
218 self.kernel_argv.remove(a)
219
220 def init_connection_file(self):
221 """find the connection file, and load the info if found.
222
223 The current working directory and the current profile's security
224 directory will be searched for the file if it is not given by
225 absolute path.
226
227 When attempting to connect to an existing kernel and the `--existing`
228 argument does not match an existing file, it will be interpreted as a
229 fileglob, and the matching file in the current profile's security dir
230 with the latest access time will be used.
231 """
232 if self.existing:
233 try:
234 cf = find_connection_file(self.existing)
235 except Exception:
236 self.log.critical("Could not find existing kernel connection file %s", self.existing)
237 self.exit(1)
238 self.log.info("Connecting to existing kernel: %s" % cf)
239 self.connection_file = cf
240 # should load_connection_file only be used for existing?
241 # as it is now, this allows reusing ports if an existing
242 # file is requested
243 try:
244 self.load_connection_file()
245 except Exception:
246 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
247 self.exit(1)
248
249 def load_connection_file(self):
250 """load ip/port/hmac config from JSON connection file"""
251 # this is identical to KernelApp.load_connection_file
252 # perhaps it can be centralized somewhere?
253 try:
254 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
255 except IOError:
256 self.log.debug("Connection File not found: %s", self.connection_file)
257 return
258 self.log.debug(u"Loading connection file %s", fname)
259 with open(fname) as f:
260 s = f.read()
261 cfg = json.loads(s)
262 if self.ip == LOCALHOST and 'ip' in cfg:
263 # not overridden by config or cl_args
264 self.ip = cfg['ip']
265 for channel in ('hb', 'shell', 'iopub', 'stdin'):
266 name = channel + '_port'
267 if getattr(self, name) == 0 and name in cfg:
268 # not overridden by config or cl_args
269 setattr(self, name, cfg[name])
270 if 'key' in cfg:
271 self.config.Session.key = str_to_bytes(cfg['key'])
272
273 def init_ssh(self):
274 """set up ssh tunnels, if needed."""
275 if not self.sshserver and not self.sshkey:
276 return
277
278 if self.sshkey and not self.sshserver:
279 # specifying just the key implies that we are connecting directly
280 self.sshserver = self.ip
281 self.ip = LOCALHOST
282
283 # build connection dict for tunnels:
284 info = dict(ip=self.ip,
285 shell_port=self.shell_port,
286 iopub_port=self.iopub_port,
287 stdin_port=self.stdin_port,
288 hb_port=self.hb_port
289 )
290
291 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
292
293 # tunnels return a new set of ports, which will be on localhost:
294 self.ip = LOCALHOST
295 try:
296 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
297 except:
298 # even catch KeyboardInterrupt
299 self.log.error("Could not setup tunnels", exc_info=True)
300 self.exit(1)
301
302 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
303
304 cf = self.connection_file
305 base,ext = os.path.splitext(cf)
306 base = os.path.basename(base)
307 self.connection_file = os.path.basename(base)+'-ssh'+ext
308 self.log.critical("To connect another client via this tunnel, use:")
309 self.log.critical("--existing %s" % self.connection_file)
310
311 def _new_connection_file(self):
312 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
313
314 def init_kernel_manager(self):
315 # Don't let Qt or ZMQ swallow KeyboardInterupts.
316 signal.signal(signal.SIGINT, signal.SIG_DFL)
317 sec = self.profile_dir.security_dir
318 try:
319 cf = filefind(self.connection_file, ['.', sec])
320 except IOError:
321 # file might not exist
322 if self.connection_file == os.path.basename(self.connection_file):
323 # just shortname, put it in security dir
324 cf = os.path.join(sec, self.connection_file)
325 else:
326 cf = self.connection_file
327
328 # Create a KernelManager and start a kernel.
329 self.kernel_manager = self.kernel_manager_class(
330 ip=self.ip,
331 shell_port=self.shell_port,
332 iopub_port=self.iopub_port,
333 stdin_port=self.stdin_port,
334 hb_port=self.hb_port,
335 connection_file=cf,
336 config=self.config,
337 )
338 # start the kernel
339 if not self.existing:
340 kwargs = dict(ipython=not self.pure)
341 kwargs['extra_arguments'] = self.kernel_argv
342 self.kernel_manager.start_kernel(**kwargs)
343 elif self.sshserver:
344 # ssh, write new connection file
345 self.kernel_manager.write_connection_file()
346 self.kernel_manager.start_channels()
347
348
349 def initialize(self, argv=None):
350 super(IPythonMixinConsoleApp, self).initialize(argv)
351 self.init_connection_file()
352 default_secure(self.config)
353 self.init_ssh()
354 self.init_kernel_manager()
355 self.init_colors()
356
357 #-----------------------------------------------------------------------------
358 # Main entry point
359 #-----------------------------------------------------------------------------
360
361 def main():
362 raise NotImplementedError
363
364
365 if __name__ == '__main__':
366 main()
@@ -1,549 +1,371 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
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.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14
15
15 """
16 """
16
17
17 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
18 # Imports
19 # Imports
19 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
20
21
21 # stdlib imports
22 # stdlib imports
22 import json
23 import json
23 import os
24 import os
24 import signal
25 import signal
25 import sys
26 import sys
26 import uuid
27 import uuid
27
28
28 # System library imports
29 # System library imports
29 from IPython.external.qt import QtGui
30 from IPython.external.qt import QtGui
30
31
31 # Local imports
32 # Local imports
32 from IPython.config.application import boolean_flag, catch_config_error
33 from IPython.config.application import boolean_flag, catch_config_error
33 from IPython.core.application import BaseIPythonApplication
34 from IPython.core.application import BaseIPythonApplication
34 from IPython.core.profiledir import ProfileDir
35 from IPython.core.profiledir import ProfileDir
35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 from IPython.frontend.qt.console import styles
40 from IPython.frontend.qt.console import styles
40 from IPython.frontend.qt.console.mainwindow import MainWindow
41 from IPython.frontend.qt.console.mainwindow import MainWindow
41 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.traitlets import (
45 from IPython.utils.traitlets import (
45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 )
47 )
47 from IPython.zmq.ipkernel import (
48 from IPython.zmq.ipkernel import IPKernelApp
48 flags as ipkernel_flags,
49 aliases as ipkernel_aliases,
50 IPKernelApp
51 )
52 from IPython.zmq.session import Session, default_secure
49 from IPython.zmq.session import Session, default_secure
53 from IPython.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 from IPython.frontend.kernelmixinapp import (
52 IPythonMixinConsoleApp, app_aliases, app_flags
53 )
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Network Constants
56 # Network Constants
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Globals
62 # Globals
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 _examples = """
65 _examples = """
66 ipython qtconsole # start the qtconsole
66 ipython qtconsole # start the qtconsole
67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 """
68 """
69
69
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71 # Aliases and Flags
71 # Aliases and Flags
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73
73
74 flags = dict(ipkernel_flags)
74 flags = dict(app_flags)
75 qt_flags = {
75 qt_flags = {
76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
76 #'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
77 "Connect to an existing kernel. If no argument specified, guess most recent"),
77 # "Connect to an existing kernel. If no argument specified, guess most recent"),
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 "Use a pure Python kernel instead of an IPython kernel."),
79 "Use a pure Python kernel instead of an IPython kernel."),
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 "Disable rich text support."),
81 "Disable rich text support."),
82 }
82 }
83 qt_flags.update(boolean_flag(
83 qt_flags.update(boolean_flag(
84 'gui-completion', 'ConsoleWidget.gui_completion',
84 'gui-completion', 'ConsoleWidget.gui_completion',
85 "use a GUI widget for tab completion",
85 "use a GUI widget for tab completion",
86 "use plaintext output for completion"
86 "use plaintext output for completion"
87 ))
87 ))
88 qt_flags.update(boolean_flag(
89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
91 to force a direct exit without any confirmation.
92 """,
93 """Don't prompt the user when exiting. This will terminate the kernel
94 if it is owned by the frontend, and leave it alive if it is external.
95 """
96 ))
97 flags.update(qt_flags)
88 flags.update(qt_flags)
98
89
99 aliases = dict(ipkernel_aliases)
90 aliases = dict(app_aliases)
100
91
101 qt_aliases = dict(
92 qt_aliases = dict(
102 hb = 'IPythonQtConsoleApp.hb_port',
103 shell = 'IPythonQtConsoleApp.shell_port',
104 iopub = 'IPythonQtConsoleApp.iopub_port',
105 stdin = 'IPythonQtConsoleApp.stdin_port',
106 ip = 'IPythonQtConsoleApp.ip',
107 existing = 'IPythonQtConsoleApp.existing',
93 existing = 'IPythonQtConsoleApp.existing',
108 f = 'IPythonQtConsoleApp.connection_file',
94 f = 'IPythonQtConsoleApp.connection_file',
109
95
110 style = 'IPythonWidget.syntax_style',
96 style = 'IPythonWidget.syntax_style',
111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
97 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 colors = 'ZMQInteractiveShell.colors',
98 colors = 'ZMQInteractiveShell.colors',
113
99
114 editor = 'IPythonWidget.editor',
100 editor = 'IPythonWidget.editor',
115 paging = 'ConsoleWidget.paging',
101 paging = 'ConsoleWidget.paging',
116 ssh = 'IPythonQtConsoleApp.sshserver',
117 )
102 )
118 aliases.update(qt_aliases)
103 aliases.update(qt_aliases)
119
104
120 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
121 # Classes
106 # Classes
122 #-----------------------------------------------------------------------------
107 #-----------------------------------------------------------------------------
123
108
124 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
125 # IPythonQtConsole
110 # IPythonQtConsole
126 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
127
112
128
113
129 class IPythonQtConsoleApp(BaseIPythonApplication):
114 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonMixinConsoleApp):
130 name = 'ipython-qtconsole'
115 name = 'ipython-qtconsole'
131 default_config_file_name='ipython_config.py'
116 default_config_file_name='ipython_config.py'
132
117
133 description = """
118 description = """
134 The IPython QtConsole.
119 The IPython QtConsole.
135
120
136 This launches a Console-style application using Qt. It is not a full
121 This launches a Console-style application using Qt. It is not a full
137 console, in that launched terminal subprocesses will not be able to accept
122 console, in that launched terminal subprocesses will not be able to accept
138 input.
123 input.
139
124
140 The QtConsole supports various extra features beyond the Terminal IPython
125 The QtConsole supports various extra features beyond the Terminal IPython
141 shell, such as inline plotting with matplotlib, via:
126 shell, such as inline plotting with matplotlib, via:
142
127
143 ipython qtconsole --pylab=inline
128 ipython qtconsole --pylab=inline
144
129
145 as well as saving your session as HTML, and printing the output.
130 as well as saving your session as HTML, and printing the output.
146
131
147 """
132 """
148 examples = _examples
133 examples = _examples
149
134
150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
135 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
151 flags = Dict(flags)
136 flags = Dict(flags)
152 aliases = Dict(aliases)
137 aliases = Dict(aliases)
153
138 kernel_manager_class = QtKernelManager
154 kernel_argv = List(Unicode)
155
156 # create requested profiles by default, if they don't exist:
157 auto_create = CBool(True)
158 # connection info:
159 ip = Unicode(LOCALHOST, config=True,
160 help="""Set the kernel\'s IP address [default localhost].
161 If the IP address is something other than localhost, then
162 Consoles on other machines will be able to connect
163 to the Kernel, so be careful!"""
164 )
165
166 sshserver = Unicode('', config=True,
167 help="""The SSH server to use to connect to the kernel.""")
168 sshkey = Unicode('', config=True,
169 help="""Path to the ssh key to use for logging in to the ssh server.""")
170
171 hb_port = Integer(0, config=True,
172 help="set the heartbeat port [default: random]")
173 shell_port = Integer(0, config=True,
174 help="set the shell (XREP) port [default: random]")
175 iopub_port = Integer(0, config=True,
176 help="set the iopub (PUB) port [default: random]")
177 stdin_port = Integer(0, config=True,
178 help="set the stdin (XREQ) port [default: random]")
179 connection_file = Unicode('', config=True,
180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
181
182 This file will contain the IP, ports, and authentication key needed to connect
183 clients to this kernel. By default, this file will be created in the security-dir
184 of the current profile, but can be specified by absolute path.
185 """)
186 def _connection_file_default(self):
187 return 'kernel-%i.json' % os.getpid()
188
189 existing = Unicode('', config=True,
190 help="""Connect to an already running kernel""")
191
139
192 stylesheet = Unicode('', config=True,
140 stylesheet = Unicode('', config=True,
193 help="path to a custom CSS stylesheet")
141 help="path to a custom CSS stylesheet")
194
142
195 pure = CBool(False, config=True,
143 pure = CBool(False, config=True,
196 help="Use a pure Python kernel instead of an IPython kernel.")
144 help="Use a pure Python kernel instead of an IPython kernel.")
197 plain = CBool(False, config=True,
145 plain = CBool(False, config=True,
198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
146 help="Use a plaintext widget instead of rich text (plain can't print/save).")
199
147
200 def _pure_changed(self, name, old, new):
148 def _pure_changed(self, name, old, new):
201 kind = 'plain' if self.plain else 'rich'
149 kind = 'plain' if self.plain else 'rich'
202 self.config.ConsoleWidget.kind = kind
150 self.config.ConsoleWidget.kind = kind
203 if self.pure:
151 if self.pure:
204 self.widget_factory = FrontendWidget
152 self.widget_factory = FrontendWidget
205 elif self.plain:
153 elif self.plain:
206 self.widget_factory = IPythonWidget
154 self.widget_factory = IPythonWidget
207 else:
155 else:
208 self.widget_factory = RichIPythonWidget
156 self.widget_factory = RichIPythonWidget
209
157
210 _plain_changed = _pure_changed
158 _plain_changed = _pure_changed
211
159
212 confirm_exit = CBool(True, config=True,
160 confirm_exit = CBool(True, config=True,
213 help="""
161 help="""
214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
162 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
215 to force a direct exit without any confirmation.""",
163 to force a direct exit without any confirmation.""",
216 )
164 )
217
165
218 # the factory for creating a widget
166 # the factory for creating a widget
219 widget_factory = Any(RichIPythonWidget)
167 widget_factory = Any(RichIPythonWidget)
220
168
221 def parse_command_line(self, argv=None):
169 def parse_command_line(self, argv=None):
222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
170 super(IPythonQtConsoleApp, self).parse_command_line(argv)
223 if argv is None:
171 if argv is None:
224 argv = sys.argv[1:]
172 argv = sys.argv[1:]
225 self.kernel_argv = list(argv) # copy
173 self.kernel_argv = list(argv) # copy
226 # kernel should inherit default config file from frontend
174 # kernel should inherit default config file from frontend
227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
175 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
228 # Scrub frontend-specific flags
176 # Scrub frontend-specific flags
229 swallow_next = False
177 swallow_next = False
230 was_flag = False
178 was_flag = False
231 # copy again, in case some aliases have the same name as a flag
179 # copy again, in case some aliases have the same name as a flag
232 # argv = list(self.kernel_argv)
180 # argv = list(self.kernel_argv)
233 for a in argv:
181 for a in argv:
234 if swallow_next:
182 if swallow_next:
235 swallow_next = False
183 swallow_next = False
236 # last arg was an alias, remove the next one
184 # last arg was an alias, remove the next one
237 # *unless* the last alias has a no-arg flag version, in which
185 # *unless* the last alias has a no-arg flag version, in which
238 # case, don't swallow the next arg if it's also a flag:
186 # case, don't swallow the next arg if it's also a flag:
239 if not (was_flag and a.startswith('-')):
187 if not (was_flag and a.startswith('-')):
240 self.kernel_argv.remove(a)
188 self.kernel_argv.remove(a)
241 continue
189 continue
242 if a.startswith('-'):
190 if a.startswith('-'):
243 split = a.lstrip('-').split('=')
191 split = a.lstrip('-').split('=')
244 alias = split[0]
192 alias = split[0]
245 if alias in qt_aliases:
193 if alias in qt_aliases:
246 self.kernel_argv.remove(a)
194 self.kernel_argv.remove(a)
247 if len(split) == 1:
195 if len(split) == 1:
248 # alias passed with arg via space
196 # alias passed with arg via space
249 swallow_next = True
197 swallow_next = True
250 # could have been a flag that matches an alias, e.g. `existing`
198 # could have been a flag that matches an alias, e.g. `existing`
251 # in which case, we might not swallow the next arg
199 # in which case, we might not swallow the next arg
252 was_flag = alias in qt_flags
200 was_flag = alias in qt_flags
253 elif alias in qt_flags:
201 elif alias in qt_flags:
254 # strip flag, but don't swallow next, as flags don't take args
202 # strip flag, but don't swallow next, as flags don't take args
255 self.kernel_argv.remove(a)
203 self.kernel_argv.remove(a)
256
204
257 def init_connection_file(self):
258 """find the connection file, and load the info if found.
259
260 The current working directory and the current profile's security
261 directory will be searched for the file if it is not given by
262 absolute path.
263
264 When attempting to connect to an existing kernel and the `--existing`
265 argument does not match an existing file, it will be interpreted as a
266 fileglob, and the matching file in the current profile's security dir
267 with the latest access time will be used.
268 """
269 if self.existing:
270 try:
271 cf = find_connection_file(self.existing)
272 except Exception:
273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
274 self.exit(1)
275 self.log.info("Connecting to existing kernel: %s" % cf)
276 self.connection_file = cf
277 # should load_connection_file only be used for existing?
278 # as it is now, this allows reusing ports if an existing
279 # file is requested
280 try:
281 self.load_connection_file()
282 except Exception:
283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
284 self.exit(1)
285
286 def load_connection_file(self):
287 """load ip/port/hmac config from JSON connection file"""
288 # this is identical to KernelApp.load_connection_file
289 # perhaps it can be centralized somewhere?
290 try:
291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
292 except IOError:
293 self.log.debug("Connection File not found: %s", self.connection_file)
294 return
295 self.log.debug(u"Loading connection file %s", fname)
296 with open(fname) as f:
297 s = f.read()
298 cfg = json.loads(s)
299 if self.ip == LOCALHOST and 'ip' in cfg:
300 # not overridden by config or cl_args
301 self.ip = cfg['ip']
302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
303 name = channel + '_port'
304 if getattr(self, name) == 0 and name in cfg:
305 # not overridden by config or cl_args
306 setattr(self, name, cfg[name])
307 if 'key' in cfg:
308 self.config.Session.key = str_to_bytes(cfg['key'])
309
310 def init_ssh(self):
311 """set up ssh tunnels, if needed."""
312 if not self.sshserver and not self.sshkey:
313 return
314
315 if self.sshkey and not self.sshserver:
316 # specifying just the key implies that we are connecting directly
317 self.sshserver = self.ip
318 self.ip = LOCALHOST
319
320 # build connection dict for tunnels:
321 info = dict(ip=self.ip,
322 shell_port=self.shell_port,
323 iopub_port=self.iopub_port,
324 stdin_port=self.stdin_port,
325 hb_port=self.hb_port
326 )
327
328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
329
330 # tunnels return a new set of ports, which will be on localhost:
331 self.ip = LOCALHOST
332 try:
333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
334 except:
335 # even catch KeyboardInterrupt
336 self.log.error("Could not setup tunnels", exc_info=True)
337 self.exit(1)
338
339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
340
341 cf = self.connection_file
342 base,ext = os.path.splitext(cf)
343 base = os.path.basename(base)
344 self.connection_file = os.path.basename(base)+'-ssh'+ext
345 self.log.critical("To connect another client via this tunnel, use:")
346 self.log.critical("--existing %s" % self.connection_file)
347
348 def _new_connection_file(self):
349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
350
205
351 def init_kernel_manager(self):
352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
353 signal.signal(signal.SIGINT, signal.SIG_DFL)
354 sec = self.profile_dir.security_dir
355 try:
356 cf = filefind(self.connection_file, ['.', sec])
357 except IOError:
358 # file might not exist
359 if self.connection_file == os.path.basename(self.connection_file):
360 # just shortname, put it in security dir
361 cf = os.path.join(sec, self.connection_file)
362 else:
363 cf = self.connection_file
364
365 # Create a KernelManager and start a kernel.
366 self.kernel_manager = QtKernelManager(
367 ip=self.ip,
368 shell_port=self.shell_port,
369 iopub_port=self.iopub_port,
370 stdin_port=self.stdin_port,
371 hb_port=self.hb_port,
372 connection_file=cf,
373 config=self.config,
374 )
375 # start the kernel
376 if not self.existing:
377 kwargs = dict(ipython=not self.pure)
378 kwargs['extra_arguments'] = self.kernel_argv
379 self.kernel_manager.start_kernel(**kwargs)
380 elif self.sshserver:
381 # ssh, write new connection file
382 self.kernel_manager.write_connection_file()
383 self.kernel_manager.start_channels()
384
206
385 def new_frontend_master(self):
207 def new_frontend_master(self):
386 """ Create and return new frontend attached to new kernel, launched on localhost.
208 """ Create and return new frontend attached to new kernel, launched on localhost.
387 """
209 """
388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
210 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
389 kernel_manager = QtKernelManager(
211 kernel_manager = QtKernelManager(
390 ip=ip,
212 ip=ip,
391 connection_file=self._new_connection_file(),
213 connection_file=self._new_connection_file(),
392 config=self.config,
214 config=self.config,
393 )
215 )
394 # start the kernel
216 # start the kernel
395 kwargs = dict(ipython=not self.pure)
217 kwargs = dict(ipython=not self.pure)
396 kwargs['extra_arguments'] = self.kernel_argv
218 kwargs['extra_arguments'] = self.kernel_argv
397 kernel_manager.start_kernel(**kwargs)
219 kernel_manager.start_kernel(**kwargs)
398 kernel_manager.start_channels()
220 kernel_manager.start_channels()
399 widget = self.widget_factory(config=self.config,
221 widget = self.widget_factory(config=self.config,
400 local_kernel=True)
222 local_kernel=True)
401 widget.kernel_manager = kernel_manager
223 widget.kernel_manager = kernel_manager
402 widget._existing = False
224 widget._existing = False
403 widget._may_close = True
225 widget._may_close = True
404 widget._confirm_exit = self.confirm_exit
226 widget._confirm_exit = self.confirm_exit
405 return widget
227 return widget
406
228
407 def new_frontend_slave(self, current_widget):
229 def new_frontend_slave(self, current_widget):
408 """Create and return a new frontend attached to an existing kernel.
230 """Create and return a new frontend attached to an existing kernel.
409
231
410 Parameters
232 Parameters
411 ----------
233 ----------
412 current_widget : IPythonWidget
234 current_widget : IPythonWidget
413 The IPythonWidget whose kernel this frontend is to share
235 The IPythonWidget whose kernel this frontend is to share
414 """
236 """
415 kernel_manager = QtKernelManager(
237 kernel_manager = QtKernelManager(
416 connection_file=current_widget.kernel_manager.connection_file,
238 connection_file=current_widget.kernel_manager.connection_file,
417 config = self.config,
239 config = self.config,
418 )
240 )
419 kernel_manager.load_connection_file()
241 kernel_manager.load_connection_file()
420 kernel_manager.start_channels()
242 kernel_manager.start_channels()
421 widget = self.widget_factory(config=self.config,
243 widget = self.widget_factory(config=self.config,
422 local_kernel=False)
244 local_kernel=False)
423 widget._existing = True
245 widget._existing = True
424 widget._may_close = False
246 widget._may_close = False
425 widget._confirm_exit = False
247 widget._confirm_exit = False
426 widget.kernel_manager = kernel_manager
248 widget.kernel_manager = kernel_manager
427 return widget
249 return widget
428
250
429 def init_qt_elements(self):
251 def init_qt_elements(self):
430 # Create the widget.
252 # Create the widget.
431 self.app = QtGui.QApplication([])
253 self.app = QtGui.QApplication([])
432
254
433 base_path = os.path.abspath(os.path.dirname(__file__))
255 base_path = os.path.abspath(os.path.dirname(__file__))
434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
256 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
435 self.app.icon = QtGui.QIcon(icon_path)
257 self.app.icon = QtGui.QIcon(icon_path)
436 QtGui.QApplication.setWindowIcon(self.app.icon)
258 QtGui.QApplication.setWindowIcon(self.app.icon)
437
259
438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
260 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
439 self.widget = self.widget_factory(config=self.config,
261 self.widget = self.widget_factory(config=self.config,
440 local_kernel=local_kernel)
262 local_kernel=local_kernel)
441 self.widget._existing = self.existing
263 self.widget._existing = self.existing
442 self.widget._may_close = not self.existing
264 self.widget._may_close = not self.existing
443 self.widget._confirm_exit = self.confirm_exit
265 self.widget._confirm_exit = self.confirm_exit
444
266
445 self.widget.kernel_manager = self.kernel_manager
267 self.widget.kernel_manager = self.kernel_manager
446 self.window = MainWindow(self.app,
268 self.window = MainWindow(self.app,
447 confirm_exit=self.confirm_exit,
269 confirm_exit=self.confirm_exit,
448 new_frontend_factory=self.new_frontend_master,
270 new_frontend_factory=self.new_frontend_master,
449 slave_frontend_factory=self.new_frontend_slave,
271 slave_frontend_factory=self.new_frontend_slave,
450 )
272 )
451 self.window.log = self.log
273 self.window.log = self.log
452 self.window.add_tab_with_frontend(self.widget)
274 self.window.add_tab_with_frontend(self.widget)
453 self.window.init_menu_bar()
275 self.window.init_menu_bar()
454
276
455 self.window.setWindowTitle('Python' if self.pure else 'IPython')
277 self.window.setWindowTitle('Python' if self.pure else 'IPython')
456
278
457 def init_colors(self):
279 def init_colors(self):
458 """Configure the coloring of the widget"""
280 """Configure the coloring of the widget"""
459 # Note: This will be dramatically simplified when colors
281 # Note: This will be dramatically simplified when colors
460 # are removed from the backend.
282 # are removed from the backend.
461
283
462 if self.pure:
284 if self.pure:
463 # only IPythonWidget supports styling
285 # only IPythonWidget supports styling
464 return
286 return
465
287
466 # parse the colors arg down to current known labels
288 # parse the colors arg down to current known labels
467 try:
289 try:
468 colors = self.config.ZMQInteractiveShell.colors
290 colors = self.config.ZMQInteractiveShell.colors
469 except AttributeError:
291 except AttributeError:
470 colors = None
292 colors = None
471 try:
293 try:
472 style = self.config.IPythonWidget.syntax_style
294 style = self.config.IPythonWidget.syntax_style
473 except AttributeError:
295 except AttributeError:
474 style = None
296 style = None
475
297
476 # find the value for colors:
298 # find the value for colors:
477 if colors:
299 if colors:
478 colors=colors.lower()
300 colors=colors.lower()
479 if colors in ('lightbg', 'light'):
301 if colors in ('lightbg', 'light'):
480 colors='lightbg'
302 colors='lightbg'
481 elif colors in ('dark', 'linux'):
303 elif colors in ('dark', 'linux'):
482 colors='linux'
304 colors='linux'
483 else:
305 else:
484 colors='nocolor'
306 colors='nocolor'
485 elif style:
307 elif style:
486 if style=='bw':
308 if style=='bw':
487 colors='nocolor'
309 colors='nocolor'
488 elif styles.dark_style(style):
310 elif styles.dark_style(style):
489 colors='linux'
311 colors='linux'
490 else:
312 else:
491 colors='lightbg'
313 colors='lightbg'
492 else:
314 else:
493 colors=None
315 colors=None
494
316
495 # Configure the style.
317 # Configure the style.
496 widget = self.widget
318 widget = self.widget
497 if style:
319 if style:
498 widget.style_sheet = styles.sheet_from_template(style, colors)
320 widget.style_sheet = styles.sheet_from_template(style, colors)
499 widget.syntax_style = style
321 widget.syntax_style = style
500 widget._syntax_style_changed()
322 widget._syntax_style_changed()
501 widget._style_sheet_changed()
323 widget._style_sheet_changed()
502 elif colors:
324 elif colors:
503 # use a default style
325 # use a default style
504 widget.set_default_style(colors=colors)
326 widget.set_default_style(colors=colors)
505 else:
327 else:
506 # this is redundant for now, but allows the widget's
328 # this is redundant for now, but allows the widget's
507 # defaults to change
329 # defaults to change
508 widget.set_default_style()
330 widget.set_default_style()
509
331
510 if self.stylesheet:
332 if self.stylesheet:
511 # we got an expicit stylesheet
333 # we got an expicit stylesheet
512 if os.path.isfile(self.stylesheet):
334 if os.path.isfile(self.stylesheet):
513 with open(self.stylesheet) as f:
335 with open(self.stylesheet) as f:
514 sheet = f.read()
336 sheet = f.read()
515 widget.style_sheet = sheet
337 widget.style_sheet = sheet
516 widget._style_sheet_changed()
338 widget._style_sheet_changed()
517 else:
339 else:
518 raise IOError("Stylesheet %r not found."%self.stylesheet)
340 raise IOError("Stylesheet %r not found."%self.stylesheet)
519
341
520 @catch_config_error
342 @catch_config_error
521 def initialize(self, argv=None):
343 def initialize(self, argv=None):
522 super(IPythonQtConsoleApp, self).initialize(argv)
344 super(IPythonQtConsoleApp, self).initialize(argv)
523 self.init_connection_file()
345 self.init_connection_file()
524 default_secure(self.config)
346 default_secure(self.config)
525 self.init_ssh()
347 self.init_ssh()
526 self.init_kernel_manager()
348 self.init_kernel_manager()
527 self.init_qt_elements()
349 self.init_qt_elements()
528 self.init_colors()
350 self.init_colors()
529
351
530 def start(self):
352 def start(self):
531
353
532 # draw the window
354 # draw the window
533 self.window.show()
355 self.window.show()
534
356
535 # Start the application main loop.
357 # Start the application main loop.
536 self.app.exec_()
358 self.app.exec_()
537
359
538 #-----------------------------------------------------------------------------
360 #-----------------------------------------------------------------------------
539 # Main entry point
361 # Main entry point
540 #-----------------------------------------------------------------------------
362 #-----------------------------------------------------------------------------
541
363
542 def main():
364 def main():
543 app = IPythonQtConsoleApp()
365 app = IPythonQtConsoleApp()
544 app.initialize()
366 app.initialize()
545 app.start()
367 app.start()
546
368
547
369
548 if __name__ == '__main__':
370 if __name__ == '__main__':
549 main()
371 main()
General Comments 0
You need to be logged in to leave comments. Login now