##// END OF EJS Templates
Merge remote-tracking branch 'upstream/master'
Nathan Heijermans -
r19175:e2ea8a76 merge
parent child Browse files
Show More
@@ -0,0 +1,4 b''
1 # URI for the CSP Report. Included here to prevent a cyclic dependency.
2 # csp_report_uri is needed both by the BaseHandler (for setting the report-uri)
3 # and by the CSPReportHandler (which depends on the BaseHandler).
4 csp_report_uri = r"/api/security/csp-report"
@@ -0,0 +1,23 b''
1 """Tornado handlers for security logging."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 from tornado import gen, web
7
8 from ...base.handlers import IPythonHandler, json_errors
9 from . import csp_report_uri
10
11 class CSPReportHandler(IPythonHandler):
12 '''Accepts a content security policy violation report'''
13 @web.authenticated
14 @json_errors
15 def post(self):
16 '''Log a content security policy violation report'''
17 csp_report = self.get_json_body()
18 self.log.warn("Content security violation: %s",
19 self.request.body.decode('utf8', 'replace'))
20
21 default_handlers = [
22 (csp_report_uri, CSPReportHandler)
23 ]
@@ -1,601 +1,611 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A base class for a configurable application."""
2 """A base class for a configurable application."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import json
9 import logging
10 import logging
10 import os
11 import os
11 import re
12 import re
12 import sys
13 import sys
13 from copy import deepcopy
14 from copy import deepcopy
14 from collections import defaultdict
15 from collections import defaultdict
15
16
16 from IPython.external.decorator import decorator
17 from IPython.external.decorator import decorator
17
18
18 from IPython.config.configurable import SingletonConfigurable
19 from IPython.config.configurable import SingletonConfigurable
19 from IPython.config.loader import (
20 from IPython.config.loader import (
20 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
21 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
21 )
22 )
22
23
23 from IPython.utils.traitlets import (
24 from IPython.utils.traitlets import (
24 Unicode, List, Enum, Dict, Instance, TraitError
25 Unicode, List, Enum, Dict, Instance, TraitError
25 )
26 )
26 from IPython.utils.importstring import import_item
27 from IPython.utils.importstring import import_item
27 from IPython.utils.text import indent, wrap_paragraphs, dedent
28 from IPython.utils.text import indent, wrap_paragraphs, dedent
28 from IPython.utils import py3compat
29 from IPython.utils import py3compat
29 from IPython.utils.py3compat import string_types, iteritems
30 from IPython.utils.py3compat import string_types, iteritems
30
31
31 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
32 # Descriptions for the various sections
33 # Descriptions for the various sections
33 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
34
35
35 # merge flags&aliases into options
36 # merge flags&aliases into options
36 option_description = """
37 option_description = """
37 Arguments that take values are actually convenience aliases to full
38 Arguments that take values are actually convenience aliases to full
38 Configurables, whose aliases are listed on the help line. For more information
39 Configurables, whose aliases are listed on the help line. For more information
39 on full configurables, see '--help-all'.
40 on full configurables, see '--help-all'.
40 """.strip() # trim newlines of front and back
41 """.strip() # trim newlines of front and back
41
42
42 keyvalue_description = """
43 keyvalue_description = """
43 Parameters are set from command-line arguments of the form:
44 Parameters are set from command-line arguments of the form:
44 `--Class.trait=value`.
45 `--Class.trait=value`.
45 This line is evaluated in Python, so simple expressions are allowed, e.g.::
46 This line is evaluated in Python, so simple expressions are allowed, e.g.::
46 `--C.a='range(3)'` For setting C.a=[0,1,2].
47 `--C.a='range(3)'` For setting C.a=[0,1,2].
47 """.strip() # trim newlines of front and back
48 """.strip() # trim newlines of front and back
48
49
49 # sys.argv can be missing, for example when python is embedded. See the docs
50 # sys.argv can be missing, for example when python is embedded. See the docs
50 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
51 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
51 if not hasattr(sys, "argv"):
52 if not hasattr(sys, "argv"):
52 sys.argv = [""]
53 sys.argv = [""]
53
54
54 subcommand_description = """
55 subcommand_description = """
55 Subcommands are launched as `{app} cmd [args]`. For information on using
56 Subcommands are launched as `{app} cmd [args]`. For information on using
56 subcommand 'cmd', do: `{app} cmd -h`.
57 subcommand 'cmd', do: `{app} cmd -h`.
57 """
58 """
58 # get running program name
59 # get running program name
59
60
60 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
61 # Application class
62 # Application class
62 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
63
64
64 @decorator
65 @decorator
65 def catch_config_error(method, app, *args, **kwargs):
66 def catch_config_error(method, app, *args, **kwargs):
66 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
67 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
67
68
68 On a TraitError (generally caused by bad config), this will print the trait's
69 On a TraitError (generally caused by bad config), this will print the trait's
69 message, and exit the app.
70 message, and exit the app.
70
71
71 For use on init methods, to prevent invoking excepthook on invalid input.
72 For use on init methods, to prevent invoking excepthook on invalid input.
72 """
73 """
73 try:
74 try:
74 return method(app, *args, **kwargs)
75 return method(app, *args, **kwargs)
75 except (TraitError, ArgumentError) as e:
76 except (TraitError, ArgumentError) as e:
76 app.print_help()
77 app.print_help()
77 app.log.fatal("Bad config encountered during initialization:")
78 app.log.fatal("Bad config encountered during initialization:")
78 app.log.fatal(str(e))
79 app.log.fatal(str(e))
79 app.log.debug("Config at the time: %s", app.config)
80 app.log.debug("Config at the time: %s", app.config)
80 app.exit(1)
81 app.exit(1)
81
82
82
83
83 class ApplicationError(Exception):
84 class ApplicationError(Exception):
84 pass
85 pass
85
86
86 class LevelFormatter(logging.Formatter):
87 class LevelFormatter(logging.Formatter):
87 """Formatter with additional `highlevel` record
88 """Formatter with additional `highlevel` record
88
89
89 This field is empty if log level is less than highlevel_limit,
90 This field is empty if log level is less than highlevel_limit,
90 otherwise it is formatted with self.highlevel_format.
91 otherwise it is formatted with self.highlevel_format.
91
92
92 Useful for adding 'WARNING' to warning messages,
93 Useful for adding 'WARNING' to warning messages,
93 without adding 'INFO' to info, etc.
94 without adding 'INFO' to info, etc.
94 """
95 """
95 highlevel_limit = logging.WARN
96 highlevel_limit = logging.WARN
96 highlevel_format = " %(levelname)s |"
97 highlevel_format = " %(levelname)s |"
97
98
98 def format(self, record):
99 def format(self, record):
99 if record.levelno >= self.highlevel_limit:
100 if record.levelno >= self.highlevel_limit:
100 record.highlevel = self.highlevel_format % record.__dict__
101 record.highlevel = self.highlevel_format % record.__dict__
101 else:
102 else:
102 record.highlevel = ""
103 record.highlevel = ""
103 return super(LevelFormatter, self).format(record)
104 return super(LevelFormatter, self).format(record)
104
105
105
106
106 class Application(SingletonConfigurable):
107 class Application(SingletonConfigurable):
107 """A singleton application with full configuration support."""
108 """A singleton application with full configuration support."""
108
109
109 # The name of the application, will usually match the name of the command
110 # The name of the application, will usually match the name of the command
110 # line application
111 # line application
111 name = Unicode(u'application')
112 name = Unicode(u'application')
112
113
113 # The description of the application that is printed at the beginning
114 # The description of the application that is printed at the beginning
114 # of the help.
115 # of the help.
115 description = Unicode(u'This is an application.')
116 description = Unicode(u'This is an application.')
116 # default section descriptions
117 # default section descriptions
117 option_description = Unicode(option_description)
118 option_description = Unicode(option_description)
118 keyvalue_description = Unicode(keyvalue_description)
119 keyvalue_description = Unicode(keyvalue_description)
119 subcommand_description = Unicode(subcommand_description)
120 subcommand_description = Unicode(subcommand_description)
120
121
121 # The usage and example string that goes at the end of the help string.
122 # The usage and example string that goes at the end of the help string.
122 examples = Unicode()
123 examples = Unicode()
123
124
124 # A sequence of Configurable subclasses whose config=True attributes will
125 # A sequence of Configurable subclasses whose config=True attributes will
125 # be exposed at the command line.
126 # be exposed at the command line.
126 classes = []
127 classes = []
127 @property
128 @property
128 def _help_classes(self):
129 def _help_classes(self):
129 """Define `App.help_classes` if CLI classes should differ from config file classes"""
130 """Define `App.help_classes` if CLI classes should differ from config file classes"""
130 return getattr(self, 'help_classes', self.classes)
131 return getattr(self, 'help_classes', self.classes)
131
132
132 @property
133 @property
133 def _config_classes(self):
134 def _config_classes(self):
134 """Define `App.config_classes` if config file classes should differ from CLI classes."""
135 """Define `App.config_classes` if config file classes should differ from CLI classes."""
135 return getattr(self, 'config_classes', self.classes)
136 return getattr(self, 'config_classes', self.classes)
136
137
137 # The version string of this application.
138 # The version string of this application.
138 version = Unicode(u'0.0')
139 version = Unicode(u'0.0')
139
140
140 # the argv used to initialize the application
141 # the argv used to initialize the application
141 argv = List()
142 argv = List()
142
143
143 # The log level for the application
144 # The log level for the application
144 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
145 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
145 default_value=logging.WARN,
146 default_value=logging.WARN,
146 config=True,
147 config=True,
147 help="Set the log level by value or name.")
148 help="Set the log level by value or name.")
148 def _log_level_changed(self, name, old, new):
149 def _log_level_changed(self, name, old, new):
149 """Adjust the log level when log_level is set."""
150 """Adjust the log level when log_level is set."""
150 if isinstance(new, string_types):
151 if isinstance(new, string_types):
151 new = getattr(logging, new)
152 new = getattr(logging, new)
152 self.log_level = new
153 self.log_level = new
153 self.log.setLevel(new)
154 self.log.setLevel(new)
154
155
155 _log_formatter_cls = LevelFormatter
156 _log_formatter_cls = LevelFormatter
156
157
157 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
158 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
158 help="The date format used by logging formatters for %(asctime)s"
159 help="The date format used by logging formatters for %(asctime)s"
159 )
160 )
160 def _log_datefmt_changed(self, name, old, new):
161 def _log_datefmt_changed(self, name, old, new):
161 self._log_format_changed()
162 self._log_format_changed()
162
163
163 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
164 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
164 help="The Logging format template",
165 help="The Logging format template",
165 )
166 )
166 def _log_format_changed(self, name, old, new):
167 def _log_format_changed(self, name, old, new):
167 """Change the log formatter when log_format is set."""
168 """Change the log formatter when log_format is set."""
168 _log_handler = self.log.handlers[0]
169 _log_handler = self.log.handlers[0]
169 _log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
170 _log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
170 _log_handler.setFormatter(_log_formatter)
171 _log_handler.setFormatter(_log_formatter)
171
172
172
173
173 log = Instance(logging.Logger)
174 log = Instance(logging.Logger)
174 def _log_default(self):
175 def _log_default(self):
175 """Start logging for this application.
176 """Start logging for this application.
176
177
177 The default is to log to stderr using a StreamHandler, if no default
178 The default is to log to stderr using a StreamHandler, if no default
178 handler already exists. The log level starts at logging.WARN, but this
179 handler already exists. The log level starts at logging.WARN, but this
179 can be adjusted by setting the ``log_level`` attribute.
180 can be adjusted by setting the ``log_level`` attribute.
180 """
181 """
181 log = logging.getLogger(self.__class__.__name__)
182 log = logging.getLogger(self.__class__.__name__)
182 log.setLevel(self.log_level)
183 log.setLevel(self.log_level)
183 log.propagate = False
184 log.propagate = False
184 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
185 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
185 while _log:
186 while _log:
186 if _log.handlers:
187 if _log.handlers:
187 return log
188 return log
188 if not _log.propagate:
189 if not _log.propagate:
189 break
190 break
190 else:
191 else:
191 _log = _log.parent
192 _log = _log.parent
192 if sys.executable.endswith('pythonw.exe'):
193 if sys.executable.endswith('pythonw.exe'):
193 # this should really go to a file, but file-logging is only
194 # this should really go to a file, but file-logging is only
194 # hooked up in parallel applications
195 # hooked up in parallel applications
195 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
196 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
196 else:
197 else:
197 _log_handler = logging.StreamHandler()
198 _log_handler = logging.StreamHandler()
198 _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
199 _log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
199 _log_handler.setFormatter(_log_formatter)
200 _log_handler.setFormatter(_log_formatter)
200 log.addHandler(_log_handler)
201 log.addHandler(_log_handler)
201 return log
202 return log
202
203
203 # the alias map for configurables
204 # the alias map for configurables
204 aliases = Dict({'log-level' : 'Application.log_level'})
205 aliases = Dict({'log-level' : 'Application.log_level'})
205
206
206 # flags for loading Configurables or store_const style flags
207 # flags for loading Configurables or store_const style flags
207 # flags are loaded from this dict by '--key' flags
208 # flags are loaded from this dict by '--key' flags
208 # this must be a dict of two-tuples, the first element being the Config/dict
209 # this must be a dict of two-tuples, the first element being the Config/dict
209 # and the second being the help string for the flag
210 # and the second being the help string for the flag
210 flags = Dict()
211 flags = Dict()
211 def _flags_changed(self, name, old, new):
212 def _flags_changed(self, name, old, new):
212 """ensure flags dict is valid"""
213 """ensure flags dict is valid"""
213 for key,value in iteritems(new):
214 for key,value in iteritems(new):
214 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
215 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
215 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
216 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
216 assert isinstance(value[1], string_types), "Bad flag: %r:%s"%(key,value)
217 assert isinstance(value[1], string_types), "Bad flag: %r:%s"%(key,value)
217
218
218
219
219 # subcommands for launching other applications
220 # subcommands for launching other applications
220 # if this is not empty, this will be a parent Application
221 # if this is not empty, this will be a parent Application
221 # this must be a dict of two-tuples,
222 # this must be a dict of two-tuples,
222 # the first element being the application class/import string
223 # the first element being the application class/import string
223 # and the second being the help string for the subcommand
224 # and the second being the help string for the subcommand
224 subcommands = Dict()
225 subcommands = Dict()
225 # parse_command_line will initialize a subapp, if requested
226 # parse_command_line will initialize a subapp, if requested
226 subapp = Instance('IPython.config.application.Application', allow_none=True)
227 subapp = Instance('IPython.config.application.Application', allow_none=True)
227
228
228 # extra command-line arguments that don't set config values
229 # extra command-line arguments that don't set config values
229 extra_args = List(Unicode)
230 extra_args = List(Unicode)
230
231
231
232
232 def __init__(self, **kwargs):
233 def __init__(self, **kwargs):
233 SingletonConfigurable.__init__(self, **kwargs)
234 SingletonConfigurable.__init__(self, **kwargs)
234 # Ensure my class is in self.classes, so my attributes appear in command line
235 # Ensure my class is in self.classes, so my attributes appear in command line
235 # options and config files.
236 # options and config files.
236 if self.__class__ not in self.classes:
237 if self.__class__ not in self.classes:
237 self.classes.insert(0, self.__class__)
238 self.classes.insert(0, self.__class__)
238
239
239 def _config_changed(self, name, old, new):
240 def _config_changed(self, name, old, new):
240 SingletonConfigurable._config_changed(self, name, old, new)
241 SingletonConfigurable._config_changed(self, name, old, new)
241 self.log.debug('Config changed:')
242 self.log.debug('Config changed:')
242 self.log.debug(repr(new))
243 self.log.debug(repr(new))
243
244
244 @catch_config_error
245 @catch_config_error
245 def initialize(self, argv=None):
246 def initialize(self, argv=None):
246 """Do the basic steps to configure me.
247 """Do the basic steps to configure me.
247
248
248 Override in subclasses.
249 Override in subclasses.
249 """
250 """
250 self.parse_command_line(argv)
251 self.parse_command_line(argv)
251
252
252
253
253 def start(self):
254 def start(self):
254 """Start the app mainloop.
255 """Start the app mainloop.
255
256
256 Override in subclasses.
257 Override in subclasses.
257 """
258 """
258 if self.subapp is not None:
259 if self.subapp is not None:
259 return self.subapp.start()
260 return self.subapp.start()
260
261
261 def print_alias_help(self):
262 def print_alias_help(self):
262 """Print the alias part of the help."""
263 """Print the alias part of the help."""
263 if not self.aliases:
264 if not self.aliases:
264 return
265 return
265
266
266 lines = []
267 lines = []
267 classdict = {}
268 classdict = {}
268 for cls in self._help_classes:
269 for cls in self._help_classes:
269 # include all parents (up to, but excluding Configurable) in available names
270 # include all parents (up to, but excluding Configurable) in available names
270 for c in cls.mro()[:-3]:
271 for c in cls.mro()[:-3]:
271 classdict[c.__name__] = c
272 classdict[c.__name__] = c
272
273
273 for alias, longname in iteritems(self.aliases):
274 for alias, longname in iteritems(self.aliases):
274 classname, traitname = longname.split('.',1)
275 classname, traitname = longname.split('.',1)
275 cls = classdict[classname]
276 cls = classdict[classname]
276
277
277 trait = cls.class_traits(config=True)[traitname]
278 trait = cls.class_traits(config=True)[traitname]
278 help = cls.class_get_trait_help(trait).splitlines()
279 help = cls.class_get_trait_help(trait).splitlines()
279 # reformat first line
280 # reformat first line
280 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
281 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
281 if len(alias) == 1:
282 if len(alias) == 1:
282 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
283 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
283 lines.extend(help)
284 lines.extend(help)
284 # lines.append('')
285 # lines.append('')
285 print(os.linesep.join(lines))
286 print(os.linesep.join(lines))
286
287
287 def print_flag_help(self):
288 def print_flag_help(self):
288 """Print the flag part of the help."""
289 """Print the flag part of the help."""
289 if not self.flags:
290 if not self.flags:
290 return
291 return
291
292
292 lines = []
293 lines = []
293 for m, (cfg,help) in iteritems(self.flags):
294 for m, (cfg,help) in iteritems(self.flags):
294 prefix = '--' if len(m) > 1 else '-'
295 prefix = '--' if len(m) > 1 else '-'
295 lines.append(prefix+m)
296 lines.append(prefix+m)
296 lines.append(indent(dedent(help.strip())))
297 lines.append(indent(dedent(help.strip())))
297 # lines.append('')
298 # lines.append('')
298 print(os.linesep.join(lines))
299 print(os.linesep.join(lines))
299
300
300 def print_options(self):
301 def print_options(self):
301 if not self.flags and not self.aliases:
302 if not self.flags and not self.aliases:
302 return
303 return
303 lines = ['Options']
304 lines = ['Options']
304 lines.append('-'*len(lines[0]))
305 lines.append('-'*len(lines[0]))
305 lines.append('')
306 lines.append('')
306 for p in wrap_paragraphs(self.option_description):
307 for p in wrap_paragraphs(self.option_description):
307 lines.append(p)
308 lines.append(p)
308 lines.append('')
309 lines.append('')
309 print(os.linesep.join(lines))
310 print(os.linesep.join(lines))
310 self.print_flag_help()
311 self.print_flag_help()
311 self.print_alias_help()
312 self.print_alias_help()
312 print()
313 print()
313
314
314 def print_subcommands(self):
315 def print_subcommands(self):
315 """Print the subcommand part of the help."""
316 """Print the subcommand part of the help."""
316 if not self.subcommands:
317 if not self.subcommands:
317 return
318 return
318
319
319 lines = ["Subcommands"]
320 lines = ["Subcommands"]
320 lines.append('-'*len(lines[0]))
321 lines.append('-'*len(lines[0]))
321 lines.append('')
322 lines.append('')
322 for p in wrap_paragraphs(self.subcommand_description.format(
323 for p in wrap_paragraphs(self.subcommand_description.format(
323 app=self.name)):
324 app=self.name)):
324 lines.append(p)
325 lines.append(p)
325 lines.append('')
326 lines.append('')
326 for subc, (cls, help) in iteritems(self.subcommands):
327 for subc, (cls, help) in iteritems(self.subcommands):
327 lines.append(subc)
328 lines.append(subc)
328 if help:
329 if help:
329 lines.append(indent(dedent(help.strip())))
330 lines.append(indent(dedent(help.strip())))
330 lines.append('')
331 lines.append('')
331 print(os.linesep.join(lines))
332 print(os.linesep.join(lines))
332
333
333 def print_help(self, classes=False):
334 def print_help(self, classes=False):
334 """Print the help for each Configurable class in self.classes.
335 """Print the help for each Configurable class in self.classes.
335
336
336 If classes=False (the default), only flags and aliases are printed.
337 If classes=False (the default), only flags and aliases are printed.
337 """
338 """
338 self.print_description()
339 self.print_description()
339 self.print_subcommands()
340 self.print_subcommands()
340 self.print_options()
341 self.print_options()
341
342
342 if classes:
343 if classes:
343 help_classes = self._help_classes
344 help_classes = self._help_classes
344 if help_classes:
345 if help_classes:
345 print("Class parameters")
346 print("Class parameters")
346 print("----------------")
347 print("----------------")
347 print()
348 print()
348 for p in wrap_paragraphs(self.keyvalue_description):
349 for p in wrap_paragraphs(self.keyvalue_description):
349 print(p)
350 print(p)
350 print()
351 print()
351
352
352 for cls in help_classes:
353 for cls in help_classes:
353 cls.class_print_help()
354 cls.class_print_help()
354 print()
355 print()
355 else:
356 else:
356 print("To see all available configurables, use `--help-all`")
357 print("To see all available configurables, use `--help-all`")
357 print()
358 print()
358
359
359 self.print_examples()
360 self.print_examples()
360
361
361
362
362 def print_description(self):
363 def print_description(self):
363 """Print the application description."""
364 """Print the application description."""
364 for p in wrap_paragraphs(self.description):
365 for p in wrap_paragraphs(self.description):
365 print(p)
366 print(p)
366 print()
367 print()
367
368
368 def print_examples(self):
369 def print_examples(self):
369 """Print usage and examples.
370 """Print usage and examples.
370
371
371 This usage string goes at the end of the command line help string
372 This usage string goes at the end of the command line help string
372 and should contain examples of the application's usage.
373 and should contain examples of the application's usage.
373 """
374 """
374 if self.examples:
375 if self.examples:
375 print("Examples")
376 print("Examples")
376 print("--------")
377 print("--------")
377 print()
378 print()
378 print(indent(dedent(self.examples.strip())))
379 print(indent(dedent(self.examples.strip())))
379 print()
380 print()
380
381
381 def print_version(self):
382 def print_version(self):
382 """Print the version string."""
383 """Print the version string."""
383 print(self.version)
384 print(self.version)
384
385
385 def update_config(self, config):
386 def update_config(self, config):
386 """Fire the traits events when the config is updated."""
387 """Fire the traits events when the config is updated."""
387 # Save a copy of the current config.
388 # Save a copy of the current config.
388 newconfig = deepcopy(self.config)
389 newconfig = deepcopy(self.config)
389 # Merge the new config into the current one.
390 # Merge the new config into the current one.
390 newconfig.merge(config)
391 newconfig.merge(config)
391 # Save the combined config as self.config, which triggers the traits
392 # Save the combined config as self.config, which triggers the traits
392 # events.
393 # events.
393 self.config = newconfig
394 self.config = newconfig
394
395
395 @catch_config_error
396 @catch_config_error
396 def initialize_subcommand(self, subc, argv=None):
397 def initialize_subcommand(self, subc, argv=None):
397 """Initialize a subcommand with argv."""
398 """Initialize a subcommand with argv."""
398 subapp,help = self.subcommands.get(subc)
399 subapp,help = self.subcommands.get(subc)
399
400
400 if isinstance(subapp, string_types):
401 if isinstance(subapp, string_types):
401 subapp = import_item(subapp)
402 subapp = import_item(subapp)
402
403
403 # clear existing instances
404 # clear existing instances
404 self.__class__.clear_instance()
405 self.__class__.clear_instance()
405 # instantiate
406 # instantiate
406 self.subapp = subapp.instance(config=self.config)
407 self.subapp = subapp.instance(config=self.config)
407 # and initialize subapp
408 # and initialize subapp
408 self.subapp.initialize(argv)
409 self.subapp.initialize(argv)
409
410
410 def flatten_flags(self):
411 def flatten_flags(self):
411 """flatten flags and aliases, so cl-args override as expected.
412 """flatten flags and aliases, so cl-args override as expected.
412
413
413 This prevents issues such as an alias pointing to InteractiveShell,
414 This prevents issues such as an alias pointing to InteractiveShell,
414 but a config file setting the same trait in TerminalInteraciveShell
415 but a config file setting the same trait in TerminalInteraciveShell
415 getting inappropriate priority over the command-line arg.
416 getting inappropriate priority over the command-line arg.
416
417
417 Only aliases with exactly one descendent in the class list
418 Only aliases with exactly one descendent in the class list
418 will be promoted.
419 will be promoted.
419
420
420 """
421 """
421 # build a tree of classes in our list that inherit from a particular
422 # build a tree of classes in our list that inherit from a particular
422 # it will be a dict by parent classname of classes in our list
423 # it will be a dict by parent classname of classes in our list
423 # that are descendents
424 # that are descendents
424 mro_tree = defaultdict(list)
425 mro_tree = defaultdict(list)
425 for cls in self._help_classes:
426 for cls in self._help_classes:
426 clsname = cls.__name__
427 clsname = cls.__name__
427 for parent in cls.mro()[1:-3]:
428 for parent in cls.mro()[1:-3]:
428 # exclude cls itself and Configurable,HasTraits,object
429 # exclude cls itself and Configurable,HasTraits,object
429 mro_tree[parent.__name__].append(clsname)
430 mro_tree[parent.__name__].append(clsname)
430 # flatten aliases, which have the form:
431 # flatten aliases, which have the form:
431 # { 'alias' : 'Class.trait' }
432 # { 'alias' : 'Class.trait' }
432 aliases = {}
433 aliases = {}
433 for alias, cls_trait in iteritems(self.aliases):
434 for alias, cls_trait in iteritems(self.aliases):
434 cls,trait = cls_trait.split('.',1)
435 cls,trait = cls_trait.split('.',1)
435 children = mro_tree[cls]
436 children = mro_tree[cls]
436 if len(children) == 1:
437 if len(children) == 1:
437 # exactly one descendent, promote alias
438 # exactly one descendent, promote alias
438 cls = children[0]
439 cls = children[0]
439 aliases[alias] = '.'.join([cls,trait])
440 aliases[alias] = '.'.join([cls,trait])
440
441
441 # flatten flags, which are of the form:
442 # flatten flags, which are of the form:
442 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
443 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
443 flags = {}
444 flags = {}
444 for key, (flagdict, help) in iteritems(self.flags):
445 for key, (flagdict, help) in iteritems(self.flags):
445 newflag = {}
446 newflag = {}
446 for cls, subdict in iteritems(flagdict):
447 for cls, subdict in iteritems(flagdict):
447 children = mro_tree[cls]
448 children = mro_tree[cls]
448 # exactly one descendent, promote flag section
449 # exactly one descendent, promote flag section
449 if len(children) == 1:
450 if len(children) == 1:
450 cls = children[0]
451 cls = children[0]
451 newflag[cls] = subdict
452 newflag[cls] = subdict
452 flags[key] = (newflag, help)
453 flags[key] = (newflag, help)
453 return flags, aliases
454 return flags, aliases
454
455
455 @catch_config_error
456 @catch_config_error
456 def parse_command_line(self, argv=None):
457 def parse_command_line(self, argv=None):
457 """Parse the command line arguments."""
458 """Parse the command line arguments."""
458 argv = sys.argv[1:] if argv is None else argv
459 argv = sys.argv[1:] if argv is None else argv
459 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
460 self.argv = [ py3compat.cast_unicode(arg) for arg in argv ]
460
461
461 if argv and argv[0] == 'help':
462 if argv and argv[0] == 'help':
462 # turn `ipython help notebook` into `ipython notebook -h`
463 # turn `ipython help notebook` into `ipython notebook -h`
463 argv = argv[1:] + ['-h']
464 argv = argv[1:] + ['-h']
464
465
465 if self.subcommands and len(argv) > 0:
466 if self.subcommands and len(argv) > 0:
466 # we have subcommands, and one may have been specified
467 # we have subcommands, and one may have been specified
467 subc, subargv = argv[0], argv[1:]
468 subc, subargv = argv[0], argv[1:]
468 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
469 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
469 # it's a subcommand, and *not* a flag or class parameter
470 # it's a subcommand, and *not* a flag or class parameter
470 return self.initialize_subcommand(subc, subargv)
471 return self.initialize_subcommand(subc, subargv)
471
472
472 # Arguments after a '--' argument are for the script IPython may be
473 # Arguments after a '--' argument are for the script IPython may be
473 # about to run, not IPython iteslf. For arguments parsed here (help and
474 # about to run, not IPython iteslf. For arguments parsed here (help and
474 # version), we want to only search the arguments up to the first
475 # version), we want to only search the arguments up to the first
475 # occurrence of '--', which we're calling interpreted_argv.
476 # occurrence of '--', which we're calling interpreted_argv.
476 try:
477 try:
477 interpreted_argv = argv[:argv.index('--')]
478 interpreted_argv = argv[:argv.index('--')]
478 except ValueError:
479 except ValueError:
479 interpreted_argv = argv
480 interpreted_argv = argv
480
481
481 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
482 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
482 self.print_help('--help-all' in interpreted_argv)
483 self.print_help('--help-all' in interpreted_argv)
483 self.exit(0)
484 self.exit(0)
484
485
485 if '--version' in interpreted_argv or '-V' in interpreted_argv:
486 if '--version' in interpreted_argv or '-V' in interpreted_argv:
486 self.print_version()
487 self.print_version()
487 self.exit(0)
488 self.exit(0)
488
489
489 # flatten flags&aliases, so cl-args get appropriate priority:
490 # flatten flags&aliases, so cl-args get appropriate priority:
490 flags,aliases = self.flatten_flags()
491 flags,aliases = self.flatten_flags()
491 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
492 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
492 flags=flags, log=self.log)
493 flags=flags, log=self.log)
493 config = loader.load_config()
494 config = loader.load_config()
494 self.update_config(config)
495 self.update_config(config)
495 # store unparsed args in extra_args
496 # store unparsed args in extra_args
496 self.extra_args = loader.extra_args
497 self.extra_args = loader.extra_args
497
498
498 @classmethod
499 @classmethod
499 def _load_config_files(cls, basefilename, path=None, log=None):
500 def _load_config_files(cls, basefilename, path=None, log=None):
500 """Load config files (py,json) by filename and path.
501 """Load config files (py,json) by filename and path.
501
502
502 yield each config object in turn.
503 yield each config object in turn.
503 """
504 """
504
505
505 if not isinstance(path, list):
506 if not isinstance(path, list):
506 path = [path]
507 path = [path]
507 for path in path[::-1]:
508 for path in path[::-1]:
508 # path list is in descending priority order, so load files backwards:
509 # path list is in descending priority order, so load files backwards:
509 pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log)
510 pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log)
510 jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log)
511 jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log)
511 config = None
512 config = None
512 for loader in [pyloader, jsonloader]:
513 for loader in [pyloader, jsonloader]:
513 try:
514 try:
514 config = loader.load_config()
515 config = loader.load_config()
515 except ConfigFileNotFound:
516 except ConfigFileNotFound:
516 pass
517 pass
517 except Exception:
518 except Exception:
518 # try to get the full filename, but it will be empty in the
519 # try to get the full filename, but it will be empty in the
519 # unlikely event that the error raised before filefind finished
520 # unlikely event that the error raised before filefind finished
520 filename = loader.full_filename or basefilename
521 filename = loader.full_filename or basefilename
521 # problem while running the file
522 # problem while running the file
522 if log:
523 if log:
523 log.error("Exception while loading config file %s",
524 log.error("Exception while loading config file %s",
524 filename, exc_info=True)
525 filename, exc_info=True)
525 else:
526 else:
526 if log:
527 if log:
527 log.debug("Loaded config file: %s", loader.full_filename)
528 log.debug("Loaded config file: %s", loader.full_filename)
528 if config:
529 if config:
529 yield config
530 yield config
530
531
531 raise StopIteration
532 raise StopIteration
532
533
533
534
534 @catch_config_error
535 @catch_config_error
535 def load_config_file(self, filename, path=None):
536 def load_config_file(self, filename, path=None):
536 """Load config files by filename and path."""
537 """Load config files by filename and path."""
537 filename, ext = os.path.splitext(filename)
538 filename, ext = os.path.splitext(filename)
539 loaded = []
538 for config in self._load_config_files(filename, path=path, log=self.log):
540 for config in self._load_config_files(filename, path=path, log=self.log):
541 loaded.append(config)
539 self.update_config(config)
542 self.update_config(config)
543 if len(loaded) > 1:
544 collisions = loaded[0].collisions(loaded[1])
545 if collisions:
546 self.log.warn("Collisions detected in {0}.py and {0}.json config files."
547 " {0}.json has higher priority: {1}".format(
548 filename, json.dumps(collisions, indent=2),
549 ))
540
550
541
551
542 def generate_config_file(self):
552 def generate_config_file(self):
543 """generate default config file from Configurables"""
553 """generate default config file from Configurables"""
544 lines = ["# Configuration file for %s."%self.name]
554 lines = ["# Configuration file for %s."%self.name]
545 lines.append('')
555 lines.append('')
546 lines.append('c = get_config()')
556 lines.append('c = get_config()')
547 lines.append('')
557 lines.append('')
548 for cls in self._config_classes:
558 for cls in self._config_classes:
549 lines.append(cls.class_config_section())
559 lines.append(cls.class_config_section())
550 return '\n'.join(lines)
560 return '\n'.join(lines)
551
561
552 def exit(self, exit_status=0):
562 def exit(self, exit_status=0):
553 self.log.debug("Exiting application: %s" % self.name)
563 self.log.debug("Exiting application: %s" % self.name)
554 sys.exit(exit_status)
564 sys.exit(exit_status)
555
565
556 @classmethod
566 @classmethod
557 def launch_instance(cls, argv=None, **kwargs):
567 def launch_instance(cls, argv=None, **kwargs):
558 """Launch a global instance of this Application
568 """Launch a global instance of this Application
559
569
560 If a global instance already exists, this reinitializes and starts it
570 If a global instance already exists, this reinitializes and starts it
561 """
571 """
562 app = cls.instance(**kwargs)
572 app = cls.instance(**kwargs)
563 app.initialize(argv)
573 app.initialize(argv)
564 app.start()
574 app.start()
565
575
566 #-----------------------------------------------------------------------------
576 #-----------------------------------------------------------------------------
567 # utility functions, for convenience
577 # utility functions, for convenience
568 #-----------------------------------------------------------------------------
578 #-----------------------------------------------------------------------------
569
579
570 def boolean_flag(name, configurable, set_help='', unset_help=''):
580 def boolean_flag(name, configurable, set_help='', unset_help=''):
571 """Helper for building basic --trait, --no-trait flags.
581 """Helper for building basic --trait, --no-trait flags.
572
582
573 Parameters
583 Parameters
574 ----------
584 ----------
575
585
576 name : str
586 name : str
577 The name of the flag.
587 The name of the flag.
578 configurable : str
588 configurable : str
579 The 'Class.trait' string of the trait to be set/unset with the flag
589 The 'Class.trait' string of the trait to be set/unset with the flag
580 set_help : unicode
590 set_help : unicode
581 help string for --name flag
591 help string for --name flag
582 unset_help : unicode
592 unset_help : unicode
583 help string for --no-name flag
593 help string for --no-name flag
584
594
585 Returns
595 Returns
586 -------
596 -------
587
597
588 cfg : dict
598 cfg : dict
589 A dict with two keys: 'name', and 'no-name', for setting and unsetting
599 A dict with two keys: 'name', and 'no-name', for setting and unsetting
590 the trait, respectively.
600 the trait, respectively.
591 """
601 """
592 # default helpstrings
602 # default helpstrings
593 set_help = set_help or "set %s=True"%configurable
603 set_help = set_help or "set %s=True"%configurable
594 unset_help = unset_help or "set %s=False"%configurable
604 unset_help = unset_help or "set %s=False"%configurable
595
605
596 cls,trait = configurable.split('.')
606 cls,trait = configurable.split('.')
597
607
598 setter = {cls : {trait : True}}
608 setter = {cls : {trait : True}}
599 unsetter = {cls : {trait : False}}
609 unsetter = {cls : {trait : False}}
600 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
610 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
601
611
@@ -1,824 +1,844 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A simple configuration system."""
2 """A simple configuration system."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 import argparse
7 import argparse
8 import copy
8 import copy
9 import logging
9 import logging
10 import os
10 import os
11 import re
11 import re
12 import sys
12 import sys
13 import json
13 import json
14
14
15 from IPython.utils.path import filefind, get_ipython_dir
15 from IPython.utils.path import filefind, get_ipython_dir
16 from IPython.utils import py3compat
16 from IPython.utils import py3compat
17 from IPython.utils.encoding import DEFAULT_ENCODING
17 from IPython.utils.encoding import DEFAULT_ENCODING
18 from IPython.utils.py3compat import unicode_type, iteritems
18 from IPython.utils.py3compat import unicode_type, iteritems
19 from IPython.utils.traitlets import HasTraits, List, Any
19 from IPython.utils.traitlets import HasTraits, List, Any
20
20
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22 # Exceptions
22 # Exceptions
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24
24
25
25
26 class ConfigError(Exception):
26 class ConfigError(Exception):
27 pass
27 pass
28
28
29 class ConfigLoaderError(ConfigError):
29 class ConfigLoaderError(ConfigError):
30 pass
30 pass
31
31
32 class ConfigFileNotFound(ConfigError):
32 class ConfigFileNotFound(ConfigError):
33 pass
33 pass
34
34
35 class ArgumentError(ConfigLoaderError):
35 class ArgumentError(ConfigLoaderError):
36 pass
36 pass
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Argparse fix
39 # Argparse fix
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 # Unfortunately argparse by default prints help messages to stderr instead of
42 # Unfortunately argparse by default prints help messages to stderr instead of
43 # stdout. This makes it annoying to capture long help screens at the command
43 # stdout. This makes it annoying to capture long help screens at the command
44 # line, since one must know how to pipe stderr, which many users don't know how
44 # line, since one must know how to pipe stderr, which many users don't know how
45 # to do. So we override the print_help method with one that defaults to
45 # to do. So we override the print_help method with one that defaults to
46 # stdout and use our class instead.
46 # stdout and use our class instead.
47
47
48 class ArgumentParser(argparse.ArgumentParser):
48 class ArgumentParser(argparse.ArgumentParser):
49 """Simple argparse subclass that prints help to stdout by default."""
49 """Simple argparse subclass that prints help to stdout by default."""
50
50
51 def print_help(self, file=None):
51 def print_help(self, file=None):
52 if file is None:
52 if file is None:
53 file = sys.stdout
53 file = sys.stdout
54 return super(ArgumentParser, self).print_help(file)
54 return super(ArgumentParser, self).print_help(file)
55
55
56 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
56 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
57
57
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59 # Config class for holding config information
59 # Config class for holding config information
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61
61
62 class LazyConfigValue(HasTraits):
62 class LazyConfigValue(HasTraits):
63 """Proxy object for exposing methods on configurable containers
63 """Proxy object for exposing methods on configurable containers
64
64
65 Exposes:
65 Exposes:
66
66
67 - append, extend, insert on lists
67 - append, extend, insert on lists
68 - update on dicts
68 - update on dicts
69 - update, add on sets
69 - update, add on sets
70 """
70 """
71
71
72 _value = None
72 _value = None
73
73
74 # list methods
74 # list methods
75 _extend = List()
75 _extend = List()
76 _prepend = List()
76 _prepend = List()
77
77
78 def append(self, obj):
78 def append(self, obj):
79 self._extend.append(obj)
79 self._extend.append(obj)
80
80
81 def extend(self, other):
81 def extend(self, other):
82 self._extend.extend(other)
82 self._extend.extend(other)
83
83
84 def prepend(self, other):
84 def prepend(self, other):
85 """like list.extend, but for the front"""
85 """like list.extend, but for the front"""
86 self._prepend[:0] = other
86 self._prepend[:0] = other
87
87
88 _inserts = List()
88 _inserts = List()
89 def insert(self, index, other):
89 def insert(self, index, other):
90 if not isinstance(index, int):
90 if not isinstance(index, int):
91 raise TypeError("An integer is required")
91 raise TypeError("An integer is required")
92 self._inserts.append((index, other))
92 self._inserts.append((index, other))
93
93
94 # dict methods
94 # dict methods
95 # update is used for both dict and set
95 # update is used for both dict and set
96 _update = Any()
96 _update = Any()
97 def update(self, other):
97 def update(self, other):
98 if self._update is None:
98 if self._update is None:
99 if isinstance(other, dict):
99 if isinstance(other, dict):
100 self._update = {}
100 self._update = {}
101 else:
101 else:
102 self._update = set()
102 self._update = set()
103 self._update.update(other)
103 self._update.update(other)
104
104
105 # set methods
105 # set methods
106 def add(self, obj):
106 def add(self, obj):
107 self.update({obj})
107 self.update({obj})
108
108
109 def get_value(self, initial):
109 def get_value(self, initial):
110 """construct the value from the initial one
110 """construct the value from the initial one
111
111
112 after applying any insert / extend / update changes
112 after applying any insert / extend / update changes
113 """
113 """
114 if self._value is not None:
114 if self._value is not None:
115 return self._value
115 return self._value
116 value = copy.deepcopy(initial)
116 value = copy.deepcopy(initial)
117 if isinstance(value, list):
117 if isinstance(value, list):
118 for idx, obj in self._inserts:
118 for idx, obj in self._inserts:
119 value.insert(idx, obj)
119 value.insert(idx, obj)
120 value[:0] = self._prepend
120 value[:0] = self._prepend
121 value.extend(self._extend)
121 value.extend(self._extend)
122
122
123 elif isinstance(value, dict):
123 elif isinstance(value, dict):
124 if self._update:
124 if self._update:
125 value.update(self._update)
125 value.update(self._update)
126 elif isinstance(value, set):
126 elif isinstance(value, set):
127 if self._update:
127 if self._update:
128 value.update(self._update)
128 value.update(self._update)
129 self._value = value
129 self._value = value
130 return value
130 return value
131
131
132 def to_dict(self):
132 def to_dict(self):
133 """return JSONable dict form of my data
133 """return JSONable dict form of my data
134
134
135 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
135 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
136 """
136 """
137 d = {}
137 d = {}
138 if self._update:
138 if self._update:
139 d['update'] = self._update
139 d['update'] = self._update
140 if self._extend:
140 if self._extend:
141 d['extend'] = self._extend
141 d['extend'] = self._extend
142 if self._prepend:
142 if self._prepend:
143 d['prepend'] = self._prepend
143 d['prepend'] = self._prepend
144 elif self._inserts:
144 elif self._inserts:
145 d['inserts'] = self._inserts
145 d['inserts'] = self._inserts
146 return d
146 return d
147
147
148
148
149 def _is_section_key(key):
149 def _is_section_key(key):
150 """Is a Config key a section name (does it start with a capital)?"""
150 """Is a Config key a section name (does it start with a capital)?"""
151 if key and key[0].upper()==key[0] and not key.startswith('_'):
151 if key and key[0].upper()==key[0] and not key.startswith('_'):
152 return True
152 return True
153 else:
153 else:
154 return False
154 return False
155
155
156
156
157 class Config(dict):
157 class Config(dict):
158 """An attribute based dict that can do smart merges."""
158 """An attribute based dict that can do smart merges."""
159
159
160 def __init__(self, *args, **kwds):
160 def __init__(self, *args, **kwds):
161 dict.__init__(self, *args, **kwds)
161 dict.__init__(self, *args, **kwds)
162 self._ensure_subconfig()
162 self._ensure_subconfig()
163
163
164 def _ensure_subconfig(self):
164 def _ensure_subconfig(self):
165 """ensure that sub-dicts that should be Config objects are
165 """ensure that sub-dicts that should be Config objects are
166
166
167 casts dicts that are under section keys to Config objects,
167 casts dicts that are under section keys to Config objects,
168 which is necessary for constructing Config objects from dict literals.
168 which is necessary for constructing Config objects from dict literals.
169 """
169 """
170 for key in self:
170 for key in self:
171 obj = self[key]
171 obj = self[key]
172 if _is_section_key(key) \
172 if _is_section_key(key) \
173 and isinstance(obj, dict) \
173 and isinstance(obj, dict) \
174 and not isinstance(obj, Config):
174 and not isinstance(obj, Config):
175 setattr(self, key, Config(obj))
175 setattr(self, key, Config(obj))
176
176
177 def _merge(self, other):
177 def _merge(self, other):
178 """deprecated alias, use Config.merge()"""
178 """deprecated alias, use Config.merge()"""
179 self.merge(other)
179 self.merge(other)
180
180
181 def merge(self, other):
181 def merge(self, other):
182 """merge another config object into this one"""
182 """merge another config object into this one"""
183 to_update = {}
183 to_update = {}
184 for k, v in iteritems(other):
184 for k, v in iteritems(other):
185 if k not in self:
185 if k not in self:
186 to_update[k] = copy.deepcopy(v)
186 to_update[k] = copy.deepcopy(v)
187 else: # I have this key
187 else: # I have this key
188 if isinstance(v, Config) and isinstance(self[k], Config):
188 if isinstance(v, Config) and isinstance(self[k], Config):
189 # Recursively merge common sub Configs
189 # Recursively merge common sub Configs
190 self[k].merge(v)
190 self[k].merge(v)
191 else:
191 else:
192 # Plain updates for non-Configs
192 # Plain updates for non-Configs
193 to_update[k] = copy.deepcopy(v)
193 to_update[k] = copy.deepcopy(v)
194
194
195 self.update(to_update)
195 self.update(to_update)
196
196
197 def collisions(self, other):
198 """Check for collisions between two config objects.
199
200 Returns a dict of the form {"Class": {"trait": "collision message"}}`,
201 indicating which values have been ignored.
202
203 An empty dict indicates no collisions.
204 """
205 collisions = {}
206 for section in self:
207 if section not in other:
208 continue
209 mine = self[section]
210 theirs = other[section]
211 for key in mine:
212 if key in theirs and mine[key] != theirs[key]:
213 collisions.setdefault(section, {})
214 collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
215 return collisions
216
197 def __contains__(self, key):
217 def __contains__(self, key):
198 # allow nested contains of the form `"Section.key" in config`
218 # allow nested contains of the form `"Section.key" in config`
199 if '.' in key:
219 if '.' in key:
200 first, remainder = key.split('.', 1)
220 first, remainder = key.split('.', 1)
201 if first not in self:
221 if first not in self:
202 return False
222 return False
203 return remainder in self[first]
223 return remainder in self[first]
204
224
205 return super(Config, self).__contains__(key)
225 return super(Config, self).__contains__(key)
206
226
207 # .has_key is deprecated for dictionaries.
227 # .has_key is deprecated for dictionaries.
208 has_key = __contains__
228 has_key = __contains__
209
229
210 def _has_section(self, key):
230 def _has_section(self, key):
211 return _is_section_key(key) and key in self
231 return _is_section_key(key) and key in self
212
232
213 def copy(self):
233 def copy(self):
214 return type(self)(dict.copy(self))
234 return type(self)(dict.copy(self))
215
235
216 def __copy__(self):
236 def __copy__(self):
217 return self.copy()
237 return self.copy()
218
238
219 def __deepcopy__(self, memo):
239 def __deepcopy__(self, memo):
220 import copy
240 import copy
221 return type(self)(copy.deepcopy(list(self.items())))
241 return type(self)(copy.deepcopy(list(self.items())))
222
242
223 def __getitem__(self, key):
243 def __getitem__(self, key):
224 try:
244 try:
225 return dict.__getitem__(self, key)
245 return dict.__getitem__(self, key)
226 except KeyError:
246 except KeyError:
227 if _is_section_key(key):
247 if _is_section_key(key):
228 c = Config()
248 c = Config()
229 dict.__setitem__(self, key, c)
249 dict.__setitem__(self, key, c)
230 return c
250 return c
231 elif not key.startswith('_'):
251 elif not key.startswith('_'):
232 # undefined, create lazy value, used for container methods
252 # undefined, create lazy value, used for container methods
233 v = LazyConfigValue()
253 v = LazyConfigValue()
234 dict.__setitem__(self, key, v)
254 dict.__setitem__(self, key, v)
235 return v
255 return v
236 else:
256 else:
237 raise KeyError
257 raise KeyError
238
258
239 def __setitem__(self, key, value):
259 def __setitem__(self, key, value):
240 if _is_section_key(key):
260 if _is_section_key(key):
241 if not isinstance(value, Config):
261 if not isinstance(value, Config):
242 raise ValueError('values whose keys begin with an uppercase '
262 raise ValueError('values whose keys begin with an uppercase '
243 'char must be Config instances: %r, %r' % (key, value))
263 'char must be Config instances: %r, %r' % (key, value))
244 dict.__setitem__(self, key, value)
264 dict.__setitem__(self, key, value)
245
265
246 def __getattr__(self, key):
266 def __getattr__(self, key):
247 if key.startswith('__'):
267 if key.startswith('__'):
248 return dict.__getattr__(self, key)
268 return dict.__getattr__(self, key)
249 try:
269 try:
250 return self.__getitem__(key)
270 return self.__getitem__(key)
251 except KeyError as e:
271 except KeyError as e:
252 raise AttributeError(e)
272 raise AttributeError(e)
253
273
254 def __setattr__(self, key, value):
274 def __setattr__(self, key, value):
255 if key.startswith('__'):
275 if key.startswith('__'):
256 return dict.__setattr__(self, key, value)
276 return dict.__setattr__(self, key, value)
257 try:
277 try:
258 self.__setitem__(key, value)
278 self.__setitem__(key, value)
259 except KeyError as e:
279 except KeyError as e:
260 raise AttributeError(e)
280 raise AttributeError(e)
261
281
262 def __delattr__(self, key):
282 def __delattr__(self, key):
263 if key.startswith('__'):
283 if key.startswith('__'):
264 return dict.__delattr__(self, key)
284 return dict.__delattr__(self, key)
265 try:
285 try:
266 dict.__delitem__(self, key)
286 dict.__delitem__(self, key)
267 except KeyError as e:
287 except KeyError as e:
268 raise AttributeError(e)
288 raise AttributeError(e)
269
289
270
290
271 #-----------------------------------------------------------------------------
291 #-----------------------------------------------------------------------------
272 # Config loading classes
292 # Config loading classes
273 #-----------------------------------------------------------------------------
293 #-----------------------------------------------------------------------------
274
294
275
295
276 class ConfigLoader(object):
296 class ConfigLoader(object):
277 """A object for loading configurations from just about anywhere.
297 """A object for loading configurations from just about anywhere.
278
298
279 The resulting configuration is packaged as a :class:`Config`.
299 The resulting configuration is packaged as a :class:`Config`.
280
300
281 Notes
301 Notes
282 -----
302 -----
283 A :class:`ConfigLoader` does one thing: load a config from a source
303 A :class:`ConfigLoader` does one thing: load a config from a source
284 (file, command line arguments) and returns the data as a :class:`Config` object.
304 (file, command line arguments) and returns the data as a :class:`Config` object.
285 There are lots of things that :class:`ConfigLoader` does not do. It does
305 There are lots of things that :class:`ConfigLoader` does not do. It does
286 not implement complex logic for finding config files. It does not handle
306 not implement complex logic for finding config files. It does not handle
287 default values or merge multiple configs. These things need to be
307 default values or merge multiple configs. These things need to be
288 handled elsewhere.
308 handled elsewhere.
289 """
309 """
290
310
291 def _log_default(self):
311 def _log_default(self):
292 from IPython.utils.log import get_logger
312 from IPython.utils.log import get_logger
293 return get_logger()
313 return get_logger()
294
314
295 def __init__(self, log=None):
315 def __init__(self, log=None):
296 """A base class for config loaders.
316 """A base class for config loaders.
297
317
298 log : instance of :class:`logging.Logger` to use.
318 log : instance of :class:`logging.Logger` to use.
299 By default loger of :meth:`IPython.config.application.Application.instance()`
319 By default loger of :meth:`IPython.config.application.Application.instance()`
300 will be used
320 will be used
301
321
302 Examples
322 Examples
303 --------
323 --------
304
324
305 >>> cl = ConfigLoader()
325 >>> cl = ConfigLoader()
306 >>> config = cl.load_config()
326 >>> config = cl.load_config()
307 >>> config
327 >>> config
308 {}
328 {}
309 """
329 """
310 self.clear()
330 self.clear()
311 if log is None:
331 if log is None:
312 self.log = self._log_default()
332 self.log = self._log_default()
313 self.log.debug('Using default logger')
333 self.log.debug('Using default logger')
314 else:
334 else:
315 self.log = log
335 self.log = log
316
336
317 def clear(self):
337 def clear(self):
318 self.config = Config()
338 self.config = Config()
319
339
320 def load_config(self):
340 def load_config(self):
321 """Load a config from somewhere, return a :class:`Config` instance.
341 """Load a config from somewhere, return a :class:`Config` instance.
322
342
323 Usually, this will cause self.config to be set and then returned.
343 Usually, this will cause self.config to be set and then returned.
324 However, in most cases, :meth:`ConfigLoader.clear` should be called
344 However, in most cases, :meth:`ConfigLoader.clear` should be called
325 to erase any previous state.
345 to erase any previous state.
326 """
346 """
327 self.clear()
347 self.clear()
328 return self.config
348 return self.config
329
349
330
350
331 class FileConfigLoader(ConfigLoader):
351 class FileConfigLoader(ConfigLoader):
332 """A base class for file based configurations.
352 """A base class for file based configurations.
333
353
334 As we add more file based config loaders, the common logic should go
354 As we add more file based config loaders, the common logic should go
335 here.
355 here.
336 """
356 """
337
357
338 def __init__(self, filename, path=None, **kw):
358 def __init__(self, filename, path=None, **kw):
339 """Build a config loader for a filename and path.
359 """Build a config loader for a filename and path.
340
360
341 Parameters
361 Parameters
342 ----------
362 ----------
343 filename : str
363 filename : str
344 The file name of the config file.
364 The file name of the config file.
345 path : str, list, tuple
365 path : str, list, tuple
346 The path to search for the config file on, or a sequence of
366 The path to search for the config file on, or a sequence of
347 paths to try in order.
367 paths to try in order.
348 """
368 """
349 super(FileConfigLoader, self).__init__(**kw)
369 super(FileConfigLoader, self).__init__(**kw)
350 self.filename = filename
370 self.filename = filename
351 self.path = path
371 self.path = path
352 self.full_filename = ''
372 self.full_filename = ''
353
373
354 def _find_file(self):
374 def _find_file(self):
355 """Try to find the file by searching the paths."""
375 """Try to find the file by searching the paths."""
356 self.full_filename = filefind(self.filename, self.path)
376 self.full_filename = filefind(self.filename, self.path)
357
377
358 class JSONFileConfigLoader(FileConfigLoader):
378 class JSONFileConfigLoader(FileConfigLoader):
359 """A Json file loader for config"""
379 """A Json file loader for config"""
360
380
361 def load_config(self):
381 def load_config(self):
362 """Load the config from a file and return it as a Config object."""
382 """Load the config from a file and return it as a Config object."""
363 self.clear()
383 self.clear()
364 try:
384 try:
365 self._find_file()
385 self._find_file()
366 except IOError as e:
386 except IOError as e:
367 raise ConfigFileNotFound(str(e))
387 raise ConfigFileNotFound(str(e))
368 dct = self._read_file_as_dict()
388 dct = self._read_file_as_dict()
369 self.config = self._convert_to_config(dct)
389 self.config = self._convert_to_config(dct)
370 return self.config
390 return self.config
371
391
372 def _read_file_as_dict(self):
392 def _read_file_as_dict(self):
373 with open(self.full_filename) as f:
393 with open(self.full_filename) as f:
374 return json.load(f)
394 return json.load(f)
375
395
376 def _convert_to_config(self, dictionary):
396 def _convert_to_config(self, dictionary):
377 if 'version' in dictionary:
397 if 'version' in dictionary:
378 version = dictionary.pop('version')
398 version = dictionary.pop('version')
379 else:
399 else:
380 version = 1
400 version = 1
381 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
401 self.log.warn("Unrecognized JSON config file version, assuming version {}".format(version))
382
402
383 if version == 1:
403 if version == 1:
384 return Config(dictionary)
404 return Config(dictionary)
385 else:
405 else:
386 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
406 raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
387
407
388
408
389 class PyFileConfigLoader(FileConfigLoader):
409 class PyFileConfigLoader(FileConfigLoader):
390 """A config loader for pure python files.
410 """A config loader for pure python files.
391
411
392 This is responsible for locating a Python config file by filename and
412 This is responsible for locating a Python config file by filename and
393 path, then executing it to construct a Config object.
413 path, then executing it to construct a Config object.
394 """
414 """
395
415
396 def load_config(self):
416 def load_config(self):
397 """Load the config from a file and return it as a Config object."""
417 """Load the config from a file and return it as a Config object."""
398 self.clear()
418 self.clear()
399 try:
419 try:
400 self._find_file()
420 self._find_file()
401 except IOError as e:
421 except IOError as e:
402 raise ConfigFileNotFound(str(e))
422 raise ConfigFileNotFound(str(e))
403 self._read_file_as_dict()
423 self._read_file_as_dict()
404 return self.config
424 return self.config
405
425
406
426
407 def _read_file_as_dict(self):
427 def _read_file_as_dict(self):
408 """Load the config file into self.config, with recursive loading."""
428 """Load the config file into self.config, with recursive loading."""
409 # This closure is made available in the namespace that is used
429 # This closure is made available in the namespace that is used
410 # to exec the config file. It allows users to call
430 # to exec the config file. It allows users to call
411 # load_subconfig('myconfig.py') to load config files recursively.
431 # load_subconfig('myconfig.py') to load config files recursively.
412 # It needs to be a closure because it has references to self.path
432 # It needs to be a closure because it has references to self.path
413 # and self.config. The sub-config is loaded with the same path
433 # and self.config. The sub-config is loaded with the same path
414 # as the parent, but it uses an empty config which is then merged
434 # as the parent, but it uses an empty config which is then merged
415 # with the parents.
435 # with the parents.
416
436
417 # If a profile is specified, the config file will be loaded
437 # If a profile is specified, the config file will be loaded
418 # from that profile
438 # from that profile
419
439
420 def load_subconfig(fname, profile=None):
440 def load_subconfig(fname, profile=None):
421 # import here to prevent circular imports
441 # import here to prevent circular imports
422 from IPython.core.profiledir import ProfileDir, ProfileDirError
442 from IPython.core.profiledir import ProfileDir, ProfileDirError
423 if profile is not None:
443 if profile is not None:
424 try:
444 try:
425 profile_dir = ProfileDir.find_profile_dir_by_name(
445 profile_dir = ProfileDir.find_profile_dir_by_name(
426 get_ipython_dir(),
446 get_ipython_dir(),
427 profile,
447 profile,
428 )
448 )
429 except ProfileDirError:
449 except ProfileDirError:
430 return
450 return
431 path = profile_dir.location
451 path = profile_dir.location
432 else:
452 else:
433 path = self.path
453 path = self.path
434 loader = PyFileConfigLoader(fname, path)
454 loader = PyFileConfigLoader(fname, path)
435 try:
455 try:
436 sub_config = loader.load_config()
456 sub_config = loader.load_config()
437 except ConfigFileNotFound:
457 except ConfigFileNotFound:
438 # Pass silently if the sub config is not there. This happens
458 # Pass silently if the sub config is not there. This happens
439 # when a user s using a profile, but not the default config.
459 # when a user s using a profile, but not the default config.
440 pass
460 pass
441 else:
461 else:
442 self.config.merge(sub_config)
462 self.config.merge(sub_config)
443
463
444 # Again, this needs to be a closure and should be used in config
464 # Again, this needs to be a closure and should be used in config
445 # files to get the config being loaded.
465 # files to get the config being loaded.
446 def get_config():
466 def get_config():
447 return self.config
467 return self.config
448
468
449 namespace = dict(
469 namespace = dict(
450 load_subconfig=load_subconfig,
470 load_subconfig=load_subconfig,
451 get_config=get_config,
471 get_config=get_config,
452 __file__=self.full_filename,
472 __file__=self.full_filename,
453 )
473 )
454 fs_encoding = sys.getfilesystemencoding() or 'ascii'
474 fs_encoding = sys.getfilesystemencoding() or 'ascii'
455 conf_filename = self.full_filename.encode(fs_encoding)
475 conf_filename = self.full_filename.encode(fs_encoding)
456 py3compat.execfile(conf_filename, namespace)
476 py3compat.execfile(conf_filename, namespace)
457
477
458
478
459 class CommandLineConfigLoader(ConfigLoader):
479 class CommandLineConfigLoader(ConfigLoader):
460 """A config loader for command line arguments.
480 """A config loader for command line arguments.
461
481
462 As we add more command line based loaders, the common logic should go
482 As we add more command line based loaders, the common logic should go
463 here.
483 here.
464 """
484 """
465
485
466 def _exec_config_str(self, lhs, rhs):
486 def _exec_config_str(self, lhs, rhs):
467 """execute self.config.<lhs> = <rhs>
487 """execute self.config.<lhs> = <rhs>
468
488
469 * expands ~ with expanduser
489 * expands ~ with expanduser
470 * tries to assign with raw eval, otherwise assigns with just the string,
490 * tries to assign with raw eval, otherwise assigns with just the string,
471 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
491 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
472 equivalent are `--C.a=4` and `--C.a='4'`.
492 equivalent are `--C.a=4` and `--C.a='4'`.
473 """
493 """
474 rhs = os.path.expanduser(rhs)
494 rhs = os.path.expanduser(rhs)
475 try:
495 try:
476 # Try to see if regular Python syntax will work. This
496 # Try to see if regular Python syntax will work. This
477 # won't handle strings as the quote marks are removed
497 # won't handle strings as the quote marks are removed
478 # by the system shell.
498 # by the system shell.
479 value = eval(rhs)
499 value = eval(rhs)
480 except (NameError, SyntaxError):
500 except (NameError, SyntaxError):
481 # This case happens if the rhs is a string.
501 # This case happens if the rhs is a string.
482 value = rhs
502 value = rhs
483
503
484 exec(u'self.config.%s = value' % lhs)
504 exec(u'self.config.%s = value' % lhs)
485
505
486 def _load_flag(self, cfg):
506 def _load_flag(self, cfg):
487 """update self.config from a flag, which can be a dict or Config"""
507 """update self.config from a flag, which can be a dict or Config"""
488 if isinstance(cfg, (dict, Config)):
508 if isinstance(cfg, (dict, Config)):
489 # don't clobber whole config sections, update
509 # don't clobber whole config sections, update
490 # each section from config:
510 # each section from config:
491 for sec,c in iteritems(cfg):
511 for sec,c in iteritems(cfg):
492 self.config[sec].update(c)
512 self.config[sec].update(c)
493 else:
513 else:
494 raise TypeError("Invalid flag: %r" % cfg)
514 raise TypeError("Invalid flag: %r" % cfg)
495
515
496 # raw --identifier=value pattern
516 # raw --identifier=value pattern
497 # but *also* accept '-' as wordsep, for aliases
517 # but *also* accept '-' as wordsep, for aliases
498 # accepts: --foo=a
518 # accepts: --foo=a
499 # --Class.trait=value
519 # --Class.trait=value
500 # --alias-name=value
520 # --alias-name=value
501 # rejects: -foo=value
521 # rejects: -foo=value
502 # --foo
522 # --foo
503 # --Class.trait
523 # --Class.trait
504 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
524 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
505
525
506 # just flags, no assignments, with two *or one* leading '-'
526 # just flags, no assignments, with two *or one* leading '-'
507 # accepts: --foo
527 # accepts: --foo
508 # -foo-bar-again
528 # -foo-bar-again
509 # rejects: --anything=anything
529 # rejects: --anything=anything
510 # --two.word
530 # --two.word
511
531
512 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
532 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
513
533
514 class KeyValueConfigLoader(CommandLineConfigLoader):
534 class KeyValueConfigLoader(CommandLineConfigLoader):
515 """A config loader that loads key value pairs from the command line.
535 """A config loader that loads key value pairs from the command line.
516
536
517 This allows command line options to be gives in the following form::
537 This allows command line options to be gives in the following form::
518
538
519 ipython --profile="foo" --InteractiveShell.autocall=False
539 ipython --profile="foo" --InteractiveShell.autocall=False
520 """
540 """
521
541
522 def __init__(self, argv=None, aliases=None, flags=None, **kw):
542 def __init__(self, argv=None, aliases=None, flags=None, **kw):
523 """Create a key value pair config loader.
543 """Create a key value pair config loader.
524
544
525 Parameters
545 Parameters
526 ----------
546 ----------
527 argv : list
547 argv : list
528 A list that has the form of sys.argv[1:] which has unicode
548 A list that has the form of sys.argv[1:] which has unicode
529 elements of the form u"key=value". If this is None (default),
549 elements of the form u"key=value". If this is None (default),
530 then sys.argv[1:] will be used.
550 then sys.argv[1:] will be used.
531 aliases : dict
551 aliases : dict
532 A dict of aliases for configurable traits.
552 A dict of aliases for configurable traits.
533 Keys are the short aliases, Values are the resolved trait.
553 Keys are the short aliases, Values are the resolved trait.
534 Of the form: `{'alias' : 'Configurable.trait'}`
554 Of the form: `{'alias' : 'Configurable.trait'}`
535 flags : dict
555 flags : dict
536 A dict of flags, keyed by str name. Vaues can be Config objects,
556 A dict of flags, keyed by str name. Vaues can be Config objects,
537 dicts, or "key=value" strings. If Config or dict, when the flag
557 dicts, or "key=value" strings. If Config or dict, when the flag
538 is triggered, The flag is loaded as `self.config.update(m)`.
558 is triggered, The flag is loaded as `self.config.update(m)`.
539
559
540 Returns
560 Returns
541 -------
561 -------
542 config : Config
562 config : Config
543 The resulting Config object.
563 The resulting Config object.
544
564
545 Examples
565 Examples
546 --------
566 --------
547
567
548 >>> from IPython.config.loader import KeyValueConfigLoader
568 >>> from IPython.config.loader import KeyValueConfigLoader
549 >>> cl = KeyValueConfigLoader()
569 >>> cl = KeyValueConfigLoader()
550 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
570 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
551 >>> sorted(d.items())
571 >>> sorted(d.items())
552 [('A', {'name': 'brian'}), ('B', {'number': 0})]
572 [('A', {'name': 'brian'}), ('B', {'number': 0})]
553 """
573 """
554 super(KeyValueConfigLoader, self).__init__(**kw)
574 super(KeyValueConfigLoader, self).__init__(**kw)
555 if argv is None:
575 if argv is None:
556 argv = sys.argv[1:]
576 argv = sys.argv[1:]
557 self.argv = argv
577 self.argv = argv
558 self.aliases = aliases or {}
578 self.aliases = aliases or {}
559 self.flags = flags or {}
579 self.flags = flags or {}
560
580
561
581
562 def clear(self):
582 def clear(self):
563 super(KeyValueConfigLoader, self).clear()
583 super(KeyValueConfigLoader, self).clear()
564 self.extra_args = []
584 self.extra_args = []
565
585
566
586
567 def _decode_argv(self, argv, enc=None):
587 def _decode_argv(self, argv, enc=None):
568 """decode argv if bytes, using stin.encoding, falling back on default enc"""
588 """decode argv if bytes, using stdin.encoding, falling back on default enc"""
569 uargv = []
589 uargv = []
570 if enc is None:
590 if enc is None:
571 enc = DEFAULT_ENCODING
591 enc = DEFAULT_ENCODING
572 for arg in argv:
592 for arg in argv:
573 if not isinstance(arg, unicode_type):
593 if not isinstance(arg, unicode_type):
574 # only decode if not already decoded
594 # only decode if not already decoded
575 arg = arg.decode(enc)
595 arg = arg.decode(enc)
576 uargv.append(arg)
596 uargv.append(arg)
577 return uargv
597 return uargv
578
598
579
599
580 def load_config(self, argv=None, aliases=None, flags=None):
600 def load_config(self, argv=None, aliases=None, flags=None):
581 """Parse the configuration and generate the Config object.
601 """Parse the configuration and generate the Config object.
582
602
583 After loading, any arguments that are not key-value or
603 After loading, any arguments that are not key-value or
584 flags will be stored in self.extra_args - a list of
604 flags will be stored in self.extra_args - a list of
585 unparsed command-line arguments. This is used for
605 unparsed command-line arguments. This is used for
586 arguments such as input files or subcommands.
606 arguments such as input files or subcommands.
587
607
588 Parameters
608 Parameters
589 ----------
609 ----------
590 argv : list, optional
610 argv : list, optional
591 A list that has the form of sys.argv[1:] which has unicode
611 A list that has the form of sys.argv[1:] which has unicode
592 elements of the form u"key=value". If this is None (default),
612 elements of the form u"key=value". If this is None (default),
593 then self.argv will be used.
613 then self.argv will be used.
594 aliases : dict
614 aliases : dict
595 A dict of aliases for configurable traits.
615 A dict of aliases for configurable traits.
596 Keys are the short aliases, Values are the resolved trait.
616 Keys are the short aliases, Values are the resolved trait.
597 Of the form: `{'alias' : 'Configurable.trait'}`
617 Of the form: `{'alias' : 'Configurable.trait'}`
598 flags : dict
618 flags : dict
599 A dict of flags, keyed by str name. Values can be Config objects
619 A dict of flags, keyed by str name. Values can be Config objects
600 or dicts. When the flag is triggered, The config is loaded as
620 or dicts. When the flag is triggered, The config is loaded as
601 `self.config.update(cfg)`.
621 `self.config.update(cfg)`.
602 """
622 """
603 self.clear()
623 self.clear()
604 if argv is None:
624 if argv is None:
605 argv = self.argv
625 argv = self.argv
606 if aliases is None:
626 if aliases is None:
607 aliases = self.aliases
627 aliases = self.aliases
608 if flags is None:
628 if flags is None:
609 flags = self.flags
629 flags = self.flags
610
630
611 # ensure argv is a list of unicode strings:
631 # ensure argv is a list of unicode strings:
612 uargv = self._decode_argv(argv)
632 uargv = self._decode_argv(argv)
613 for idx,raw in enumerate(uargv):
633 for idx,raw in enumerate(uargv):
614 # strip leading '-'
634 # strip leading '-'
615 item = raw.lstrip('-')
635 item = raw.lstrip('-')
616
636
617 if raw == '--':
637 if raw == '--':
618 # don't parse arguments after '--'
638 # don't parse arguments after '--'
619 # this is useful for relaying arguments to scripts, e.g.
639 # this is useful for relaying arguments to scripts, e.g.
620 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
640 # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
621 self.extra_args.extend(uargv[idx+1:])
641 self.extra_args.extend(uargv[idx+1:])
622 break
642 break
623
643
624 if kv_pattern.match(raw):
644 if kv_pattern.match(raw):
625 lhs,rhs = item.split('=',1)
645 lhs,rhs = item.split('=',1)
626 # Substitute longnames for aliases.
646 # Substitute longnames for aliases.
627 if lhs in aliases:
647 if lhs in aliases:
628 lhs = aliases[lhs]
648 lhs = aliases[lhs]
629 if '.' not in lhs:
649 if '.' not in lhs:
630 # probably a mistyped alias, but not technically illegal
650 # probably a mistyped alias, but not technically illegal
631 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
651 self.log.warn("Unrecognized alias: '%s', it will probably have no effect.", raw)
632 try:
652 try:
633 self._exec_config_str(lhs, rhs)
653 self._exec_config_str(lhs, rhs)
634 except Exception:
654 except Exception:
635 raise ArgumentError("Invalid argument: '%s'" % raw)
655 raise ArgumentError("Invalid argument: '%s'" % raw)
636
656
637 elif flag_pattern.match(raw):
657 elif flag_pattern.match(raw):
638 if item in flags:
658 if item in flags:
639 cfg,help = flags[item]
659 cfg,help = flags[item]
640 self._load_flag(cfg)
660 self._load_flag(cfg)
641 else:
661 else:
642 raise ArgumentError("Unrecognized flag: '%s'"%raw)
662 raise ArgumentError("Unrecognized flag: '%s'"%raw)
643 elif raw.startswith('-'):
663 elif raw.startswith('-'):
644 kv = '--'+item
664 kv = '--'+item
645 if kv_pattern.match(kv):
665 if kv_pattern.match(kv):
646 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
666 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
647 else:
667 else:
648 raise ArgumentError("Invalid argument: '%s'"%raw)
668 raise ArgumentError("Invalid argument: '%s'"%raw)
649 else:
669 else:
650 # keep all args that aren't valid in a list,
670 # keep all args that aren't valid in a list,
651 # in case our parent knows what to do with them.
671 # in case our parent knows what to do with them.
652 self.extra_args.append(item)
672 self.extra_args.append(item)
653 return self.config
673 return self.config
654
674
655 class ArgParseConfigLoader(CommandLineConfigLoader):
675 class ArgParseConfigLoader(CommandLineConfigLoader):
656 """A loader that uses the argparse module to load from the command line."""
676 """A loader that uses the argparse module to load from the command line."""
657
677
658 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
678 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
659 """Create a config loader for use with argparse.
679 """Create a config loader for use with argparse.
660
680
661 Parameters
681 Parameters
662 ----------
682 ----------
663
683
664 argv : optional, list
684 argv : optional, list
665 If given, used to read command-line arguments from, otherwise
685 If given, used to read command-line arguments from, otherwise
666 sys.argv[1:] is used.
686 sys.argv[1:] is used.
667
687
668 parser_args : tuple
688 parser_args : tuple
669 A tuple of positional arguments that will be passed to the
689 A tuple of positional arguments that will be passed to the
670 constructor of :class:`argparse.ArgumentParser`.
690 constructor of :class:`argparse.ArgumentParser`.
671
691
672 parser_kw : dict
692 parser_kw : dict
673 A tuple of keyword arguments that will be passed to the
693 A tuple of keyword arguments that will be passed to the
674 constructor of :class:`argparse.ArgumentParser`.
694 constructor of :class:`argparse.ArgumentParser`.
675
695
676 Returns
696 Returns
677 -------
697 -------
678 config : Config
698 config : Config
679 The resulting Config object.
699 The resulting Config object.
680 """
700 """
681 super(CommandLineConfigLoader, self).__init__(log=log)
701 super(CommandLineConfigLoader, self).__init__(log=log)
682 self.clear()
702 self.clear()
683 if argv is None:
703 if argv is None:
684 argv = sys.argv[1:]
704 argv = sys.argv[1:]
685 self.argv = argv
705 self.argv = argv
686 self.aliases = aliases or {}
706 self.aliases = aliases or {}
687 self.flags = flags or {}
707 self.flags = flags or {}
688
708
689 self.parser_args = parser_args
709 self.parser_args = parser_args
690 self.version = parser_kw.pop("version", None)
710 self.version = parser_kw.pop("version", None)
691 kwargs = dict(argument_default=argparse.SUPPRESS)
711 kwargs = dict(argument_default=argparse.SUPPRESS)
692 kwargs.update(parser_kw)
712 kwargs.update(parser_kw)
693 self.parser_kw = kwargs
713 self.parser_kw = kwargs
694
714
695 def load_config(self, argv=None, aliases=None, flags=None):
715 def load_config(self, argv=None, aliases=None, flags=None):
696 """Parse command line arguments and return as a Config object.
716 """Parse command line arguments and return as a Config object.
697
717
698 Parameters
718 Parameters
699 ----------
719 ----------
700
720
701 args : optional, list
721 args : optional, list
702 If given, a list with the structure of sys.argv[1:] to parse
722 If given, a list with the structure of sys.argv[1:] to parse
703 arguments from. If not given, the instance's self.argv attribute
723 arguments from. If not given, the instance's self.argv attribute
704 (given at construction time) is used."""
724 (given at construction time) is used."""
705 self.clear()
725 self.clear()
706 if argv is None:
726 if argv is None:
707 argv = self.argv
727 argv = self.argv
708 if aliases is None:
728 if aliases is None:
709 aliases = self.aliases
729 aliases = self.aliases
710 if flags is None:
730 if flags is None:
711 flags = self.flags
731 flags = self.flags
712 self._create_parser(aliases, flags)
732 self._create_parser(aliases, flags)
713 self._parse_args(argv)
733 self._parse_args(argv)
714 self._convert_to_config()
734 self._convert_to_config()
715 return self.config
735 return self.config
716
736
717 def get_extra_args(self):
737 def get_extra_args(self):
718 if hasattr(self, 'extra_args'):
738 if hasattr(self, 'extra_args'):
719 return self.extra_args
739 return self.extra_args
720 else:
740 else:
721 return []
741 return []
722
742
723 def _create_parser(self, aliases=None, flags=None):
743 def _create_parser(self, aliases=None, flags=None):
724 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
744 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
725 self._add_arguments(aliases, flags)
745 self._add_arguments(aliases, flags)
726
746
727 def _add_arguments(self, aliases=None, flags=None):
747 def _add_arguments(self, aliases=None, flags=None):
728 raise NotImplementedError("subclasses must implement _add_arguments")
748 raise NotImplementedError("subclasses must implement _add_arguments")
729
749
730 def _parse_args(self, args):
750 def _parse_args(self, args):
731 """self.parser->self.parsed_data"""
751 """self.parser->self.parsed_data"""
732 # decode sys.argv to support unicode command-line options
752 # decode sys.argv to support unicode command-line options
733 enc = DEFAULT_ENCODING
753 enc = DEFAULT_ENCODING
734 uargs = [py3compat.cast_unicode(a, enc) for a in args]
754 uargs = [py3compat.cast_unicode(a, enc) for a in args]
735 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
755 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
736
756
737 def _convert_to_config(self):
757 def _convert_to_config(self):
738 """self.parsed_data->self.config"""
758 """self.parsed_data->self.config"""
739 for k, v in iteritems(vars(self.parsed_data)):
759 for k, v in iteritems(vars(self.parsed_data)):
740 exec("self.config.%s = v"%k, locals(), globals())
760 exec("self.config.%s = v"%k, locals(), globals())
741
761
742 class KVArgParseConfigLoader(ArgParseConfigLoader):
762 class KVArgParseConfigLoader(ArgParseConfigLoader):
743 """A config loader that loads aliases and flags with argparse,
763 """A config loader that loads aliases and flags with argparse,
744 but will use KVLoader for the rest. This allows better parsing
764 but will use KVLoader for the rest. This allows better parsing
745 of common args, such as `ipython -c 'print 5'`, but still gets
765 of common args, such as `ipython -c 'print 5'`, but still gets
746 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
766 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
747
767
748 def _add_arguments(self, aliases=None, flags=None):
768 def _add_arguments(self, aliases=None, flags=None):
749 self.alias_flags = {}
769 self.alias_flags = {}
750 # print aliases, flags
770 # print aliases, flags
751 if aliases is None:
771 if aliases is None:
752 aliases = self.aliases
772 aliases = self.aliases
753 if flags is None:
773 if flags is None:
754 flags = self.flags
774 flags = self.flags
755 paa = self.parser.add_argument
775 paa = self.parser.add_argument
756 for key,value in iteritems(aliases):
776 for key,value in iteritems(aliases):
757 if key in flags:
777 if key in flags:
758 # flags
778 # flags
759 nargs = '?'
779 nargs = '?'
760 else:
780 else:
761 nargs = None
781 nargs = None
762 if len(key) is 1:
782 if len(key) is 1:
763 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
783 paa('-'+key, '--'+key, type=unicode_type, dest=value, nargs=nargs)
764 else:
784 else:
765 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
785 paa('--'+key, type=unicode_type, dest=value, nargs=nargs)
766 for key, (value, help) in iteritems(flags):
786 for key, (value, help) in iteritems(flags):
767 if key in self.aliases:
787 if key in self.aliases:
768 #
788 #
769 self.alias_flags[self.aliases[key]] = value
789 self.alias_flags[self.aliases[key]] = value
770 continue
790 continue
771 if len(key) is 1:
791 if len(key) is 1:
772 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
792 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
773 else:
793 else:
774 paa('--'+key, action='append_const', dest='_flags', const=value)
794 paa('--'+key, action='append_const', dest='_flags', const=value)
775
795
776 def _convert_to_config(self):
796 def _convert_to_config(self):
777 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
797 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
778 # remove subconfigs list from namespace before transforming the Namespace
798 # remove subconfigs list from namespace before transforming the Namespace
779 if '_flags' in self.parsed_data:
799 if '_flags' in self.parsed_data:
780 subcs = self.parsed_data._flags
800 subcs = self.parsed_data._flags
781 del self.parsed_data._flags
801 del self.parsed_data._flags
782 else:
802 else:
783 subcs = []
803 subcs = []
784
804
785 for k, v in iteritems(vars(self.parsed_data)):
805 for k, v in iteritems(vars(self.parsed_data)):
786 if v is None:
806 if v is None:
787 # it was a flag that shares the name of an alias
807 # it was a flag that shares the name of an alias
788 subcs.append(self.alias_flags[k])
808 subcs.append(self.alias_flags[k])
789 else:
809 else:
790 # eval the KV assignment
810 # eval the KV assignment
791 self._exec_config_str(k, v)
811 self._exec_config_str(k, v)
792
812
793 for subc in subcs:
813 for subc in subcs:
794 self._load_flag(subc)
814 self._load_flag(subc)
795
815
796 if self.extra_args:
816 if self.extra_args:
797 sub_parser = KeyValueConfigLoader(log=self.log)
817 sub_parser = KeyValueConfigLoader(log=self.log)
798 sub_parser.load_config(self.extra_args)
818 sub_parser.load_config(self.extra_args)
799 self.config.merge(sub_parser.config)
819 self.config.merge(sub_parser.config)
800 self.extra_args = sub_parser.extra_args
820 self.extra_args = sub_parser.extra_args
801
821
802
822
803 def load_pyconfig_files(config_files, path):
823 def load_pyconfig_files(config_files, path):
804 """Load multiple Python config files, merging each of them in turn.
824 """Load multiple Python config files, merging each of them in turn.
805
825
806 Parameters
826 Parameters
807 ==========
827 ==========
808 config_files : list of str
828 config_files : list of str
809 List of config files names to load and merge into the config.
829 List of config files names to load and merge into the config.
810 path : unicode
830 path : unicode
811 The full path to the location of the config files.
831 The full path to the location of the config files.
812 """
832 """
813 config = Config()
833 config = Config()
814 for cf in config_files:
834 for cf in config_files:
815 loader = PyFileConfigLoader(cf, path=path)
835 loader = PyFileConfigLoader(cf, path=path)
816 try:
836 try:
817 next_config = loader.load_config()
837 next_config = loader.load_config()
818 except ConfigFileNotFound:
838 except ConfigFileNotFound:
819 pass
839 pass
820 except:
840 except:
821 raise
841 raise
822 else:
842 else:
823 config.merge(next_config)
843 config.merge(next_config)
824 return config
844 return config
@@ -1,396 +1,404 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """Tests for IPython.config.loader"""
3 Tests for IPython.config.loader
4
5 Authors:
6
7 * Brian Granger
8 * Fernando Perez (design help)
9 """
10
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008 The IPython Development Team
13 #
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
17
3
18 #-----------------------------------------------------------------------------
4 # Copyright (c) IPython Development Team.
19 # Imports
5 # Distributed under the terms of the Modified BSD License.
20 #-----------------------------------------------------------------------------
21
6
22 import os
7 import os
23 import pickle
8 import pickle
24 import sys
9 import sys
25 import json
26
10
27 from tempfile import mkstemp
11 from tempfile import mkstemp
28 from unittest import TestCase
12 from unittest import TestCase
29
13
30 from nose import SkipTest
14 from nose import SkipTest
31 import nose.tools as nt
15 import nose.tools as nt
32
16
33
17
34
18
35 from IPython.config.loader import (
19 from IPython.config.loader import (
36 Config,
20 Config,
37 LazyConfigValue,
21 LazyConfigValue,
38 PyFileConfigLoader,
22 PyFileConfigLoader,
39 JSONFileConfigLoader,
23 JSONFileConfigLoader,
40 KeyValueConfigLoader,
24 KeyValueConfigLoader,
41 ArgParseConfigLoader,
25 ArgParseConfigLoader,
42 KVArgParseConfigLoader,
26 KVArgParseConfigLoader,
43 ConfigError,
27 ConfigError,
44 )
28 )
45
29
46 #-----------------------------------------------------------------------------
47 # Actual tests
48 #-----------------------------------------------------------------------------
49
50
30
51 pyfile = """
31 pyfile = """
52 c = get_config()
32 c = get_config()
53 c.a=10
33 c.a=10
54 c.b=20
34 c.b=20
55 c.Foo.Bar.value=10
35 c.Foo.Bar.value=10
56 c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3
36 c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3
57 c.D.C.value='hi there'
37 c.D.C.value='hi there'
58 """
38 """
59
39
60 json1file = """
40 json1file = """
61 {
41 {
62 "version": 1,
42 "version": 1,
63 "a": 10,
43 "a": 10,
64 "b": 20,
44 "b": 20,
65 "Foo": {
45 "Foo": {
66 "Bam": {
46 "Bam": {
67 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
47 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
68 },
48 },
69 "Bar": {
49 "Bar": {
70 "value": 10
50 "value": 10
71 }
51 }
72 },
52 },
73 "D": {
53 "D": {
74 "C": {
54 "C": {
75 "value": "hi there"
55 "value": "hi there"
76 }
56 }
77 }
57 }
78 }
58 }
79 """
59 """
80
60
81 # should not load
61 # should not load
82 json2file = """
62 json2file = """
83 {
63 {
84 "version": 2
64 "version": 2
85 }
65 }
86 """
66 """
87
67
88 import logging
68 import logging
89 log = logging.getLogger('devnull')
69 log = logging.getLogger('devnull')
90 log.setLevel(0)
70 log.setLevel(0)
91
71
92 class TestFileCL(TestCase):
72 class TestFileCL(TestCase):
93
73
94 def _check_conf(self, config):
74 def _check_conf(self, config):
95 self.assertEqual(config.a, 10)
75 self.assertEqual(config.a, 10)
96 self.assertEqual(config.b, 20)
76 self.assertEqual(config.b, 20)
97 self.assertEqual(config.Foo.Bar.value, 10)
77 self.assertEqual(config.Foo.Bar.value, 10)
98 self.assertEqual(config.Foo.Bam.value, list(range(10)))
78 self.assertEqual(config.Foo.Bam.value, list(range(10)))
99 self.assertEqual(config.D.C.value, 'hi there')
79 self.assertEqual(config.D.C.value, 'hi there')
100
80
101 def test_python(self):
81 def test_python(self):
102 fd, fname = mkstemp('.py')
82 fd, fname = mkstemp('.py')
103 f = os.fdopen(fd, 'w')
83 f = os.fdopen(fd, 'w')
104 f.write(pyfile)
84 f.write(pyfile)
105 f.close()
85 f.close()
106 # Unlink the file
86 # Unlink the file
107 cl = PyFileConfigLoader(fname, log=log)
87 cl = PyFileConfigLoader(fname, log=log)
108 config = cl.load_config()
88 config = cl.load_config()
109 self._check_conf(config)
89 self._check_conf(config)
110
90
111 def test_json(self):
91 def test_json(self):
112 fd, fname = mkstemp('.json')
92 fd, fname = mkstemp('.json')
113 f = os.fdopen(fd, 'w')
93 f = os.fdopen(fd, 'w')
114 f.write(json1file)
94 f.write(json1file)
115 f.close()
95 f.close()
116 # Unlink the file
96 # Unlink the file
117 cl = JSONFileConfigLoader(fname, log=log)
97 cl = JSONFileConfigLoader(fname, log=log)
118 config = cl.load_config()
98 config = cl.load_config()
119 self._check_conf(config)
99 self._check_conf(config)
120
100
101 def test_collision(self):
102 a = Config()
103 b = Config()
104 self.assertEqual(a.collisions(b), {})
105 a.A.trait1 = 1
106 b.A.trait2 = 2
107 self.assertEqual(a.collisions(b), {})
108 b.A.trait1 = 1
109 self.assertEqual(a.collisions(b), {})
110 b.A.trait1 = 0
111 self.assertEqual(a.collisions(b), {
112 'A': {
113 'trait1': "1 ignored, using 0",
114 }
115 })
116 self.assertEqual(b.collisions(a), {
117 'A': {
118 'trait1': "0 ignored, using 1",
119 }
120 })
121 a.A.trait2 = 3
122 self.assertEqual(b.collisions(a), {
123 'A': {
124 'trait1': "0 ignored, using 1",
125 'trait2': "2 ignored, using 3",
126 }
127 })
128
121 def test_v2raise(self):
129 def test_v2raise(self):
122 fd, fname = mkstemp('.json')
130 fd, fname = mkstemp('.json')
123 f = os.fdopen(fd, 'w')
131 f = os.fdopen(fd, 'w')
124 f.write(json2file)
132 f.write(json2file)
125 f.close()
133 f.close()
126 # Unlink the file
134 # Unlink the file
127 cl = JSONFileConfigLoader(fname, log=log)
135 cl = JSONFileConfigLoader(fname, log=log)
128 with nt.assert_raises(ValueError):
136 with nt.assert_raises(ValueError):
129 cl.load_config()
137 cl.load_config()
130
138
131
139
132 class MyLoader1(ArgParseConfigLoader):
140 class MyLoader1(ArgParseConfigLoader):
133 def _add_arguments(self, aliases=None, flags=None):
141 def _add_arguments(self, aliases=None, flags=None):
134 p = self.parser
142 p = self.parser
135 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
143 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
136 p.add_argument('-b', dest='MyClass.bar', type=int)
144 p.add_argument('-b', dest='MyClass.bar', type=int)
137 p.add_argument('-n', dest='n', action='store_true')
145 p.add_argument('-n', dest='n', action='store_true')
138 p.add_argument('Global.bam', type=str)
146 p.add_argument('Global.bam', type=str)
139
147
140 class MyLoader2(ArgParseConfigLoader):
148 class MyLoader2(ArgParseConfigLoader):
141 def _add_arguments(self, aliases=None, flags=None):
149 def _add_arguments(self, aliases=None, flags=None):
142 subparsers = self.parser.add_subparsers(dest='subparser_name')
150 subparsers = self.parser.add_subparsers(dest='subparser_name')
143 subparser1 = subparsers.add_parser('1')
151 subparser1 = subparsers.add_parser('1')
144 subparser1.add_argument('-x',dest='Global.x')
152 subparser1.add_argument('-x',dest='Global.x')
145 subparser2 = subparsers.add_parser('2')
153 subparser2 = subparsers.add_parser('2')
146 subparser2.add_argument('y')
154 subparser2.add_argument('y')
147
155
148 class TestArgParseCL(TestCase):
156 class TestArgParseCL(TestCase):
149
157
150 def test_basic(self):
158 def test_basic(self):
151 cl = MyLoader1()
159 cl = MyLoader1()
152 config = cl.load_config('-f hi -b 10 -n wow'.split())
160 config = cl.load_config('-f hi -b 10 -n wow'.split())
153 self.assertEqual(config.Global.foo, 'hi')
161 self.assertEqual(config.Global.foo, 'hi')
154 self.assertEqual(config.MyClass.bar, 10)
162 self.assertEqual(config.MyClass.bar, 10)
155 self.assertEqual(config.n, True)
163 self.assertEqual(config.n, True)
156 self.assertEqual(config.Global.bam, 'wow')
164 self.assertEqual(config.Global.bam, 'wow')
157 config = cl.load_config(['wow'])
165 config = cl.load_config(['wow'])
158 self.assertEqual(list(config.keys()), ['Global'])
166 self.assertEqual(list(config.keys()), ['Global'])
159 self.assertEqual(list(config.Global.keys()), ['bam'])
167 self.assertEqual(list(config.Global.keys()), ['bam'])
160 self.assertEqual(config.Global.bam, 'wow')
168 self.assertEqual(config.Global.bam, 'wow')
161
169
162 def test_add_arguments(self):
170 def test_add_arguments(self):
163 cl = MyLoader2()
171 cl = MyLoader2()
164 config = cl.load_config('2 frobble'.split())
172 config = cl.load_config('2 frobble'.split())
165 self.assertEqual(config.subparser_name, '2')
173 self.assertEqual(config.subparser_name, '2')
166 self.assertEqual(config.y, 'frobble')
174 self.assertEqual(config.y, 'frobble')
167 config = cl.load_config('1 -x frobble'.split())
175 config = cl.load_config('1 -x frobble'.split())
168 self.assertEqual(config.subparser_name, '1')
176 self.assertEqual(config.subparser_name, '1')
169 self.assertEqual(config.Global.x, 'frobble')
177 self.assertEqual(config.Global.x, 'frobble')
170
178
171 def test_argv(self):
179 def test_argv(self):
172 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
180 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
173 config = cl.load_config()
181 config = cl.load_config()
174 self.assertEqual(config.Global.foo, 'hi')
182 self.assertEqual(config.Global.foo, 'hi')
175 self.assertEqual(config.MyClass.bar, 10)
183 self.assertEqual(config.MyClass.bar, 10)
176 self.assertEqual(config.n, True)
184 self.assertEqual(config.n, True)
177 self.assertEqual(config.Global.bam, 'wow')
185 self.assertEqual(config.Global.bam, 'wow')
178
186
179
187
180 class TestKeyValueCL(TestCase):
188 class TestKeyValueCL(TestCase):
181 klass = KeyValueConfigLoader
189 klass = KeyValueConfigLoader
182
190
183 def test_basic(self):
191 def test_basic(self):
184 cl = self.klass(log=log)
192 cl = self.klass(log=log)
185 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
193 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
186 config = cl.load_config(argv)
194 config = cl.load_config(argv)
187 self.assertEqual(config.a, 10)
195 self.assertEqual(config.a, 10)
188 self.assertEqual(config.b, 20)
196 self.assertEqual(config.b, 20)
189 self.assertEqual(config.Foo.Bar.value, 10)
197 self.assertEqual(config.Foo.Bar.value, 10)
190 self.assertEqual(config.Foo.Bam.value, list(range(10)))
198 self.assertEqual(config.Foo.Bam.value, list(range(10)))
191 self.assertEqual(config.D.C.value, 'hi there')
199 self.assertEqual(config.D.C.value, 'hi there')
192
200
193 def test_expanduser(self):
201 def test_expanduser(self):
194 cl = self.klass(log=log)
202 cl = self.klass(log=log)
195 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
203 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
196 config = cl.load_config(argv)
204 config = cl.load_config(argv)
197 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
205 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
198 self.assertEqual(config.b, os.path.expanduser('~'))
206 self.assertEqual(config.b, os.path.expanduser('~'))
199 self.assertEqual(config.c, os.path.expanduser('~/'))
207 self.assertEqual(config.c, os.path.expanduser('~/'))
200 self.assertEqual(config.d, '~/')
208 self.assertEqual(config.d, '~/')
201
209
202 def test_extra_args(self):
210 def test_extra_args(self):
203 cl = self.klass(log=log)
211 cl = self.klass(log=log)
204 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
212 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
205 self.assertEqual(cl.extra_args, ['b', 'd'])
213 self.assertEqual(cl.extra_args, ['b', 'd'])
206 self.assertEqual(config.a, 5)
214 self.assertEqual(config.a, 5)
207 self.assertEqual(config.c, 10)
215 self.assertEqual(config.c, 10)
208 config = cl.load_config(['--', '--a=5', '--c=10'])
216 config = cl.load_config(['--', '--a=5', '--c=10'])
209 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
217 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
210
218
211 def test_unicode_args(self):
219 def test_unicode_args(self):
212 cl = self.klass(log=log)
220 cl = self.klass(log=log)
213 argv = [u'--a=épsîlön']
221 argv = [u'--a=épsîlön']
214 config = cl.load_config(argv)
222 config = cl.load_config(argv)
215 self.assertEqual(config.a, u'épsîlön')
223 self.assertEqual(config.a, u'épsîlön')
216
224
217 def test_unicode_bytes_args(self):
225 def test_unicode_bytes_args(self):
218 uarg = u'--a=é'
226 uarg = u'--a=é'
219 try:
227 try:
220 barg = uarg.encode(sys.stdin.encoding)
228 barg = uarg.encode(sys.stdin.encoding)
221 except (TypeError, UnicodeEncodeError):
229 except (TypeError, UnicodeEncodeError):
222 raise SkipTest("sys.stdin.encoding can't handle 'é'")
230 raise SkipTest("sys.stdin.encoding can't handle 'é'")
223
231
224 cl = self.klass(log=log)
232 cl = self.klass(log=log)
225 config = cl.load_config([barg])
233 config = cl.load_config([barg])
226 self.assertEqual(config.a, u'é')
234 self.assertEqual(config.a, u'é')
227
235
228 def test_unicode_alias(self):
236 def test_unicode_alias(self):
229 cl = self.klass(log=log)
237 cl = self.klass(log=log)
230 argv = [u'--a=épsîlön']
238 argv = [u'--a=épsîlön']
231 config = cl.load_config(argv, aliases=dict(a='A.a'))
239 config = cl.load_config(argv, aliases=dict(a='A.a'))
232 self.assertEqual(config.A.a, u'épsîlön')
240 self.assertEqual(config.A.a, u'épsîlön')
233
241
234
242
235 class TestArgParseKVCL(TestKeyValueCL):
243 class TestArgParseKVCL(TestKeyValueCL):
236 klass = KVArgParseConfigLoader
244 klass = KVArgParseConfigLoader
237
245
238 def test_expanduser2(self):
246 def test_expanduser2(self):
239 cl = self.klass(log=log)
247 cl = self.klass(log=log)
240 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
248 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
241 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
249 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
242 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
250 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
243 self.assertEqual(config.A.b, '~/1/2/3')
251 self.assertEqual(config.A.b, '~/1/2/3')
244
252
245 def test_eval(self):
253 def test_eval(self):
246 cl = self.klass(log=log)
254 cl = self.klass(log=log)
247 argv = ['-c', 'a=5']
255 argv = ['-c', 'a=5']
248 config = cl.load_config(argv, aliases=dict(c='A.c'))
256 config = cl.load_config(argv, aliases=dict(c='A.c'))
249 self.assertEqual(config.A.c, u"a=5")
257 self.assertEqual(config.A.c, u"a=5")
250
258
251
259
252 class TestConfig(TestCase):
260 class TestConfig(TestCase):
253
261
254 def test_setget(self):
262 def test_setget(self):
255 c = Config()
263 c = Config()
256 c.a = 10
264 c.a = 10
257 self.assertEqual(c.a, 10)
265 self.assertEqual(c.a, 10)
258 self.assertEqual('b' in c, False)
266 self.assertEqual('b' in c, False)
259
267
260 def test_auto_section(self):
268 def test_auto_section(self):
261 c = Config()
269 c = Config()
262 self.assertNotIn('A', c)
270 self.assertNotIn('A', c)
263 assert not c._has_section('A')
271 assert not c._has_section('A')
264 A = c.A
272 A = c.A
265 A.foo = 'hi there'
273 A.foo = 'hi there'
266 self.assertIn('A', c)
274 self.assertIn('A', c)
267 assert c._has_section('A')
275 assert c._has_section('A')
268 self.assertEqual(c.A.foo, 'hi there')
276 self.assertEqual(c.A.foo, 'hi there')
269 del c.A
277 del c.A
270 self.assertEqual(c.A, Config())
278 self.assertEqual(c.A, Config())
271
279
272 def test_merge_doesnt_exist(self):
280 def test_merge_doesnt_exist(self):
273 c1 = Config()
281 c1 = Config()
274 c2 = Config()
282 c2 = Config()
275 c2.bar = 10
283 c2.bar = 10
276 c2.Foo.bar = 10
284 c2.Foo.bar = 10
277 c1.merge(c2)
285 c1.merge(c2)
278 self.assertEqual(c1.Foo.bar, 10)
286 self.assertEqual(c1.Foo.bar, 10)
279 self.assertEqual(c1.bar, 10)
287 self.assertEqual(c1.bar, 10)
280 c2.Bar.bar = 10
288 c2.Bar.bar = 10
281 c1.merge(c2)
289 c1.merge(c2)
282 self.assertEqual(c1.Bar.bar, 10)
290 self.assertEqual(c1.Bar.bar, 10)
283
291
284 def test_merge_exists(self):
292 def test_merge_exists(self):
285 c1 = Config()
293 c1 = Config()
286 c2 = Config()
294 c2 = Config()
287 c1.Foo.bar = 10
295 c1.Foo.bar = 10
288 c1.Foo.bam = 30
296 c1.Foo.bam = 30
289 c2.Foo.bar = 20
297 c2.Foo.bar = 20
290 c2.Foo.wow = 40
298 c2.Foo.wow = 40
291 c1.merge(c2)
299 c1.merge(c2)
292 self.assertEqual(c1.Foo.bam, 30)
300 self.assertEqual(c1.Foo.bam, 30)
293 self.assertEqual(c1.Foo.bar, 20)
301 self.assertEqual(c1.Foo.bar, 20)
294 self.assertEqual(c1.Foo.wow, 40)
302 self.assertEqual(c1.Foo.wow, 40)
295 c2.Foo.Bam.bam = 10
303 c2.Foo.Bam.bam = 10
296 c1.merge(c2)
304 c1.merge(c2)
297 self.assertEqual(c1.Foo.Bam.bam, 10)
305 self.assertEqual(c1.Foo.Bam.bam, 10)
298
306
299 def test_deepcopy(self):
307 def test_deepcopy(self):
300 c1 = Config()
308 c1 = Config()
301 c1.Foo.bar = 10
309 c1.Foo.bar = 10
302 c1.Foo.bam = 30
310 c1.Foo.bam = 30
303 c1.a = 'asdf'
311 c1.a = 'asdf'
304 c1.b = range(10)
312 c1.b = range(10)
305 import copy
313 import copy
306 c2 = copy.deepcopy(c1)
314 c2 = copy.deepcopy(c1)
307 self.assertEqual(c1, c2)
315 self.assertEqual(c1, c2)
308 self.assertTrue(c1 is not c2)
316 self.assertTrue(c1 is not c2)
309 self.assertTrue(c1.Foo is not c2.Foo)
317 self.assertTrue(c1.Foo is not c2.Foo)
310
318
311 def test_builtin(self):
319 def test_builtin(self):
312 c1 = Config()
320 c1 = Config()
313 c1.format = "json"
321 c1.format = "json"
314
322
315 def test_fromdict(self):
323 def test_fromdict(self):
316 c1 = Config({'Foo' : {'bar' : 1}})
324 c1 = Config({'Foo' : {'bar' : 1}})
317 self.assertEqual(c1.Foo.__class__, Config)
325 self.assertEqual(c1.Foo.__class__, Config)
318 self.assertEqual(c1.Foo.bar, 1)
326 self.assertEqual(c1.Foo.bar, 1)
319
327
320 def test_fromdictmerge(self):
328 def test_fromdictmerge(self):
321 c1 = Config()
329 c1 = Config()
322 c2 = Config({'Foo' : {'bar' : 1}})
330 c2 = Config({'Foo' : {'bar' : 1}})
323 c1.merge(c2)
331 c1.merge(c2)
324 self.assertEqual(c1.Foo.__class__, Config)
332 self.assertEqual(c1.Foo.__class__, Config)
325 self.assertEqual(c1.Foo.bar, 1)
333 self.assertEqual(c1.Foo.bar, 1)
326
334
327 def test_fromdictmerge2(self):
335 def test_fromdictmerge2(self):
328 c1 = Config({'Foo' : {'baz' : 2}})
336 c1 = Config({'Foo' : {'baz' : 2}})
329 c2 = Config({'Foo' : {'bar' : 1}})
337 c2 = Config({'Foo' : {'bar' : 1}})
330 c1.merge(c2)
338 c1.merge(c2)
331 self.assertEqual(c1.Foo.__class__, Config)
339 self.assertEqual(c1.Foo.__class__, Config)
332 self.assertEqual(c1.Foo.bar, 1)
340 self.assertEqual(c1.Foo.bar, 1)
333 self.assertEqual(c1.Foo.baz, 2)
341 self.assertEqual(c1.Foo.baz, 2)
334 self.assertNotIn('baz', c2.Foo)
342 self.assertNotIn('baz', c2.Foo)
335
343
336 def test_contains(self):
344 def test_contains(self):
337 c1 = Config({'Foo' : {'baz' : 2}})
345 c1 = Config({'Foo' : {'baz' : 2}})
338 c2 = Config({'Foo' : {'bar' : 1}})
346 c2 = Config({'Foo' : {'bar' : 1}})
339 self.assertIn('Foo', c1)
347 self.assertIn('Foo', c1)
340 self.assertIn('Foo.baz', c1)
348 self.assertIn('Foo.baz', c1)
341 self.assertIn('Foo.bar', c2)
349 self.assertIn('Foo.bar', c2)
342 self.assertNotIn('Foo.bar', c1)
350 self.assertNotIn('Foo.bar', c1)
343
351
344 def test_pickle_config(self):
352 def test_pickle_config(self):
345 cfg = Config()
353 cfg = Config()
346 cfg.Foo.bar = 1
354 cfg.Foo.bar = 1
347 pcfg = pickle.dumps(cfg)
355 pcfg = pickle.dumps(cfg)
348 cfg2 = pickle.loads(pcfg)
356 cfg2 = pickle.loads(pcfg)
349 self.assertEqual(cfg2, cfg)
357 self.assertEqual(cfg2, cfg)
350
358
351 def test_getattr_section(self):
359 def test_getattr_section(self):
352 cfg = Config()
360 cfg = Config()
353 self.assertNotIn('Foo', cfg)
361 self.assertNotIn('Foo', cfg)
354 Foo = cfg.Foo
362 Foo = cfg.Foo
355 assert isinstance(Foo, Config)
363 assert isinstance(Foo, Config)
356 self.assertIn('Foo', cfg)
364 self.assertIn('Foo', cfg)
357
365
358 def test_getitem_section(self):
366 def test_getitem_section(self):
359 cfg = Config()
367 cfg = Config()
360 self.assertNotIn('Foo', cfg)
368 self.assertNotIn('Foo', cfg)
361 Foo = cfg['Foo']
369 Foo = cfg['Foo']
362 assert isinstance(Foo, Config)
370 assert isinstance(Foo, Config)
363 self.assertIn('Foo', cfg)
371 self.assertIn('Foo', cfg)
364
372
365 def test_getattr_not_section(self):
373 def test_getattr_not_section(self):
366 cfg = Config()
374 cfg = Config()
367 self.assertNotIn('foo', cfg)
375 self.assertNotIn('foo', cfg)
368 foo = cfg.foo
376 foo = cfg.foo
369 assert isinstance(foo, LazyConfigValue)
377 assert isinstance(foo, LazyConfigValue)
370 self.assertIn('foo', cfg)
378 self.assertIn('foo', cfg)
371
379
372 def test_getattr_private_missing(self):
380 def test_getattr_private_missing(self):
373 cfg = Config()
381 cfg = Config()
374 self.assertNotIn('_repr_html_', cfg)
382 self.assertNotIn('_repr_html_', cfg)
375 with self.assertRaises(AttributeError):
383 with self.assertRaises(AttributeError):
376 _ = cfg._repr_html_
384 _ = cfg._repr_html_
377 self.assertNotIn('_repr_html_', cfg)
385 self.assertNotIn('_repr_html_', cfg)
378 self.assertEqual(len(cfg), 0)
386 self.assertEqual(len(cfg), 0)
379
387
380 def test_getitem_not_section(self):
388 def test_getitem_not_section(self):
381 cfg = Config()
389 cfg = Config()
382 self.assertNotIn('foo', cfg)
390 self.assertNotIn('foo', cfg)
383 foo = cfg['foo']
391 foo = cfg['foo']
384 assert isinstance(foo, LazyConfigValue)
392 assert isinstance(foo, LazyConfigValue)
385 self.assertIn('foo', cfg)
393 self.assertIn('foo', cfg)
386
394
387 def test_merge_copies(self):
395 def test_merge_copies(self):
388 c = Config()
396 c = Config()
389 c2 = Config()
397 c2 = Config()
390 c2.Foo.trait = []
398 c2.Foo.trait = []
391 c.merge(c2)
399 c.merge(c2)
392 c2.Foo.trait.append(1)
400 c2.Foo.trait.append(1)
393 self.assertIsNot(c.Foo, c2.Foo)
401 self.assertIsNot(c.Foo, c2.Foo)
394 self.assertEqual(c.Foo.trait, [])
402 self.assertEqual(c.Foo.trait, [])
395 self.assertEqual(c2.Foo.trait, [1])
403 self.assertEqual(c2.Foo.trait, [1])
396
404
@@ -1,507 +1,514 b''
1 """Base Tornado handlers for the notebook server."""
1 """Base Tornado handlers for the notebook server."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import functools
6 import functools
7 import json
7 import json
8 import logging
8 import logging
9 import os
9 import os
10 import re
10 import re
11 import sys
11 import sys
12 import traceback
12 import traceback
13 try:
13 try:
14 # py3
14 # py3
15 from http.client import responses
15 from http.client import responses
16 except ImportError:
16 except ImportError:
17 from httplib import responses
17 from httplib import responses
18
18
19 from jinja2 import TemplateNotFound
19 from jinja2 import TemplateNotFound
20 from tornado import web
20 from tornado import web
21
21
22 try:
22 try:
23 from tornado.log import app_log
23 from tornado.log import app_log
24 except ImportError:
24 except ImportError:
25 app_log = logging.getLogger()
25 app_log = logging.getLogger()
26
26
27 import IPython
27 import IPython
28 from IPython.utils.sysinfo import get_sys_info
28 from IPython.utils.sysinfo import get_sys_info
29
29
30 from IPython.config import Application
30 from IPython.config import Application
31 from IPython.utils.path import filefind
31 from IPython.utils.path import filefind
32 from IPython.utils.py3compat import string_types
32 from IPython.utils.py3compat import string_types
33 from IPython.html.utils import is_hidden, url_path_join, url_escape
33 from IPython.html.utils import is_hidden, url_path_join, url_escape
34
34
35 from IPython.html.services.security import csp_report_uri
36
35 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
36 # Top-level handlers
38 # Top-level handlers
37 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
38 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40 non_alphanum = re.compile(r'[^A-Za-z0-9]')
39
41
40 sys_info = json.dumps(get_sys_info())
42 sys_info = json.dumps(get_sys_info())
41
43
42 class AuthenticatedHandler(web.RequestHandler):
44 class AuthenticatedHandler(web.RequestHandler):
43 """A RequestHandler with an authenticated user."""
45 """A RequestHandler with an authenticated user."""
44
46
45 def set_default_headers(self):
47 def set_default_headers(self):
46 headers = self.settings.get('headers', {})
48 headers = self.settings.get('headers', {})
47
49
48 if "X-Frame-Options" not in headers:
50 if "Content-Security-Policy" not in headers:
49 headers["X-Frame-Options"] = "SAMEORIGIN"
51 headers["Content-Security-Policy"] = (
52 "frame-ancestors 'self'; "
53 # Make sure the report-uri is relative to the base_url
54 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
55 )
50
56
57 # Allow for overriding headers
51 for header_name,value in headers.items() :
58 for header_name,value in headers.items() :
52 try:
59 try:
53 self.set_header(header_name, value)
60 self.set_header(header_name, value)
54 except Exception:
61 except Exception as e:
55 # tornado raise Exception (not a subclass)
62 # tornado raise Exception (not a subclass)
56 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 # if method is unsupported (websocket and Access-Control-Allow-Origin
57 # for example, so just ignore)
64 # for example, so just ignore)
58 pass
65 self.log.debug(e)
59
66
60 def clear_login_cookie(self):
67 def clear_login_cookie(self):
61 self.clear_cookie(self.cookie_name)
68 self.clear_cookie(self.cookie_name)
62
69
63 def get_current_user(self):
70 def get_current_user(self):
64 user_id = self.get_secure_cookie(self.cookie_name)
71 user_id = self.get_secure_cookie(self.cookie_name)
65 # For now the user_id should not return empty, but it could eventually
72 # For now the user_id should not return empty, but it could eventually
66 if user_id == '':
73 if user_id == '':
67 user_id = 'anonymous'
74 user_id = 'anonymous'
68 if user_id is None:
75 if user_id is None:
69 # prevent extra Invalid cookie sig warnings:
76 # prevent extra Invalid cookie sig warnings:
70 self.clear_login_cookie()
77 self.clear_login_cookie()
71 if not self.login_available:
78 if not self.login_available:
72 user_id = 'anonymous'
79 user_id = 'anonymous'
73 return user_id
80 return user_id
74
81
75 @property
82 @property
76 def cookie_name(self):
83 def cookie_name(self):
77 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
84 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
78 self.request.host
85 self.request.host
79 ))
86 ))
80 return self.settings.get('cookie_name', default_cookie_name)
87 return self.settings.get('cookie_name', default_cookie_name)
81
88
82 @property
89 @property
83 def password(self):
90 def password(self):
84 """our password"""
91 """our password"""
85 return self.settings.get('password', '')
92 return self.settings.get('password', '')
86
93
87 @property
94 @property
88 def logged_in(self):
95 def logged_in(self):
89 """Is a user currently logged in?
96 """Is a user currently logged in?
90
97
91 """
98 """
92 user = self.get_current_user()
99 user = self.get_current_user()
93 return (user and not user == 'anonymous')
100 return (user and not user == 'anonymous')
94
101
95 @property
102 @property
96 def login_available(self):
103 def login_available(self):
97 """May a user proceed to log in?
104 """May a user proceed to log in?
98
105
99 This returns True if login capability is available, irrespective of
106 This returns True if login capability is available, irrespective of
100 whether the user is already logged in or not.
107 whether the user is already logged in or not.
101
108
102 """
109 """
103 return bool(self.settings.get('password', ''))
110 return bool(self.settings.get('password', ''))
104
111
105
112
106 class IPythonHandler(AuthenticatedHandler):
113 class IPythonHandler(AuthenticatedHandler):
107 """IPython-specific extensions to authenticated handling
114 """IPython-specific extensions to authenticated handling
108
115
109 Mostly property shortcuts to IPython-specific settings.
116 Mostly property shortcuts to IPython-specific settings.
110 """
117 """
111
118
112 @property
119 @property
113 def config(self):
120 def config(self):
114 return self.settings.get('config', None)
121 return self.settings.get('config', None)
115
122
116 @property
123 @property
117 def log(self):
124 def log(self):
118 """use the IPython log by default, falling back on tornado's logger"""
125 """use the IPython log by default, falling back on tornado's logger"""
119 if Application.initialized():
126 if Application.initialized():
120 return Application.instance().log
127 return Application.instance().log
121 else:
128 else:
122 return app_log
129 return app_log
123
130
124 #---------------------------------------------------------------
131 #---------------------------------------------------------------
125 # URLs
132 # URLs
126 #---------------------------------------------------------------
133 #---------------------------------------------------------------
127
134
128 @property
135 @property
129 def version_hash(self):
136 def version_hash(self):
130 """The version hash to use for cache hints for static files"""
137 """The version hash to use for cache hints for static files"""
131 return self.settings.get('version_hash', '')
138 return self.settings.get('version_hash', '')
132
139
133 @property
140 @property
134 def mathjax_url(self):
141 def mathjax_url(self):
135 return self.settings.get('mathjax_url', '')
142 return self.settings.get('mathjax_url', '')
136
143
137 @property
144 @property
138 def base_url(self):
145 def base_url(self):
139 return self.settings.get('base_url', '/')
146 return self.settings.get('base_url', '/')
140
147
141 @property
148 @property
142 def ws_url(self):
149 def ws_url(self):
143 return self.settings.get('websocket_url', '')
150 return self.settings.get('websocket_url', '')
144
151
145 @property
152 @property
146 def contents_js_source(self):
153 def contents_js_source(self):
147 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
154 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
148 'services/contents'))
155 'services/contents'))
149 return self.settings.get('contents_js_source', 'services/contents')
156 return self.settings.get('contents_js_source', 'services/contents')
150
157
151 #---------------------------------------------------------------
158 #---------------------------------------------------------------
152 # Manager objects
159 # Manager objects
153 #---------------------------------------------------------------
160 #---------------------------------------------------------------
154
161
155 @property
162 @property
156 def kernel_manager(self):
163 def kernel_manager(self):
157 return self.settings['kernel_manager']
164 return self.settings['kernel_manager']
158
165
159 @property
166 @property
160 def contents_manager(self):
167 def contents_manager(self):
161 return self.settings['contents_manager']
168 return self.settings['contents_manager']
162
169
163 @property
170 @property
164 def cluster_manager(self):
171 def cluster_manager(self):
165 return self.settings['cluster_manager']
172 return self.settings['cluster_manager']
166
173
167 @property
174 @property
168 def session_manager(self):
175 def session_manager(self):
169 return self.settings['session_manager']
176 return self.settings['session_manager']
170
177
171 @property
178 @property
172 def terminal_manager(self):
179 def terminal_manager(self):
173 return self.settings['terminal_manager']
180 return self.settings['terminal_manager']
174
181
175 @property
182 @property
176 def kernel_spec_manager(self):
183 def kernel_spec_manager(self):
177 return self.settings['kernel_spec_manager']
184 return self.settings['kernel_spec_manager']
178
185
179 @property
186 @property
180 def config_manager(self):
187 def config_manager(self):
181 return self.settings['config_manager']
188 return self.settings['config_manager']
182
189
183 #---------------------------------------------------------------
190 #---------------------------------------------------------------
184 # CORS
191 # CORS
185 #---------------------------------------------------------------
192 #---------------------------------------------------------------
186
193
187 @property
194 @property
188 def allow_origin(self):
195 def allow_origin(self):
189 """Normal Access-Control-Allow-Origin"""
196 """Normal Access-Control-Allow-Origin"""
190 return self.settings.get('allow_origin', '')
197 return self.settings.get('allow_origin', '')
191
198
192 @property
199 @property
193 def allow_origin_pat(self):
200 def allow_origin_pat(self):
194 """Regular expression version of allow_origin"""
201 """Regular expression version of allow_origin"""
195 return self.settings.get('allow_origin_pat', None)
202 return self.settings.get('allow_origin_pat', None)
196
203
197 @property
204 @property
198 def allow_credentials(self):
205 def allow_credentials(self):
199 """Whether to set Access-Control-Allow-Credentials"""
206 """Whether to set Access-Control-Allow-Credentials"""
200 return self.settings.get('allow_credentials', False)
207 return self.settings.get('allow_credentials', False)
201
208
202 def set_default_headers(self):
209 def set_default_headers(self):
203 """Add CORS headers, if defined"""
210 """Add CORS headers, if defined"""
204 super(IPythonHandler, self).set_default_headers()
211 super(IPythonHandler, self).set_default_headers()
205 if self.allow_origin:
212 if self.allow_origin:
206 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
213 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
207 elif self.allow_origin_pat:
214 elif self.allow_origin_pat:
208 origin = self.get_origin()
215 origin = self.get_origin()
209 if origin and self.allow_origin_pat.match(origin):
216 if origin and self.allow_origin_pat.match(origin):
210 self.set_header("Access-Control-Allow-Origin", origin)
217 self.set_header("Access-Control-Allow-Origin", origin)
211 if self.allow_credentials:
218 if self.allow_credentials:
212 self.set_header("Access-Control-Allow-Credentials", 'true')
219 self.set_header("Access-Control-Allow-Credentials", 'true')
213
220
214 def get_origin(self):
221 def get_origin(self):
215 # Handle WebSocket Origin naming convention differences
222 # Handle WebSocket Origin naming convention differences
216 # The difference between version 8 and 13 is that in 8 the
223 # The difference between version 8 and 13 is that in 8 the
217 # client sends a "Sec-Websocket-Origin" header and in 13 it's
224 # client sends a "Sec-Websocket-Origin" header and in 13 it's
218 # simply "Origin".
225 # simply "Origin".
219 if "Origin" in self.request.headers:
226 if "Origin" in self.request.headers:
220 origin = self.request.headers.get("Origin")
227 origin = self.request.headers.get("Origin")
221 else:
228 else:
222 origin = self.request.headers.get("Sec-Websocket-Origin", None)
229 origin = self.request.headers.get("Sec-Websocket-Origin", None)
223 return origin
230 return origin
224
231
225 #---------------------------------------------------------------
232 #---------------------------------------------------------------
226 # template rendering
233 # template rendering
227 #---------------------------------------------------------------
234 #---------------------------------------------------------------
228
235
229 def get_template(self, name):
236 def get_template(self, name):
230 """Return the jinja template object for a given name"""
237 """Return the jinja template object for a given name"""
231 return self.settings['jinja2_env'].get_template(name)
238 return self.settings['jinja2_env'].get_template(name)
232
239
233 def render_template(self, name, **ns):
240 def render_template(self, name, **ns):
234 ns.update(self.template_namespace)
241 ns.update(self.template_namespace)
235 template = self.get_template(name)
242 template = self.get_template(name)
236 return template.render(**ns)
243 return template.render(**ns)
237
244
238 @property
245 @property
239 def template_namespace(self):
246 def template_namespace(self):
240 return dict(
247 return dict(
241 base_url=self.base_url,
248 base_url=self.base_url,
242 ws_url=self.ws_url,
249 ws_url=self.ws_url,
243 logged_in=self.logged_in,
250 logged_in=self.logged_in,
244 login_available=self.login_available,
251 login_available=self.login_available,
245 static_url=self.static_url,
252 static_url=self.static_url,
246 sys_info=sys_info,
253 sys_info=sys_info,
247 contents_js_source=self.contents_js_source,
254 contents_js_source=self.contents_js_source,
248 version_hash=self.version_hash,
255 version_hash=self.version_hash,
249 )
256 )
250
257
251 def get_json_body(self):
258 def get_json_body(self):
252 """Return the body of the request as JSON data."""
259 """Return the body of the request as JSON data."""
253 if not self.request.body:
260 if not self.request.body:
254 return None
261 return None
255 # Do we need to call body.decode('utf-8') here?
262 # Do we need to call body.decode('utf-8') here?
256 body = self.request.body.strip().decode(u'utf-8')
263 body = self.request.body.strip().decode(u'utf-8')
257 try:
264 try:
258 model = json.loads(body)
265 model = json.loads(body)
259 except Exception:
266 except Exception:
260 self.log.debug("Bad JSON: %r", body)
267 self.log.debug("Bad JSON: %r", body)
261 self.log.error("Couldn't parse JSON", exc_info=True)
268 self.log.error("Couldn't parse JSON", exc_info=True)
262 raise web.HTTPError(400, u'Invalid JSON in body of request')
269 raise web.HTTPError(400, u'Invalid JSON in body of request')
263 return model
270 return model
264
271
265 def write_error(self, status_code, **kwargs):
272 def write_error(self, status_code, **kwargs):
266 """render custom error pages"""
273 """render custom error pages"""
267 exc_info = kwargs.get('exc_info')
274 exc_info = kwargs.get('exc_info')
268 message = ''
275 message = ''
269 status_message = responses.get(status_code, 'Unknown HTTP Error')
276 status_message = responses.get(status_code, 'Unknown HTTP Error')
270 if exc_info:
277 if exc_info:
271 exception = exc_info[1]
278 exception = exc_info[1]
272 # get the custom message, if defined
279 # get the custom message, if defined
273 try:
280 try:
274 message = exception.log_message % exception.args
281 message = exception.log_message % exception.args
275 except Exception:
282 except Exception:
276 pass
283 pass
277
284
278 # construct the custom reason, if defined
285 # construct the custom reason, if defined
279 reason = getattr(exception, 'reason', '')
286 reason = getattr(exception, 'reason', '')
280 if reason:
287 if reason:
281 status_message = reason
288 status_message = reason
282
289
283 # build template namespace
290 # build template namespace
284 ns = dict(
291 ns = dict(
285 status_code=status_code,
292 status_code=status_code,
286 status_message=status_message,
293 status_message=status_message,
287 message=message,
294 message=message,
288 exception=exception,
295 exception=exception,
289 )
296 )
290
297
291 self.set_header('Content-Type', 'text/html')
298 self.set_header('Content-Type', 'text/html')
292 # render the template
299 # render the template
293 try:
300 try:
294 html = self.render_template('%s.html' % status_code, **ns)
301 html = self.render_template('%s.html' % status_code, **ns)
295 except TemplateNotFound:
302 except TemplateNotFound:
296 self.log.debug("No template for %d", status_code)
303 self.log.debug("No template for %d", status_code)
297 html = self.render_template('error.html', **ns)
304 html = self.render_template('error.html', **ns)
298
305
299 self.write(html)
306 self.write(html)
300
307
301
308
302
309
303 class Template404(IPythonHandler):
310 class Template404(IPythonHandler):
304 """Render our 404 template"""
311 """Render our 404 template"""
305 def prepare(self):
312 def prepare(self):
306 raise web.HTTPError(404)
313 raise web.HTTPError(404)
307
314
308
315
309 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
316 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
310 """static files should only be accessible when logged in"""
317 """static files should only be accessible when logged in"""
311
318
312 @web.authenticated
319 @web.authenticated
313 def get(self, path):
320 def get(self, path):
314 if os.path.splitext(path)[1] == '.ipynb':
321 if os.path.splitext(path)[1] == '.ipynb':
315 name = path.rsplit('/', 1)[-1]
322 name = path.rsplit('/', 1)[-1]
316 self.set_header('Content-Type', 'application/json')
323 self.set_header('Content-Type', 'application/json')
317 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
324 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
318
325
319 return web.StaticFileHandler.get(self, path)
326 return web.StaticFileHandler.get(self, path)
320
327
321 def set_headers(self):
328 def set_headers(self):
322 super(AuthenticatedFileHandler, self).set_headers()
329 super(AuthenticatedFileHandler, self).set_headers()
323 # disable browser caching, rely on 304 replies for savings
330 # disable browser caching, rely on 304 replies for savings
324 if "v" not in self.request.arguments:
331 if "v" not in self.request.arguments:
325 self.add_header("Cache-Control", "no-cache")
332 self.add_header("Cache-Control", "no-cache")
326
333
327 def compute_etag(self):
334 def compute_etag(self):
328 return None
335 return None
329
336
330 def validate_absolute_path(self, root, absolute_path):
337 def validate_absolute_path(self, root, absolute_path):
331 """Validate and return the absolute path.
338 """Validate and return the absolute path.
332
339
333 Requires tornado 3.1
340 Requires tornado 3.1
334
341
335 Adding to tornado's own handling, forbids the serving of hidden files.
342 Adding to tornado's own handling, forbids the serving of hidden files.
336 """
343 """
337 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
344 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
338 abs_root = os.path.abspath(root)
345 abs_root = os.path.abspath(root)
339 if is_hidden(abs_path, abs_root):
346 if is_hidden(abs_path, abs_root):
340 self.log.info("Refusing to serve hidden file, via 404 Error")
347 self.log.info("Refusing to serve hidden file, via 404 Error")
341 raise web.HTTPError(404)
348 raise web.HTTPError(404)
342 return abs_path
349 return abs_path
343
350
344
351
345 def json_errors(method):
352 def json_errors(method):
346 """Decorate methods with this to return GitHub style JSON errors.
353 """Decorate methods with this to return GitHub style JSON errors.
347
354
348 This should be used on any JSON API on any handler method that can raise HTTPErrors.
355 This should be used on any JSON API on any handler method that can raise HTTPErrors.
349
356
350 This will grab the latest HTTPError exception using sys.exc_info
357 This will grab the latest HTTPError exception using sys.exc_info
351 and then:
358 and then:
352
359
353 1. Set the HTTP status code based on the HTTPError
360 1. Set the HTTP status code based on the HTTPError
354 2. Create and return a JSON body with a message field describing
361 2. Create and return a JSON body with a message field describing
355 the error in a human readable form.
362 the error in a human readable form.
356 """
363 """
357 @functools.wraps(method)
364 @functools.wraps(method)
358 def wrapper(self, *args, **kwargs):
365 def wrapper(self, *args, **kwargs):
359 try:
366 try:
360 result = method(self, *args, **kwargs)
367 result = method(self, *args, **kwargs)
361 except web.HTTPError as e:
368 except web.HTTPError as e:
362 status = e.status_code
369 status = e.status_code
363 message = e.log_message
370 message = e.log_message
364 self.log.warn(message)
371 self.log.warn(message)
365 self.set_status(e.status_code)
372 self.set_status(e.status_code)
366 self.finish(json.dumps(dict(message=message)))
373 self.finish(json.dumps(dict(message=message)))
367 except Exception:
374 except Exception:
368 self.log.error("Unhandled error in API request", exc_info=True)
375 self.log.error("Unhandled error in API request", exc_info=True)
369 status = 500
376 status = 500
370 message = "Unknown server error"
377 message = "Unknown server error"
371 t, value, tb = sys.exc_info()
378 t, value, tb = sys.exc_info()
372 self.set_status(status)
379 self.set_status(status)
373 tb_text = ''.join(traceback.format_exception(t, value, tb))
380 tb_text = ''.join(traceback.format_exception(t, value, tb))
374 reply = dict(message=message, traceback=tb_text)
381 reply = dict(message=message, traceback=tb_text)
375 self.finish(json.dumps(reply))
382 self.finish(json.dumps(reply))
376 else:
383 else:
377 return result
384 return result
378 return wrapper
385 return wrapper
379
386
380
387
381
388
382 #-----------------------------------------------------------------------------
389 #-----------------------------------------------------------------------------
383 # File handler
390 # File handler
384 #-----------------------------------------------------------------------------
391 #-----------------------------------------------------------------------------
385
392
386 # to minimize subclass changes:
393 # to minimize subclass changes:
387 HTTPError = web.HTTPError
394 HTTPError = web.HTTPError
388
395
389 class FileFindHandler(web.StaticFileHandler):
396 class FileFindHandler(web.StaticFileHandler):
390 """subclass of StaticFileHandler for serving files from a search path"""
397 """subclass of StaticFileHandler for serving files from a search path"""
391
398
392 # cache search results, don't search for files more than once
399 # cache search results, don't search for files more than once
393 _static_paths = {}
400 _static_paths = {}
394
401
395 def set_headers(self):
402 def set_headers(self):
396 super(FileFindHandler, self).set_headers()
403 super(FileFindHandler, self).set_headers()
397 # disable browser caching, rely on 304 replies for savings
404 # disable browser caching, rely on 304 replies for savings
398 if "v" not in self.request.arguments or \
405 if "v" not in self.request.arguments or \
399 any(self.request.path.startswith(path) for path in self.no_cache_paths):
406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
400 self.add_header("Cache-Control", "no-cache")
407 self.add_header("Cache-Control", "no-cache")
401
408
402 def initialize(self, path, default_filename=None, no_cache_paths=None):
409 def initialize(self, path, default_filename=None, no_cache_paths=None):
403 self.no_cache_paths = no_cache_paths or []
410 self.no_cache_paths = no_cache_paths or []
404
411
405 if isinstance(path, string_types):
412 if isinstance(path, string_types):
406 path = [path]
413 path = [path]
407
414
408 self.root = tuple(
415 self.root = tuple(
409 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
410 )
417 )
411 self.default_filename = default_filename
418 self.default_filename = default_filename
412
419
413 def compute_etag(self):
420 def compute_etag(self):
414 return None
421 return None
415
422
416 @classmethod
423 @classmethod
417 def get_absolute_path(cls, roots, path):
424 def get_absolute_path(cls, roots, path):
418 """locate a file to serve on our static file search path"""
425 """locate a file to serve on our static file search path"""
419 with cls._lock:
426 with cls._lock:
420 if path in cls._static_paths:
427 if path in cls._static_paths:
421 return cls._static_paths[path]
428 return cls._static_paths[path]
422 try:
429 try:
423 abspath = os.path.abspath(filefind(path, roots))
430 abspath = os.path.abspath(filefind(path, roots))
424 except IOError:
431 except IOError:
425 # IOError means not found
432 # IOError means not found
426 return ''
433 return ''
427
434
428 cls._static_paths[path] = abspath
435 cls._static_paths[path] = abspath
429 return abspath
436 return abspath
430
437
431 def validate_absolute_path(self, root, absolute_path):
438 def validate_absolute_path(self, root, absolute_path):
432 """check if the file should be served (raises 404, 403, etc.)"""
439 """check if the file should be served (raises 404, 403, etc.)"""
433 if absolute_path == '':
440 if absolute_path == '':
434 raise web.HTTPError(404)
441 raise web.HTTPError(404)
435
442
436 for root in self.root:
443 for root in self.root:
437 if (absolute_path + os.sep).startswith(root):
444 if (absolute_path + os.sep).startswith(root):
438 break
445 break
439
446
440 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
441
448
442
449
443 class ApiVersionHandler(IPythonHandler):
450 class ApiVersionHandler(IPythonHandler):
444
451
445 @json_errors
452 @json_errors
446 def get(self):
453 def get(self):
447 # not authenticated, so give as few info as possible
454 # not authenticated, so give as few info as possible
448 self.finish(json.dumps({"version":IPython.__version__}))
455 self.finish(json.dumps({"version":IPython.__version__}))
449
456
450
457
451 class TrailingSlashHandler(web.RequestHandler):
458 class TrailingSlashHandler(web.RequestHandler):
452 """Simple redirect handler that strips trailing slashes
459 """Simple redirect handler that strips trailing slashes
453
460
454 This should be the first, highest priority handler.
461 This should be the first, highest priority handler.
455 """
462 """
456
463
457 def get(self):
464 def get(self):
458 self.redirect(self.request.uri.rstrip('/'))
465 self.redirect(self.request.uri.rstrip('/'))
459
466
460 post = put = get
467 post = put = get
461
468
462
469
463 class FilesRedirectHandler(IPythonHandler):
470 class FilesRedirectHandler(IPythonHandler):
464 """Handler for redirecting relative URLs to the /files/ handler"""
471 """Handler for redirecting relative URLs to the /files/ handler"""
465 def get(self, path=''):
472 def get(self, path=''):
466 cm = self.contents_manager
473 cm = self.contents_manager
467 if cm.dir_exists(path):
474 if cm.dir_exists(path):
468 # it's a *directory*, redirect to /tree
475 # it's a *directory*, redirect to /tree
469 url = url_path_join(self.base_url, 'tree', path)
476 url = url_path_join(self.base_url, 'tree', path)
470 else:
477 else:
471 orig_path = path
478 orig_path = path
472 # otherwise, redirect to /files
479 # otherwise, redirect to /files
473 parts = path.split('/')
480 parts = path.split('/')
474
481
475 if not cm.file_exists(path=path) and 'files' in parts:
482 if not cm.file_exists(path=path) and 'files' in parts:
476 # redirect without files/ iff it would 404
483 # redirect without files/ iff it would 404
477 # this preserves pre-2.0-style 'files/' links
484 # this preserves pre-2.0-style 'files/' links
478 self.log.warn("Deprecated files/ URL: %s", orig_path)
485 self.log.warn("Deprecated files/ URL: %s", orig_path)
479 parts.remove('files')
486 parts.remove('files')
480 path = '/'.join(parts)
487 path = '/'.join(parts)
481
488
482 if not cm.file_exists(path=path):
489 if not cm.file_exists(path=path):
483 raise web.HTTPError(404)
490 raise web.HTTPError(404)
484
491
485 url = url_path_join(self.base_url, 'files', path)
492 url = url_path_join(self.base_url, 'files', path)
486 url = url_escape(url)
493 url = url_escape(url)
487 self.log.debug("Redirecting %s to %s", self.request.path, url)
494 self.log.debug("Redirecting %s to %s", self.request.path, url)
488 self.redirect(url)
495 self.redirect(url)
489
496
490
497
491 #-----------------------------------------------------------------------------
498 #-----------------------------------------------------------------------------
492 # URL pattern fragments for re-use
499 # URL pattern fragments for re-use
493 #-----------------------------------------------------------------------------
500 #-----------------------------------------------------------------------------
494
501
495 # path matches any number of `/foo[/bar...]` or just `/` or ''
502 # path matches any number of `/foo[/bar...]` or just `/` or ''
496 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
503 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
497 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
504 notebook_path_regex = r"(?P<path>(?:/[^/]+)+\.ipynb)"
498
505
499 #-----------------------------------------------------------------------------
506 #-----------------------------------------------------------------------------
500 # URL to handler mappings
507 # URL to handler mappings
501 #-----------------------------------------------------------------------------
508 #-----------------------------------------------------------------------------
502
509
503
510
504 default_handlers = [
511 default_handlers = [
505 (r".*/", TrailingSlashHandler),
512 (r".*/", TrailingSlashHandler),
506 (r"api", ApiVersionHandler)
513 (r"api", ApiVersionHandler)
507 ]
514 ]
@@ -1,1041 +1,1041 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import datetime
10 import datetime
11 import errno
11 import errno
12 import io
12 import io
13 import json
13 import json
14 import logging
14 import logging
15 import os
15 import os
16 import random
16 import random
17 import re
17 import re
18 import select
18 import select
19 import signal
19 import signal
20 import socket
20 import socket
21 import sys
21 import sys
22 import threading
22 import threading
23 import time
23 import time
24 import webbrowser
24 import webbrowser
25
25
26
26
27 # check for pyzmq 2.1.11
27 # check for pyzmq 2.1.11
28 from IPython.utils.zmqrelated import check_for_zmq
28 from IPython.utils.zmqrelated import check_for_zmq
29 check_for_zmq('2.1.11', 'IPython.html')
29 check_for_zmq('2.1.11', 'IPython.html')
30
30
31 from jinja2 import Environment, FileSystemLoader
31 from jinja2 import Environment, FileSystemLoader
32
32
33 # Install the pyzmq ioloop. This has to be done before anything else from
33 # Install the pyzmq ioloop. This has to be done before anything else from
34 # tornado is imported.
34 # tornado is imported.
35 from zmq.eventloop import ioloop
35 from zmq.eventloop import ioloop
36 ioloop.install()
36 ioloop.install()
37
37
38 # check for tornado 3.1.0
38 # check for tornado 3.1.0
39 msg = "The IPython Notebook requires tornado >= 4.0"
39 msg = "The IPython Notebook requires tornado >= 4.0"
40 try:
40 try:
41 import tornado
41 import tornado
42 except ImportError:
42 except ImportError:
43 raise ImportError(msg)
43 raise ImportError(msg)
44 try:
44 try:
45 version_info = tornado.version_info
45 version_info = tornado.version_info
46 except AttributeError:
46 except AttributeError:
47 raise ImportError(msg + ", but you have < 1.1.0")
47 raise ImportError(msg + ", but you have < 1.1.0")
48 if version_info < (4,0):
48 if version_info < (4,0):
49 raise ImportError(msg + ", but you have %s" % tornado.version)
49 raise ImportError(msg + ", but you have %s" % tornado.version)
50
50
51 from tornado import httpserver
51 from tornado import httpserver
52 from tornado import web
52 from tornado import web
53 from tornado.log import LogFormatter, app_log, access_log, gen_log
53 from tornado.log import LogFormatter, app_log, access_log, gen_log
54
54
55 from IPython.html import (
55 from IPython.html import (
56 DEFAULT_STATIC_FILES_PATH,
56 DEFAULT_STATIC_FILES_PATH,
57 DEFAULT_TEMPLATE_PATH_LIST,
57 DEFAULT_TEMPLATE_PATH_LIST,
58 )
58 )
59 from .base.handlers import Template404
59 from .base.handlers import Template404
60 from .log import log_request
60 from .log import log_request
61 from .services.kernels.kernelmanager import MappingKernelManager
61 from .services.kernels.kernelmanager import MappingKernelManager
62 from .services.contents.manager import ContentsManager
62 from .services.contents.manager import ContentsManager
63 from .services.contents.filemanager import FileContentsManager
63 from .services.contents.filemanager import FileContentsManager
64 from .services.clusters.clustermanager import ClusterManager
64 from .services.clusters.clustermanager import ClusterManager
65 from .services.sessions.sessionmanager import SessionManager
65 from .services.sessions.sessionmanager import SessionManager
66
66
67 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
67 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
68
68
69 from IPython.config import Config
69 from IPython.config import Config
70 from IPython.config.application import catch_config_error, boolean_flag
70 from IPython.config.application import catch_config_error, boolean_flag
71 from IPython.core.application import (
71 from IPython.core.application import (
72 BaseIPythonApplication, base_flags, base_aliases,
72 BaseIPythonApplication, base_flags, base_aliases,
73 )
73 )
74 from IPython.core.profiledir import ProfileDir
74 from IPython.core.profiledir import ProfileDir
75 from IPython.kernel import KernelManager
75 from IPython.kernel import KernelManager
76 from IPython.kernel.kernelspec import KernelSpecManager
76 from IPython.kernel.kernelspec import KernelSpecManager
77 from IPython.kernel.zmq.session import default_secure, Session
77 from IPython.kernel.zmq.session import default_secure, Session
78 from IPython.nbformat.sign import NotebookNotary
78 from IPython.nbformat.sign import NotebookNotary
79 from IPython.utils.importstring import import_item
79 from IPython.utils.importstring import import_item
80 from IPython.utils import submodule
80 from IPython.utils import submodule
81 from IPython.utils.process import check_pid
81 from IPython.utils.process import check_pid
82 from IPython.utils.traitlets import (
82 from IPython.utils.traitlets import (
83 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
84 DottedObjectName, TraitError,
84 DottedObjectName, TraitError,
85 )
85 )
86 from IPython.utils import py3compat
86 from IPython.utils import py3compat
87 from IPython.utils.path import filefind, get_ipython_dir
87 from IPython.utils.path import filefind, get_ipython_dir
88 from IPython.utils.sysinfo import get_sys_info
88 from IPython.utils.sysinfo import get_sys_info
89
89
90 from .utils import url_path_join
90 from .utils import url_path_join
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Module globals
93 # Module globals
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95
95
96 _examples = """
96 _examples = """
97 ipython notebook # start the notebook
97 ipython notebook # start the notebook
98 ipython notebook --profile=sympy # use the sympy profile
98 ipython notebook --profile=sympy # use the sympy profile
99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
100 """
100 """
101
101
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103 # Helper functions
103 # Helper functions
104 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
105
105
106 def random_ports(port, n):
106 def random_ports(port, n):
107 """Generate a list of n random ports near the given port.
107 """Generate a list of n random ports near the given port.
108
108
109 The first 5 ports will be sequential, and the remaining n-5 will be
109 The first 5 ports will be sequential, and the remaining n-5 will be
110 randomly selected in the range [port-2*n, port+2*n].
110 randomly selected in the range [port-2*n, port+2*n].
111 """
111 """
112 for i in range(min(5, n)):
112 for i in range(min(5, n)):
113 yield port + i
113 yield port + i
114 for i in range(n-5):
114 for i in range(n-5):
115 yield max(1, port + random.randint(-2*n, 2*n))
115 yield max(1, port + random.randint(-2*n, 2*n))
116
116
117 def load_handlers(name):
117 def load_handlers(name):
118 """Load the (URL pattern, handler) tuples for each component."""
118 """Load the (URL pattern, handler) tuples for each component."""
119 name = 'IPython.html.' + name
119 name = 'IPython.html.' + name
120 mod = __import__(name, fromlist=['default_handlers'])
120 mod = __import__(name, fromlist=['default_handlers'])
121 return mod.default_handlers
121 return mod.default_handlers
122
122
123 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
124 # The Tornado web application
124 # The Tornado web application
125 #-----------------------------------------------------------------------------
125 #-----------------------------------------------------------------------------
126
126
127 class NotebookWebApplication(web.Application):
127 class NotebookWebApplication(web.Application):
128
128
129 def __init__(self, ipython_app, kernel_manager, contents_manager,
129 def __init__(self, ipython_app, kernel_manager, contents_manager,
130 cluster_manager, session_manager, kernel_spec_manager,
130 cluster_manager, session_manager, kernel_spec_manager,
131 config_manager, log,
131 config_manager, log,
132 base_url, default_url, settings_overrides, jinja_env_options):
132 base_url, default_url, settings_overrides, jinja_env_options):
133
133
134 settings = self.init_settings(
134 settings = self.init_settings(
135 ipython_app, kernel_manager, contents_manager, cluster_manager,
135 ipython_app, kernel_manager, contents_manager, cluster_manager,
136 session_manager, kernel_spec_manager, config_manager, log, base_url,
136 session_manager, kernel_spec_manager, config_manager, log, base_url,
137 default_url, settings_overrides, jinja_env_options)
137 default_url, settings_overrides, jinja_env_options)
138 handlers = self.init_handlers(settings)
138 handlers = self.init_handlers(settings)
139
139
140 super(NotebookWebApplication, self).__init__(handlers, **settings)
140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141
141
142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
143 cluster_manager, session_manager, kernel_spec_manager,
143 cluster_manager, session_manager, kernel_spec_manager,
144 config_manager,
144 config_manager,
145 log, base_url, default_url, settings_overrides,
145 log, base_url, default_url, settings_overrides,
146 jinja_env_options=None):
146 jinja_env_options=None):
147
147
148 _template_path = settings_overrides.get(
148 _template_path = settings_overrides.get(
149 "template_path",
149 "template_path",
150 ipython_app.template_file_path,
150 ipython_app.template_file_path,
151 )
151 )
152 if isinstance(_template_path, str):
152 if isinstance(_template_path, str):
153 _template_path = (_template_path,)
153 _template_path = (_template_path,)
154 template_path = [os.path.expanduser(path) for path in _template_path]
154 template_path = [os.path.expanduser(path) for path in _template_path]
155
155
156 jenv_opt = jinja_env_options if jinja_env_options else {}
156 jenv_opt = jinja_env_options if jinja_env_options else {}
157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
158
158
159 sys_info = get_sys_info()
159 sys_info = get_sys_info()
160 if sys_info['commit_source'] == 'repository':
160 if sys_info['commit_source'] == 'repository':
161 # don't cache (rely on 304) when working from master
161 # don't cache (rely on 304) when working from master
162 version_hash = ''
162 version_hash = ''
163 else:
163 else:
164 # reset the cache on server restart
164 # reset the cache on server restart
165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
166
166
167 settings = dict(
167 settings = dict(
168 # basics
168 # basics
169 log_function=log_request,
169 log_function=log_request,
170 base_url=base_url,
170 base_url=base_url,
171 default_url=default_url,
171 default_url=default_url,
172 template_path=template_path,
172 template_path=template_path,
173 static_path=ipython_app.static_file_path,
173 static_path=ipython_app.static_file_path,
174 static_handler_class = FileFindHandler,
174 static_handler_class = FileFindHandler,
175 static_url_prefix = url_path_join(base_url,'/static/'),
175 static_url_prefix = url_path_join(base_url,'/static/'),
176 static_handler_args = {
176 static_handler_args = {
177 # don't cache custom.js
177 # don't cache custom.js
178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
179 },
179 },
180 version_hash=version_hash,
180 version_hash=version_hash,
181
181
182 # authentication
182 # authentication
183 cookie_secret=ipython_app.cookie_secret,
183 cookie_secret=ipython_app.cookie_secret,
184 login_url=url_path_join(base_url,'/login'),
184 login_url=url_path_join(base_url,'/login'),
185 password=ipython_app.password,
185 password=ipython_app.password,
186
186
187 # managers
187 # managers
188 kernel_manager=kernel_manager,
188 kernel_manager=kernel_manager,
189 contents_manager=contents_manager,
189 contents_manager=contents_manager,
190 cluster_manager=cluster_manager,
190 cluster_manager=cluster_manager,
191 session_manager=session_manager,
191 session_manager=session_manager,
192 kernel_spec_manager=kernel_spec_manager,
192 kernel_spec_manager=kernel_spec_manager,
193 config_manager=config_manager,
193 config_manager=config_manager,
194
194
195 # IPython stuff
195 # IPython stuff
196 nbextensions_path = ipython_app.nbextensions_path,
196 nbextensions_path = ipython_app.nbextensions_path,
197 websocket_url=ipython_app.websocket_url,
197 websocket_url=ipython_app.websocket_url,
198 mathjax_url=ipython_app.mathjax_url,
198 mathjax_url=ipython_app.mathjax_url,
199 config=ipython_app.config,
199 config=ipython_app.config,
200 jinja2_env=env,
200 jinja2_env=env,
201 terminals_available=False, # Set later if terminals are available
201 terminals_available=False, # Set later if terminals are available
202 )
202 )
203
203
204 # allow custom overrides for the tornado web app.
204 # allow custom overrides for the tornado web app.
205 settings.update(settings_overrides)
205 settings.update(settings_overrides)
206 return settings
206 return settings
207
207
208 def init_handlers(self, settings):
208 def init_handlers(self, settings):
209 """Load the (URL pattern, handler) tuples for each component."""
209 """Load the (URL pattern, handler) tuples for each component."""
210
210
211 # Order matters. The first handler to match the URL will handle the request.
211 # Order matters. The first handler to match the URL will handle the request.
212 handlers = []
212 handlers = []
213 handlers.extend(load_handlers('tree.handlers'))
213 handlers.extend(load_handlers('tree.handlers'))
214 handlers.extend(load_handlers('auth.login'))
214 handlers.extend(load_handlers('auth.login'))
215 handlers.extend(load_handlers('auth.logout'))
215 handlers.extend(load_handlers('auth.logout'))
216 handlers.extend(load_handlers('files.handlers'))
216 handlers.extend(load_handlers('files.handlers'))
217 handlers.extend(load_handlers('notebook.handlers'))
217 handlers.extend(load_handlers('notebook.handlers'))
218 handlers.extend(load_handlers('nbconvert.handlers'))
218 handlers.extend(load_handlers('nbconvert.handlers'))
219 handlers.extend(load_handlers('kernelspecs.handlers'))
219 handlers.extend(load_handlers('kernelspecs.handlers'))
220 handlers.extend(load_handlers('edit.handlers'))
220 handlers.extend(load_handlers('edit.handlers'))
221 handlers.extend(load_handlers('services.config.handlers'))
221 handlers.extend(load_handlers('services.config.handlers'))
222 handlers.extend(load_handlers('services.kernels.handlers'))
222 handlers.extend(load_handlers('services.kernels.handlers'))
223 handlers.extend(load_handlers('services.contents.handlers'))
223 handlers.extend(load_handlers('services.contents.handlers'))
224 handlers.extend(load_handlers('services.clusters.handlers'))
224 handlers.extend(load_handlers('services.clusters.handlers'))
225 handlers.extend(load_handlers('services.sessions.handlers'))
225 handlers.extend(load_handlers('services.sessions.handlers'))
226 handlers.extend(load_handlers('services.nbconvert.handlers'))
226 handlers.extend(load_handlers('services.nbconvert.handlers'))
227 handlers.extend(load_handlers('services.kernelspecs.handlers'))
227 handlers.extend(load_handlers('services.kernelspecs.handlers'))
228
228 handlers.extend(load_handlers('services.security.handlers'))
229 handlers.append(
229 handlers.append(
230 (r"/nbextensions/(.*)", FileFindHandler, {
230 (r"/nbextensions/(.*)", FileFindHandler, {
231 'path': settings['nbextensions_path'],
231 'path': settings['nbextensions_path'],
232 'no_cache_paths': ['/'], # don't cache anything in nbextensions
232 'no_cache_paths': ['/'], # don't cache anything in nbextensions
233 }),
233 }),
234 )
234 )
235 # register base handlers last
235 # register base handlers last
236 handlers.extend(load_handlers('base.handlers'))
236 handlers.extend(load_handlers('base.handlers'))
237 # set the URL that will be redirected from `/`
237 # set the URL that will be redirected from `/`
238 handlers.append(
238 handlers.append(
239 (r'/?', web.RedirectHandler, {
239 (r'/?', web.RedirectHandler, {
240 'url' : url_path_join(settings['base_url'], settings['default_url']),
240 'url' : url_path_join(settings['base_url'], settings['default_url']),
241 'permanent': False, # want 302, not 301
241 'permanent': False, # want 302, not 301
242 })
242 })
243 )
243 )
244 # prepend base_url onto the patterns that we match
244 # prepend base_url onto the patterns that we match
245 new_handlers = []
245 new_handlers = []
246 for handler in handlers:
246 for handler in handlers:
247 pattern = url_path_join(settings['base_url'], handler[0])
247 pattern = url_path_join(settings['base_url'], handler[0])
248 new_handler = tuple([pattern] + list(handler[1:]))
248 new_handler = tuple([pattern] + list(handler[1:]))
249 new_handlers.append(new_handler)
249 new_handlers.append(new_handler)
250 # add 404 on the end, which will catch everything that falls through
250 # add 404 on the end, which will catch everything that falls through
251 new_handlers.append((r'(.*)', Template404))
251 new_handlers.append((r'(.*)', Template404))
252 return new_handlers
252 return new_handlers
253
253
254
254
255 class NbserverListApp(BaseIPythonApplication):
255 class NbserverListApp(BaseIPythonApplication):
256
256
257 description="List currently running notebook servers in this profile."
257 description="List currently running notebook servers in this profile."
258
258
259 flags = dict(
259 flags = dict(
260 json=({'NbserverListApp': {'json': True}},
260 json=({'NbserverListApp': {'json': True}},
261 "Produce machine-readable JSON output."),
261 "Produce machine-readable JSON output."),
262 )
262 )
263
263
264 json = Bool(False, config=True,
264 json = Bool(False, config=True,
265 help="If True, each line of output will be a JSON object with the "
265 help="If True, each line of output will be a JSON object with the "
266 "details from the server info file.")
266 "details from the server info file.")
267
267
268 def start(self):
268 def start(self):
269 if not self.json:
269 if not self.json:
270 print("Currently running servers:")
270 print("Currently running servers:")
271 for serverinfo in list_running_servers(self.profile):
271 for serverinfo in list_running_servers(self.profile):
272 if self.json:
272 if self.json:
273 print(json.dumps(serverinfo))
273 print(json.dumps(serverinfo))
274 else:
274 else:
275 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
275 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
276
276
277 #-----------------------------------------------------------------------------
277 #-----------------------------------------------------------------------------
278 # Aliases and Flags
278 # Aliases and Flags
279 #-----------------------------------------------------------------------------
279 #-----------------------------------------------------------------------------
280
280
281 flags = dict(base_flags)
281 flags = dict(base_flags)
282 flags['no-browser']=(
282 flags['no-browser']=(
283 {'NotebookApp' : {'open_browser' : False}},
283 {'NotebookApp' : {'open_browser' : False}},
284 "Don't open the notebook in a browser after startup."
284 "Don't open the notebook in a browser after startup."
285 )
285 )
286 flags['pylab']=(
286 flags['pylab']=(
287 {'NotebookApp' : {'pylab' : 'warn'}},
287 {'NotebookApp' : {'pylab' : 'warn'}},
288 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
288 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
289 )
289 )
290 flags['no-mathjax']=(
290 flags['no-mathjax']=(
291 {'NotebookApp' : {'enable_mathjax' : False}},
291 {'NotebookApp' : {'enable_mathjax' : False}},
292 """Disable MathJax
292 """Disable MathJax
293
293
294 MathJax is the javascript library IPython uses to render math/LaTeX. It is
294 MathJax is the javascript library IPython uses to render math/LaTeX. It is
295 very large, so you may want to disable it if you have a slow internet
295 very large, so you may want to disable it if you have a slow internet
296 connection, or for offline use of the notebook.
296 connection, or for offline use of the notebook.
297
297
298 When disabled, equations etc. will appear as their untransformed TeX source.
298 When disabled, equations etc. will appear as their untransformed TeX source.
299 """
299 """
300 )
300 )
301
301
302 # Add notebook manager flags
302 # Add notebook manager flags
303 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
303 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
304 'DEPRECATED, IGNORED',
304 'DEPRECATED, IGNORED',
305 'DEPRECATED, IGNORED'))
305 'DEPRECATED, IGNORED'))
306
306
307 aliases = dict(base_aliases)
307 aliases = dict(base_aliases)
308
308
309 aliases.update({
309 aliases.update({
310 'ip': 'NotebookApp.ip',
310 'ip': 'NotebookApp.ip',
311 'port': 'NotebookApp.port',
311 'port': 'NotebookApp.port',
312 'port-retries': 'NotebookApp.port_retries',
312 'port-retries': 'NotebookApp.port_retries',
313 'transport': 'KernelManager.transport',
313 'transport': 'KernelManager.transport',
314 'keyfile': 'NotebookApp.keyfile',
314 'keyfile': 'NotebookApp.keyfile',
315 'certfile': 'NotebookApp.certfile',
315 'certfile': 'NotebookApp.certfile',
316 'notebook-dir': 'NotebookApp.notebook_dir',
316 'notebook-dir': 'NotebookApp.notebook_dir',
317 'browser': 'NotebookApp.browser',
317 'browser': 'NotebookApp.browser',
318 'pylab': 'NotebookApp.pylab',
318 'pylab': 'NotebookApp.pylab',
319 })
319 })
320
320
321 #-----------------------------------------------------------------------------
321 #-----------------------------------------------------------------------------
322 # NotebookApp
322 # NotebookApp
323 #-----------------------------------------------------------------------------
323 #-----------------------------------------------------------------------------
324
324
325 class NotebookApp(BaseIPythonApplication):
325 class NotebookApp(BaseIPythonApplication):
326
326
327 name = 'ipython-notebook'
327 name = 'ipython-notebook'
328
328
329 description = """
329 description = """
330 The IPython HTML Notebook.
330 The IPython HTML Notebook.
331
331
332 This launches a Tornado based HTML Notebook Server that serves up an
332 This launches a Tornado based HTML Notebook Server that serves up an
333 HTML5/Javascript Notebook client.
333 HTML5/Javascript Notebook client.
334 """
334 """
335 examples = _examples
335 examples = _examples
336 aliases = aliases
336 aliases = aliases
337 flags = flags
337 flags = flags
338
338
339 classes = [
339 classes = [
340 KernelManager, ProfileDir, Session, MappingKernelManager,
340 KernelManager, ProfileDir, Session, MappingKernelManager,
341 ContentsManager, FileContentsManager, NotebookNotary,
341 ContentsManager, FileContentsManager, NotebookNotary,
342 ]
342 ]
343 flags = Dict(flags)
343 flags = Dict(flags)
344 aliases = Dict(aliases)
344 aliases = Dict(aliases)
345
345
346 subcommands = dict(
346 subcommands = dict(
347 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
347 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
348 )
348 )
349
349
350 ipython_kernel_argv = List(Unicode)
350 ipython_kernel_argv = List(Unicode)
351
351
352 _log_formatter_cls = LogFormatter
352 _log_formatter_cls = LogFormatter
353
353
354 def _log_level_default(self):
354 def _log_level_default(self):
355 return logging.INFO
355 return logging.INFO
356
356
357 def _log_datefmt_default(self):
357 def _log_datefmt_default(self):
358 """Exclude date from default date format"""
358 """Exclude date from default date format"""
359 return "%H:%M:%S"
359 return "%H:%M:%S"
360
360
361 def _log_format_default(self):
361 def _log_format_default(self):
362 """override default log format to include time"""
362 """override default log format to include time"""
363 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
363 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
364
364
365 # create requested profiles by default, if they don't exist:
365 # create requested profiles by default, if they don't exist:
366 auto_create = Bool(True)
366 auto_create = Bool(True)
367
367
368 # file to be opened in the notebook server
368 # file to be opened in the notebook server
369 file_to_run = Unicode('', config=True)
369 file_to_run = Unicode('', config=True)
370
370
371 # Network related information
371 # Network related information
372
372
373 allow_origin = Unicode('', config=True,
373 allow_origin = Unicode('', config=True,
374 help="""Set the Access-Control-Allow-Origin header
374 help="""Set the Access-Control-Allow-Origin header
375
375
376 Use '*' to allow any origin to access your server.
376 Use '*' to allow any origin to access your server.
377
377
378 Takes precedence over allow_origin_pat.
378 Takes precedence over allow_origin_pat.
379 """
379 """
380 )
380 )
381
381
382 allow_origin_pat = Unicode('', config=True,
382 allow_origin_pat = Unicode('', config=True,
383 help="""Use a regular expression for the Access-Control-Allow-Origin header
383 help="""Use a regular expression for the Access-Control-Allow-Origin header
384
384
385 Requests from an origin matching the expression will get replies with:
385 Requests from an origin matching the expression will get replies with:
386
386
387 Access-Control-Allow-Origin: origin
387 Access-Control-Allow-Origin: origin
388
388
389 where `origin` is the origin of the request.
389 where `origin` is the origin of the request.
390
390
391 Ignored if allow_origin is set.
391 Ignored if allow_origin is set.
392 """
392 """
393 )
393 )
394
394
395 allow_credentials = Bool(False, config=True,
395 allow_credentials = Bool(False, config=True,
396 help="Set the Access-Control-Allow-Credentials: true header"
396 help="Set the Access-Control-Allow-Credentials: true header"
397 )
397 )
398
398
399 default_url = Unicode('/tree', config=True,
399 default_url = Unicode('/tree', config=True,
400 help="The default URL to redirect to from `/`"
400 help="The default URL to redirect to from `/`"
401 )
401 )
402
402
403 ip = Unicode('localhost', config=True,
403 ip = Unicode('localhost', config=True,
404 help="The IP address the notebook server will listen on."
404 help="The IP address the notebook server will listen on."
405 )
405 )
406
406
407 def _ip_changed(self, name, old, new):
407 def _ip_changed(self, name, old, new):
408 if new == u'*': self.ip = u''
408 if new == u'*': self.ip = u''
409
409
410 port = Integer(8888, config=True,
410 port = Integer(8888, config=True,
411 help="The port the notebook server will listen on."
411 help="The port the notebook server will listen on."
412 )
412 )
413 port_retries = Integer(50, config=True,
413 port_retries = Integer(50, config=True,
414 help="The number of additional ports to try if the specified port is not available."
414 help="The number of additional ports to try if the specified port is not available."
415 )
415 )
416
416
417 certfile = Unicode(u'', config=True,
417 certfile = Unicode(u'', config=True,
418 help="""The full path to an SSL/TLS certificate file."""
418 help="""The full path to an SSL/TLS certificate file."""
419 )
419 )
420
420
421 keyfile = Unicode(u'', config=True,
421 keyfile = Unicode(u'', config=True,
422 help="""The full path to a private key file for usage with SSL/TLS."""
422 help="""The full path to a private key file for usage with SSL/TLS."""
423 )
423 )
424
424
425 cookie_secret_file = Unicode(config=True,
425 cookie_secret_file = Unicode(config=True,
426 help="""The file where the cookie secret is stored."""
426 help="""The file where the cookie secret is stored."""
427 )
427 )
428 def _cookie_secret_file_default(self):
428 def _cookie_secret_file_default(self):
429 if self.profile_dir is None:
429 if self.profile_dir is None:
430 return ''
430 return ''
431 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
431 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
432
432
433 cookie_secret = Bytes(b'', config=True,
433 cookie_secret = Bytes(b'', config=True,
434 help="""The random bytes used to secure cookies.
434 help="""The random bytes used to secure cookies.
435 By default this is a new random number every time you start the Notebook.
435 By default this is a new random number every time you start the Notebook.
436 Set it to a value in a config file to enable logins to persist across server sessions.
436 Set it to a value in a config file to enable logins to persist across server sessions.
437
437
438 Note: Cookie secrets should be kept private, do not share config files with
438 Note: Cookie secrets should be kept private, do not share config files with
439 cookie_secret stored in plaintext (you can read the value from a file).
439 cookie_secret stored in plaintext (you can read the value from a file).
440 """
440 """
441 )
441 )
442 def _cookie_secret_default(self):
442 def _cookie_secret_default(self):
443 if os.path.exists(self.cookie_secret_file):
443 if os.path.exists(self.cookie_secret_file):
444 with io.open(self.cookie_secret_file, 'rb') as f:
444 with io.open(self.cookie_secret_file, 'rb') as f:
445 return f.read()
445 return f.read()
446 else:
446 else:
447 secret = base64.encodestring(os.urandom(1024))
447 secret = base64.encodestring(os.urandom(1024))
448 self._write_cookie_secret_file(secret)
448 self._write_cookie_secret_file(secret)
449 return secret
449 return secret
450
450
451 def _write_cookie_secret_file(self, secret):
451 def _write_cookie_secret_file(self, secret):
452 """write my secret to my secret_file"""
452 """write my secret to my secret_file"""
453 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
453 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
454 with io.open(self.cookie_secret_file, 'wb') as f:
454 with io.open(self.cookie_secret_file, 'wb') as f:
455 f.write(secret)
455 f.write(secret)
456 try:
456 try:
457 os.chmod(self.cookie_secret_file, 0o600)
457 os.chmod(self.cookie_secret_file, 0o600)
458 except OSError:
458 except OSError:
459 self.log.warn(
459 self.log.warn(
460 "Could not set permissions on %s",
460 "Could not set permissions on %s",
461 self.cookie_secret_file
461 self.cookie_secret_file
462 )
462 )
463
463
464 password = Unicode(u'', config=True,
464 password = Unicode(u'', config=True,
465 help="""Hashed password to use for web authentication.
465 help="""Hashed password to use for web authentication.
466
466
467 To generate, type in a python/IPython shell:
467 To generate, type in a python/IPython shell:
468
468
469 from IPython.lib import passwd; passwd()
469 from IPython.lib import passwd; passwd()
470
470
471 The string should be of the form type:salt:hashed-password.
471 The string should be of the form type:salt:hashed-password.
472 """
472 """
473 )
473 )
474
474
475 open_browser = Bool(True, config=True,
475 open_browser = Bool(True, config=True,
476 help="""Whether to open in a browser after starting.
476 help="""Whether to open in a browser after starting.
477 The specific browser used is platform dependent and
477 The specific browser used is platform dependent and
478 determined by the python standard library `webbrowser`
478 determined by the python standard library `webbrowser`
479 module, unless it is overridden using the --browser
479 module, unless it is overridden using the --browser
480 (NotebookApp.browser) configuration option.
480 (NotebookApp.browser) configuration option.
481 """)
481 """)
482
482
483 browser = Unicode(u'', config=True,
483 browser = Unicode(u'', config=True,
484 help="""Specify what command to use to invoke a web
484 help="""Specify what command to use to invoke a web
485 browser when opening the notebook. If not specified, the
485 browser when opening the notebook. If not specified, the
486 default browser will be determined by the `webbrowser`
486 default browser will be determined by the `webbrowser`
487 standard library module, which allows setting of the
487 standard library module, which allows setting of the
488 BROWSER environment variable to override it.
488 BROWSER environment variable to override it.
489 """)
489 """)
490
490
491 webapp_settings = Dict(config=True,
491 webapp_settings = Dict(config=True,
492 help="DEPRECATED, use tornado_settings"
492 help="DEPRECATED, use tornado_settings"
493 )
493 )
494 def _webapp_settings_changed(self, name, old, new):
494 def _webapp_settings_changed(self, name, old, new):
495 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
495 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
496 self.tornado_settings = new
496 self.tornado_settings = new
497
497
498 tornado_settings = Dict(config=True,
498 tornado_settings = Dict(config=True,
499 help="Supply overrides for the tornado.web.Application that the "
499 help="Supply overrides for the tornado.web.Application that the "
500 "IPython notebook uses.")
500 "IPython notebook uses.")
501
501
502 jinja_environment_options = Dict(config=True,
502 jinja_environment_options = Dict(config=True,
503 help="Supply extra arguments that will be passed to Jinja environment.")
503 help="Supply extra arguments that will be passed to Jinja environment.")
504
504
505
505
506 enable_mathjax = Bool(True, config=True,
506 enable_mathjax = Bool(True, config=True,
507 help="""Whether to enable MathJax for typesetting math/TeX
507 help="""Whether to enable MathJax for typesetting math/TeX
508
508
509 MathJax is the javascript library IPython uses to render math/LaTeX. It is
509 MathJax is the javascript library IPython uses to render math/LaTeX. It is
510 very large, so you may want to disable it if you have a slow internet
510 very large, so you may want to disable it if you have a slow internet
511 connection, or for offline use of the notebook.
511 connection, or for offline use of the notebook.
512
512
513 When disabled, equations etc. will appear as their untransformed TeX source.
513 When disabled, equations etc. will appear as their untransformed TeX source.
514 """
514 """
515 )
515 )
516 def _enable_mathjax_changed(self, name, old, new):
516 def _enable_mathjax_changed(self, name, old, new):
517 """set mathjax url to empty if mathjax is disabled"""
517 """set mathjax url to empty if mathjax is disabled"""
518 if not new:
518 if not new:
519 self.mathjax_url = u''
519 self.mathjax_url = u''
520
520
521 base_url = Unicode('/', config=True,
521 base_url = Unicode('/', config=True,
522 help='''The base URL for the notebook server.
522 help='''The base URL for the notebook server.
523
523
524 Leading and trailing slashes can be omitted,
524 Leading and trailing slashes can be omitted,
525 and will automatically be added.
525 and will automatically be added.
526 ''')
526 ''')
527 def _base_url_changed(self, name, old, new):
527 def _base_url_changed(self, name, old, new):
528 if not new.startswith('/'):
528 if not new.startswith('/'):
529 self.base_url = '/'+new
529 self.base_url = '/'+new
530 elif not new.endswith('/'):
530 elif not new.endswith('/'):
531 self.base_url = new+'/'
531 self.base_url = new+'/'
532
532
533 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
533 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
534 def _base_project_url_changed(self, name, old, new):
534 def _base_project_url_changed(self, name, old, new):
535 self.log.warn("base_project_url is deprecated, use base_url")
535 self.log.warn("base_project_url is deprecated, use base_url")
536 self.base_url = new
536 self.base_url = new
537
537
538 extra_static_paths = List(Unicode, config=True,
538 extra_static_paths = List(Unicode, config=True,
539 help="""Extra paths to search for serving static files.
539 help="""Extra paths to search for serving static files.
540
540
541 This allows adding javascript/css to be available from the notebook server machine,
541 This allows adding javascript/css to be available from the notebook server machine,
542 or overriding individual files in the IPython"""
542 or overriding individual files in the IPython"""
543 )
543 )
544 def _extra_static_paths_default(self):
544 def _extra_static_paths_default(self):
545 return [os.path.join(self.profile_dir.location, 'static')]
545 return [os.path.join(self.profile_dir.location, 'static')]
546
546
547 @property
547 @property
548 def static_file_path(self):
548 def static_file_path(self):
549 """return extra paths + the default location"""
549 """return extra paths + the default location"""
550 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
550 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
551
551
552 extra_template_paths = List(Unicode, config=True,
552 extra_template_paths = List(Unicode, config=True,
553 help="""Extra paths to search for serving jinja templates.
553 help="""Extra paths to search for serving jinja templates.
554
554
555 Can be used to override templates from IPython.html.templates."""
555 Can be used to override templates from IPython.html.templates."""
556 )
556 )
557 def _extra_template_paths_default(self):
557 def _extra_template_paths_default(self):
558 return []
558 return []
559
559
560 @property
560 @property
561 def template_file_path(self):
561 def template_file_path(self):
562 """return extra paths + the default locations"""
562 """return extra paths + the default locations"""
563 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
563 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
564
564
565 nbextensions_path = List(Unicode, config=True,
565 nbextensions_path = List(Unicode, config=True,
566 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
566 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
567 )
567 )
568 def _nbextensions_path_default(self):
568 def _nbextensions_path_default(self):
569 return [os.path.join(get_ipython_dir(), 'nbextensions')]
569 return [os.path.join(get_ipython_dir(), 'nbextensions')]
570
570
571 websocket_url = Unicode("", config=True,
571 websocket_url = Unicode("", config=True,
572 help="""The base URL for websockets,
572 help="""The base URL for websockets,
573 if it differs from the HTTP server (hint: it almost certainly doesn't).
573 if it differs from the HTTP server (hint: it almost certainly doesn't).
574
574
575 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
575 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
576 """
576 """
577 )
577 )
578 mathjax_url = Unicode("", config=True,
578 mathjax_url = Unicode("", config=True,
579 help="""The url for MathJax.js."""
579 help="""The url for MathJax.js."""
580 )
580 )
581 def _mathjax_url_default(self):
581 def _mathjax_url_default(self):
582 if not self.enable_mathjax:
582 if not self.enable_mathjax:
583 return u''
583 return u''
584 static_url_prefix = self.tornado_settings.get("static_url_prefix",
584 static_url_prefix = self.tornado_settings.get("static_url_prefix",
585 url_path_join(self.base_url, "static")
585 url_path_join(self.base_url, "static")
586 )
586 )
587
587
588 # try local mathjax, either in nbextensions/mathjax or static/mathjax
588 # try local mathjax, either in nbextensions/mathjax or static/mathjax
589 for (url_prefix, search_path) in [
589 for (url_prefix, search_path) in [
590 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
590 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
591 (static_url_prefix, self.static_file_path),
591 (static_url_prefix, self.static_file_path),
592 ]:
592 ]:
593 self.log.debug("searching for local mathjax in %s", search_path)
593 self.log.debug("searching for local mathjax in %s", search_path)
594 try:
594 try:
595 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
595 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
596 except IOError:
596 except IOError:
597 continue
597 continue
598 else:
598 else:
599 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
599 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
600 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
600 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
601 return url
601 return url
602
602
603 # no local mathjax, serve from CDN
603 # no local mathjax, serve from CDN
604 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
604 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
605 self.log.info("Using MathJax from CDN: %s", url)
605 self.log.info("Using MathJax from CDN: %s", url)
606 return url
606 return url
607
607
608 def _mathjax_url_changed(self, name, old, new):
608 def _mathjax_url_changed(self, name, old, new):
609 if new and not self.enable_mathjax:
609 if new and not self.enable_mathjax:
610 # enable_mathjax=False overrides mathjax_url
610 # enable_mathjax=False overrides mathjax_url
611 self.mathjax_url = u''
611 self.mathjax_url = u''
612 else:
612 else:
613 self.log.info("Using MathJax: %s", new)
613 self.log.info("Using MathJax: %s", new)
614
614
615 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
615 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
616 config=True,
616 config=True,
617 help='The notebook manager class to use.'
617 help='The notebook manager class to use.'
618 )
618 )
619 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
619 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
620 config=True,
620 config=True,
621 help='The kernel manager class to use.'
621 help='The kernel manager class to use.'
622 )
622 )
623 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
623 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
624 config=True,
624 config=True,
625 help='The session manager class to use.'
625 help='The session manager class to use.'
626 )
626 )
627 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
627 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
628 config=True,
628 config=True,
629 help='The cluster manager class to use.'
629 help='The cluster manager class to use.'
630 )
630 )
631
631
632 config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
632 config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
633 config = True,
633 config = True,
634 help='The config manager class to use'
634 help='The config manager class to use'
635 )
635 )
636
636
637 kernel_spec_manager = Instance(KernelSpecManager)
637 kernel_spec_manager = Instance(KernelSpecManager)
638
638
639 def _kernel_spec_manager_default(self):
639 def _kernel_spec_manager_default(self):
640 return KernelSpecManager(ipython_dir=self.ipython_dir)
640 return KernelSpecManager(ipython_dir=self.ipython_dir)
641
641
642
642
643 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
643 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
644 config=True,
644 config=True,
645 help="""
645 help="""
646 The kernel spec manager class to use. Should be a subclass
646 The kernel spec manager class to use. Should be a subclass
647 of `IPython.kernel.kernelspec.KernelSpecManager`.
647 of `IPython.kernel.kernelspec.KernelSpecManager`.
648
648
649 The Api of KernelSpecManager is provisional and might change
649 The Api of KernelSpecManager is provisional and might change
650 without warning between this version of IPython and the next stable one.
650 without warning between this version of IPython and the next stable one.
651 """)
651 """)
652
652
653 trust_xheaders = Bool(False, config=True,
653 trust_xheaders = Bool(False, config=True,
654 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
654 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
655 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
655 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
656 )
656 )
657
657
658 info_file = Unicode()
658 info_file = Unicode()
659
659
660 def _info_file_default(self):
660 def _info_file_default(self):
661 info_file = "nbserver-%s.json"%os.getpid()
661 info_file = "nbserver-%s.json"%os.getpid()
662 return os.path.join(self.profile_dir.security_dir, info_file)
662 return os.path.join(self.profile_dir.security_dir, info_file)
663
663
664 pylab = Unicode('disabled', config=True,
664 pylab = Unicode('disabled', config=True,
665 help="""
665 help="""
666 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
666 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
667 """
667 """
668 )
668 )
669 def _pylab_changed(self, name, old, new):
669 def _pylab_changed(self, name, old, new):
670 """when --pylab is specified, display a warning and exit"""
670 """when --pylab is specified, display a warning and exit"""
671 if new != 'warn':
671 if new != 'warn':
672 backend = ' %s' % new
672 backend = ' %s' % new
673 else:
673 else:
674 backend = ''
674 backend = ''
675 self.log.error("Support for specifying --pylab on the command line has been removed.")
675 self.log.error("Support for specifying --pylab on the command line has been removed.")
676 self.log.error(
676 self.log.error(
677 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
677 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
678 )
678 )
679 self.exit(1)
679 self.exit(1)
680
680
681 notebook_dir = Unicode(config=True,
681 notebook_dir = Unicode(config=True,
682 help="The directory to use for notebooks and kernels."
682 help="The directory to use for notebooks and kernels."
683 )
683 )
684
684
685 def _notebook_dir_default(self):
685 def _notebook_dir_default(self):
686 if self.file_to_run:
686 if self.file_to_run:
687 return os.path.dirname(os.path.abspath(self.file_to_run))
687 return os.path.dirname(os.path.abspath(self.file_to_run))
688 else:
688 else:
689 return py3compat.getcwd()
689 return py3compat.getcwd()
690
690
691 def _notebook_dir_changed(self, name, old, new):
691 def _notebook_dir_changed(self, name, old, new):
692 """Do a bit of validation of the notebook dir."""
692 """Do a bit of validation of the notebook dir."""
693 if not os.path.isabs(new):
693 if not os.path.isabs(new):
694 # If we receive a non-absolute path, make it absolute.
694 # If we receive a non-absolute path, make it absolute.
695 self.notebook_dir = os.path.abspath(new)
695 self.notebook_dir = os.path.abspath(new)
696 return
696 return
697 if not os.path.isdir(new):
697 if not os.path.isdir(new):
698 raise TraitError("No such notebook dir: %r" % new)
698 raise TraitError("No such notebook dir: %r" % new)
699
699
700 # setting App.notebook_dir implies setting notebook and kernel dirs as well
700 # setting App.notebook_dir implies setting notebook and kernel dirs as well
701 self.config.FileContentsManager.root_dir = new
701 self.config.FileContentsManager.root_dir = new
702 self.config.MappingKernelManager.root_dir = new
702 self.config.MappingKernelManager.root_dir = new
703
703
704
704
705 def parse_command_line(self, argv=None):
705 def parse_command_line(self, argv=None):
706 super(NotebookApp, self).parse_command_line(argv)
706 super(NotebookApp, self).parse_command_line(argv)
707
707
708 if self.extra_args:
708 if self.extra_args:
709 arg0 = self.extra_args[0]
709 arg0 = self.extra_args[0]
710 f = os.path.abspath(arg0)
710 f = os.path.abspath(arg0)
711 self.argv.remove(arg0)
711 self.argv.remove(arg0)
712 if not os.path.exists(f):
712 if not os.path.exists(f):
713 self.log.critical("No such file or directory: %s", f)
713 self.log.critical("No such file or directory: %s", f)
714 self.exit(1)
714 self.exit(1)
715
715
716 # Use config here, to ensure that it takes higher priority than
716 # Use config here, to ensure that it takes higher priority than
717 # anything that comes from the profile.
717 # anything that comes from the profile.
718 c = Config()
718 c = Config()
719 if os.path.isdir(f):
719 if os.path.isdir(f):
720 c.NotebookApp.notebook_dir = f
720 c.NotebookApp.notebook_dir = f
721 elif os.path.isfile(f):
721 elif os.path.isfile(f):
722 c.NotebookApp.file_to_run = f
722 c.NotebookApp.file_to_run = f
723 self.update_config(c)
723 self.update_config(c)
724
724
725 def init_kernel_argv(self):
725 def init_kernel_argv(self):
726 """add the profile-dir to arguments to be passed to IPython kernels"""
726 """add the profile-dir to arguments to be passed to IPython kernels"""
727 # FIXME: remove special treatment of IPython kernels
727 # FIXME: remove special treatment of IPython kernels
728 # Kernel should get *absolute* path to profile directory
728 # Kernel should get *absolute* path to profile directory
729 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
729 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
730
730
731 def init_configurables(self):
731 def init_configurables(self):
732 # force Session default to be secure
732 # force Session default to be secure
733 default_secure(self.config)
733 default_secure(self.config)
734 kls = import_item(self.kernel_spec_manager_class)
734 kls = import_item(self.kernel_spec_manager_class)
735 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
735 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
736
736
737 kls = import_item(self.kernel_manager_class)
737 kls = import_item(self.kernel_manager_class)
738 self.kernel_manager = kls(
738 self.kernel_manager = kls(
739 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
739 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
740 connection_dir = self.profile_dir.security_dir,
740 connection_dir = self.profile_dir.security_dir,
741 )
741 )
742 kls = import_item(self.contents_manager_class)
742 kls = import_item(self.contents_manager_class)
743 self.contents_manager = kls(parent=self, log=self.log)
743 self.contents_manager = kls(parent=self, log=self.log)
744 kls = import_item(self.session_manager_class)
744 kls = import_item(self.session_manager_class)
745 self.session_manager = kls(parent=self, log=self.log,
745 self.session_manager = kls(parent=self, log=self.log,
746 kernel_manager=self.kernel_manager,
746 kernel_manager=self.kernel_manager,
747 contents_manager=self.contents_manager)
747 contents_manager=self.contents_manager)
748 kls = import_item(self.cluster_manager_class)
748 kls = import_item(self.cluster_manager_class)
749 self.cluster_manager = kls(parent=self, log=self.log)
749 self.cluster_manager = kls(parent=self, log=self.log)
750 self.cluster_manager.update_profiles()
750 self.cluster_manager.update_profiles()
751
751
752 kls = import_item(self.config_manager_class)
752 kls = import_item(self.config_manager_class)
753 self.config_manager = kls(parent=self, log=self.log,
753 self.config_manager = kls(parent=self, log=self.log,
754 profile_dir=self.profile_dir.location)
754 profile_dir=self.profile_dir.location)
755
755
756 def init_logging(self):
756 def init_logging(self):
757 # This prevents double log messages because tornado use a root logger that
757 # This prevents double log messages because tornado use a root logger that
758 # self.log is a child of. The logging module dipatches log messages to a log
758 # self.log is a child of. The logging module dipatches log messages to a log
759 # and all of its ancenstors until propagate is set to False.
759 # and all of its ancenstors until propagate is set to False.
760 self.log.propagate = False
760 self.log.propagate = False
761
761
762 for log in app_log, access_log, gen_log:
762 for log in app_log, access_log, gen_log:
763 # consistent log output name (NotebookApp instead of tornado.access, etc.)
763 # consistent log output name (NotebookApp instead of tornado.access, etc.)
764 log.name = self.log.name
764 log.name = self.log.name
765 # hook up tornado 3's loggers to our app handlers
765 # hook up tornado 3's loggers to our app handlers
766 logger = logging.getLogger('tornado')
766 logger = logging.getLogger('tornado')
767 logger.propagate = True
767 logger.propagate = True
768 logger.parent = self.log
768 logger.parent = self.log
769 logger.setLevel(self.log.level)
769 logger.setLevel(self.log.level)
770
770
771 def init_webapp(self):
771 def init_webapp(self):
772 """initialize tornado webapp and httpserver"""
772 """initialize tornado webapp and httpserver"""
773 self.tornado_settings['allow_origin'] = self.allow_origin
773 self.tornado_settings['allow_origin'] = self.allow_origin
774 if self.allow_origin_pat:
774 if self.allow_origin_pat:
775 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
775 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
776 self.tornado_settings['allow_credentials'] = self.allow_credentials
776 self.tornado_settings['allow_credentials'] = self.allow_credentials
777
777
778 self.web_app = NotebookWebApplication(
778 self.web_app = NotebookWebApplication(
779 self, self.kernel_manager, self.contents_manager,
779 self, self.kernel_manager, self.contents_manager,
780 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
780 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
781 self.config_manager,
781 self.config_manager,
782 self.log, self.base_url, self.default_url, self.tornado_settings,
782 self.log, self.base_url, self.default_url, self.tornado_settings,
783 self.jinja_environment_options
783 self.jinja_environment_options
784 )
784 )
785 if self.certfile:
785 if self.certfile:
786 ssl_options = dict(certfile=self.certfile)
786 ssl_options = dict(certfile=self.certfile)
787 if self.keyfile:
787 if self.keyfile:
788 ssl_options['keyfile'] = self.keyfile
788 ssl_options['keyfile'] = self.keyfile
789 else:
789 else:
790 ssl_options = None
790 ssl_options = None
791 self.web_app.password = self.password
791 self.web_app.password = self.password
792 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
792 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
793 xheaders=self.trust_xheaders)
793 xheaders=self.trust_xheaders)
794 if not self.ip:
794 if not self.ip:
795 warning = "WARNING: The notebook server is listening on all IP addresses"
795 warning = "WARNING: The notebook server is listening on all IP addresses"
796 if ssl_options is None:
796 if ssl_options is None:
797 self.log.critical(warning + " and not using encryption. This "
797 self.log.critical(warning + " and not using encryption. This "
798 "is not recommended.")
798 "is not recommended.")
799 if not self.password:
799 if not self.password:
800 self.log.critical(warning + " and not using authentication. "
800 self.log.critical(warning + " and not using authentication. "
801 "This is highly insecure and not recommended.")
801 "This is highly insecure and not recommended.")
802 success = None
802 success = None
803 for port in random_ports(self.port, self.port_retries+1):
803 for port in random_ports(self.port, self.port_retries+1):
804 try:
804 try:
805 self.http_server.listen(port, self.ip)
805 self.http_server.listen(port, self.ip)
806 except socket.error as e:
806 except socket.error as e:
807 if e.errno == errno.EADDRINUSE:
807 if e.errno == errno.EADDRINUSE:
808 self.log.info('The port %i is already in use, trying another random port.' % port)
808 self.log.info('The port %i is already in use, trying another random port.' % port)
809 continue
809 continue
810 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
810 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
811 self.log.warn("Permission to listen on port %i denied" % port)
811 self.log.warn("Permission to listen on port %i denied" % port)
812 continue
812 continue
813 else:
813 else:
814 raise
814 raise
815 else:
815 else:
816 self.port = port
816 self.port = port
817 success = True
817 success = True
818 break
818 break
819 if not success:
819 if not success:
820 self.log.critical('ERROR: the notebook server could not be started because '
820 self.log.critical('ERROR: the notebook server could not be started because '
821 'no available port could be found.')
821 'no available port could be found.')
822 self.exit(1)
822 self.exit(1)
823
823
824 @property
824 @property
825 def display_url(self):
825 def display_url(self):
826 ip = self.ip if self.ip else '[all ip addresses on your system]'
826 ip = self.ip if self.ip else '[all ip addresses on your system]'
827 return self._url(ip)
827 return self._url(ip)
828
828
829 @property
829 @property
830 def connection_url(self):
830 def connection_url(self):
831 ip = self.ip if self.ip else 'localhost'
831 ip = self.ip if self.ip else 'localhost'
832 return self._url(ip)
832 return self._url(ip)
833
833
834 def _url(self, ip):
834 def _url(self, ip):
835 proto = 'https' if self.certfile else 'http'
835 proto = 'https' if self.certfile else 'http'
836 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
836 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
837
837
838 def init_terminals(self):
838 def init_terminals(self):
839 try:
839 try:
840 from .terminal import initialize
840 from .terminal import initialize
841 initialize(self.web_app)
841 initialize(self.web_app)
842 self.web_app.settings['terminals_available'] = True
842 self.web_app.settings['terminals_available'] = True
843 except ImportError as e:
843 except ImportError as e:
844 self.log.info("Terminals not available (error was %s)", e)
844 self.log.info("Terminals not available (error was %s)", e)
845
845
846 def init_signal(self):
846 def init_signal(self):
847 if not sys.platform.startswith('win'):
847 if not sys.platform.startswith('win'):
848 signal.signal(signal.SIGINT, self._handle_sigint)
848 signal.signal(signal.SIGINT, self._handle_sigint)
849 signal.signal(signal.SIGTERM, self._signal_stop)
849 signal.signal(signal.SIGTERM, self._signal_stop)
850 if hasattr(signal, 'SIGUSR1'):
850 if hasattr(signal, 'SIGUSR1'):
851 # Windows doesn't support SIGUSR1
851 # Windows doesn't support SIGUSR1
852 signal.signal(signal.SIGUSR1, self._signal_info)
852 signal.signal(signal.SIGUSR1, self._signal_info)
853 if hasattr(signal, 'SIGINFO'):
853 if hasattr(signal, 'SIGINFO'):
854 # only on BSD-based systems
854 # only on BSD-based systems
855 signal.signal(signal.SIGINFO, self._signal_info)
855 signal.signal(signal.SIGINFO, self._signal_info)
856
856
857 def _handle_sigint(self, sig, frame):
857 def _handle_sigint(self, sig, frame):
858 """SIGINT handler spawns confirmation dialog"""
858 """SIGINT handler spawns confirmation dialog"""
859 # register more forceful signal handler for ^C^C case
859 # register more forceful signal handler for ^C^C case
860 signal.signal(signal.SIGINT, self._signal_stop)
860 signal.signal(signal.SIGINT, self._signal_stop)
861 # request confirmation dialog in bg thread, to avoid
861 # request confirmation dialog in bg thread, to avoid
862 # blocking the App
862 # blocking the App
863 thread = threading.Thread(target=self._confirm_exit)
863 thread = threading.Thread(target=self._confirm_exit)
864 thread.daemon = True
864 thread.daemon = True
865 thread.start()
865 thread.start()
866
866
867 def _restore_sigint_handler(self):
867 def _restore_sigint_handler(self):
868 """callback for restoring original SIGINT handler"""
868 """callback for restoring original SIGINT handler"""
869 signal.signal(signal.SIGINT, self._handle_sigint)
869 signal.signal(signal.SIGINT, self._handle_sigint)
870
870
871 def _confirm_exit(self):
871 def _confirm_exit(self):
872 """confirm shutdown on ^C
872 """confirm shutdown on ^C
873
873
874 A second ^C, or answering 'y' within 5s will cause shutdown,
874 A second ^C, or answering 'y' within 5s will cause shutdown,
875 otherwise original SIGINT handler will be restored.
875 otherwise original SIGINT handler will be restored.
876
876
877 This doesn't work on Windows.
877 This doesn't work on Windows.
878 """
878 """
879 info = self.log.info
879 info = self.log.info
880 info('interrupted')
880 info('interrupted')
881 print(self.notebook_info())
881 print(self.notebook_info())
882 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
882 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
883 sys.stdout.flush()
883 sys.stdout.flush()
884 r,w,x = select.select([sys.stdin], [], [], 5)
884 r,w,x = select.select([sys.stdin], [], [], 5)
885 if r:
885 if r:
886 line = sys.stdin.readline()
886 line = sys.stdin.readline()
887 if line.lower().startswith('y') and 'n' not in line.lower():
887 if line.lower().startswith('y') and 'n' not in line.lower():
888 self.log.critical("Shutdown confirmed")
888 self.log.critical("Shutdown confirmed")
889 ioloop.IOLoop.instance().stop()
889 ioloop.IOLoop.instance().stop()
890 return
890 return
891 else:
891 else:
892 print("No answer for 5s:", end=' ')
892 print("No answer for 5s:", end=' ')
893 print("resuming operation...")
893 print("resuming operation...")
894 # no answer, or answer is no:
894 # no answer, or answer is no:
895 # set it back to original SIGINT handler
895 # set it back to original SIGINT handler
896 # use IOLoop.add_callback because signal.signal must be called
896 # use IOLoop.add_callback because signal.signal must be called
897 # from main thread
897 # from main thread
898 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
898 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
899
899
900 def _signal_stop(self, sig, frame):
900 def _signal_stop(self, sig, frame):
901 self.log.critical("received signal %s, stopping", sig)
901 self.log.critical("received signal %s, stopping", sig)
902 ioloop.IOLoop.instance().stop()
902 ioloop.IOLoop.instance().stop()
903
903
904 def _signal_info(self, sig, frame):
904 def _signal_info(self, sig, frame):
905 print(self.notebook_info())
905 print(self.notebook_info())
906
906
907 def init_components(self):
907 def init_components(self):
908 """Check the components submodule, and warn if it's unclean"""
908 """Check the components submodule, and warn if it's unclean"""
909 status = submodule.check_submodule_status()
909 status = submodule.check_submodule_status()
910 if status == 'missing':
910 if status == 'missing':
911 self.log.warn("components submodule missing, running `git submodule update`")
911 self.log.warn("components submodule missing, running `git submodule update`")
912 submodule.update_submodules(submodule.ipython_parent())
912 submodule.update_submodules(submodule.ipython_parent())
913 elif status == 'unclean':
913 elif status == 'unclean':
914 self.log.warn("components submodule unclean, you may see 404s on static/components")
914 self.log.warn("components submodule unclean, you may see 404s on static/components")
915 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
915 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
916
916
917 @catch_config_error
917 @catch_config_error
918 def initialize(self, argv=None):
918 def initialize(self, argv=None):
919 super(NotebookApp, self).initialize(argv)
919 super(NotebookApp, self).initialize(argv)
920 self.init_logging()
920 self.init_logging()
921 self.init_kernel_argv()
921 self.init_kernel_argv()
922 self.init_configurables()
922 self.init_configurables()
923 self.init_components()
923 self.init_components()
924 self.init_webapp()
924 self.init_webapp()
925 self.init_terminals()
925 self.init_terminals()
926 self.init_signal()
926 self.init_signal()
927
927
928 def cleanup_kernels(self):
928 def cleanup_kernels(self):
929 """Shutdown all kernels.
929 """Shutdown all kernels.
930
930
931 The kernels will shutdown themselves when this process no longer exists,
931 The kernels will shutdown themselves when this process no longer exists,
932 but explicit shutdown allows the KernelManagers to cleanup the connection files.
932 but explicit shutdown allows the KernelManagers to cleanup the connection files.
933 """
933 """
934 self.log.info('Shutting down kernels')
934 self.log.info('Shutting down kernels')
935 self.kernel_manager.shutdown_all()
935 self.kernel_manager.shutdown_all()
936
936
937 def notebook_info(self):
937 def notebook_info(self):
938 "Return the current working directory and the server url information"
938 "Return the current working directory and the server url information"
939 info = self.contents_manager.info_string() + "\n"
939 info = self.contents_manager.info_string() + "\n"
940 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
940 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
941 return info + "The IPython Notebook is running at: %s" % self.display_url
941 return info + "The IPython Notebook is running at: %s" % self.display_url
942
942
943 def server_info(self):
943 def server_info(self):
944 """Return a JSONable dict of information about this server."""
944 """Return a JSONable dict of information about this server."""
945 return {'url': self.connection_url,
945 return {'url': self.connection_url,
946 'hostname': self.ip if self.ip else 'localhost',
946 'hostname': self.ip if self.ip else 'localhost',
947 'port': self.port,
947 'port': self.port,
948 'secure': bool(self.certfile),
948 'secure': bool(self.certfile),
949 'base_url': self.base_url,
949 'base_url': self.base_url,
950 'notebook_dir': os.path.abspath(self.notebook_dir),
950 'notebook_dir': os.path.abspath(self.notebook_dir),
951 'pid': os.getpid()
951 'pid': os.getpid()
952 }
952 }
953
953
954 def write_server_info_file(self):
954 def write_server_info_file(self):
955 """Write the result of server_info() to the JSON file info_file."""
955 """Write the result of server_info() to the JSON file info_file."""
956 with open(self.info_file, 'w') as f:
956 with open(self.info_file, 'w') as f:
957 json.dump(self.server_info(), f, indent=2)
957 json.dump(self.server_info(), f, indent=2)
958
958
959 def remove_server_info_file(self):
959 def remove_server_info_file(self):
960 """Remove the nbserver-<pid>.json file created for this server.
960 """Remove the nbserver-<pid>.json file created for this server.
961
961
962 Ignores the error raised when the file has already been removed.
962 Ignores the error raised when the file has already been removed.
963 """
963 """
964 try:
964 try:
965 os.unlink(self.info_file)
965 os.unlink(self.info_file)
966 except OSError as e:
966 except OSError as e:
967 if e.errno != errno.ENOENT:
967 if e.errno != errno.ENOENT:
968 raise
968 raise
969
969
970 def start(self):
970 def start(self):
971 """ Start the IPython Notebook server app, after initialization
971 """ Start the IPython Notebook server app, after initialization
972
972
973 This method takes no arguments so all configuration and initialization
973 This method takes no arguments so all configuration and initialization
974 must be done prior to calling this method."""
974 must be done prior to calling this method."""
975 if self.subapp is not None:
975 if self.subapp is not None:
976 return self.subapp.start()
976 return self.subapp.start()
977
977
978 info = self.log.info
978 info = self.log.info
979 for line in self.notebook_info().split("\n"):
979 for line in self.notebook_info().split("\n"):
980 info(line)
980 info(line)
981 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
981 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
982
982
983 self.write_server_info_file()
983 self.write_server_info_file()
984
984
985 if self.open_browser or self.file_to_run:
985 if self.open_browser or self.file_to_run:
986 try:
986 try:
987 browser = webbrowser.get(self.browser or None)
987 browser = webbrowser.get(self.browser or None)
988 except webbrowser.Error as e:
988 except webbrowser.Error as e:
989 self.log.warn('No web browser found: %s.' % e)
989 self.log.warn('No web browser found: %s.' % e)
990 browser = None
990 browser = None
991
991
992 if self.file_to_run:
992 if self.file_to_run:
993 if not os.path.exists(self.file_to_run):
993 if not os.path.exists(self.file_to_run):
994 self.log.critical("%s does not exist" % self.file_to_run)
994 self.log.critical("%s does not exist" % self.file_to_run)
995 self.exit(1)
995 self.exit(1)
996
996
997 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
997 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
998 uri = url_path_join('notebooks', *relpath.split(os.sep))
998 uri = url_path_join('notebooks', *relpath.split(os.sep))
999 else:
999 else:
1000 uri = 'tree'
1000 uri = 'tree'
1001 if browser:
1001 if browser:
1002 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1002 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1003 new=2)
1003 new=2)
1004 threading.Thread(target=b).start()
1004 threading.Thread(target=b).start()
1005 try:
1005 try:
1006 ioloop.IOLoop.instance().start()
1006 ioloop.IOLoop.instance().start()
1007 except KeyboardInterrupt:
1007 except KeyboardInterrupt:
1008 info("Interrupted...")
1008 info("Interrupted...")
1009 finally:
1009 finally:
1010 self.cleanup_kernels()
1010 self.cleanup_kernels()
1011 self.remove_server_info_file()
1011 self.remove_server_info_file()
1012
1012
1013
1013
1014 def list_running_servers(profile='default'):
1014 def list_running_servers(profile='default'):
1015 """Iterate over the server info files of running notebook servers.
1015 """Iterate over the server info files of running notebook servers.
1016
1016
1017 Given a profile name, find nbserver-* files in the security directory of
1017 Given a profile name, find nbserver-* files in the security directory of
1018 that profile, and yield dicts of their information, each one pertaining to
1018 that profile, and yield dicts of their information, each one pertaining to
1019 a currently running notebook server instance.
1019 a currently running notebook server instance.
1020 """
1020 """
1021 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1021 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1022 for file in os.listdir(pd.security_dir):
1022 for file in os.listdir(pd.security_dir):
1023 if file.startswith('nbserver-'):
1023 if file.startswith('nbserver-'):
1024 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1024 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1025 info = json.load(f)
1025 info = json.load(f)
1026
1026
1027 # Simple check whether that process is really still running
1027 # Simple check whether that process is really still running
1028 if check_pid(info['pid']):
1028 if check_pid(info['pid']):
1029 yield info
1029 yield info
1030 else:
1030 else:
1031 # If the process has died, try to delete its info file
1031 # If the process has died, try to delete its info file
1032 try:
1032 try:
1033 os.unlink(file)
1033 os.unlink(file)
1034 except OSError:
1034 except OSError:
1035 pass # TODO: This should warn or log or something
1035 pass # TODO: This should warn or log or something
1036 #-----------------------------------------------------------------------------
1036 #-----------------------------------------------------------------------------
1037 # Main entry point
1037 # Main entry point
1038 #-----------------------------------------------------------------------------
1038 #-----------------------------------------------------------------------------
1039
1039
1040 launch_new_instance = NotebookApp.launch_instance
1040 launch_new_instance = NotebookApp.launch_instance
1041
1041
@@ -0,0 +1,1 b''
1 from .manager import ConfigManager
@@ -1,133 +1,139 b''
1 """Test the kernels service API."""
1 """Test the kernels service API."""
2
2
3 import json
3 import json
4 import requests
4 import requests
5
5
6 from IPython.html.utils import url_path_join
6 from IPython.html.utils import url_path_join
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
7 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
8
8
9 class KernelAPI(object):
9 class KernelAPI(object):
10 """Wrapper for kernel REST API requests"""
10 """Wrapper for kernel REST API requests"""
11 def __init__(self, base_url):
11 def __init__(self, base_url):
12 self.base_url = base_url
12 self.base_url = base_url
13
13
14 def _req(self, verb, path, body=None):
14 def _req(self, verb, path, body=None):
15 response = requests.request(verb,
15 response = requests.request(verb,
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
16 url_path_join(self.base_url, 'api/kernels', path), data=body)
17
17
18 if 400 <= response.status_code < 600:
18 if 400 <= response.status_code < 600:
19 try:
19 try:
20 response.reason = response.json()['message']
20 response.reason = response.json()['message']
21 except:
21 except:
22 pass
22 pass
23 response.raise_for_status()
23 response.raise_for_status()
24
24
25 return response
25 return response
26
26
27 def list(self):
27 def list(self):
28 return self._req('GET', '')
28 return self._req('GET', '')
29
29
30 def get(self, id):
30 def get(self, id):
31 return self._req('GET', id)
31 return self._req('GET', id)
32
32
33 def start(self, name='python'):
33 def start(self, name='python'):
34 body = json.dumps({'name': name})
34 body = json.dumps({'name': name})
35 return self._req('POST', '', body)
35 return self._req('POST', '', body)
36
36
37 def shutdown(self, id):
37 def shutdown(self, id):
38 return self._req('DELETE', id)
38 return self._req('DELETE', id)
39
39
40 def interrupt(self, id):
40 def interrupt(self, id):
41 return self._req('POST', url_path_join(id, 'interrupt'))
41 return self._req('POST', url_path_join(id, 'interrupt'))
42
42
43 def restart(self, id):
43 def restart(self, id):
44 return self._req('POST', url_path_join(id, 'restart'))
44 return self._req('POST', url_path_join(id, 'restart'))
45
45
46 class KernelAPITest(NotebookTestBase):
46 class KernelAPITest(NotebookTestBase):
47 """Test the kernels web service API"""
47 """Test the kernels web service API"""
48 def setUp(self):
48 def setUp(self):
49 self.kern_api = KernelAPI(self.base_url())
49 self.kern_api = KernelAPI(self.base_url())
50
50
51 def tearDown(self):
51 def tearDown(self):
52 for k in self.kern_api.list().json():
52 for k in self.kern_api.list().json():
53 self.kern_api.shutdown(k['id'])
53 self.kern_api.shutdown(k['id'])
54
54
55 def test__no_kernels(self):
55 def test__no_kernels(self):
56 """Make sure there are no kernels running at the start"""
56 """Make sure there are no kernels running at the start"""
57 kernels = self.kern_api.list().json()
57 kernels = self.kern_api.list().json()
58 self.assertEqual(kernels, [])
58 self.assertEqual(kernels, [])
59
59
60 def test_default_kernel(self):
60 def test_default_kernel(self):
61 # POST request
61 # POST request
62 r = self.kern_api._req('POST', '')
62 r = self.kern_api._req('POST', '')
63 kern1 = r.json()
63 kern1 = r.json()
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
64 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
65 self.assertEqual(r.status_code, 201)
65 self.assertEqual(r.status_code, 201)
66 self.assertIsInstance(kern1, dict)
66 self.assertIsInstance(kern1, dict)
67
67
68 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
68 self.assertEqual(r.headers['Content-Security-Policy'], (
69 "frame-ancestors 'self'; "
70 "report-uri /api/security/csp-report;"
71 ))
69
72
70 def test_main_kernel_handler(self):
73 def test_main_kernel_handler(self):
71 # POST request
74 # POST request
72 r = self.kern_api.start()
75 r = self.kern_api.start()
73 kern1 = r.json()
76 kern1 = r.json()
74 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
77 self.assertEqual(r.headers['location'], '/api/kernels/' + kern1['id'])
75 self.assertEqual(r.status_code, 201)
78 self.assertEqual(r.status_code, 201)
76 self.assertIsInstance(kern1, dict)
79 self.assertIsInstance(kern1, dict)
77
80
78 self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN")
81 self.assertEqual(r.headers['Content-Security-Policy'], (
82 "frame-ancestors 'self'; "
83 "report-uri /api/security/csp-report;"
84 ))
79
85
80 # GET request
86 # GET request
81 r = self.kern_api.list()
87 r = self.kern_api.list()
82 self.assertEqual(r.status_code, 200)
88 self.assertEqual(r.status_code, 200)
83 assert isinstance(r.json(), list)
89 assert isinstance(r.json(), list)
84 self.assertEqual(r.json()[0]['id'], kern1['id'])
90 self.assertEqual(r.json()[0]['id'], kern1['id'])
85 self.assertEqual(r.json()[0]['name'], kern1['name'])
91 self.assertEqual(r.json()[0]['name'], kern1['name'])
86
92
87 # create another kernel and check that they both are added to the
93 # create another kernel and check that they both are added to the
88 # list of kernels from a GET request
94 # list of kernels from a GET request
89 kern2 = self.kern_api.start().json()
95 kern2 = self.kern_api.start().json()
90 assert isinstance(kern2, dict)
96 assert isinstance(kern2, dict)
91 r = self.kern_api.list()
97 r = self.kern_api.list()
92 kernels = r.json()
98 kernels = r.json()
93 self.assertEqual(r.status_code, 200)
99 self.assertEqual(r.status_code, 200)
94 assert isinstance(kernels, list)
100 assert isinstance(kernels, list)
95 self.assertEqual(len(kernels), 2)
101 self.assertEqual(len(kernels), 2)
96
102
97 # Interrupt a kernel
103 # Interrupt a kernel
98 r = self.kern_api.interrupt(kern2['id'])
104 r = self.kern_api.interrupt(kern2['id'])
99 self.assertEqual(r.status_code, 204)
105 self.assertEqual(r.status_code, 204)
100
106
101 # Restart a kernel
107 # Restart a kernel
102 r = self.kern_api.restart(kern2['id'])
108 r = self.kern_api.restart(kern2['id'])
103 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
109 self.assertEqual(r.headers['Location'], '/api/kernels/'+kern2['id'])
104 rekern = r.json()
110 rekern = r.json()
105 self.assertEqual(rekern['id'], kern2['id'])
111 self.assertEqual(rekern['id'], kern2['id'])
106 self.assertEqual(rekern['name'], kern2['name'])
112 self.assertEqual(rekern['name'], kern2['name'])
107
113
108 def test_kernel_handler(self):
114 def test_kernel_handler(self):
109 # GET kernel with given id
115 # GET kernel with given id
110 kid = self.kern_api.start().json()['id']
116 kid = self.kern_api.start().json()['id']
111 r = self.kern_api.get(kid)
117 r = self.kern_api.get(kid)
112 kern1 = r.json()
118 kern1 = r.json()
113 self.assertEqual(r.status_code, 200)
119 self.assertEqual(r.status_code, 200)
114 assert isinstance(kern1, dict)
120 assert isinstance(kern1, dict)
115 self.assertIn('id', kern1)
121 self.assertIn('id', kern1)
116 self.assertEqual(kern1['id'], kid)
122 self.assertEqual(kern1['id'], kid)
117
123
118 # Request a bad kernel id and check that a JSON
124 # Request a bad kernel id and check that a JSON
119 # message is returned!
125 # message is returned!
120 bad_id = '111-111-111-111-111'
126 bad_id = '111-111-111-111-111'
121 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
127 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
122 self.kern_api.get(bad_id)
128 self.kern_api.get(bad_id)
123
129
124 # DELETE kernel with id
130 # DELETE kernel with id
125 r = self.kern_api.shutdown(kid)
131 r = self.kern_api.shutdown(kid)
126 self.assertEqual(r.status_code, 204)
132 self.assertEqual(r.status_code, 204)
127 kernels = self.kern_api.list().json()
133 kernels = self.kern_api.list().json()
128 self.assertEqual(kernels, [])
134 self.assertEqual(kernels, [])
129
135
130 # Request to delete a non-existent kernel id
136 # Request to delete a non-existent kernel id
131 bad_id = '111-111-111-111-111'
137 bad_id = '111-111-111-111-111'
132 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
138 with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
133 self.kern_api.shutdown(bad_id)
139 self.kern_api.shutdown(bad_id)
@@ -1,1073 +1,1073 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 'base/js/namespace',
5 'base/js/namespace',
6 'jquery',
6 'jquery',
7 'base/js/utils',
7 'base/js/utils',
8 './comm',
8 './comm',
9 './serialize',
9 './serialize',
10 'widgets/js/init'
10 'widgets/js/init'
11 ], function(IPython, $, utils, comm, serialize, widgetmanager) {
11 ], function(IPython, $, utils, comm, serialize, widgetmanager) {
12 "use strict";
12 "use strict";
13
13
14 /**
14 /**
15 * A Kernel class to communicate with the Python kernel. This
15 * A Kernel class to communicate with the Python kernel. This
16 * should generally not be constructed directly, but be created
16 * should generally not be constructed directly, but be created
17 * by. the `Session` object. Once created, this object should be
17 * by. the `Session` object. Once created, this object should be
18 * used to communicate with the kernel.
18 * used to communicate with the kernel.
19 *
19 *
20 * @class Kernel
20 * @class Kernel
21 * @param {string} kernel_service_url - the URL to access the kernel REST api
21 * @param {string} kernel_service_url - the URL to access the kernel REST api
22 * @param {string} ws_url - the websockets URL
22 * @param {string} ws_url - the websockets URL
23 * @param {Notebook} notebook - notebook object
23 * @param {Notebook} notebook - notebook object
24 * @param {string} name - the kernel type (e.g. python3)
24 * @param {string} name - the kernel type (e.g. python3)
25 */
25 */
26 var Kernel = function (kernel_service_url, ws_url, notebook, name) {
26 var Kernel = function (kernel_service_url, ws_url, notebook, name) {
27 this.events = notebook.events;
27 this.events = notebook.events;
28
28
29 this.id = null;
29 this.id = null;
30 this.name = name;
30 this.name = name;
31
31
32 this.channels = {
32 this.channels = {
33 'shell': null,
33 'shell': null,
34 'iopub': null,
34 'iopub': null,
35 'stdin': null
35 'stdin': null
36 };
36 };
37
37
38 this.kernel_service_url = kernel_service_url;
38 this.kernel_service_url = kernel_service_url;
39 this.kernel_url = null;
39 this.kernel_url = null;
40 this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
40 this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
41 if (!this.ws_url) {
41 if (!this.ws_url) {
42 // trailing 's' in https will become wss for secure web sockets
42 // trailing 's' in https will become wss for secure web sockets
43 this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
43 this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
44 }
44 }
45
45
46 this.username = "username";
46 this.username = "username";
47 this.session_id = utils.uuid();
47 this.session_id = utils.uuid();
48 this._msg_callbacks = {};
48 this._msg_callbacks = {};
49 this.info_reply = {}; // kernel_info_reply stored here after starting
49 this.info_reply = {}; // kernel_info_reply stored here after starting
50 this.unsolicited_msg_callback = null;
50 this.unsolicited_msg_callback = null;
51
51
52 if (typeof(WebSocket) !== 'undefined') {
52 if (typeof(WebSocket) !== 'undefined') {
53 this.WebSocket = WebSocket;
53 this.WebSocket = WebSocket;
54 } else if (typeof(MozWebSocket) !== 'undefined') {
54 } else if (typeof(MozWebSocket) !== 'undefined') {
55 this.WebSocket = MozWebSocket;
55 this.WebSocket = MozWebSocket;
56 } else {
56 } else {
57 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
57 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox ≥ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
58 }
58 }
59
59
60 this.bind_events();
60 this.bind_events();
61 this.init_iopub_handlers();
61 this.init_iopub_handlers();
62 this.comm_manager = new comm.CommManager(this);
62 this.comm_manager = new comm.CommManager(this);
63 this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
63 this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
64
64
65 this.last_msg_id = null;
65 this.last_msg_id = null;
66 this.last_msg_callbacks = {};
66 this.last_msg_callbacks = {};
67
67
68 this._autorestart_attempt = 0;
68 this._autorestart_attempt = 0;
69 this._reconnect_attempt = 0;
69 this._reconnect_attempt = 0;
70 this.reconnect_limit = 7;
70 this.reconnect_limit = 7;
71 };
71 };
72
72
73 /**
73 /**
74 * @function _get_msg
74 * @function _get_msg
75 */
75 */
76 Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) {
76 Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) {
77 var msg = {
77 var msg = {
78 header : {
78 header : {
79 msg_id : utils.uuid(),
79 msg_id : utils.uuid(),
80 username : this.username,
80 username : this.username,
81 session : this.session_id,
81 session : this.session_id,
82 msg_type : msg_type,
82 msg_type : msg_type,
83 version : "5.0"
83 version : "5.0"
84 },
84 },
85 metadata : metadata || {},
85 metadata : metadata || {},
86 content : content,
86 content : content,
87 buffers : buffers || [],
87 buffers : buffers || [],
88 parent_header : {}
88 parent_header : {}
89 };
89 };
90 return msg;
90 return msg;
91 };
91 };
92
92
93 /**
93 /**
94 * @function bind_events
94 * @function bind_events
95 */
95 */
96 Kernel.prototype.bind_events = function () {
96 Kernel.prototype.bind_events = function () {
97 var that = this;
97 var that = this;
98 this.events.on('send_input_reply.Kernel', function(evt, data) {
98 this.events.on('send_input_reply.Kernel', function(evt, data) {
99 that.send_input_reply(data);
99 that.send_input_reply(data);
100 });
100 });
101
101
102 var record_status = function (evt, info) {
102 var record_status = function (evt, info) {
103 console.log('Kernel: ' + evt.type + ' (' + info.kernel.id + ')');
103 console.log('Kernel: ' + evt.type + ' (' + info.kernel.id + ')');
104 };
104 };
105
105
106 this.events.on('kernel_created.Kernel', record_status);
106 this.events.on('kernel_created.Kernel', record_status);
107 this.events.on('kernel_reconnecting.Kernel', record_status);
107 this.events.on('kernel_reconnecting.Kernel', record_status);
108 this.events.on('kernel_connected.Kernel', record_status);
108 this.events.on('kernel_connected.Kernel', record_status);
109 this.events.on('kernel_starting.Kernel', record_status);
109 this.events.on('kernel_starting.Kernel', record_status);
110 this.events.on('kernel_restarting.Kernel', record_status);
110 this.events.on('kernel_restarting.Kernel', record_status);
111 this.events.on('kernel_autorestarting.Kernel', record_status);
111 this.events.on('kernel_autorestarting.Kernel', record_status);
112 this.events.on('kernel_interrupting.Kernel', record_status);
112 this.events.on('kernel_interrupting.Kernel', record_status);
113 this.events.on('kernel_disconnected.Kernel', record_status);
113 this.events.on('kernel_disconnected.Kernel', record_status);
114 // these are commented out because they are triggered a lot, but can
114 // these are commented out because they are triggered a lot, but can
115 // be uncommented for debugging purposes
115 // be uncommented for debugging purposes
116 //this.events.on('kernel_idle.Kernel', record_status);
116 //this.events.on('kernel_idle.Kernel', record_status);
117 //this.events.on('kernel_busy.Kernel', record_status);
117 //this.events.on('kernel_busy.Kernel', record_status);
118 this.events.on('kernel_ready.Kernel', record_status);
118 this.events.on('kernel_ready.Kernel', record_status);
119 this.events.on('kernel_killed.Kernel', record_status);
119 this.events.on('kernel_killed.Kernel', record_status);
120 this.events.on('kernel_dead.Kernel', record_status);
120 this.events.on('kernel_dead.Kernel', record_status);
121
121
122 this.events.on('kernel_ready.Kernel', function () {
122 this.events.on('kernel_ready.Kernel', function () {
123 that._autorestart_attempt = 0;
123 that._autorestart_attempt = 0;
124 });
124 });
125 this.events.on('kernel_connected.Kernel', function () {
125 this.events.on('kernel_connected.Kernel', function () {
126 that._reconnect_attempt = 0;
126 that._reconnect_attempt = 0;
127 });
127 });
128 };
128 };
129
129
130 /**
130 /**
131 * Initialize the iopub handlers.
131 * Initialize the iopub handlers.
132 *
132 *
133 * @function init_iopub_handlers
133 * @function init_iopub_handlers
134 */
134 */
135 Kernel.prototype.init_iopub_handlers = function () {
135 Kernel.prototype.init_iopub_handlers = function () {
136 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
136 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
137 this._iopub_handlers = {};
137 this._iopub_handlers = {};
138 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
138 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
139 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
139 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
140 this.register_iopub_handler('execute_input', $.proxy(this._handle_input_message, this));
140 this.register_iopub_handler('execute_input', $.proxy(this._handle_input_message, this));
141
141
142 for (var i=0; i < output_msg_types.length; i++) {
142 for (var i=0; i < output_msg_types.length; i++) {
143 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
143 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
144 }
144 }
145 };
145 };
146
146
147 /**
147 /**
148 * GET /api/kernels
148 * GET /api/kernels
149 *
149 *
150 * Get the list of running kernels.
150 * Get the list of running kernels.
151 *
151 *
152 * @function list
152 * @function list
153 * @param {function} [success] - function executed on ajax success
153 * @param {function} [success] - function executed on ajax success
154 * @param {function} [error] - functon executed on ajax error
154 * @param {function} [error] - functon executed on ajax error
155 */
155 */
156 Kernel.prototype.list = function (success, error) {
156 Kernel.prototype.list = function (success, error) {
157 $.ajax(this.kernel_service_url, {
157 $.ajax(this.kernel_service_url, {
158 processData: false,
158 processData: false,
159 cache: false,
159 cache: false,
160 type: "GET",
160 type: "GET",
161 dataType: "json",
161 dataType: "json",
162 success: success,
162 success: success,
163 error: this._on_error(error)
163 error: this._on_error(error)
164 });
164 });
165 };
165 };
166
166
167 /**
167 /**
168 * POST /api/kernels
168 * POST /api/kernels
169 *
169 *
170 * Start a new kernel.
170 * Start a new kernel.
171 *
171 *
172 * In general this shouldn't be used -- the kernel should be
172 * In general this shouldn't be used -- the kernel should be
173 * started through the session API. If you use this function and
173 * started through the session API. If you use this function and
174 * are also using the session API then your session and kernel
174 * are also using the session API then your session and kernel
175 * WILL be out of sync!
175 * WILL be out of sync!
176 *
176 *
177 * @function start
177 * @function start
178 * @param {params} [Object] - parameters to include in the query string
178 * @param {params} [Object] - parameters to include in the query string
179 * @param {function} [success] - function executed on ajax success
179 * @param {function} [success] - function executed on ajax success
180 * @param {function} [error] - functon executed on ajax error
180 * @param {function} [error] - functon executed on ajax error
181 */
181 */
182 Kernel.prototype.start = function (params, success, error) {
182 Kernel.prototype.start = function (params, success, error) {
183 var url = this.kernel_service_url;
183 var url = this.kernel_service_url;
184 var qs = $.param(params || {}); // query string for sage math stuff
184 var qs = $.param(params || {}); // query string for sage math stuff
185 if (qs !== "") {
185 if (qs !== "") {
186 url = url + "?" + qs;
186 url = url + "?" + qs;
187 }
187 }
188
188
189 var that = this;
189 var that = this;
190 var on_success = function (data, status, xhr) {
190 var on_success = function (data, status, xhr) {
191 that.events.trigger('kernel_created.Kernel', {kernel: that});
191 that.events.trigger('kernel_created.Kernel', {kernel: that});
192 that._kernel_created(data);
192 that._kernel_created(data);
193 if (success) {
193 if (success) {
194 success(data, status, xhr);
194 success(data, status, xhr);
195 }
195 }
196 };
196 };
197
197
198 $.ajax(url, {
198 $.ajax(url, {
199 processData: false,
199 processData: false,
200 cache: false,
200 cache: false,
201 type: "POST",
201 type: "POST",
202 data: JSON.stringify({name: this.name}),
202 data: JSON.stringify({name: this.name}),
203 dataType: "json",
203 dataType: "json",
204 success: this._on_success(on_success),
204 success: this._on_success(on_success),
205 error: this._on_error(error)
205 error: this._on_error(error)
206 });
206 });
207
207
208 return url;
208 return url;
209 };
209 };
210
210
211 /**
211 /**
212 * GET /api/kernels/[:kernel_id]
212 * GET /api/kernels/[:kernel_id]
213 *
213 *
214 * Get information about the kernel.
214 * Get information about the kernel.
215 *
215 *
216 * @function get_info
216 * @function get_info
217 * @param {function} [success] - function executed on ajax success
217 * @param {function} [success] - function executed on ajax success
218 * @param {function} [error] - functon executed on ajax error
218 * @param {function} [error] - functon executed on ajax error
219 */
219 */
220 Kernel.prototype.get_info = function (success, error) {
220 Kernel.prototype.get_info = function (success, error) {
221 $.ajax(this.kernel_url, {
221 $.ajax(this.kernel_url, {
222 processData: false,
222 processData: false,
223 cache: false,
223 cache: false,
224 type: "GET",
224 type: "GET",
225 dataType: "json",
225 dataType: "json",
226 success: this._on_success(success),
226 success: this._on_success(success),
227 error: this._on_error(error)
227 error: this._on_error(error)
228 });
228 });
229 };
229 };
230
230
231 /**
231 /**
232 * DELETE /api/kernels/[:kernel_id]
232 * DELETE /api/kernels/[:kernel_id]
233 *
233 *
234 * Shutdown the kernel.
234 * Shutdown the kernel.
235 *
235 *
236 * If you are also using sessions, then this function shoul NOT be
236 * If you are also using sessions, then this function shoul NOT be
237 * used. Instead, use Session.delete. Otherwise, the session and
237 * used. Instead, use Session.delete. Otherwise, the session and
238 * kernel WILL be out of sync.
238 * kernel WILL be out of sync.
239 *
239 *
240 * @function kill
240 * @function kill
241 * @param {function} [success] - function executed on ajax success
241 * @param {function} [success] - function executed on ajax success
242 * @param {function} [error] - functon executed on ajax error
242 * @param {function} [error] - functon executed on ajax error
243 */
243 */
244 Kernel.prototype.kill = function (success, error) {
244 Kernel.prototype.kill = function (success, error) {
245 this.events.trigger('kernel_killed.Kernel', {kernel: this});
245 this.events.trigger('kernel_killed.Kernel', {kernel: this});
246 this._kernel_dead();
246 this._kernel_dead();
247 $.ajax(this.kernel_url, {
247 $.ajax(this.kernel_url, {
248 processData: false,
248 processData: false,
249 cache: false,
249 cache: false,
250 type: "DELETE",
250 type: "DELETE",
251 dataType: "json",
251 dataType: "json",
252 success: this._on_success(success),
252 success: this._on_success(success),
253 error: this._on_error(error)
253 error: this._on_error(error)
254 });
254 });
255 };
255 };
256
256
257 /**
257 /**
258 * POST /api/kernels/[:kernel_id]/interrupt
258 * POST /api/kernels/[:kernel_id]/interrupt
259 *
259 *
260 * Interrupt the kernel.
260 * Interrupt the kernel.
261 *
261 *
262 * @function interrupt
262 * @function interrupt
263 * @param {function} [success] - function executed on ajax success
263 * @param {function} [success] - function executed on ajax success
264 * @param {function} [error] - functon executed on ajax error
264 * @param {function} [error] - functon executed on ajax error
265 */
265 */
266 Kernel.prototype.interrupt = function (success, error) {
266 Kernel.prototype.interrupt = function (success, error) {
267 this.events.trigger('kernel_interrupting.Kernel', {kernel: this});
267 this.events.trigger('kernel_interrupting.Kernel', {kernel: this});
268
268
269 var that = this;
269 var that = this;
270 var on_success = function (data, status, xhr) {
270 var on_success = function (data, status, xhr) {
271 // get kernel info so we know what state the kernel is in
271 // get kernel info so we know what state the kernel is in
272 that.kernel_info();
272 that.kernel_info();
273 if (success) {
273 if (success) {
274 success(data, status, xhr);
274 success(data, status, xhr);
275 }
275 }
276 };
276 };
277
277
278 var url = utils.url_join_encode(this.kernel_url, 'interrupt');
278 var url = utils.url_join_encode(this.kernel_url, 'interrupt');
279 $.ajax(url, {
279 $.ajax(url, {
280 processData: false,
280 processData: false,
281 cache: false,
281 cache: false,
282 type: "POST",
282 type: "POST",
283 dataType: "json",
283 dataType: "json",
284 success: this._on_success(on_success),
284 success: this._on_success(on_success),
285 error: this._on_error(error)
285 error: this._on_error(error)
286 });
286 });
287 };
287 };
288
288
289 Kernel.prototype.restart = function (success, error) {
289 /**
290 /**
290 * POST /api/kernels/[:kernel_id]/restart
291 * POST /api/kernels/[:kernel_id]/restart
291 *
292 *
292 * Restart the kernel.
293 * Restart the kernel.
293 *
294 *
294 * @function interrupt
295 * @function interrupt
295 * @param {function} [success] - function executed on ajax success
296 * @param {function} [success] - function executed on ajax success
296 * @param {function} [error] - functon executed on ajax error
297 * @param {function} [error] - functon executed on ajax error
297 */
298 */
298 Kernel.prototype.restart = function (success, error) {
299 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
299 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
300 this.stop_channels();
300 this.stop_channels();
301
301
302 var that = this;
302 var that = this;
303 var on_success = function (data, status, xhr) {
303 var on_success = function (data, status, xhr) {
304 that.events.trigger('kernel_created.Kernel', {kernel: that});
304 that.events.trigger('kernel_created.Kernel', {kernel: that});
305 that._kernel_created(data);
305 that._kernel_created(data);
306 if (success) {
306 if (success) {
307 success(data, status, xhr);
307 success(data, status, xhr);
308 }
308 }
309 };
309 };
310
310
311 var on_error = function (xhr, status, err) {
311 var on_error = function (xhr, status, err) {
312 that.events.trigger('kernel_dead.Kernel', {kernel: that});
312 that.events.trigger('kernel_dead.Kernel', {kernel: that});
313 that._kernel_dead();
313 that._kernel_dead();
314 if (error) {
314 if (error) {
315 error(xhr, status, err);
315 error(xhr, status, err);
316 }
316 }
317 };
317 };
318
318
319 var url = utils.url_join_encode(this.kernel_url, 'restart');
319 var url = utils.url_join_encode(this.kernel_url, 'restart');
320 $.ajax(url, {
320 $.ajax(url, {
321 processData: false,
321 processData: false,
322 cache: false,
322 cache: false,
323 type: "POST",
323 type: "POST",
324 dataType: "json",
324 dataType: "json",
325 success: this._on_success(on_success),
325 success: this._on_success(on_success),
326 error: this._on_error(on_error)
326 error: this._on_error(on_error)
327 });
327 });
328 };
328 };
329
329
330 Kernel.prototype.reconnect = function () {
330 /**
331 /**
331 * Reconnect to a disconnected kernel. This is not actually a
332 * Reconnect to a disconnected kernel. This is not actually a
332 * standard HTTP request, but useful function nonetheless for
333 * standard HTTP request, but useful function nonetheless for
333 * reconnecting to the kernel if the connection is somehow lost.
334 * reconnecting to the kernel if the connection is somehow lost.
334 *
335 *
335 * @function reconnect
336 * @function reconnect
336 */
337 */
337 Kernel.prototype.reconnect = function () {
338 if (this.is_connected()) {
338 if (this.is_connected()) {
339 return;
339 return;
340 }
340 }
341 this._reconnect_attempt = this._reconnect_attempt + 1;
341 this._reconnect_attempt = this._reconnect_attempt + 1;
342 this.events.trigger('kernel_reconnecting.Kernel', {
342 this.events.trigger('kernel_reconnecting.Kernel', {
343 kernel: this,
343 kernel: this,
344 attempt: this._reconnect_attempt,
344 attempt: this._reconnect_attempt,
345 });
345 });
346 this.start_channels();
346 this.start_channels();
347 };
347 };
348
348
349 Kernel.prototype._on_success = function (success) {
349 /**
350 /**
350 * Handle a successful AJAX request by updating the kernel id and
351 * Handle a successful AJAX request by updating the kernel id and
351 * name from the response, and then optionally calling a provided
352 * name from the response, and then optionally calling a provided
352 * callback.
353 * callback.
353 *
354 *
354 * @function _on_success
355 * @function _on_success
355 * @param {function} success - callback
356 * @param {function} success - callback
356 */
357 */
357 Kernel.prototype._on_success = function (success) {
358 var that = this;
358 var that = this;
359 return function (data, status, xhr) {
359 return function (data, status, xhr) {
360 if (data) {
360 if (data) {
361 that.id = data.id;
361 that.id = data.id;
362 that.name = data.name;
362 that.name = data.name;
363 }
363 }
364 that.kernel_url = utils.url_join_encode(that.kernel_service_url, that.id);
364 that.kernel_url = utils.url_join_encode(that.kernel_service_url, that.id);
365 if (success) {
365 if (success) {
366 success(data, status, xhr);
366 success(data, status, xhr);
367 }
367 }
368 };
368 };
369 };
369 };
370
370
371 Kernel.prototype._on_error = function (error) {
371 /**
372 /**
372 * Handle a failed AJAX request by logging the error message, and
373 * Handle a failed AJAX request by logging the error message, and
373 * then optionally calling a provided callback.
374 * then optionally calling a provided callback.
374 *
375 *
375 * @function _on_error
376 * @function _on_error
376 * @param {function} error - callback
377 * @param {function} error - callback
377 */
378 */
378 Kernel.prototype._on_error = function (error) {
379 return function (xhr, status, err) {
379 return function (xhr, status, err) {
380 utils.log_ajax_error(xhr, status, err);
380 utils.log_ajax_error(xhr, status, err);
381 if (error) {
381 if (error) {
382 error(xhr, status, err);
382 error(xhr, status, err);
383 }
383 }
384 };
384 };
385 };
385 };
386
386
387 Kernel.prototype._kernel_created = function (data) {
387 /**
388 /**
388 * Perform necessary tasks once the kernel has been started,
389 * Perform necessary tasks once the kernel has been started,
389 * including actually connecting to the kernel.
390 * including actually connecting to the kernel.
390 *
391 *
391 * @function _kernel_created
392 * @function _kernel_created
392 * @param {Object} data - information about the kernel including id
393 * @param {Object} data - information about the kernel including id
393 */
394 */
394 Kernel.prototype._kernel_created = function (data) {
395 this.id = data.id;
395 this.id = data.id;
396 this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id);
396 this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id);
397 this.start_channels();
397 this.start_channels();
398 };
398 };
399
399
400 Kernel.prototype._kernel_connected = function () {
400 /**
401 /**
401 * Perform necessary tasks once the connection to the kernel has
402 * Perform necessary tasks once the connection to the kernel has
402 * been established. This includes requesting information about
403 * been established. This includes requesting information about
403 * the kernel.
404 * the kernel.
404 *
405 *
405 * @function _kernel_connected
406 * @function _kernel_connected
406 */
407 */
407 Kernel.prototype._kernel_connected = function () {
408 this.events.trigger('kernel_connected.Kernel', {kernel: this});
408 this.events.trigger('kernel_connected.Kernel', {kernel: this});
409 this.events.trigger('kernel_starting.Kernel', {kernel: this});
409 this.events.trigger('kernel_starting.Kernel', {kernel: this});
410 // get kernel info so we know what state the kernel is in
410 // get kernel info so we know what state the kernel is in
411 var that = this;
411 var that = this;
412 this.kernel_info(function (reply) {
412 this.kernel_info(function (reply) {
413 that.info_reply = reply.content;
413 that.info_reply = reply.content;
414 that.events.trigger('kernel_ready.Kernel', {kernel: that});
414 that.events.trigger('kernel_ready.Kernel', {kernel: that});
415 });
415 });
416 };
416 };
417
417
418 Kernel.prototype._kernel_dead = function () {
418 /**
419 /**
419 * Perform necessary tasks after the kernel has died. This closing
420 * Perform necessary tasks after the kernel has died. This closing
420 * communication channels to the kernel if they are still somehow
421 * communication channels to the kernel if they are still somehow
421 * open.
422 * open.
422 *
423 *
423 * @function _kernel_dead
424 * @function _kernel_dead
424 */
425 */
425 Kernel.prototype._kernel_dead = function () {
426 this.stop_channels();
426 this.stop_channels();
427 };
427 };
428
428
429 Kernel.prototype.start_channels = function () {
429 /**
430 /**
430 * Start the `shell`and `iopub` channels.
431 * Start the `shell`and `iopub` channels.
431 * Will stop and restart them if they already exist.
432 * Will stop and restart them if they already exist.
432 *
433 *
433 * @function start_channels
434 * @function start_channels
434 */
435 */
435 Kernel.prototype.start_channels = function () {
436 var that = this;
436 var that = this;
437 this.stop_channels();
437 this.stop_channels();
438 var ws_host_url = this.ws_url + this.kernel_url;
438 var ws_host_url = this.ws_url + this.kernel_url;
439
439
440 console.log("Starting WebSockets:", ws_host_url);
440 console.log("Starting WebSockets:", ws_host_url);
441
441
442 var channel_url = function(channel) {
442 var channel_url = function(channel) {
443 return [
443 return [
444 that.ws_url,
444 that.ws_url,
445 utils.url_join_encode(that.kernel_url, channel),
445 utils.url_join_encode(that.kernel_url, channel),
446 "?session_id=" + that.session_id
446 "?session_id=" + that.session_id
447 ].join('');
447 ].join('');
448 };
448 };
449 this.channels.shell = new this.WebSocket(channel_url("shell"));
449 this.channels.shell = new this.WebSocket(channel_url("shell"));
450 this.channels.stdin = new this.WebSocket(channel_url("stdin"));
450 this.channels.stdin = new this.WebSocket(channel_url("stdin"));
451 this.channels.iopub = new this.WebSocket(channel_url("iopub"));
451 this.channels.iopub = new this.WebSocket(channel_url("iopub"));
452
452
453 var already_called_onclose = false; // only alert once
453 var already_called_onclose = false; // only alert once
454 var ws_closed_early = function(evt){
454 var ws_closed_early = function(evt){
455 if (already_called_onclose){
455 if (already_called_onclose){
456 return;
456 return;
457 }
457 }
458 already_called_onclose = true;
458 already_called_onclose = true;
459 if ( ! evt.wasClean ){
459 if ( ! evt.wasClean ){
460 // If the websocket was closed early, that could mean
460 // If the websocket was closed early, that could mean
461 // that the kernel is actually dead. Try getting
461 // that the kernel is actually dead. Try getting
462 // information about the kernel from the API call --
462 // information about the kernel from the API call --
463 // if that fails, then assume the kernel is dead,
463 // if that fails, then assume the kernel is dead,
464 // otherwise just follow the typical websocket closed
464 // otherwise just follow the typical websocket closed
465 // protocol.
465 // protocol.
466 that.get_info(function () {
466 that.get_info(function () {
467 that._ws_closed(ws_host_url, false);
467 that._ws_closed(ws_host_url, false);
468 }, function () {
468 }, function () {
469 that.events.trigger('kernel_dead.Kernel', {kernel: that});
469 that.events.trigger('kernel_dead.Kernel', {kernel: that});
470 that._kernel_dead();
470 that._kernel_dead();
471 });
471 });
472 }
472 }
473 };
473 };
474 var ws_closed_late = function(evt){
474 var ws_closed_late = function(evt){
475 if (already_called_onclose){
475 if (already_called_onclose){
476 return;
476 return;
477 }
477 }
478 already_called_onclose = true;
478 already_called_onclose = true;
479 if ( ! evt.wasClean ){
479 if ( ! evt.wasClean ){
480 that._ws_closed(ws_host_url, false);
480 that._ws_closed(ws_host_url, false);
481 }
481 }
482 };
482 };
483 var ws_error = function(evt){
483 var ws_error = function(evt){
484 if (already_called_onclose){
484 if (already_called_onclose){
485 return;
485 return;
486 }
486 }
487 already_called_onclose = true;
487 already_called_onclose = true;
488 that._ws_closed(ws_host_url, true);
488 that._ws_closed(ws_host_url, true);
489 };
489 };
490
490
491 for (var c in this.channels) {
491 for (var c in this.channels) {
492 this.channels[c].onopen = $.proxy(this._ws_opened, this);
492 this.channels[c].onopen = $.proxy(this._ws_opened, this);
493 this.channels[c].onclose = ws_closed_early;
493 this.channels[c].onclose = ws_closed_early;
494 this.channels[c].onerror = ws_error;
494 this.channels[c].onerror = ws_error;
495 }
495 }
496 // switch from early-close to late-close message after 1s
496 // switch from early-close to late-close message after 1s
497 setTimeout(function() {
497 setTimeout(function() {
498 for (var c in that.channels) {
498 for (var c in that.channels) {
499 if (that.channels[c] !== null) {
499 if (that.channels[c] !== null) {
500 that.channels[c].onclose = ws_closed_late;
500 that.channels[c].onclose = ws_closed_late;
501 }
501 }
502 }
502 }
503 }, 1000);
503 }, 1000);
504 this.channels.shell.onmessage = $.proxy(this._handle_shell_reply, this);
504 this.channels.shell.onmessage = $.proxy(this._handle_shell_reply, this);
505 this.channels.iopub.onmessage = $.proxy(this._handle_iopub_message, this);
505 this.channels.iopub.onmessage = $.proxy(this._handle_iopub_message, this);
506 this.channels.stdin.onmessage = $.proxy(this._handle_input_request, this);
506 this.channels.stdin.onmessage = $.proxy(this._handle_input_request, this);
507 };
507 };
508
508
509 Kernel.prototype._ws_opened = function (evt) {
509 /**
510 /**
510 * Handle a websocket entering the open state,
511 * Handle a websocket entering the open state,
511 * signaling that the kernel is connected when all channels are open.
512 * signaling that the kernel is connected when all channels are open.
512 *
513 *
513 * @function _ws_opened
514 * @function _ws_opened
514 */
515 */
515 Kernel.prototype._ws_opened = function (evt) {
516 if (this.is_connected()) {
516 if (this.is_connected()) {
517 // all events ready, trigger started event.
517 // all events ready, trigger started event.
518 this._kernel_connected();
518 this._kernel_connected();
519 }
519 }
520 };
520 };
521
521
522 Kernel.prototype._ws_closed = function(ws_url, error) {
522 /**
523 /**
523 * Handle a websocket entering the closed state. This closes the
524 * Handle a websocket entering the closed state. This closes the
524 * other communication channels if they are open. If the websocket
525 * other communication channels if they are open. If the websocket
525 * was not closed due to an error, try to reconnect to the kernel.
526 * was not closed due to an error, try to reconnect to the kernel.
526 *
527 *
527 * @function _ws_closed
528 * @function _ws_closed
528 * @param {string} ws_url - the websocket url
529 * @param {string} ws_url - the websocket url
529 * @param {bool} error - whether the connection was closed due to an error
530 * @param {bool} error - whether the connection was closed due to an error
530 */
531 */
531 Kernel.prototype._ws_closed = function(ws_url, error) {
532 this.stop_channels();
532 this.stop_channels();
533
533
534 this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
534 this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
535 if (error) {
535 if (error) {
536 console.log('WebSocket connection failed: ', ws_url);
536 console.log('WebSocket connection failed: ', ws_url);
537 this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
537 this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
538 }
538 }
539 this._schedule_reconnect();
539 this._schedule_reconnect();
540 };
540 };
541
541
542 Kernel.prototype._schedule_reconnect = function () {
542 Kernel.prototype._schedule_reconnect = function () {
543 // function to call when kernel connection is lost
543 // function to call when kernel connection is lost
544 // schedules reconnect, or fires 'connection_dead' if reconnect limit is hit
544 // schedules reconnect, or fires 'connection_dead' if reconnect limit is hit
545 if (this._reconnect_attempt < this.reconnect_limit) {
545 if (this._reconnect_attempt < this.reconnect_limit) {
546 var timeout = Math.pow(2, this._reconnect_attempt);
546 var timeout = Math.pow(2, this._reconnect_attempt);
547 console.log("Connection lost, reconnecting in " + timeout + " seconds.");
547 console.log("Connection lost, reconnecting in " + timeout + " seconds.");
548 setTimeout($.proxy(this.reconnect, this), 1e3 * timeout);
548 setTimeout($.proxy(this.reconnect, this), 1e3 * timeout);
549 } else {
549 } else {
550 this.events.trigger('kernel_connection_dead.Kernel', {
550 this.events.trigger('kernel_connection_dead.Kernel', {
551 kernel: this,
551 kernel: this,
552 reconnect_attempt: this._reconnect_attempt,
552 reconnect_attempt: this._reconnect_attempt,
553 });
553 });
554 console.log("Failed to reconnect, giving up.");
554 console.log("Failed to reconnect, giving up.");
555 }
555 }
556 };
556 };
557
557
558 Kernel.prototype.stop_channels = function () {
558 /**
559 /**
559 * Close the websocket channels. After successful close, the value
560 * Close the websocket channels. After successful close, the value
560 * in `this.channels[channel_name]` will be null.
561 * in `this.channels[channel_name]` will be null.
561 *
562 *
562 * @function stop_channels
563 * @function stop_channels
563 */
564 */
564 Kernel.prototype.stop_channels = function () {
565 var that = this;
565 var that = this;
566 var close = function (c) {
566 var close = function (c) {
567 return function () {
567 return function () {
568 if (that.channels[c] && that.channels[c].readyState === WebSocket.CLOSED) {
568 if (that.channels[c] && that.channels[c].readyState === WebSocket.CLOSED) {
569 that.channels[c] = null;
569 that.channels[c] = null;
570 }
570 }
571 };
571 };
572 };
572 };
573 for (var c in this.channels) {
573 for (var c in this.channels) {
574 if ( this.channels[c] !== null ) {
574 if ( this.channels[c] !== null ) {
575 if (this.channels[c].readyState === WebSocket.OPEN) {
575 if (this.channels[c].readyState === WebSocket.OPEN) {
576 this.channels[c].onclose = close(c);
576 this.channels[c].onclose = close(c);
577 this.channels[c].close();
577 this.channels[c].close();
578 } else {
578 } else {
579 close(c)();
579 close(c)();
580 }
580 }
581 }
581 }
582 }
582 }
583 };
583 };
584
584
585 Kernel.prototype.is_connected = function () {
585 /**
586 /**
586 * Check whether there is a connection to the kernel. This
587 * Check whether there is a connection to the kernel. This
587 * function only returns true if all channel objects have been
588 * function only returns true if all channel objects have been
588 * created and have a state of WebSocket.OPEN.
589 * created and have a state of WebSocket.OPEN.
589 *
590 *
590 * @function is_connected
591 * @function is_connected
591 * @returns {bool} - whether there is a connection
592 * @returns {bool} - whether there is a connection
592 */
593 */
593 Kernel.prototype.is_connected = function () {
594 for (var c in this.channels) {
594 for (var c in this.channels) {
595 // if any channel is not ready, then we're not connected
595 // if any channel is not ready, then we're not connected
596 if (this.channels[c] === null) {
596 if (this.channels[c] === null) {
597 return false;
597 return false;
598 }
598 }
599 if (this.channels[c].readyState !== WebSocket.OPEN) {
599 if (this.channels[c].readyState !== WebSocket.OPEN) {
600 return false;
600 return false;
601 }
601 }
602 }
602 }
603 return true;
603 return true;
604 };
604 };
605
605
606 Kernel.prototype.is_fully_disconnected = function () {
606 /**
607 /**
607 * Check whether the connection to the kernel has been completely
608 * Check whether the connection to the kernel has been completely
608 * severed. This function only returns true if all channel objects
609 * severed. This function only returns true if all channel objects
609 * are null.
610 * are null.
610 *
611 *
611 * @function is_fully_disconnected
612 * @function is_fully_disconnected
612 * @returns {bool} - whether the kernel is fully disconnected
613 * @returns {bool} - whether the kernel is fully disconnected
613 */
614 */
614 Kernel.prototype.is_fully_disconnected = function () {
615 for (var c in this.channels) {
615 for (var c in this.channels) {
616 if (this.channels[c] === null) {
616 if (this.channels[c] === null) {
617 return true;
617 return true;
618 }
618 }
619 }
619 }
620 return false;
620 return false;
621 };
621 };
622
622
623 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
623 /**
624 /**
624 * Send a message on the Kernel's shell channel
625 * Send a message on the Kernel's shell channel
625 *
626 *
626 * @function send_shell_message
627 * @function send_shell_message
627 */
628 */
628 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
629 if (!this.is_connected()) {
629 if (!this.is_connected()) {
630 throw new Error("kernel is not connected");
630 throw new Error("kernel is not connected");
631 }
631 }
632 var msg = this._get_msg(msg_type, content, metadata, buffers);
632 var msg = this._get_msg(msg_type, content, metadata, buffers);
633 this.channels.shell.send(serialize.serialize(msg));
633 this.channels.shell.send(serialize.serialize(msg));
634 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
634 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
635 return msg.header.msg_id;
635 return msg.header.msg_id;
636 };
636 };
637
637
638 Kernel.prototype.kernel_info = function (callback) {
638 /**
639 /**
639 * Get kernel info
640 * Get kernel info
640 *
641 *
641 * @function kernel_info
642 * @function kernel_info
642 * @param callback {function}
643 * @param callback {function}
643 *
644 *
644 * When calling this method, pass a callback function that expects one argument.
645 * When calling this method, pass a callback function that expects one argument.
645 * The callback will be passed the complete `kernel_info_reply` message documented
646 * The callback will be passed the complete `kernel_info_reply` message documented
646 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
647 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
647 */
648 */
648 Kernel.prototype.kernel_info = function (callback) {
649 var callbacks;
649 var callbacks;
650 if (callback) {
650 if (callback) {
651 callbacks = { shell : { reply : callback } };
651 callbacks = { shell : { reply : callback } };
652 }
652 }
653 return this.send_shell_message("kernel_info_request", {}, callbacks);
653 return this.send_shell_message("kernel_info_request", {}, callbacks);
654 };
654 };
655
655
656 Kernel.prototype.inspect = function (code, cursor_pos, callback) {
656 /**
657 /**
657 * Get info on an object
658 * Get info on an object
658 *
659 *
659 * When calling this method, pass a callback function that expects one argument.
660 * When calling this method, pass a callback function that expects one argument.
660 * The callback will be passed the complete `inspect_reply` message documented
661 * The callback will be passed the complete `inspect_reply` message documented
661 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
662 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
662 *
663 *
663 * @function inspect
664 * @function inspect
664 * @param code {string}
665 * @param code {string}
665 * @param cursor_pos {integer}
666 * @param cursor_pos {integer}
666 * @param callback {function}
667 * @param callback {function}
667 */
668 */
668 Kernel.prototype.inspect = function (code, cursor_pos, callback) {
669 var callbacks;
669 var callbacks;
670 if (callback) {
670 if (callback) {
671 callbacks = { shell : { reply : callback } };
671 callbacks = { shell : { reply : callback } };
672 }
672 }
673
673
674 var content = {
674 var content = {
675 code : code,
675 code : code,
676 cursor_pos : cursor_pos,
676 cursor_pos : cursor_pos,
677 detail_level : 0
677 detail_level : 0
678 };
678 };
679 return this.send_shell_message("inspect_request", content, callbacks);
679 return this.send_shell_message("inspect_request", content, callbacks);
680 };
680 };
681
681
682 Kernel.prototype.execute = function (code, callbacks, options) {
682 /**
683 /**
683 * Execute given code into kernel, and pass result to callback.
684 * Execute given code into kernel, and pass result to callback.
684 *
685 *
685 * @async
686 * @async
686 * @function execute
687 * @function execute
687 * @param {string} code
688 * @param {string} code
688 * @param [callbacks] {Object} With the following keys (all optional)
689 * @param [callbacks] {Object} With the following keys (all optional)
689 * @param callbacks.shell.reply {function}
690 * @param callbacks.shell.reply {function}
690 * @param callbacks.shell.payload.[payload_name] {function}
691 * @param callbacks.shell.payload.[payload_name] {function}
691 * @param callbacks.iopub.output {function}
692 * @param callbacks.iopub.output {function}
692 * @param callbacks.iopub.clear_output {function}
693 * @param callbacks.iopub.clear_output {function}
693 * @param callbacks.input {function}
694 * @param callbacks.input {function}
694 * @param {object} [options]
695 * @param {object} [options]
695 * @param [options.silent=false] {Boolean}
696 * @param [options.silent=false] {Boolean}
696 * @param [options.user_expressions=empty_dict] {Dict}
697 * @param [options.user_expressions=empty_dict] {Dict}
697 * @param [options.allow_stdin=false] {Boolean} true|false
698 * @param [options.allow_stdin=false] {Boolean} true|false
698 *
699 *
699 * @example
700 * @example
700 *
701 *
701 * The options object should contain the options for the execute
702 * The options object should contain the options for the execute
702 * call. Its default values are:
703 * call. Its default values are:
703 *
704 *
704 * options = {
705 * options = {
705 * silent : true,
706 * silent : true,
706 * user_expressions : {},
707 * user_expressions : {},
707 * allow_stdin : false
708 * allow_stdin : false
708 * }
709 * }
709 *
710 *
710 * When calling this method pass a callbacks structure of the
711 * When calling this method pass a callbacks structure of the
711 * form:
712 * form:
712 *
713 *
713 * callbacks = {
714 * callbacks = {
714 * shell : {
715 * shell : {
715 * reply : execute_reply_callback,
716 * reply : execute_reply_callback,
716 * payload : {
717 * payload : {
717 * set_next_input : set_next_input_callback,
718 * set_next_input : set_next_input_callback,
718 * }
719 * }
719 * },
720 * },
720 * iopub : {
721 * iopub : {
721 * output : output_callback,
722 * output : output_callback,
722 * clear_output : clear_output_callback,
723 * clear_output : clear_output_callback,
723 * },
724 * },
724 * input : raw_input_callback
725 * input : raw_input_callback
725 * }
726 * }
726 *
727 *
727 * Each callback will be passed the entire message as a single
728 * Each callback will be passed the entire message as a single
728 * arugment. Payload handlers will be passed the corresponding
729 * arugment. Payload handlers will be passed the corresponding
729 * payload and the execute_reply message.
730 * payload and the execute_reply message.
730 */
731 */
731 Kernel.prototype.execute = function (code, callbacks, options) {
732 var content = {
732 var content = {
733 code : code,
733 code : code,
734 silent : true,
734 silent : true,
735 store_history : false,
735 store_history : false,
736 user_expressions : {},
736 user_expressions : {},
737 allow_stdin : false
737 allow_stdin : false
738 };
738 };
739 callbacks = callbacks || {};
739 callbacks = callbacks || {};
740 if (callbacks.input !== undefined) {
740 if (callbacks.input !== undefined) {
741 content.allow_stdin = true;
741 content.allow_stdin = true;
742 }
742 }
743 $.extend(true, content, options);
743 $.extend(true, content, options);
744 this.events.trigger('execution_request.Kernel', {kernel: this, content: content});
744 this.events.trigger('execution_request.Kernel', {kernel: this, content: content});
745 return this.send_shell_message("execute_request", content, callbacks);
745 return this.send_shell_message("execute_request", content, callbacks);
746 };
746 };
747
747
748 /**
748 /**
749 * When calling this method, pass a function to be called with the
749 * When calling this method, pass a function to be called with the
750 * `complete_reply` message as its only argument when it arrives.
750 * `complete_reply` message as its only argument when it arrives.
751 *
751 *
752 * `complete_reply` is documented
752 * `complete_reply` is documented
753 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
753 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
754 *
754 *
755 * @function complete
755 * @function complete
756 * @param code {string}
756 * @param code {string}
757 * @param cursor_pos {integer}
757 * @param cursor_pos {integer}
758 * @param callback {function}
758 * @param callback {function}
759 */
759 */
760 Kernel.prototype.complete = function (code, cursor_pos, callback) {
760 Kernel.prototype.complete = function (code, cursor_pos, callback) {
761 var callbacks;
761 var callbacks;
762 if (callback) {
762 if (callback) {
763 callbacks = { shell : { reply : callback } };
763 callbacks = { shell : { reply : callback } };
764 }
764 }
765 var content = {
765 var content = {
766 code : code,
766 code : code,
767 cursor_pos : cursor_pos
767 cursor_pos : cursor_pos
768 };
768 };
769 return this.send_shell_message("complete_request", content, callbacks);
769 return this.send_shell_message("complete_request", content, callbacks);
770 };
770 };
771
771
772 /**
772 /**
773 * @function send_input_reply
773 * @function send_input_reply
774 */
774 */
775 Kernel.prototype.send_input_reply = function (input) {
775 Kernel.prototype.send_input_reply = function (input) {
776 if (!this.is_connected()) {
776 if (!this.is_connected()) {
777 throw new Error("kernel is not connected");
777 throw new Error("kernel is not connected");
778 }
778 }
779 var content = {
779 var content = {
780 value : input
780 value : input
781 };
781 };
782 this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
782 this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
783 var msg = this._get_msg("input_reply", content);
783 var msg = this._get_msg("input_reply", content);
784 this.channels.stdin.send(serialize.serialize(msg));
784 this.channels.stdin.send(serialize.serialize(msg));
785 return msg.header.msg_id;
785 return msg.header.msg_id;
786 };
786 };
787
787
788 /**
788 /**
789 * @function register_iopub_handler
789 * @function register_iopub_handler
790 */
790 */
791 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
791 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
792 this._iopub_handlers[msg_type] = callback;
792 this._iopub_handlers[msg_type] = callback;
793 };
793 };
794
794
795 /**
795 /**
796 * Get the iopub handler for a specific message type.
796 * Get the iopub handler for a specific message type.
797 *
797 *
798 * @function get_iopub_handler
798 * @function get_iopub_handler
799 */
799 */
800 Kernel.prototype.get_iopub_handler = function (msg_type) {
800 Kernel.prototype.get_iopub_handler = function (msg_type) {
801 return this._iopub_handlers[msg_type];
801 return this._iopub_handlers[msg_type];
802 };
802 };
803
803
804 /**
804 /**
805 * Get callbacks for a specific message.
805 * Get callbacks for a specific message.
806 *
806 *
807 * @function get_callbacks_for_msg
807 * @function get_callbacks_for_msg
808 */
808 */
809 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
809 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
810 if (msg_id == this.last_msg_id) {
810 if (msg_id == this.last_msg_id) {
811 return this.last_msg_callbacks;
811 return this.last_msg_callbacks;
812 } else {
812 } else {
813 return this._msg_callbacks[msg_id];
813 return this._msg_callbacks[msg_id];
814 }
814 }
815 };
815 };
816
816
817 /**
817 /**
818 * Clear callbacks for a specific message.
818 * Clear callbacks for a specific message.
819 *
819 *
820 * @function clear_callbacks_for_msg
820 * @function clear_callbacks_for_msg
821 */
821 */
822 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
822 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
823 if (this._msg_callbacks[msg_id] !== undefined ) {
823 if (this._msg_callbacks[msg_id] !== undefined ) {
824 delete this._msg_callbacks[msg_id];
824 delete this._msg_callbacks[msg_id];
825 }
825 }
826 };
826 };
827
827
828 /**
828 /**
829 * @function _finish_shell
829 * @function _finish_shell
830 */
830 */
831 Kernel.prototype._finish_shell = function (msg_id) {
831 Kernel.prototype._finish_shell = function (msg_id) {
832 var callbacks = this._msg_callbacks[msg_id];
832 var callbacks = this._msg_callbacks[msg_id];
833 if (callbacks !== undefined) {
833 if (callbacks !== undefined) {
834 callbacks.shell_done = true;
834 callbacks.shell_done = true;
835 if (callbacks.iopub_done) {
835 if (callbacks.iopub_done) {
836 this.clear_callbacks_for_msg(msg_id);
836 this.clear_callbacks_for_msg(msg_id);
837 }
837 }
838 }
838 }
839 };
839 };
840
840
841 /**
841 /**
842 * @function _finish_iopub
842 * @function _finish_iopub
843 */
843 */
844 Kernel.prototype._finish_iopub = function (msg_id) {
844 Kernel.prototype._finish_iopub = function (msg_id) {
845 var callbacks = this._msg_callbacks[msg_id];
845 var callbacks = this._msg_callbacks[msg_id];
846 if (callbacks !== undefined) {
846 if (callbacks !== undefined) {
847 callbacks.iopub_done = true;
847 callbacks.iopub_done = true;
848 if (callbacks.shell_done) {
848 if (callbacks.shell_done) {
849 this.clear_callbacks_for_msg(msg_id);
849 this.clear_callbacks_for_msg(msg_id);
850 }
850 }
851 }
851 }
852 };
852 };
853
853
854 /**
854 /**
855 * Set callbacks for a particular message.
855 * Set callbacks for a particular message.
856 * Callbacks should be a struct of the following form:
856 * Callbacks should be a struct of the following form:
857 * shell : {
857 * shell : {
858 *
858 *
859 * }
859 * }
860 *
860 *
861 * @function set_callbacks_for_msg
861 * @function set_callbacks_for_msg
862 */
862 */
863 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
863 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
864 this.last_msg_id = msg_id;
864 this.last_msg_id = msg_id;
865 if (callbacks) {
865 if (callbacks) {
866 // shallow-copy mapping, because we will modify it at the top level
866 // shallow-copy mapping, because we will modify it at the top level
867 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
867 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
868 cbcopy.shell = callbacks.shell;
868 cbcopy.shell = callbacks.shell;
869 cbcopy.iopub = callbacks.iopub;
869 cbcopy.iopub = callbacks.iopub;
870 cbcopy.input = callbacks.input;
870 cbcopy.input = callbacks.input;
871 cbcopy.shell_done = (!callbacks.shell);
871 cbcopy.shell_done = (!callbacks.shell);
872 cbcopy.iopub_done = (!callbacks.iopub);
872 cbcopy.iopub_done = (!callbacks.iopub);
873 } else {
873 } else {
874 this.last_msg_callbacks = {};
874 this.last_msg_callbacks = {};
875 }
875 }
876 };
876 };
877
877
878 /**
878 /**
879 * @function _handle_shell_reply
879 * @function _handle_shell_reply
880 */
880 */
881 Kernel.prototype._handle_shell_reply = function (e) {
881 Kernel.prototype._handle_shell_reply = function (e) {
882 serialize.deserialize(e.data, $.proxy(this._finish_shell_reply, this));
882 serialize.deserialize(e.data, $.proxy(this._finish_shell_reply, this));
883 };
883 };
884
884
885 Kernel.prototype._finish_shell_reply = function (reply) {
885 Kernel.prototype._finish_shell_reply = function (reply) {
886 this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
886 this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
887 var content = reply.content;
887 var content = reply.content;
888 var metadata = reply.metadata;
888 var metadata = reply.metadata;
889 var parent_id = reply.parent_header.msg_id;
889 var parent_id = reply.parent_header.msg_id;
890 var callbacks = this.get_callbacks_for_msg(parent_id);
890 var callbacks = this.get_callbacks_for_msg(parent_id);
891 if (!callbacks || !callbacks.shell) {
891 if (!callbacks || !callbacks.shell) {
892 return;
892 return;
893 }
893 }
894 var shell_callbacks = callbacks.shell;
894 var shell_callbacks = callbacks.shell;
895
895
896 // signal that shell callbacks are done
896 // signal that shell callbacks are done
897 this._finish_shell(parent_id);
897 this._finish_shell(parent_id);
898
898
899 if (shell_callbacks.reply !== undefined) {
899 if (shell_callbacks.reply !== undefined) {
900 shell_callbacks.reply(reply);
900 shell_callbacks.reply(reply);
901 }
901 }
902 if (content.payload && shell_callbacks.payload) {
902 if (content.payload && shell_callbacks.payload) {
903 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
903 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
904 }
904 }
905 };
905 };
906
906
907 /**
907 /**
908 * @function _handle_payloads
908 * @function _handle_payloads
909 */
909 */
910 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
910 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
911 var l = payloads.length;
911 var l = payloads.length;
912 // Payloads are handled by triggering events because we don't want the Kernel
912 // Payloads are handled by triggering events because we don't want the Kernel
913 // to depend on the Notebook or Pager classes.
913 // to depend on the Notebook or Pager classes.
914 for (var i=0; i<l; i++) {
914 for (var i=0; i<l; i++) {
915 var payload = payloads[i];
915 var payload = payloads[i];
916 var callback = payload_callbacks[payload.source];
916 var callback = payload_callbacks[payload.source];
917 if (callback) {
917 if (callback) {
918 callback(payload, msg);
918 callback(payload, msg);
919 }
919 }
920 }
920 }
921 };
921 };
922
922
923 /**
923 /**
924 * @function _handle_status_message
924 * @function _handle_status_message
925 */
925 */
926 Kernel.prototype._handle_status_message = function (msg) {
926 Kernel.prototype._handle_status_message = function (msg) {
927 var execution_state = msg.content.execution_state;
927 var execution_state = msg.content.execution_state;
928 var parent_id = msg.parent_header.msg_id;
928 var parent_id = msg.parent_header.msg_id;
929
929
930 // dispatch status msg callbacks, if any
930 // dispatch status msg callbacks, if any
931 var callbacks = this.get_callbacks_for_msg(parent_id);
931 var callbacks = this.get_callbacks_for_msg(parent_id);
932 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
932 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
933 try {
933 try {
934 callbacks.iopub.status(msg);
934 callbacks.iopub.status(msg);
935 } catch (e) {
935 } catch (e) {
936 console.log("Exception in status msg handler", e, e.stack);
936 console.log("Exception in status msg handler", e, e.stack);
937 }
937 }
938 }
938 }
939
939
940 if (execution_state === 'busy') {
940 if (execution_state === 'busy') {
941 this.events.trigger('kernel_busy.Kernel', {kernel: this});
941 this.events.trigger('kernel_busy.Kernel', {kernel: this});
942
942
943 } else if (execution_state === 'idle') {
943 } else if (execution_state === 'idle') {
944 // signal that iopub callbacks are (probably) done
944 // signal that iopub callbacks are (probably) done
945 // async output may still arrive,
945 // async output may still arrive,
946 // but only for the most recent request
946 // but only for the most recent request
947 this._finish_iopub(parent_id);
947 this._finish_iopub(parent_id);
948
948
949 // trigger status_idle event
949 // trigger status_idle event
950 this.events.trigger('kernel_idle.Kernel', {kernel: this});
950 this.events.trigger('kernel_idle.Kernel', {kernel: this});
951
951
952 } else if (execution_state === 'starting') {
952 } else if (execution_state === 'starting') {
953 this.events.trigger('kernel_starting.Kernel', {kernel: this});
953 this.events.trigger('kernel_starting.Kernel', {kernel: this});
954 var that = this;
954 var that = this;
955 this.kernel_info(function (reply) {
955 this.kernel_info(function (reply) {
956 that.info_reply = reply.content;
956 that.info_reply = reply.content;
957 that.events.trigger('kernel_ready.Kernel', {kernel: that});
957 that.events.trigger('kernel_ready.Kernel', {kernel: that});
958 });
958 });
959
959
960 } else if (execution_state === 'restarting') {
960 } else if (execution_state === 'restarting') {
961 // autorestarting is distinct from restarting,
961 // autorestarting is distinct from restarting,
962 // in that it means the kernel died and the server is restarting it.
962 // in that it means the kernel died and the server is restarting it.
963 // kernel_restarting sets the notification widget,
963 // kernel_restarting sets the notification widget,
964 // autorestart shows the more prominent dialog.
964 // autorestart shows the more prominent dialog.
965 this._autorestart_attempt = this._autorestart_attempt + 1;
965 this._autorestart_attempt = this._autorestart_attempt + 1;
966 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
966 this.events.trigger('kernel_restarting.Kernel', {kernel: this});
967 this.events.trigger('kernel_autorestarting.Kernel', {kernel: this, attempt: this._autorestart_attempt});
967 this.events.trigger('kernel_autorestarting.Kernel', {kernel: this, attempt: this._autorestart_attempt});
968
968
969 } else if (execution_state === 'dead') {
969 } else if (execution_state === 'dead') {
970 this.events.trigger('kernel_dead.Kernel', {kernel: this});
970 this.events.trigger('kernel_dead.Kernel', {kernel: this});
971 this._kernel_dead();
971 this._kernel_dead();
972 }
972 }
973 };
973 };
974
974
975 /**
975 /**
976 * Handle clear_output message
976 * Handle clear_output message
977 *
977 *
978 * @function _handle_clear_output
978 * @function _handle_clear_output
979 */
979 */
980 Kernel.prototype._handle_clear_output = function (msg) {
980 Kernel.prototype._handle_clear_output = function (msg) {
981 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
981 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
982 if (!callbacks || !callbacks.iopub) {
982 if (!callbacks || !callbacks.iopub) {
983 return;
983 return;
984 }
984 }
985 var callback = callbacks.iopub.clear_output;
985 var callback = callbacks.iopub.clear_output;
986 if (callback) {
986 if (callback) {
987 callback(msg);
987 callback(msg);
988 }
988 }
989 };
989 };
990
990
991 /**
991 /**
992 * handle an output message (execute_result, display_data, etc.)
992 * handle an output message (execute_result, display_data, etc.)
993 *
993 *
994 * @function _handle_output_message
994 * @function _handle_output_message
995 */
995 */
996 Kernel.prototype._handle_output_message = function (msg) {
996 Kernel.prototype._handle_output_message = function (msg) {
997 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
997 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
998 if (!callbacks || !callbacks.iopub) {
998 if (!callbacks || !callbacks.iopub) {
999 if (this.unsolicited_msg_callback) {
999 if (this.unsolicited_msg_callback) {
1000 // The message came from another client. Let the UI decide what
1000 // The message came from another client. Let the UI decide what
1001 // to do with it.
1001 // to do with it.
1002 this.unsolicited_msg_callback(msg);
1002 this.unsolicited_msg_callback(msg);
1003 }
1003 }
1004 return;
1004 return;
1005 }
1005 }
1006 var callback = callbacks.iopub.output;
1006 var callback = callbacks.iopub.output;
1007 if (callback) {
1007 if (callback) {
1008 callback(msg);
1008 callback(msg);
1009 }
1009 }
1010 };
1010 };
1011
1011
1012 /**
1012 /**
1013 * Handle an input message (execute_input).
1013 * Handle an input message (execute_input).
1014 *
1014 *
1015 * @function _handle_input message
1015 * @function _handle_input message
1016 */
1016 */
1017 Kernel.prototype._handle_input_message = function (msg) {
1017 Kernel.prototype._handle_input_message = function (msg) {
1018 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
1018 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
1019 if (!callbacks && this.unsolicited_msg_callback) {
1019 if (!callbacks && this.unsolicited_msg_callback) {
1020 // The message came from another client. Let the UI decide what to
1020 // The message came from another client. Let the UI decide what to
1021 // do with it.
1021 // do with it.
1022 this.unsolicited_msg_callback(msg);
1022 this.unsolicited_msg_callback(msg);
1023 }
1023 }
1024 };
1024 };
1025
1025
1026 /**
1026 /**
1027 * Dispatch IOPub messages to respective handlers. Each message
1027 * Dispatch IOPub messages to respective handlers. Each message
1028 * type should have a handler.
1028 * type should have a handler.
1029 *
1029 *
1030 * @function _handle_iopub_message
1030 * @function _handle_iopub_message
1031 */
1031 */
1032 Kernel.prototype._handle_iopub_message = function (e) {
1032 Kernel.prototype._handle_iopub_message = function (e) {
1033 serialize.deserialize(e.data, $.proxy(this._finish_iopub_message, this));
1033 serialize.deserialize(e.data, $.proxy(this._finish_iopub_message, this));
1034 };
1034 };
1035
1035
1036
1036
1037 Kernel.prototype._finish_iopub_message = function (msg) {
1037 Kernel.prototype._finish_iopub_message = function (msg) {
1038 var handler = this.get_iopub_handler(msg.header.msg_type);
1038 var handler = this.get_iopub_handler(msg.header.msg_type);
1039 if (handler !== undefined) {
1039 if (handler !== undefined) {
1040 handler(msg);
1040 handler(msg);
1041 }
1041 }
1042 };
1042 };
1043
1043
1044 /**
1044 /**
1045 * @function _handle_input_request
1045 * @function _handle_input_request
1046 */
1046 */
1047 Kernel.prototype._handle_input_request = function (e) {
1047 Kernel.prototype._handle_input_request = function (e) {
1048 serialize.deserialize(e.data, $.proxy(this._finish_input_request, this));
1048 serialize.deserialize(e.data, $.proxy(this._finish_input_request, this));
1049 };
1049 };
1050
1050
1051
1051
1052 Kernel.prototype._finish_input_request = function (request) {
1052 Kernel.prototype._finish_input_request = function (request) {
1053 var header = request.header;
1053 var header = request.header;
1054 var content = request.content;
1054 var content = request.content;
1055 var metadata = request.metadata;
1055 var metadata = request.metadata;
1056 var msg_type = header.msg_type;
1056 var msg_type = header.msg_type;
1057 if (msg_type !== 'input_request') {
1057 if (msg_type !== 'input_request') {
1058 console.log("Invalid input request!", request);
1058 console.log("Invalid input request!", request);
1059 return;
1059 return;
1060 }
1060 }
1061 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
1061 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
1062 if (callbacks) {
1062 if (callbacks) {
1063 if (callbacks.input) {
1063 if (callbacks.input) {
1064 callbacks.input(request);
1064 callbacks.input(request);
1065 }
1065 }
1066 }
1066 }
1067 };
1067 };
1068
1068
1069 // Backwards compatability.
1069 // Backwards compatability.
1070 IPython.Kernel = Kernel;
1070 IPython.Kernel = Kernel;
1071
1071
1072 return {'Kernel': Kernel};
1072 return {'Kernel': Kernel};
1073 });
1073 });
@@ -1,240 +1,239 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "underscore",
5 "underscore",
6 "backbone",
6 "backbone",
7 "jquery",
7 "jquery",
8 "base/js/utils",
8 "base/js/utils",
9 "base/js/namespace",
9 "base/js/namespace",
10 ], function (_, Backbone, $, utils, IPython) {
10 ], function (_, Backbone, $, utils, IPython) {
11 "use strict";
11 "use strict";
12 //--------------------------------------------------------------------
12 //--------------------------------------------------------------------
13 // WidgetManager class
13 // WidgetManager class
14 //--------------------------------------------------------------------
14 //--------------------------------------------------------------------
15 var WidgetManager = function (comm_manager, notebook) {
15 var WidgetManager = function (comm_manager, notebook) {
16 // Public constructor
16 // Public constructor
17 WidgetManager._managers.push(this);
17 WidgetManager._managers.push(this);
18
18
19 // Attach a comm manager to the
19 // Attach a comm manager to the
20 this.keyboard_manager = notebook.keyboard_manager;
20 this.keyboard_manager = notebook.keyboard_manager;
21 this.notebook = notebook;
21 this.notebook = notebook;
22 this.comm_manager = comm_manager;
22 this.comm_manager = comm_manager;
23 this._models = {}; /* Dictionary of model ids and model instances */
23 this._models = {}; /* Dictionary of model ids and model instances */
24
24
25 // Register with the comm manager.
25 // Register with the comm manager.
26 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
26 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
27 };
27 };
28
28
29 //--------------------------------------------------------------------
29 //--------------------------------------------------------------------
30 // Class level
30 // Class level
31 //--------------------------------------------------------------------
31 //--------------------------------------------------------------------
32 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
32 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
33 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
33 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
34 WidgetManager._managers = []; /* List of widget managers */
34 WidgetManager._managers = []; /* List of widget managers */
35
35
36 WidgetManager.register_widget_model = function (model_name, model_type) {
36 WidgetManager.register_widget_model = function (model_name, model_type) {
37 // Registers a widget model by name.
37 // Registers a widget model by name.
38 WidgetManager._model_types[model_name] = model_type;
38 WidgetManager._model_types[model_name] = model_type;
39 };
39 };
40
40
41 WidgetManager.register_widget_view = function (view_name, view_type) {
41 WidgetManager.register_widget_view = function (view_name, view_type) {
42 // Registers a widget view by name.
42 // Registers a widget view by name.
43 WidgetManager._view_types[view_name] = view_type;
43 WidgetManager._view_types[view_name] = view_type;
44 };
44 };
45
45
46 //--------------------------------------------------------------------
46 //--------------------------------------------------------------------
47 // Instance level
47 // Instance level
48 //--------------------------------------------------------------------
48 //--------------------------------------------------------------------
49 WidgetManager.prototype.display_view = function(msg, model) {
49 WidgetManager.prototype.display_view = function(msg, model) {
50 // Displays a view for a particular model.
50 // Displays a view for a particular model.
51 var that = this;
51 var that = this;
52 var cell = this.get_msg_cell(msg.parent_header.msg_id);
52 var cell = this.get_msg_cell(msg.parent_header.msg_id);
53 if (cell === null) {
53 if (cell === null) {
54 return Promise.reject(new Error("Could not determine where the display" +
54 return Promise.reject(new Error("Could not determine where the display" +
55 " message was from. Widget will not be displayed"));
55 " message was from. Widget will not be displayed"));
56 } else if (cell.widget_subarea) {
56 } else if (cell.widget_subarea) {
57 var dummy = $('<div />');
57 var dummy = $('<div />');
58 cell.widget_subarea.append(dummy);
58 cell.widget_subarea.append(dummy);
59 return this.create_view(model, {cell: cell}).then(
59 return this.create_view(model, {cell: cell}).then(
60 function(view) {
60 function(view) {
61 that._handle_display_view(view);
61 that._handle_display_view(view);
62 dummy.replaceWith(view.$el);
62 dummy.replaceWith(view.$el);
63 view.trigger('displayed');
63 view.trigger('displayed');
64 return view;
64 return view;
65 }).catch(utils.reject('Could not display view', true));
65 }).catch(utils.reject('Could not display view', true));
66 }
66 }
67 };
67 };
68
68
69 WidgetManager.prototype._handle_display_view = function (view) {
69 WidgetManager.prototype._handle_display_view = function (view) {
70 // Have the IPython keyboard manager disable its event
70 // Have the IPython keyboard manager disable its event
71 // handling so the widget can capture keyboard input.
71 // handling so the widget can capture keyboard input.
72 // Note, this is only done on the outer most widgets.
72 // Note, this is only done on the outer most widgets.
73 if (this.keyboard_manager) {
73 if (this.keyboard_manager) {
74 this.keyboard_manager.register_events(view.$el);
74 this.keyboard_manager.register_events(view.$el);
75
75
76 if (view.additional_elements) {
76 if (view.additional_elements) {
77 for (var i = 0; i < view.additional_elements.length; i++) {
77 for (var i = 0; i < view.additional_elements.length; i++) {
78 this.keyboard_manager.register_events(view.additional_elements[i]);
78 this.keyboard_manager.register_events(view.additional_elements[i]);
79 }
79 }
80 }
80 }
81 }
81 }
82 };
82 };
83
83
84 WidgetManager.prototype.create_view = function(model, options) {
84 WidgetManager.prototype.create_view = function(model, options) {
85 // Creates a promise for a view of a given model
85 // Creates a promise for a view of a given model
86
86
87 // Make sure the view creation is not out of order with
87 // Make sure the view creation is not out of order with
88 // any state updates.
88 // any state updates.
89 model.state_change = model.state_change.then(function() {
89 model.state_change = model.state_change.then(function() {
90
90
91 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
91 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
92 WidgetManager._view_types).then(function(ViewType) {
92 WidgetManager._view_types).then(function(ViewType) {
93
93
94 // If a view is passed into the method, use that view's cell as
94 // If a view is passed into the method, use that view's cell as
95 // the cell for the view that is created.
95 // the cell for the view that is created.
96 options = options || {};
96 options = options || {};
97 if (options.parent !== undefined) {
97 if (options.parent !== undefined) {
98 options.cell = options.parent.options.cell;
98 options.cell = options.parent.options.cell;
99 }
99 }
100 // Create and render the view...
100 // Create and render the view...
101 var parameters = {model: model, options: options};
101 var parameters = {model: model, options: options};
102 var view = new ViewType(parameters);
102 var view = new ViewType(parameters);
103 view.listenTo(model, 'destroy', view.remove);
103 view.listenTo(model, 'destroy', view.remove);
104 view.render();
104 return Promise.resolve(view.render()).then(function() {return view;});
105 return view;
106 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
105 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
107 });
106 });
108 return model.state_change;
107 return model.state_change;
109 };
108 };
110
109
111 WidgetManager.prototype.get_msg_cell = function (msg_id) {
110 WidgetManager.prototype.get_msg_cell = function (msg_id) {
112 var cell = null;
111 var cell = null;
113 // First, check to see if the msg was triggered by cell execution.
112 // First, check to see if the msg was triggered by cell execution.
114 if (this.notebook) {
113 if (this.notebook) {
115 cell = this.notebook.get_msg_cell(msg_id);
114 cell = this.notebook.get_msg_cell(msg_id);
116 }
115 }
117 if (cell !== null) {
116 if (cell !== null) {
118 return cell;
117 return cell;
119 }
118 }
120 // Second, check to see if a get_cell callback was defined
119 // Second, check to see if a get_cell callback was defined
121 // for the message. get_cell callbacks are registered for
120 // for the message. get_cell callbacks are registered for
122 // widget messages, so this block is actually checking to see if the
121 // widget messages, so this block is actually checking to see if the
123 // message was triggered by a widget.
122 // message was triggered by a widget.
124 var kernel = this.comm_manager.kernel;
123 var kernel = this.comm_manager.kernel;
125 if (kernel) {
124 if (kernel) {
126 var callbacks = kernel.get_callbacks_for_msg(msg_id);
125 var callbacks = kernel.get_callbacks_for_msg(msg_id);
127 if (callbacks && callbacks.iopub &&
126 if (callbacks && callbacks.iopub &&
128 callbacks.iopub.get_cell !== undefined) {
127 callbacks.iopub.get_cell !== undefined) {
129 return callbacks.iopub.get_cell();
128 return callbacks.iopub.get_cell();
130 }
129 }
131 }
130 }
132
131
133 // Not triggered by a cell or widget (no get_cell callback
132 // Not triggered by a cell or widget (no get_cell callback
134 // exists).
133 // exists).
135 return null;
134 return null;
136 };
135 };
137
136
138 WidgetManager.prototype.callbacks = function (view) {
137 WidgetManager.prototype.callbacks = function (view) {
139 // callback handlers specific a view
138 // callback handlers specific a view
140 var callbacks = {};
139 var callbacks = {};
141 if (view && view.options.cell) {
140 if (view && view.options.cell) {
142
141
143 // Try to get output handlers
142 // Try to get output handlers
144 var cell = view.options.cell;
143 var cell = view.options.cell;
145 var handle_output = null;
144 var handle_output = null;
146 var handle_clear_output = null;
145 var handle_clear_output = null;
147 if (cell.output_area) {
146 if (cell.output_area) {
148 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
147 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
149 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
148 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
150 }
149 }
151
150
152 // Create callback dictionary using what is known
151 // Create callback dictionary using what is known
153 var that = this;
152 var that = this;
154 callbacks = {
153 callbacks = {
155 iopub : {
154 iopub : {
156 output : handle_output,
155 output : handle_output,
157 clear_output : handle_clear_output,
156 clear_output : handle_clear_output,
158
157
159 // Special function only registered by widget messages.
158 // Special function only registered by widget messages.
160 // Allows us to get the cell for a message so we know
159 // Allows us to get the cell for a message so we know
161 // where to add widgets if the code requires it.
160 // where to add widgets if the code requires it.
162 get_cell : function () {
161 get_cell : function () {
163 return cell;
162 return cell;
164 },
163 },
165 },
164 },
166 };
165 };
167 }
166 }
168 return callbacks;
167 return callbacks;
169 };
168 };
170
169
171 WidgetManager.prototype.get_model = function (model_id) {
170 WidgetManager.prototype.get_model = function (model_id) {
172 // Get a promise for a model by model id.
171 // Get a promise for a model by model id.
173 return this._models[model_id];
172 return this._models[model_id];
174 };
173 };
175
174
176 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
175 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
177 // Handle when a comm is opened.
176 // Handle when a comm is opened.
178 return this.create_model({
177 return this.create_model({
179 model_name: msg.content.data.model_name,
178 model_name: msg.content.data.model_name,
180 model_module: msg.content.data.model_module,
179 model_module: msg.content.data.model_module,
181 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
180 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
182 };
181 };
183
182
184 WidgetManager.prototype.create_model = function (options) {
183 WidgetManager.prototype.create_model = function (options) {
185 // Create and return a promise for a new widget model
184 // Create and return a promise for a new widget model
186 //
185 //
187 // Minimally, one must provide the model_name and widget_class
186 // Minimally, one must provide the model_name and widget_class
188 // parameters to create a model from Javascript.
187 // parameters to create a model from Javascript.
189 //
188 //
190 // Example
189 // Example
191 // --------
190 // --------
192 // JS:
191 // JS:
193 // IPython.notebook.kernel.widget_manager.create_model({
192 // IPython.notebook.kernel.widget_manager.create_model({
194 // model_name: 'WidgetModel',
193 // model_name: 'WidgetModel',
195 // widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
194 // widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
196 // .then(function(model) { console.log('Create success!', model); },
195 // .then(function(model) { console.log('Create success!', model); },
197 // $.proxy(console.error, console));
196 // $.proxy(console.error, console));
198 //
197 //
199 // Parameters
198 // Parameters
200 // ----------
199 // ----------
201 // options: dictionary
200 // options: dictionary
202 // Dictionary of options with the following contents:
201 // Dictionary of options with the following contents:
203 // model_name: string
202 // model_name: string
204 // Target name of the widget model to create.
203 // Target name of the widget model to create.
205 // model_module: (optional) string
204 // model_module: (optional) string
206 // Module name of the widget model to create.
205 // Module name of the widget model to create.
207 // widget_class: (optional) string
206 // widget_class: (optional) string
208 // Target name of the widget in the back-end.
207 // Target name of the widget in the back-end.
209 // comm: (optional) Comm
208 // comm: (optional) Comm
210
209
211 // Create a comm if it wasn't provided.
210 // Create a comm if it wasn't provided.
212 var comm = options.comm;
211 var comm = options.comm;
213 if (!comm) {
212 if (!comm) {
214 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
213 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
215 }
214 }
216
215
217 var that = this;
216 var that = this;
218 var model_id = comm.comm_id;
217 var model_id = comm.comm_id;
219 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
218 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
220 .then(function(ModelType) {
219 .then(function(ModelType) {
221 var widget_model = new ModelType(that, model_id, comm);
220 var widget_model = new ModelType(that, model_id, comm);
222 widget_model.once('comm:close', function () {
221 widget_model.once('comm:close', function () {
223 delete that._models[model_id];
222 delete that._models[model_id];
224 });
223 });
225 return widget_model;
224 return widget_model;
226
225
227 }, function(error) {
226 }, function(error) {
228 delete that._models[model_id];
227 delete that._models[model_id];
229 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
228 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
230 return Promise.reject(wrapped_error);
229 return Promise.reject(wrapped_error);
231 });
230 });
232 this._models[model_id] = model_promise;
231 this._models[model_id] = model_promise;
233 return model_promise;
232 return model_promise;
234 };
233 };
235
234
236 // Backwards compatibility.
235 // Backwards compatibility.
237 IPython.WidgetManager = WidgetManager;
236 IPython.WidgetManager = WidgetManager;
238
237
239 return {'WidgetManager': WidgetManager};
238 return {'WidgetManager': WidgetManager};
240 });
239 });
@@ -1,195 +1,221 b''
1 =====================
1 =====================
2 Development version
2 Development version
3 =====================
3 =====================
4
4
5 This document describes in-flight development work.
5 This document describes in-flight development work.
6
6
7 .. warning::
7 .. warning::
8
8
9 Please do not edit this file by hand (doing so will likely cause merge
9 Please do not edit this file by hand (doing so will likely cause merge
10 conflicts for other Pull Requests). Instead, create a new file in the
10 conflicts for other Pull Requests). Instead, create a new file in the
11 `docs/source/whatsnew/pr` folder
11 `docs/source/whatsnew/pr` folder
12
12
13 Using different kernels
13 Using different kernels
14 -----------------------
14 -----------------------
15
15
16 .. image:: ../_images/kernel_selector_screenshot.png
16 .. image:: ../_images/kernel_selector_screenshot.png
17 :alt: Screenshot of notebook kernel selection dropdown menu
17 :alt: Screenshot of notebook kernel selection dropdown menu
18 :align: center
18 :align: center
19
19
20 You can now choose a kernel for a notebook within the user interface, rather
20 You can now choose a kernel for a notebook within the user interface, rather
21 than starting up a separate notebook server for each kernel you want to use. The
21 than starting up a separate notebook server for each kernel you want to use. The
22 syntax highlighting adapts to match the language you're working in.
22 syntax highlighting adapts to match the language you're working in.
23
23
24 Information about the kernel is stored in the notebook file, so when you open a
24 Information about the kernel is stored in the notebook file, so when you open a
25 notebook, it will automatically start the correct kernel.
25 notebook, it will automatically start the correct kernel.
26
26
27 It is also easier to use the Qt console and the terminal console with other
27 It is also easier to use the Qt console and the terminal console with other
28 kernels, using the --kernel flag::
28 kernels, using the --kernel flag::
29
29
30 ipython qtconsole --kernel bash
30 ipython qtconsole --kernel bash
31 ipython console --kernel bash
31 ipython console --kernel bash
32
32
33 # To list available kernels
33 # To list available kernels
34 ipython kernelspec list
34 ipython kernelspec list
35
35
36 Kernel authors should see :ref:`kernelspecs` for how to register their kernels
36 Kernel authors should see :ref:`kernelspecs` for how to register their kernels
37 with IPython so that these mechanisms work.
37 with IPython so that these mechanisms work.
38
38
39 Typing unicode identifiers
39 Typing unicode identifiers
40 --------------------------
40 --------------------------
41
41
42 .. image:: /_images/unicode_completion.png
42 .. image:: /_images/unicode_completion.png
43
43
44 Complex expressions can be much cleaner when written with a wider choice of
44 Complex expressions can be much cleaner when written with a wider choice of
45 characters. Python 3 allows unicode identifiers, and IPython 3 makes it easier
45 characters. Python 3 allows unicode identifiers, and IPython 3 makes it easier
46 to type those, using a feature from Julia. Type a backslash followed by a LaTeX
46 to type those, using a feature from Julia. Type a backslash followed by a LaTeX
47 style short name, such as ``\alpha``. Press tab, and it will turn into α.
47 style short name, such as ``\alpha``. Press tab, and it will turn into α.
48
48
49 Other new features
49 Other new features
50 ------------------
50 ------------------
51
51
52 * :class:`~.TextWidget` and :class:`~.TextareaWidget` objects now include a
52 * :class:`~.TextWidget` and :class:`~.TextareaWidget` objects now include a
53 ``placeholder`` attribute, for displaying placeholder text before the
53 ``placeholder`` attribute, for displaying placeholder text before the
54 user has typed anything.
54 user has typed anything.
55
55
56 * The :magic:`load` magic can now find the source for objects in the user namespace.
56 * The :magic:`load` magic can now find the source for objects in the user namespace.
57 To enable searching the namespace, use the ``-n`` option.
57 To enable searching the namespace, use the ``-n`` option.
58
58
59 .. sourcecode:: ipython
59 .. sourcecode:: ipython
60
60
61 In [1]: %load -n my_module.some_function
61 In [1]: %load -n my_module.some_function
62
62
63 * :class:`~.DirectView` objects have a new :meth:`~.DirectView.use_cloudpickle`
63 * :class:`~.DirectView` objects have a new :meth:`~.DirectView.use_cloudpickle`
64 method, which works like ``view.use_dill()``, but causes the ``cloudpickle``
64 method, which works like ``view.use_dill()``, but causes the ``cloudpickle``
65 module from PiCloud's `cloud`__ library to be used rather than dill or the
65 module from PiCloud's `cloud`__ library to be used rather than dill or the
66 builtin pickle module.
66 builtin pickle module.
67
67
68 __ https://pypi.python.org/pypi/cloud
68 __ https://pypi.python.org/pypi/cloud
69
69
70 * Added a .ipynb exporter to nbconvert. It can be used by passing `--to notebook`
70 * Added a .ipynb exporter to nbconvert. It can be used by passing `--to notebook`
71 as a commandline argument to nbconvert.
71 as a commandline argument to nbconvert.
72
72
73 * New nbconvert preprocessor called :class:`~.ClearOutputPreprocessor`. This
73 * New nbconvert preprocessor called :class:`~.ClearOutputPreprocessor`. This
74 clears the output from IPython notebooks.
74 clears the output from IPython notebooks.
75
75
76 * New preprocessor for nbconvert that executes all the code cells in a notebook.
76 * New preprocessor for nbconvert that executes all the code cells in a notebook.
77 To run a notebook and save its output in a new notebook::
77 To run a notebook and save its output in a new notebook::
78
78
79 ipython nbconvert InputNotebook --ExecutePreprocessor.enabled=True --to notebook --output Executed
79 ipython nbconvert InputNotebook --ExecutePreprocessor.enabled=True --to notebook --output Executed
80
80
81 * Consecutive stream (stdout/stderr) output is merged into a single output
81 * Consecutive stream (stdout/stderr) output is merged into a single output
82 in the notebook document.
82 in the notebook document.
83 Previously, all output messages were preserved as separate output fields in the JSON.
83 Previously, all output messages were preserved as separate output fields in the JSON.
84 Now, the same merge is applied to the stored output as the displayed output,
84 Now, the same merge is applied to the stored output as the displayed output,
85 improving document load time for notebooks with many small outputs.
85 improving document load time for notebooks with many small outputs.
86
86
87 * ``NotebookApp.webapp_settings`` is deprecated and replaced with
87 * ``NotebookApp.webapp_settings`` is deprecated and replaced with
88 the more informatively named ``NotebookApp.tornado_settings``.
88 the more informatively named ``NotebookApp.tornado_settings``.
89
89
90 * Using :magic:`timeit` prints warnings if there is atleast a 4x difference in timings
90 * Using :magic:`timeit` prints warnings if there is atleast a 4x difference in timings
91 between the slowest and fastest runs, since this might meant that the multiple
91 between the slowest and fastest runs, since this might meant that the multiple
92 runs are not independent of one another.
92 runs are not independent of one another.
93
93
94 * It's now possible to provide mechanisms to integrate IPython with other event
94 * It's now possible to provide mechanisms to integrate IPython with other event
95 loops, in addition to the ones we already support. This lets you run GUI code
95 loops, in addition to the ones we already support. This lets you run GUI code
96 in IPython with an interactive prompt, and to embed the IPython
96 in IPython with an interactive prompt, and to embed the IPython
97 kernel in GUI applications. See :doc:`/config/eventloops` for details. As part
97 kernel in GUI applications. See :doc:`/config/eventloops` for details. As part
98 of this, the direct ``enable_*`` and ``disable_*`` functions for various GUIs
98 of this, the direct ``enable_*`` and ``disable_*`` functions for various GUIs
99 in :mod:`IPython.lib.inputhook` have been deprecated in favour of
99 in :mod:`IPython.lib.inputhook` have been deprecated in favour of
100 :meth:`~.InputHookManager.enable_gui` and :meth:`~.InputHookManager.disable_gui`.
100 :meth:`~.InputHookManager.enable_gui` and :meth:`~.InputHookManager.disable_gui`.
101
101
102 * A ``ScrollManager`` was added to the notebook. The ``ScrollManager`` controls how the notebook document is scrolled using keyboard. Users can inherit from the ``ScrollManager`` or ``TargetScrollManager`` to customize how their notebook scrolls. The default ``ScrollManager`` is the ``SlideScrollManager``, which tries to scroll to the nearest slide or sub-slide cell.
102 * A ``ScrollManager`` was added to the notebook. The ``ScrollManager`` controls how the notebook document is scrolled using keyboard. Users can inherit from the ``ScrollManager`` or ``TargetScrollManager`` to customize how their notebook scrolls. The default ``ScrollManager`` is the ``SlideScrollManager``, which tries to scroll to the nearest slide or sub-slide cell.
103
103
104 * The function :func:`~IPython.html.widgets.interaction.interact_manual` has been
104 * The function :func:`~IPython.html.widgets.interaction.interact_manual` has been
105 added which behaves similarly to :func:`~IPython.html.widgets.interaction.interact`,
105 added which behaves similarly to :func:`~IPython.html.widgets.interaction.interact`,
106 but adds a button to explicitly run the interacted-with function, rather than
106 but adds a button to explicitly run the interacted-with function, rather than
107 doing it automatically for every change of the parameter widgets. This should
107 doing it automatically for every change of the parameter widgets. This should
108 be useful for long-running functions.
108 be useful for long-running functions.
109
109
110 * The ``%cython`` magic is now part of the Cython module. Use `%load_ext Cython` with a version of Cython >= 0.21 to have access to the magic now.
110 * The ``%cython`` magic is now part of the Cython module. Use `%load_ext Cython` with a version of Cython >= 0.21 to have access to the magic now.
111
111
112 * The Notebook application now offers integrated terminals on Unix platforms,
112 * The Notebook application now offers integrated terminals on Unix platforms,
113 intended for when it is used on a remote server. To enable these, install
113 intended for when it is used on a remote server. To enable these, install
114 the ``terminado`` Python package.
114 the ``terminado`` Python package.
115
115
116 * Setting the default highlighting language for nbconvert with the config option
116 * Setting the default highlighting language for nbconvert with the config option
117 ``NbConvertBase.default_language`` is deprecated. Nbconvert now respects
117 ``NbConvertBase.default_language`` is deprecated. Nbconvert now respects
118 metadata stored in the :ref:`kernel spec <kernelspecs>`.
118 metadata stored in the :ref:`kernel spec <kernelspecs>`.
119
119
120 * IPython can now be configured systemwide, with files in :file:`/etc/ipython`
120 * IPython can now be configured systemwide, with files in :file:`/etc/ipython`
121 or :file:`/usr/local/etc/ipython` on Unix systems,
121 or :file:`/usr/local/etc/ipython` on Unix systems,
122 or :file:`{%PROGRAMDATA%}\\ipython` on Windows.
122 or :file:`{%PROGRAMDATA%}\\ipython` on Windows.
123
123
124 .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT.
124 .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT.
125
125
126
126
127 Backwards incompatible changes
127 Backwards incompatible changes
128 ------------------------------
128 ------------------------------
129
129
130 * :func:`IPython.core.oinspect.getsource` call specification has changed:
130 * :func:`IPython.core.oinspect.getsource` call specification has changed:
131
131
132 * `oname` keyword argument has been added for property source formatting
132 * `oname` keyword argument has been added for property source formatting
133 * `is_binary` keyword argument has been dropped, passing ``True`` had
133 * `is_binary` keyword argument has been dropped, passing ``True`` had
134 previously short-circuited the function to return ``None`` unconditionally
134 previously short-circuited the function to return ``None`` unconditionally
135
135
136 * Removed the octavemagic extension: it is now available as ``oct2py.ipython``.
136 * Removed the octavemagic extension: it is now available as ``oct2py.ipython``.
137
137
138 * Creating PDFs with LaTeX no longer uses a post processor.
138 * Creating PDFs with LaTeX no longer uses a post processor.
139 Use `nbconvert --to pdf` instead of `nbconvert --to latex --post pdf`.
139 Use `nbconvert --to pdf` instead of `nbconvert --to latex --post pdf`.
140
140
141 * Used https://github.com/jdfreder/bootstrap2to3 to migrate the Notebook to Bootstrap 3.
141 * Used https://github.com/jdfreder/bootstrap2to3 to migrate the Notebook to Bootstrap 3.
142
142
143 Additional changes:
143 Additional changes:
144
144
145 - Set `.tab-content .row` `0px;` left and right margin (bootstrap default is `-15px;`)
145 - Set `.tab-content .row` `0px;` left and right margin (bootstrap default is `-15px;`)
146 - Removed `height: @btn_mini_height;` from `.list_header>div, .list_item>div` in `tree.less`
146 - Removed `height: @btn_mini_height;` from `.list_header>div, .list_item>div` in `tree.less`
147 - Set `#header` div `margin-bottom: 0px;`
147 - Set `#header` div `margin-bottom: 0px;`
148 - Set `#menus` to `float: left;`
148 - Set `#menus` to `float: left;`
149 - Set `#maintoolbar .navbar-text` to `float: none;`
149 - Set `#maintoolbar .navbar-text` to `float: none;`
150 - Added no-padding convienence class.
150 - Added no-padding convienence class.
151 - Set border of #maintoolbar to 0px
151 - Set border of #maintoolbar to 0px
152
152
153 * Accessing the `container` DOM object when displaying javascript has been
153 * Accessing the `container` DOM object when displaying javascript has been
154 deprecated in IPython 2.0 in favor of accessing `element`. Starting with
154 deprecated in IPython 2.0 in favor of accessing `element`. Starting with
155 IPython 3.0 trying to access `container` will raise an error in browser
155 IPython 3.0 trying to access `container` will raise an error in browser
156 javascript console.
156 javascript console.
157
157
158 * ``IPython.utils.py3compat.open`` was removed: :func:`io.open` provides all
158 * ``IPython.utils.py3compat.open`` was removed: :func:`io.open` provides all
159 the same functionality.
159 the same functionality.
160
160
161 * The NotebookManager and ``/api/notebooks`` service has been replaced by
161 * The NotebookManager and ``/api/notebooks`` service has been replaced by
162 a more generic ContentsManager and ``/api/contents`` service,
162 a more generic ContentsManager and ``/api/contents`` service,
163 which supports all kinds of files.
163 which supports all kinds of files.
164 * The Dashboard now lists all files, not just notebooks and directories.
164 * The Dashboard now lists all files, not just notebooks and directories.
165 * The ``--script`` hook for saving notebooks to Python scripts is removed,
165 * The ``--script`` hook for saving notebooks to Python scripts is removed,
166 use :samp:`ipython nbconvert --to python {notebook}` instead.
166 use :samp:`ipython nbconvert --to python {notebook}` instead.
167
167
168 * The ``rmagic`` extension is deprecated, as it is now part of rpy2. See
168 * The ``rmagic`` extension is deprecated, as it is now part of rpy2. See
169 :mod:`rpy2.ipython.rmagic`.
169 :mod:`rpy2.ipython.rmagic`.
170
170
171 * :meth:`~.KernelManager.start_kernel` and :meth:`~.KernelManager.format_kernel_cmd`
171 * :meth:`~.KernelManager.start_kernel` and :meth:`~.KernelManager.format_kernel_cmd`
172 no longer accept a ``executable`` parameter. Use the kernelspec machinery instead.
172 no longer accept a ``executable`` parameter. Use the kernelspec machinery instead.
173
173
174 * The widget classes have been renamed from `*Widget` to `*`. The old names are
174 * The widget classes have been renamed from `*Widget` to `*`. The old names are
175 still functional, but are deprecated. i.e. `IntSliderWidget` has been renamed
175 still functional, but are deprecated. i.e. `IntSliderWidget` has been renamed
176 to `IntSlider`.
176 to `IntSlider`.
177 * The ContainerWidget was renamed to Box and no longer defaults as a flexible
177 * The ContainerWidget was renamed to Box and no longer defaults as a flexible
178 box in the web browser. A new FlexBox widget was added, which allows you to
178 box in the web browser. A new FlexBox widget was added, which allows you to
179 use the flexible box model.
179 use the flexible box model.
180
180
181 .. DO NOT EDIT THIS LINE BEFORE RELEASE. INCOMPAT INSERTION POINT.
181 .. DO NOT EDIT THIS LINE BEFORE RELEASE. INCOMPAT INSERTION POINT.
182
182
183 IFrame embedding
183 Content Security Policy
184 ````````````````
184 ```````````````````````
185
185
186 The IPython Notebook and its APIs by default will only be allowed to be
186 The Content Security Policy is a web standard for adding a layer of security to
187 embedded in an iframe on the same origin.
187 detect and mitigate certain classes of attacks, including Cross Site Scripting
188 (XSS) and data injection attacks. This was introduced into the notebook to
189 ensure that the IPython Notebook and its APIs (by default) can only be embedded
190 in an iframe on the same origin.
188
191
189 To override this, set ``headers[X-Frame-Options]`` to one of
192 Override ``headers['Content-Security-Policy']`` within your notebook
193 configuration to extend for alternate domains and security settings.::
190
194
191 * DENY
195 c.NotebookApp.tornado_settings = {
192 * SAMEORIGIN
196 'headers': {
193 * ALLOW-FROM uri
197 'Content-Security-Policy': "frame-ancestors 'self'"
198 }
199 }
194
200
195 See `Mozilla's guide to X-Frame-Options <https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options>`_ for more examples.
201 Example policies::
202
203 Content-Security-Policy: default-src 'self' https://*.jupyter.org
204
205 Matches embeddings on any subdomain of jupyter.org, so long as they are served
206 over SSL.
207
208 There is a `report-uri <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/CSP_policy_directives#report-uri>`_ endpoint available for logging CSP violations, located at
209 ``/api/security/csp-report``. To use it, set ``report-uri`` as part of the CSP::
210
211 c.NotebookApp.tornado_settings = {
212 'headers': {
213 'Content-Security-Policy': "frame-ancestors 'self'; report-uri /api/security/csp-report"
214 }
215 }
216
217 It simply provides the CSP report as a warning in IPython's logs. The default
218 CSP sets this report-uri relative to the ``base_url`` (not shown above).
219
220 For a more thorough and accurate guide on Content Security Policies, check out
221 `MDN's Using Content Security Policy <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_Content_Security_Policy>`_ for more examples.
General Comments 0
You need to be logged in to leave comments. Login now