##// END OF EJS Templates
ip/transport live in KernelManager now...
MinRK -
Show More
@@ -1,378 +1,361 b''
1 1 """ A minimal application base mixin for all ZMQ based IPython frontends.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations. This is a
5 5 refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py
6 6
7 7 Authors:
8 8
9 9 * Evan Patterson
10 10 * Min RK
11 11 * Erik Tollerud
12 12 * Fernando Perez
13 13 * Bussonnier Matthias
14 14 * Thomas Kluyver
15 15 * Paul Ivanov
16 16
17 17 """
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 # stdlib imports
24 24 import atexit
25 25 import json
26 26 import os
27 27 import shutil
28 28 import signal
29 29 import sys
30 30 import uuid
31 31
32 32
33 33 # Local imports
34 34 from IPython.config.application import boolean_flag
35 35 from IPython.config.configurable import Configurable
36 36 from IPython.core.profiledir import ProfileDir
37 37 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
38 38 from IPython.zmq.blockingkernelmanager import BlockingKernelManager
39 39 from IPython.utils.path import filefind
40 40 from IPython.utils.py3compat import str_to_bytes
41 41 from IPython.utils.traitlets import (
42 42 Dict, List, Unicode, CUnicode, Int, CBool, Any, CaselessStrEnum
43 43 )
44 44 from IPython.zmq.ipkernel import (
45 45 flags as ipkernel_flags,
46 46 aliases as ipkernel_aliases,
47 47 IPKernelApp
48 48 )
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52 #-----------------------------------------------------------------------------
53 53 # Network Constants
54 54 #-----------------------------------------------------------------------------
55 55
56 56 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
57 57
58 58 #-----------------------------------------------------------------------------
59 59 # Globals
60 60 #-----------------------------------------------------------------------------
61 61
62 62
63 63 #-----------------------------------------------------------------------------
64 64 # Aliases and Flags
65 65 #-----------------------------------------------------------------------------
66 66
67 67 flags = dict(ipkernel_flags)
68 68
69 69 # the flags that are specific to the frontend
70 70 # these must be scrubbed before being passed to the kernel,
71 71 # or it will raise an error on unrecognized flags
72 72 app_flags = {
73 73 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
74 74 "Connect to an existing kernel. If no argument specified, guess most recent"),
75 75 }
76 76 app_flags.update(boolean_flag(
77 77 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
78 78 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
79 79 to force a direct exit without any confirmation.
80 80 """,
81 81 """Don't prompt the user when exiting. This will terminate the kernel
82 82 if it is owned by the frontend, and leave it alive if it is external.
83 83 """
84 84 ))
85 85 flags.update(app_flags)
86 86
87 87 aliases = dict(ipkernel_aliases)
88 88
89 89 # also scrub aliases from the frontend
90 90 app_aliases = dict(
91 ip = 'KernelManager.ip',
92 transport = 'KernelManager.transport',
91 93 hb = 'IPythonConsoleApp.hb_port',
92 94 shell = 'IPythonConsoleApp.shell_port',
93 95 iopub = 'IPythonConsoleApp.iopub_port',
94 96 stdin = 'IPythonConsoleApp.stdin_port',
95 ip = 'IPythonConsoleApp.ip',
96 97 existing = 'IPythonConsoleApp.existing',
97 98 f = 'IPythonConsoleApp.connection_file',
98 99
99 100
100 101 ssh = 'IPythonConsoleApp.sshserver',
101 102 )
102 103 aliases.update(app_aliases)
103 104
104 105 #-----------------------------------------------------------------------------
105 106 # Classes
106 107 #-----------------------------------------------------------------------------
107 108
108 109 #-----------------------------------------------------------------------------
109 110 # IPythonConsole
110 111 #-----------------------------------------------------------------------------
111 112
112 113 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session]
113 114
114 115 try:
115 116 from IPython.zmq.pylab.backend_inline import InlineBackend
116 117 except ImportError:
117 118 pass
118 119 else:
119 120 classes.append(InlineBackend)
120 121
121 122 class IPythonConsoleApp(Configurable):
122 123 name = 'ipython-console-mixin'
123 124 default_config_file_name='ipython_config.py'
124 125
125 126 description = """
126 127 The IPython Mixin Console.
127 128
128 129 This class contains the common portions of console client (QtConsole,
129 130 ZMQ-based terminal console, etc). It is not a full console, in that
130 131 launched terminal subprocesses will not be able to accept input.
131 132
132 133 The Console using this mixing supports various extra features beyond
133 134 the single-process Terminal IPython shell, such as connecting to
134 135 existing kernel, via:
135 136
136 137 ipython <appname> --existing
137 138
138 139 as well as tunnel via SSH
139 140
140 141 """
141 142
142 143 classes = classes
143 144 flags = Dict(flags)
144 145 aliases = Dict(aliases)
145 146 kernel_manager_class = BlockingKernelManager
146 147
147 148 kernel_argv = List(Unicode)
148 149 # frontend flags&aliases to be stripped when building kernel_argv
149 150 frontend_flags = Any(app_flags)
150 151 frontend_aliases = Any(app_aliases)
151 152
152 153 # create requested profiles by default, if they don't exist:
153 154 auto_create = CBool(True)
154 155 # connection info:
155 156
156 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
157
158 ip = Unicode(config=True,
159 help="""Set the kernel\'s IP address [default localhost].
160 If the IP address is something other than localhost, then
161 Consoles on other machines will be able to connect
162 to the Kernel, so be careful!"""
163 )
164 def _ip_default(self):
165 if self.transport == 'tcp':
166 return LOCALHOST
167 else:
168 # this can fire early if ip is given,
169 # in which case our return value is meaningless
170 if not hasattr(self, 'profile_dir'):
171 return ''
172 ipcdir = os.path.join(self.profile_dir.security_dir, 'kernel-%s' % os.getpid())
173 os.makedirs(ipcdir)
174 atexit.register(lambda : shutil.rmtree(ipcdir))
175 return os.path.join(ipcdir, 'ipc')
176
177 157 sshserver = Unicode('', config=True,
178 158 help="""The SSH server to use to connect to the kernel.""")
179 159 sshkey = Unicode('', config=True,
180 160 help="""Path to the ssh key to use for logging in to the ssh server.""")
181 161
182 162 hb_port = Int(0, config=True,
183 163 help="set the heartbeat port [default: random]")
184 164 shell_port = Int(0, config=True,
185 165 help="set the shell (ROUTER) port [default: random]")
186 166 iopub_port = Int(0, config=True,
187 167 help="set the iopub (PUB) port [default: random]")
188 168 stdin_port = Int(0, config=True,
189 169 help="set the stdin (DEALER) port [default: random]")
190 170 connection_file = Unicode('', config=True,
191 171 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
192 172
193 173 This file will contain the IP, ports, and authentication key needed to connect
194 174 clients to this kernel. By default, this file will be created in the security-dir
195 175 of the current profile, but can be specified by absolute path.
196 176 """)
197 177 def _connection_file_default(self):
198 178 return 'kernel-%i.json' % os.getpid()
199 179
200 180 existing = CUnicode('', config=True,
201 181 help="""Connect to an already running kernel""")
202 182
203 183 confirm_exit = CBool(True, config=True,
204 184 help="""
205 185 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
206 186 to force a direct exit without any confirmation.""",
207 187 )
208 188
209 189
210 190 def build_kernel_argv(self, argv=None):
211 191 """build argv to be passed to kernel subprocess"""
212 192 if argv is None:
213 193 argv = sys.argv[1:]
214 194 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
215 195 # kernel should inherit default config file from frontend
216 196 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
217 197
218 198 def init_connection_file(self):
219 199 """find the connection file, and load the info if found.
220 200
221 201 The current working directory and the current profile's security
222 202 directory will be searched for the file if it is not given by
223 203 absolute path.
224 204
225 205 When attempting to connect to an existing kernel and the `--existing`
226 206 argument does not match an existing file, it will be interpreted as a
227 207 fileglob, and the matching file in the current profile's security dir
228 208 with the latest access time will be used.
229 209
230 210 After this method is called, self.connection_file contains the *full path*
231 211 to the connection file, never just its name.
232 212 """
233 213 if self.existing:
234 214 try:
235 215 cf = find_connection_file(self.existing)
236 216 except Exception:
237 217 self.log.critical("Could not find existing kernel connection file %s", self.existing)
238 218 self.exit(1)
239 219 self.log.info("Connecting to existing kernel: %s" % cf)
240 220 self.connection_file = cf
241 221 else:
242 222 # not existing, check if we are going to write the file
243 223 # and ensure that self.connection_file is a full path, not just the shortname
244 224 try:
245 225 cf = find_connection_file(self.connection_file)
246 226 except Exception:
247 227 # file might not exist
248 228 if self.connection_file == os.path.basename(self.connection_file):
249 229 # just shortname, put it in security dir
250 230 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
251 231 else:
252 232 cf = self.connection_file
253 233 self.connection_file = cf
254 234
255 235 # should load_connection_file only be used for existing?
256 236 # as it is now, this allows reusing ports if an existing
257 237 # file is requested
258 238 try:
259 239 self.load_connection_file()
260 240 except Exception:
261 241 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
262 242 self.exit(1)
263 243
264 244 def load_connection_file(self):
265 245 """load ip/port/hmac config from JSON connection file"""
266 246 # this is identical to KernelApp.load_connection_file
267 247 # perhaps it can be centralized somewhere?
268 248 try:
269 249 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
270 250 except IOError:
271 251 self.log.debug("Connection File not found: %s", self.connection_file)
272 252 return
273 253 self.log.debug(u"Loading connection file %s", fname)
274 254 with open(fname) as f:
275 255 cfg = json.load(f)
276 256
277 self.transport = cfg.get('transport', 'tcp')
278 if 'ip' in cfg:
279 self.ip = cfg['ip']
257 self.config.KernelManager.transport = cfg.get('transport', 'tcp')
258 self.config.KernelManager.ip = cfg.get('ip', LOCALHOST)
259
280 260 for channel in ('hb', 'shell', 'iopub', 'stdin'):
281 261 name = channel + '_port'
282 262 if getattr(self, name) == 0 and name in cfg:
283 263 # not overridden by config or cl_args
284 264 setattr(self, name, cfg[name])
285 265 if 'key' in cfg:
286 266 self.config.Session.key = str_to_bytes(cfg['key'])
287
288 267
289 268 def init_ssh(self):
290 269 """set up ssh tunnels, if needed."""
291 if not self.sshserver and not self.sshkey:
270 if not self.existing or (not self.sshserver and not self.sshkey):
292 271 return
293 272
294 if self.transport != 'tcp':
295 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", self.transport)
296 return
273 self.load_connection_file()
274
275 transport = self.config.KernelManager.transport
276 ip = self.config.KernelManager.ip
277
278 if transport != 'tcp':
279 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
280 sys.exit(-1)
297 281
298 282 if self.sshkey and not self.sshserver:
299 283 # specifying just the key implies that we are connecting directly
300 self.sshserver = self.ip
301 self.ip = LOCALHOST
284 self.sshserver = ip
285 ip = LOCALHOST
302 286
303 287 # build connection dict for tunnels:
304 info = dict(ip=self.ip,
288 info = dict(ip=ip,
305 289 shell_port=self.shell_port,
306 290 iopub_port=self.iopub_port,
307 291 stdin_port=self.stdin_port,
308 292 hb_port=self.hb_port
309 293 )
310 294
311 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
295 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
312 296
313 297 # tunnels return a new set of ports, which will be on localhost:
314 self.ip = LOCALHOST
298 self.config.KernelManager.ip = LOCALHOST
315 299 try:
316 300 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
317 301 except:
318 302 # even catch KeyboardInterrupt
319 303 self.log.error("Could not setup tunnels", exc_info=True)
320 304 self.exit(1)
321 305
322 306 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
323 307
324 308 cf = self.connection_file
325 309 base,ext = os.path.splitext(cf)
326 310 base = os.path.basename(base)
327 311 self.connection_file = os.path.basename(base)+'-ssh'+ext
328 312 self.log.critical("To connect another client via this tunnel, use:")
329 313 self.log.critical("--existing %s" % self.connection_file)
330 314
331 315 def _new_connection_file(self):
332 316 cf = ''
333 317 while not cf:
334 318 # we don't need a 128b id to distinguish kernels, use more readable
335 319 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
336 320 # kernels can subclass.
337 321 ident = str(uuid.uuid4()).split('-')[-1]
338 322 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
339 323 # only keep if it's actually new. Protect against unlikely collision
340 324 # in 48b random search space
341 325 cf = cf if not os.path.exists(cf) else ''
342 326 return cf
343 327
344 328 def init_kernel_manager(self):
345 329 # Don't let Qt or ZMQ swallow KeyboardInterupts.
346 330 signal.signal(signal.SIGINT, signal.SIG_DFL)
347 331
348 332 # Create a KernelManager and start a kernel.
349 333 self.kernel_manager = self.kernel_manager_class(
350 transport=self.transport,
351 ip=self.ip,
352 334 shell_port=self.shell_port,
353 335 iopub_port=self.iopub_port,
354 336 stdin_port=self.stdin_port,
355 337 hb_port=self.hb_port,
356 338 connection_file=self.connection_file,
357 339 config=self.config,
358 340 )
359 341 # start the kernel
360 342 if not self.existing:
361 343 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
344 atexit.register(self.kernel_manager.cleanup_ipc_files)
362 345 elif self.sshserver:
363 346 # ssh, write new connection file
364 347 self.kernel_manager.write_connection_file()
365 348 atexit.register(self.kernel_manager.cleanup_connection_file)
366 349 self.kernel_manager.start_channels()
367 350
368 351
369 352 def initialize(self, argv=None):
370 353 """
371 354 Classes which mix this class in should call:
372 355 IPythonConsoleApp.initialize(self,argv)
373 356 """
374 357 self.init_connection_file()
375 358 default_secure(self.config)
376 359 self.init_ssh()
377 360 self.init_kernel_manager()
378 361
@@ -1,644 +1,645 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-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, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import uuid
32 32 import webbrowser
33 33
34 34 # Third party
35 35 import zmq
36 36 from jinja2 import Environment, FileSystemLoader
37 37
38 38 # Install the pyzmq ioloop. This has to be done before anything else from
39 39 # tornado is imported.
40 40 from zmq.eventloop import ioloop
41 41 ioloop.install()
42 42
43 43 from tornado import httpserver
44 44 from tornado import web
45 45
46 46 # Our own libraries
47 47 from .kernelmanager import MappingKernelManager
48 48 from .handlers import (LoginHandler, LogoutHandler,
49 49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 54 FileFindHandler,
55 55 )
56 56 from .nbmanager import NotebookManager
57 57 from .filenbmanager import FileNotebookManager
58 58 from .clustermanager import ClusterManager
59 59
60 60 from IPython.config.application import catch_config_error, boolean_flag
61 61 from IPython.core.application import BaseIPythonApplication
62 62 from IPython.core.profiledir import ProfileDir
63 63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 64 from IPython.lib.kernel import swallow_argv
65 65 from IPython.zmq.session import Session, default_secure
66 66 from IPython.zmq.zmqshell import ZMQInteractiveShell
67 67 from IPython.zmq.ipkernel import (
68 68 flags as ipkernel_flags,
69 69 aliases as ipkernel_aliases,
70 70 IPKernelApp
71 71 )
72 72 from IPython.utils.importstring import import_item
73 73 from IPython.utils.traitlets import (
74 74 Dict, Unicode, Integer, List, Enum, Bool,
75 75 DottedObjectName
76 76 )
77 77 from IPython.utils import py3compat
78 78 from IPython.utils.path import filefind
79 79
80 80 #-----------------------------------------------------------------------------
81 81 # Module globals
82 82 #-----------------------------------------------------------------------------
83 83
84 84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
85 85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
86 86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
87 87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
88 88 _cluster_action_regex = r"(?P<action>start|stop)"
89 89
90 90
91 91 LOCALHOST = '127.0.0.1'
92 92
93 93 _examples = """
94 94 ipython notebook # start the notebook
95 95 ipython notebook --profile=sympy # use the sympy profile
96 96 ipython notebook --pylab=inline # pylab in inline plotting mode
97 97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
99 99 """
100 100
101 101 #-----------------------------------------------------------------------------
102 102 # Helper functions
103 103 #-----------------------------------------------------------------------------
104 104
105 105 def url_path_join(a,b):
106 106 if a.endswith('/') and b.startswith('/'):
107 107 return a[:-1]+b
108 108 else:
109 109 return a+b
110 110
111 111 def random_ports(port, n):
112 112 """Generate a list of n random ports near the given port.
113 113
114 114 The first 5 ports will be sequential, and the remaining n-5 will be
115 115 randomly selected in the range [port-2*n, port+2*n].
116 116 """
117 117 for i in range(min(5, n)):
118 118 yield port + i
119 119 for i in range(n-5):
120 120 yield port + random.randint(-2*n, 2*n)
121 121
122 122 #-----------------------------------------------------------------------------
123 123 # The Tornado web application
124 124 #-----------------------------------------------------------------------------
125 125
126 126 class NotebookWebApplication(web.Application):
127 127
128 128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
129 129 cluster_manager, log,
130 130 base_project_url, settings_overrides):
131 131 handlers = [
132 132 (r"/", ProjectDashboardHandler),
133 133 (r"/login", LoginHandler),
134 134 (r"/logout", LogoutHandler),
135 135 (r"/new", NewHandler),
136 136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
137 137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
138 138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
139 139 (r"/kernels", MainKernelHandler),
140 140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
141 141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
142 142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
143 143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
144 144 (r"/notebooks", NotebookRootHandler),
145 145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
146 146 (r"/rstservice/render", RSTHandler),
147 147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
148 148 (r"/clusters", MainClusterHandler),
149 149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
150 150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
151 151 ]
152 152
153 153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 154 # base_project_url will always be unicode, which will in turn
155 155 # make the patterns unicode, and ultimately result in unicode
156 156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 157 # This enforces that base_project_url be ascii in that situation.
158 158 #
159 159 # Note that the URLs these patterns check against are escaped,
160 160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162 162
163 163 settings = dict(
164 164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
165 165 static_path=ipython_app.static_file_path,
166 166 static_handler_class = FileFindHandler,
167 167 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 168 cookie_secret=os.urandom(1024),
169 169 login_url=url_path_join(base_project_url,'/login'),
170 170 cookie_name='username-%s' % uuid.uuid4(),
171 171 )
172 172
173 173 # allow custom overrides for the tornado web app.
174 174 settings.update(settings_overrides)
175 175
176 176 # prepend base_project_url onto the patterns that we match
177 177 new_handlers = []
178 178 for handler in handlers:
179 179 pattern = url_path_join(base_project_url, handler[0])
180 180 new_handler = tuple([pattern]+list(handler[1:]))
181 181 new_handlers.append( new_handler )
182 182
183 183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
184 184
185 185 self.kernel_manager = kernel_manager
186 186 self.notebook_manager = notebook_manager
187 187 self.cluster_manager = cluster_manager
188 188 self.ipython_app = ipython_app
189 189 self.read_only = self.ipython_app.read_only
190 190 self.config = self.ipython_app.config
191 191 self.log = log
192 192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
193 193
194 194
195 195
196 196 #-----------------------------------------------------------------------------
197 197 # Aliases and Flags
198 198 #-----------------------------------------------------------------------------
199 199
200 200 flags = dict(ipkernel_flags)
201 201 flags['no-browser']=(
202 202 {'NotebookApp' : {'open_browser' : False}},
203 203 "Don't open the notebook in a browser after startup."
204 204 )
205 205 flags['no-mathjax']=(
206 206 {'NotebookApp' : {'enable_mathjax' : False}},
207 207 """Disable MathJax
208 208
209 209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
210 210 very large, so you may want to disable it if you have a slow internet
211 211 connection, or for offline use of the notebook.
212 212
213 213 When disabled, equations etc. will appear as their untransformed TeX source.
214 214 """
215 215 )
216 216 flags['read-only'] = (
217 217 {'NotebookApp' : {'read_only' : True}},
218 218 """Allow read-only access to notebooks.
219 219
220 220 When using a password to protect the notebook server, this flag
221 221 allows unauthenticated clients to view the notebook list, and
222 222 individual notebooks, but not edit them, start kernels, or run
223 223 code.
224 224
225 225 If no password is set, the server will be entirely read-only.
226 226 """
227 227 )
228 228
229 229 # Add notebook manager flags
230 230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
231 231 'Auto-save a .py script everytime the .ipynb notebook is saved',
232 232 'Do not auto-save .py scripts for every notebook'))
233 233
234 234 # the flags that are specific to the frontend
235 235 # these must be scrubbed before being passed to the kernel,
236 236 # or it will raise an error on unrecognized flags
237 237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238 238
239 239 aliases = dict(ipkernel_aliases)
240 240
241 241 aliases.update({
242 242 'ip': 'NotebookApp.ip',
243 243 'port': 'NotebookApp.port',
244 244 'port-retries': 'NotebookApp.port_retries',
245 'transport': 'KernelManager.transport',
245 246 'keyfile': 'NotebookApp.keyfile',
246 247 'certfile': 'NotebookApp.certfile',
247 248 'notebook-dir': 'NotebookManager.notebook_dir',
248 249 'browser': 'NotebookApp.browser',
249 250 })
250 251
251 252 # remove ipkernel flags that are singletons, and don't make sense in
252 253 # multi-kernel evironment:
253 254 aliases.pop('f', None)
254 255
255 256 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 257 u'notebook-dir']
257 258
258 259 #-----------------------------------------------------------------------------
259 260 # NotebookApp
260 261 #-----------------------------------------------------------------------------
261 262
262 263 class NotebookApp(BaseIPythonApplication):
263 264
264 265 name = 'ipython-notebook'
265 266 default_config_file_name='ipython_notebook_config.py'
266 267
267 268 description = """
268 269 The IPython HTML Notebook.
269 270
270 271 This launches a Tornado based HTML Notebook Server that serves up an
271 272 HTML5/Javascript Notebook client.
272 273 """
273 274 examples = _examples
274 275
275 276 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
276 277 FileNotebookManager]
277 278 flags = Dict(flags)
278 279 aliases = Dict(aliases)
279 280
280 281 kernel_argv = List(Unicode)
281 282
282 283 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
283 284 default_value=logging.INFO,
284 285 config=True,
285 286 help="Set the log level by value or name.")
286 287
287 288 # create requested profiles by default, if they don't exist:
288 289 auto_create = Bool(True)
289 290
290 291 # file to be opened in the notebook server
291 292 file_to_run = Unicode('')
292 293
293 294 # Network related information.
294 295
295 296 ip = Unicode(LOCALHOST, config=True,
296 297 help="The IP address the notebook server will listen on."
297 298 )
298 299
299 300 def _ip_changed(self, name, old, new):
300 301 if new == u'*': self.ip = u''
301 302
302 303 port = Integer(8888, config=True,
303 304 help="The port the notebook server will listen on."
304 305 )
305 306 port_retries = Integer(50, config=True,
306 307 help="The number of additional ports to try if the specified port is not available."
307 308 )
308 309
309 310 certfile = Unicode(u'', config=True,
310 311 help="""The full path to an SSL/TLS certificate file."""
311 312 )
312 313
313 314 keyfile = Unicode(u'', config=True,
314 315 help="""The full path to a private key file for usage with SSL/TLS."""
315 316 )
316 317
317 318 password = Unicode(u'', config=True,
318 319 help="""Hashed password to use for web authentication.
319 320
320 321 To generate, type in a python/IPython shell:
321 322
322 323 from IPython.lib import passwd; passwd()
323 324
324 325 The string should be of the form type:salt:hashed-password.
325 326 """
326 327 )
327 328
328 329 open_browser = Bool(True, config=True,
329 330 help="""Whether to open in a browser after starting.
330 331 The specific browser used is platform dependent and
331 332 determined by the python standard library `webbrowser`
332 333 module, unless it is overridden using the --browser
333 334 (NotebookApp.browser) configuration option.
334 335 """)
335 336
336 337 browser = Unicode(u'', config=True,
337 338 help="""Specify what command to use to invoke a web
338 339 browser when opening the notebook. If not specified, the
339 340 default browser will be determined by the `webbrowser`
340 341 standard library module, which allows setting of the
341 342 BROWSER environment variable to override it.
342 343 """)
343 344
344 345 read_only = Bool(False, config=True,
345 346 help="Whether to prevent editing/execution of notebooks."
346 347 )
347 348
348 349 webapp_settings = Dict(config=True,
349 350 help="Supply overrides for the tornado.web.Application that the "
350 351 "IPython notebook uses.")
351 352
352 353 enable_mathjax = Bool(True, config=True,
353 354 help="""Whether to enable MathJax for typesetting math/TeX
354 355
355 356 MathJax is the javascript library IPython uses to render math/LaTeX. It is
356 357 very large, so you may want to disable it if you have a slow internet
357 358 connection, or for offline use of the notebook.
358 359
359 360 When disabled, equations etc. will appear as their untransformed TeX source.
360 361 """
361 362 )
362 363 def _enable_mathjax_changed(self, name, old, new):
363 364 """set mathjax url to empty if mathjax is disabled"""
364 365 if not new:
365 366 self.mathjax_url = u''
366 367
367 368 base_project_url = Unicode('/', config=True,
368 369 help='''The base URL for the notebook server.
369 370
370 371 Leading and trailing slashes can be omitted,
371 372 and will automatically be added.
372 373 ''')
373 374 def _base_project_url_changed(self, name, old, new):
374 375 if not new.startswith('/'):
375 376 self.base_project_url = '/'+new
376 377 elif not new.endswith('/'):
377 378 self.base_project_url = new+'/'
378 379
379 380 base_kernel_url = Unicode('/', config=True,
380 381 help='''The base URL for the kernel server
381 382
382 383 Leading and trailing slashes can be omitted,
383 384 and will automatically be added.
384 385 ''')
385 386 def _base_kernel_url_changed(self, name, old, new):
386 387 if not new.startswith('/'):
387 388 self.base_kernel_url = '/'+new
388 389 elif not new.endswith('/'):
389 390 self.base_kernel_url = new+'/'
390 391
391 392 websocket_host = Unicode("", config=True,
392 393 help="""The hostname for the websocket server."""
393 394 )
394 395
395 396 extra_static_paths = List(Unicode, config=True,
396 397 help="""Extra paths to search for serving static files.
397 398
398 399 This allows adding javascript/css to be available from the notebook server machine,
399 400 or overriding individual files in the IPython"""
400 401 )
401 402 def _extra_static_paths_default(self):
402 403 return [os.path.join(self.profile_dir.location, 'static')]
403 404
404 405 @property
405 406 def static_file_path(self):
406 407 """return extra paths + the default location"""
407 408 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
408 409
409 410 mathjax_url = Unicode("", config=True,
410 411 help="""The url for MathJax.js."""
411 412 )
412 413 def _mathjax_url_default(self):
413 414 if not self.enable_mathjax:
414 415 return u''
415 416 static_url_prefix = self.webapp_settings.get("static_url_prefix",
416 417 "/static/")
417 418 try:
418 419 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
419 420 except IOError:
420 421 if self.certfile:
421 422 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
422 423 base = u"https://c328740.ssl.cf1.rackcdn.com"
423 424 else:
424 425 base = u"http://cdn.mathjax.org"
425 426
426 427 url = base + u"/mathjax/latest/MathJax.js"
427 428 self.log.info("Using MathJax from CDN: %s", url)
428 429 return url
429 430 else:
430 431 self.log.info("Using local MathJax from %s" % mathjax)
431 432 return static_url_prefix+u"mathjax/MathJax.js"
432 433
433 434 def _mathjax_url_changed(self, name, old, new):
434 435 if new and not self.enable_mathjax:
435 436 # enable_mathjax=False overrides mathjax_url
436 437 self.mathjax_url = u''
437 438 else:
438 439 self.log.info("Using MathJax: %s", new)
439 440
440 441 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
441 442 config=True,
442 443 help='The notebook manager class to use.')
443 444
444 445 def parse_command_line(self, argv=None):
445 446 super(NotebookApp, self).parse_command_line(argv)
446 447 if argv is None:
447 448 argv = sys.argv[1:]
448 449
449 450 # Scrub frontend-specific flags
450 451 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
451 452 # Kernel should inherit default config file from frontend
452 453 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
453 454
454 455 if self.extra_args:
455 456 f = os.path.abspath(self.extra_args[0])
456 457 if os.path.isdir(f):
457 458 nbdir = f
458 459 else:
459 460 self.file_to_run = f
460 461 nbdir = os.path.dirname(f)
461 462 self.config.NotebookManager.notebook_dir = nbdir
462 463
463 464 def init_configurables(self):
464 465 # force Session default to be secure
465 466 default_secure(self.config)
466 467 self.kernel_manager = MappingKernelManager(
467 468 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
468 469 connection_dir = self.profile_dir.security_dir,
469 470 )
470 471 kls = import_item(self.notebook_manager_class)
471 472 self.notebook_manager = kls(config=self.config, log=self.log)
472 473 self.notebook_manager.log_info()
473 474 self.notebook_manager.load_notebook_names()
474 475 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
475 476 self.cluster_manager.update_profiles()
476 477
477 478 def init_logging(self):
478 479 # This prevents double log messages because tornado use a root logger that
479 480 # self.log is a child of. The logging module dipatches log messages to a log
480 481 # and all of its ancenstors until propagate is set to False.
481 482 self.log.propagate = False
482 483
483 484 def init_webapp(self):
484 485 """initialize tornado webapp and httpserver"""
485 486 self.web_app = NotebookWebApplication(
486 487 self, self.kernel_manager, self.notebook_manager,
487 488 self.cluster_manager, self.log,
488 489 self.base_project_url, self.webapp_settings
489 490 )
490 491 if self.certfile:
491 492 ssl_options = dict(certfile=self.certfile)
492 493 if self.keyfile:
493 494 ssl_options['keyfile'] = self.keyfile
494 495 else:
495 496 ssl_options = None
496 497 self.web_app.password = self.password
497 498 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
498 499 if not self.ip:
499 500 warning = "WARNING: The notebook server is listening on all IP addresses"
500 501 if ssl_options is None:
501 502 self.log.critical(warning + " and not using encryption. This"
502 503 "is not recommended.")
503 504 if not self.password and not self.read_only:
504 505 self.log.critical(warning + "and not using authentication."
505 506 "This is highly insecure and not recommended.")
506 507 success = None
507 508 for port in random_ports(self.port, self.port_retries+1):
508 509 try:
509 510 self.http_server.listen(port, self.ip)
510 511 except socket.error as e:
511 512 if e.errno != errno.EADDRINUSE:
512 513 raise
513 514 self.log.info('The port %i is already in use, trying another random port.' % port)
514 515 else:
515 516 self.port = port
516 517 success = True
517 518 break
518 519 if not success:
519 520 self.log.critical('ERROR: the notebook server could not be started because '
520 521 'no available port could be found.')
521 522 self.exit(1)
522 523
523 524 def init_signal(self):
524 525 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
525 526 # safely extract zmq version info:
526 527 try:
527 528 zmq_v = zmq.pyzmq_version_info()
528 529 except AttributeError:
529 530 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
530 531 if 'dev' in zmq.__version__:
531 532 zmq_v.append(999)
532 533 zmq_v = tuple(zmq_v)
533 534 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
534 535 # This won't work with 2.1.7 and
535 536 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
536 537 # but it will work
537 538 signal.signal(signal.SIGINT, self._handle_sigint)
538 539 signal.signal(signal.SIGTERM, self._signal_stop)
539 540
540 541 def _handle_sigint(self, sig, frame):
541 542 """SIGINT handler spawns confirmation dialog"""
542 543 # register more forceful signal handler for ^C^C case
543 544 signal.signal(signal.SIGINT, self._signal_stop)
544 545 # request confirmation dialog in bg thread, to avoid
545 546 # blocking the App
546 547 thread = threading.Thread(target=self._confirm_exit)
547 548 thread.daemon = True
548 549 thread.start()
549 550
550 551 def _restore_sigint_handler(self):
551 552 """callback for restoring original SIGINT handler"""
552 553 signal.signal(signal.SIGINT, self._handle_sigint)
553 554
554 555 def _confirm_exit(self):
555 556 """confirm shutdown on ^C
556 557
557 558 A second ^C, or answering 'y' within 5s will cause shutdown,
558 559 otherwise original SIGINT handler will be restored.
559 560
560 561 This doesn't work on Windows.
561 562 """
562 563 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
563 564 time.sleep(0.1)
564 565 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
565 566 sys.stdout.flush()
566 567 r,w,x = select.select([sys.stdin], [], [], 5)
567 568 if r:
568 569 line = sys.stdin.readline()
569 570 if line.lower().startswith('y'):
570 571 self.log.critical("Shutdown confirmed")
571 572 ioloop.IOLoop.instance().stop()
572 573 return
573 574 else:
574 575 print "No answer for 5s:",
575 576 print "resuming operation..."
576 577 # no answer, or answer is no:
577 578 # set it back to original SIGINT handler
578 579 # use IOLoop.add_callback because signal.signal must be called
579 580 # from main thread
580 581 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
581 582
582 583 def _signal_stop(self, sig, frame):
583 584 self.log.critical("received signal %s, stopping", sig)
584 585 ioloop.IOLoop.instance().stop()
585 586
586 587 @catch_config_error
587 588 def initialize(self, argv=None):
588 589 self.init_logging()
589 590 super(NotebookApp, self).initialize(argv)
590 591 self.init_configurables()
591 592 self.init_webapp()
592 593 self.init_signal()
593 594
594 595 def cleanup_kernels(self):
595 596 """Shutdown all kernels.
596 597
597 598 The kernels will shutdown themselves when this process no longer exists,
598 599 but explicit shutdown allows the KernelManagers to cleanup the connection files.
599 600 """
600 601 self.log.info('Shutting down kernels')
601 602 self.kernel_manager.shutdown_all()
602 603
603 604 def start(self):
604 605 ip = self.ip if self.ip else '[all ip addresses on your system]'
605 606 proto = 'https' if self.certfile else 'http'
606 607 info = self.log.info
607 608 info("The IPython Notebook is running at: %s://%s:%i%s" %
608 609 (proto, ip, self.port,self.base_project_url) )
609 610 info("Use Control-C to stop this server and shut down all kernels.")
610 611
611 612 if self.open_browser or self.file_to_run:
612 613 ip = self.ip or '127.0.0.1'
613 614 try:
614 615 browser = webbrowser.get(self.browser or None)
615 616 except webbrowser.Error as e:
616 617 self.log.warn('No web browser found: %s.' % e)
617 618 browser = None
618 619
619 620 if self.file_to_run:
620 621 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
621 622 url = self.notebook_manager.rev_mapping.get(name, '')
622 623 else:
623 624 url = ''
624 625 if browser:
625 626 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
626 627 self.port, self.base_project_url, url), new=2)
627 628 threading.Thread(target=b).start()
628 629 try:
629 630 ioloop.IOLoop.instance().start()
630 631 except KeyboardInterrupt:
631 632 info("Interrupted...")
632 633 finally:
633 634 self.cleanup_kernels()
634 635
635 636
636 637 #-----------------------------------------------------------------------------
637 638 # Main entry point
638 639 #-----------------------------------------------------------------------------
639 640
640 641 def launch_new_instance():
641 642 app = NotebookApp.instance()
642 643 app.initialize()
643 644 app.start()
644 645
@@ -1,368 +1,370 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import json
24 24 import os
25 25 import signal
26 26 import sys
27 27 import uuid
28 28
29 29 # If run on Windows, install an exception hook which pops up a
30 30 # message box. Pythonw.exe hides the console, so without this
31 31 # the application silently fails to load.
32 32 #
33 33 # We always install this handler, because the expectation is for
34 34 # qtconsole to bring up a GUI even if called from the console.
35 35 # The old handler is called, so the exception is printed as well.
36 36 # If desired, check for pythonw with an additional condition
37 37 # (sys.executable.lower().find('pythonw.exe') >= 0).
38 38 if os.name == 'nt':
39 39 old_excepthook = sys.excepthook
40 40
41 41 def gui_excepthook(exctype, value, tb):
42 42 try:
43 43 import ctypes, traceback
44 44 MB_ICONERROR = 0x00000010L
45 45 title = u'Error starting IPython QtConsole'
46 46 msg = u''.join(traceback.format_exception(exctype, value, tb))
47 47 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
48 48 finally:
49 49 # Also call the old exception hook to let it do
50 50 # its thing too.
51 51 old_excepthook(exctype, value, tb)
52 52
53 53 sys.excepthook = gui_excepthook
54 54
55 55 # System library imports
56 56 from IPython.external.qt import QtCore, QtGui
57 57
58 58 # Local imports
59 59 from IPython.config.application import boolean_flag, catch_config_error
60 60 from IPython.core.application import BaseIPythonApplication
61 61 from IPython.core.profiledir import ProfileDir
62 62 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
63 63 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
64 64 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
65 65 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
66 66 from IPython.frontend.qt.console import styles
67 67 from IPython.frontend.qt.console.mainwindow import MainWindow
68 68 from IPython.frontend.qt.kernelmanager import QtKernelManager
69 69 from IPython.utils.path import filefind
70 70 from IPython.utils.py3compat import str_to_bytes
71 71 from IPython.utils.traitlets import (
72 72 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
73 73 )
74 74 from IPython.zmq.ipkernel import IPKernelApp
75 75 from IPython.zmq.session import Session, default_secure
76 76 from IPython.zmq.zmqshell import ZMQInteractiveShell
77 77
78 78 from IPython.frontend.consoleapp import (
79 79 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
80 80 )
81 81
82 82 #-----------------------------------------------------------------------------
83 83 # Network Constants
84 84 #-----------------------------------------------------------------------------
85 85
86 86 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
87 87
88 88 #-----------------------------------------------------------------------------
89 89 # Globals
90 90 #-----------------------------------------------------------------------------
91 91
92 92 _examples = """
93 93 ipython qtconsole # start the qtconsole
94 94 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
95 95 """
96 96
97 97 #-----------------------------------------------------------------------------
98 98 # Aliases and Flags
99 99 #-----------------------------------------------------------------------------
100 100
101 101 # start with copy of flags
102 102 flags = dict(flags)
103 103 qt_flags = {
104 104 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
105 105 "Disable rich text support."),
106 106 }
107 107
108 108 # and app_flags from the Console Mixin
109 109 qt_flags.update(app_flags)
110 110 # add frontend flags to the full set
111 111 flags.update(qt_flags)
112 112
113 113 # start with copy of front&backend aliases list
114 114 aliases = dict(aliases)
115 115 qt_aliases = dict(
116 116 style = 'IPythonWidget.syntax_style',
117 117 stylesheet = 'IPythonQtConsoleApp.stylesheet',
118 118 colors = 'ZMQInteractiveShell.colors',
119 119
120 120 editor = 'IPythonWidget.editor',
121 121 paging = 'ConsoleWidget.paging',
122 122 )
123 123 # and app_aliases from the Console Mixin
124 124 qt_aliases.update(app_aliases)
125 125 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
126 126 # add frontend aliases to the full set
127 127 aliases.update(qt_aliases)
128 128
129 129 # get flags&aliases into sets, and remove a couple that
130 130 # shouldn't be scrubbed from backend flags:
131 131 qt_aliases = set(qt_aliases.keys())
132 132 qt_aliases.remove('colors')
133 133 qt_flags = set(qt_flags.keys())
134 134
135 135 #-----------------------------------------------------------------------------
136 136 # Classes
137 137 #-----------------------------------------------------------------------------
138 138
139 139 #-----------------------------------------------------------------------------
140 140 # IPythonQtConsole
141 141 #-----------------------------------------------------------------------------
142 142
143 143
144 144 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
145 145 name = 'ipython-qtconsole'
146 146
147 147 description = """
148 148 The IPython QtConsole.
149 149
150 150 This launches a Console-style application using Qt. It is not a full
151 151 console, in that launched terminal subprocesses will not be able to accept
152 152 input.
153 153
154 154 The QtConsole supports various extra features beyond the Terminal IPython
155 155 shell, such as inline plotting with matplotlib, via:
156 156
157 157 ipython qtconsole --pylab=inline
158 158
159 159 as well as saving your session as HTML, and printing the output.
160 160
161 161 """
162 162 examples = _examples
163 163
164 164 classes = [IPythonWidget] + IPythonConsoleApp.classes
165 165 flags = Dict(flags)
166 166 aliases = Dict(aliases)
167 167 frontend_flags = Any(qt_flags)
168 168 frontend_aliases = Any(qt_aliases)
169 169 kernel_manager_class = QtKernelManager
170 170
171 171 stylesheet = Unicode('', config=True,
172 172 help="path to a custom CSS stylesheet")
173 173
174 174 plain = CBool(False, config=True,
175 175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
176 176
177 177 def _plain_changed(self, name, old, new):
178 178 kind = 'plain' if new else 'rich'
179 179 self.config.ConsoleWidget.kind = kind
180 180 if new:
181 181 self.widget_factory = IPythonWidget
182 182 else:
183 183 self.widget_factory = RichIPythonWidget
184 184
185 185 # the factory for creating a widget
186 186 widget_factory = Any(RichIPythonWidget)
187 187
188 188 def parse_command_line(self, argv=None):
189 189 super(IPythonQtConsoleApp, self).parse_command_line(argv)
190 190 self.build_kernel_argv(argv)
191 191
192 192
193 193 def new_frontend_master(self):
194 194 """ Create and return new frontend attached to new kernel, launched on localhost.
195 195 """
196 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
197 196 kernel_manager = self.kernel_manager_class(
198 ip=ip,
199 197 connection_file=self._new_connection_file(),
200 198 config=self.config,
201 199 )
202 200 # start the kernel
203 201 kwargs = dict()
204 202 kwargs['extra_arguments'] = self.kernel_argv
205 203 kernel_manager.start_kernel(**kwargs)
206 204 kernel_manager.start_channels()
207 205 widget = self.widget_factory(config=self.config,
208 206 local_kernel=True)
209 207 self.init_colors(widget)
210 208 widget.kernel_manager = kernel_manager
211 209 widget._existing = False
212 210 widget._may_close = True
213 211 widget._confirm_exit = self.confirm_exit
214 212 return widget
215 213
216 214 def new_frontend_slave(self, current_widget):
217 215 """Create and return a new frontend attached to an existing kernel.
218 216
219 217 Parameters
220 218 ----------
221 219 current_widget : IPythonWidget
222 220 The IPythonWidget whose kernel this frontend is to share
223 221 """
224 222 kernel_manager = self.kernel_manager_class(
225 223 connection_file=current_widget.kernel_manager.connection_file,
226 224 config = self.config,
227 225 )
228 226 kernel_manager.load_connection_file()
229 227 kernel_manager.start_channels()
230 228 widget = self.widget_factory(config=self.config,
231 229 local_kernel=False)
232 230 self.init_colors(widget)
233 231 widget._existing = True
234 232 widget._may_close = False
235 233 widget._confirm_exit = False
236 234 widget.kernel_manager = kernel_manager
237 235 return widget
238 236
239 237 def init_qt_elements(self):
240 238 # Create the widget.
241 239 self.app = QtGui.QApplication([])
242 240
243 241 base_path = os.path.abspath(os.path.dirname(__file__))
244 242 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
245 243 self.app.icon = QtGui.QIcon(icon_path)
246 244 QtGui.QApplication.setWindowIcon(self.app.icon)
247 245
248 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
246 try:
247 ip = self.config.KernelManager.ip
248 except AttributeError:
249 ip = LOCALHOST
250 local_kernel = (not self.existing) or ip in LOCAL_IPS
249 251 self.widget = self.widget_factory(config=self.config,
250 252 local_kernel=local_kernel)
251 253 self.init_colors(self.widget)
252 254 self.widget._existing = self.existing
253 255 self.widget._may_close = not self.existing
254 256 self.widget._confirm_exit = self.confirm_exit
255 257
256 258 self.widget.kernel_manager = self.kernel_manager
257 259 self.window = MainWindow(self.app,
258 260 confirm_exit=self.confirm_exit,
259 261 new_frontend_factory=self.new_frontend_master,
260 262 slave_frontend_factory=self.new_frontend_slave,
261 263 )
262 264 self.window.log = self.log
263 265 self.window.add_tab_with_frontend(self.widget)
264 266 self.window.init_menu_bar()
265 267
266 268 self.window.setWindowTitle('IPython')
267 269
268 270 def init_colors(self, widget):
269 271 """Configure the coloring of the widget"""
270 272 # Note: This will be dramatically simplified when colors
271 273 # are removed from the backend.
272 274
273 275 # parse the colors arg down to current known labels
274 276 try:
275 277 colors = self.config.ZMQInteractiveShell.colors
276 278 except AttributeError:
277 279 colors = None
278 280 try:
279 281 style = self.config.IPythonWidget.syntax_style
280 282 except AttributeError:
281 283 style = None
282 284 try:
283 285 sheet = self.config.IPythonWidget.style_sheet
284 286 except AttributeError:
285 287 sheet = None
286 288
287 289 # find the value for colors:
288 290 if colors:
289 291 colors=colors.lower()
290 292 if colors in ('lightbg', 'light'):
291 293 colors='lightbg'
292 294 elif colors in ('dark', 'linux'):
293 295 colors='linux'
294 296 else:
295 297 colors='nocolor'
296 298 elif style:
297 299 if style=='bw':
298 300 colors='nocolor'
299 301 elif styles.dark_style(style):
300 302 colors='linux'
301 303 else:
302 304 colors='lightbg'
303 305 else:
304 306 colors=None
305 307
306 308 # Configure the style
307 309 if style:
308 310 widget.style_sheet = styles.sheet_from_template(style, colors)
309 311 widget.syntax_style = style
310 312 widget._syntax_style_changed()
311 313 widget._style_sheet_changed()
312 314 elif colors:
313 315 # use a default dark/light/bw style
314 316 widget.set_default_style(colors=colors)
315 317
316 318 if self.stylesheet:
317 319 # we got an explicit stylesheet
318 320 if os.path.isfile(self.stylesheet):
319 321 with open(self.stylesheet) as f:
320 322 sheet = f.read()
321 323 else:
322 324 raise IOError("Stylesheet %r not found." % self.stylesheet)
323 325 if sheet:
324 326 widget.style_sheet = sheet
325 327 widget._style_sheet_changed()
326 328
327 329
328 330 def init_signal(self):
329 331 """allow clean shutdown on sigint"""
330 332 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
331 333 # need a timer, so that QApplication doesn't block until a real
332 334 # Qt event fires (can require mouse movement)
333 335 # timer trick from http://stackoverflow.com/q/4938723/938949
334 336 timer = QtCore.QTimer()
335 337 # Let the interpreter run each 200 ms:
336 338 timer.timeout.connect(lambda: None)
337 339 timer.start(200)
338 340 # hold onto ref, so the timer doesn't get cleaned up
339 341 self._sigint_timer = timer
340 342
341 343 @catch_config_error
342 344 def initialize(self, argv=None):
343 345 super(IPythonQtConsoleApp, self).initialize(argv)
344 346 IPythonConsoleApp.initialize(self,argv)
345 347 self.init_qt_elements()
346 348 self.init_signal()
347 349
348 350 def start(self):
349 351
350 352 # draw the window
351 353 self.window.show()
352 354 self.window.raise_()
353 355
354 356 # Start the application main loop.
355 357 self.app.exec_()
356 358
357 359 #-----------------------------------------------------------------------------
358 360 # Main entry point
359 361 #-----------------------------------------------------------------------------
360 362
361 363 def main():
362 364 app = IPythonQtConsoleApp()
363 365 app.initialize()
364 366 app.start()
365 367
366 368
367 369 if __name__ == '__main__':
368 370 main()
General Comments 0
You need to be logged in to leave comments. Login now