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 " |
|
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 |
|
|
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[' |
|
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[' |
|
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