From 01e298f83d33994615f7d9f60351304b463c2b8e Mon Sep 17 00:00:00 2001 From: Max Klein Date: Fri, 22 May 2020 20:20:14 -0400 Subject: [PATCH] added `--autoreload` flag to `NotebookApp` (#4795) * added `--autoreload` flag When passed, the webapp will watch for any changes to its Python source. On change, all changed packages will be reimported and the webapp will restart. Also works on Python source files in Jupyter server extensions. Implemented using the built in `autoreload` parameter in the constructor of `tornado.web.Application`. * updated .gitignore --- .gitignore | 9 ++ notebook/notebookapp.py | 184 ++++++++++++++++++++++------------------ 2 files changed, 110 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index b4c2f49ef4..4e423996ec 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,12 @@ config.rst package-lock.json geckodriver.log *.iml + +# jetbrains IDE stuff +*.iml +.idea/ + +# ms IDE stuff +*.code-workspace +.history +.vscode diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 6f64ef82b0..01547f995f 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -181,6 +181,9 @@ def __init__(self, jupyter_app, kernel_manager, contents_manager, default_url, settings_overrides, jinja_env_options) handlers = self.init_handlers(settings) + if settings['autoreload']: + log.info('Autoreload enabled: the webapp will restart when any Python src file changes.') + super(NotebookWebApplication, self).__init__(handlers, **settings) def init_settings(self, jupyter_app, kernel_manager, contents_manager, @@ -236,7 +239,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, now = utcnow() root_dir = contents_manager.root_dir - home = py3compat.str_to_unicode(os.path.expanduser('~'), encoding=sys.getfilesystemencoding()) + home = py3compat.str_to_unicode(os.path.expanduser('~'), encoding=sys.getfilesystemencoding()) if root_dir.startswith(home + os.path.sep): # collapse $HOME to ~ root_dir = '~' + root_dir[len(home):] @@ -408,7 +411,7 @@ class NotebookPasswordApp(JupyterApp): Setting a password secures the notebook server and removes the need for token-based authentication. """ - + description = __doc__ def _config_file_default(self): @@ -558,14 +561,14 @@ def start(self): class NbserverListApp(JupyterApp): version = __version__ description=_("List currently running notebook servers.") - + flags = dict( jsonlist=({'NbserverListApp': {'jsonlist': True}}, _("Produce machine-readable JSON list output.")), json=({'NbserverListApp': {'json': True}}, _("Produce machine-readable JSON object on each line of output.")), ) - + jsonlist = Bool(False, config=True, help=_("If True, the output will be a JSON list of objects, one per " "active notebook server, each with the details from the " @@ -606,11 +609,11 @@ def start(self): flags['no-mathjax']=( {'NotebookApp' : {'enable_mathjax' : False}}, """Disable MathJax - + MathJax is the javascript library Jupyter uses to render math/LaTeX. It is very large, so you may want to disable it if you have a slow internet connection, or for offline use of the notebook. - + When disabled, equations etc. will appear as their untransformed TeX source. """ ) @@ -620,6 +623,16 @@ def start(self): _("Allow the notebook to be run from root user.") ) +flags['autoreload'] = ( + {'NotebookApp': {'autoreload': True}}, + """Autoreload the webapp + + Enable reloading of the tornado webapp and all imported Python packages + when any changes are made to any Python src files in Notebook or + extensions. + """ +) + # Add notebook manager flags flags.update(boolean_flag('script', 'FileContentsManager.save_script', 'DEPRECATED, IGNORED', @@ -652,12 +665,12 @@ class NotebookApp(JupyterApp): name = 'jupyter-notebook' version = __version__ description = _("""The Jupyter HTML Notebook. - + This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client.""") examples = _examples aliases = aliases flags = flags - + classes = [ KernelManager, Session, MappingKernelManager, KernelSpecManager, ContentsManager, FileContentsManager, NotebookNotary, TerminalManager, @@ -665,7 +678,7 @@ class NotebookApp(JupyterApp): ] flags = Dict(flags) aliases = Dict(aliases) - + subcommands = dict( list=(NbserverListApp, NbserverListApp.description.splitlines()[0]), stop=(NbserverStopApp, NbserverStopApp.description.splitlines()[0]), @@ -682,7 +695,7 @@ def _default_log_level(self): def _default_log_datefmt(self): """Exclude date from default date format""" return "%H:%M:%S" - + @default('log_format') def _default_log_format(self): """override default log format to include time""" @@ -690,66 +703,70 @@ def _default_log_format(self): ignore_minified_js = Bool(False, config=True, - help=_('Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation'), + help=_('Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation'), ) # file to be opened in the notebook server file_to_run = Unicode('', config=True) # Network related information - + allow_origin = Unicode('', config=True, help="""Set the Access-Control-Allow-Origin header - + Use '*' to allow any origin to access your server. - + Takes precedence over allow_origin_pat. """ ) - + allow_origin_pat = Unicode('', config=True, help="""Use a regular expression for the Access-Control-Allow-Origin header - + Requests from an origin matching the expression will get replies with: - + Access-Control-Allow-Origin: origin - + where `origin` is the origin of the request. - + Ignored if allow_origin is set. """ ) - + allow_credentials = Bool(False, config=True, help=_("Set the Access-Control-Allow-Credentials: true header") ) - - allow_root = Bool(False, config=True, + + allow_root = Bool(False, config=True, help=_("Whether to allow the user to run the notebook as root.") ) use_redirect_file = Bool(True, config=True, help="""Disable launching browser by redirect file - For versions of notebook > 5.7.2, a security feature measure was added that - prevented the authentication token used to launch the browser from being visible. - This feature makes it difficult for other users on a multi-user system from - running code in your Jupyter session as you. - - However, some environments (like Windows Subsystem for Linux (WSL) and Chromebooks), - launching a browser using a redirect file can lead the browser failing to load. - This is because of the difference in file structures/paths between the runtime and - the browser. - - Disabling this setting to False will disable this behavior, allowing the browser - to launch by using a URL and visible token (as before). - """ + For versions of notebook > 5.7.2, a security feature measure was added that + prevented the authentication token used to launch the browser from being visible. + This feature makes it difficult for other users on a multi-user system from + running code in your Jupyter session as you. + + However, some environments (like Windows Subsystem for Linux (WSL) and Chromebooks), + launching a browser using a redirect file can lead the browser failing to load. + This is because of the difference in file structures/paths between the runtime and + the browser. + + Disabling this setting to False will disable this behavior, allowing the browser + to launch by using a URL and visible token (as before). + """ + ) + + autoreload = Bool(False, config=True, + help= ("Reload the webapp when changes are made to any Python src files.") ) default_url = Unicode('/tree', config=True, help=_("The default URL to redirect to from `/`") ) - + ip = Unicode('localhost', config=True, help=_("The IP address the notebook server will listen on.") ) @@ -757,7 +774,7 @@ def _default_log_format(self): @default('ip') def _default_ip(self): """Return localhost if available, 127.0.0.1 otherwise. - + On some (horribly broken) systems, localhost cannot be bound. """ s = socket.socket() @@ -843,18 +860,18 @@ def _validate_sock_mode(self, proposal): return value - certfile = Unicode(u'', config=True, + certfile = Unicode(u'', config=True, help=_("""The full path to an SSL/TLS certificate file.""") ) - - keyfile = Unicode(u'', config=True, + + keyfile = Unicode(u'', config=True, help=_("""The full path to a private key file for usage with SSL/TLS.""") ) - + client_ca = Unicode(u'', config=True, help=_("""The full path to a certificate authority certificate for SSL/TLS client authentication.""") ) - + cookie_secret_file = Unicode(config=True, help=_("""The file where the cookie secret is stored.""") ) @@ -929,8 +946,8 @@ def _token_default(self): max_body_size = Integer(512 * 1024 * 1024, config=True, help=""" - Sets the maximum allowed size of the client request body, specified in - the Content-Length request header field. If the size in a request + Sets the maximum allowed size of the client request body, specified in + the Content-Length request header field. If the size in a request exceeds the configured value, a malformed HTTP message is returned to the client. @@ -940,7 +957,7 @@ def _token_default(self): max_buffer_size = Integer(512 * 1024 * 1024, config=True, help=""" - Gets or sets the maximum amount of memory, in bytes, that is allocated + Gets or sets the maximum amount of memory, in bytes, that is allocated for use by the buffer manager. """ ) @@ -995,12 +1012,12 @@ def _token_changed(self, change): """ ) - allow_password_change = Bool(True, config=True, - help="""Allow password to be changed at login for the notebook server. + allow_password_change = Bool(True, config=True, + help="""Allow password to be changed at login for the notebook server. While loggin in with a token, the notebook server UI will give the opportunity to the user to enter a new password at the same time that will replace - the token login mechanism. + the token login mechanism. This can be set to false to prevent changing password from the UI/API. """ @@ -1113,11 +1130,11 @@ def _default_allow_remote(self): help=_("DEPRECATED, use tornado_settings") ) - @observe('webapp_settings') + @observe('webapp_settings') def _update_webapp_settings(self, change): self.log.warning(_("\n webapp_settings is deprecated, use tornado_settings.\n")) self.tornado_settings = change['new'] - + tornado_settings = Dict(config=True, help=_("Supply overrides for the tornado.web.Application that the " "Jupyter notebook uses.")) @@ -1147,15 +1164,15 @@ def _update_webapp_settings(self, change): ssl_options = Dict(config=True, help=_("""Supply SSL options for the tornado HTTPServer. See the tornado docs for details.""")) - - jinja_environment_options = Dict(config=True, + + jinja_environment_options = Dict(config=True, help=_("Supply extra arguments that will be passed to Jinja environment.")) jinja_template_vars = Dict( config=True, help=_("Extra variables to supply to jinja templates when rendering."), ) - + enable_mathjax = Bool(True, config=True, help="""Whether to enable MathJax for typesetting math/TeX @@ -1188,7 +1205,7 @@ def _update_base_url(self, proposal): if not value.endswith('/'): value = value + '/' return value - + base_project_url = Unicode('/', config=True, help=_("""DEPRECATED use base_url""")) @observe('base_project_url') @@ -1198,16 +1215,16 @@ def _update_base_project_url(self, change): extra_static_paths = List(Unicode(), config=True, help="""Extra paths to search for serving static files. - + This allows adding javascript/css to be available from the notebook server machine, or overriding individual files in the IPython""" ) - + @property def static_file_path(self): """return extra paths + the default location""" return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH] - + static_custom_path = List(Unicode(), help=_("""Path to search for custom.js, css""") ) @@ -1238,7 +1255,7 @@ def template_file_path(self): extra_services = List(Unicode(), config=True, help=_("""handlers that should be loaded at higher priority than the default services""") ) - + @property def nbextensions_path(self): """The path to look for Javascript notebook extensions""" @@ -1255,7 +1272,7 @@ def nbextensions_path(self): websocket_url = Unicode("", config=True, help="""The base URL for websockets, if it differs from the HTTP server (hint: it almost certainly doesn't). - + Should be in the form of an HTTP origin: ws[s]://hostname[:port] """ ) @@ -1273,7 +1290,7 @@ def _default_mathjax_url(self): return u'' static_url_prefix = self.tornado_settings.get("static_url_prefix", "static") return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js') - + @observe('mathjax_url') def _update_mathjax_url(self, change): new = change['new'] @@ -1290,7 +1307,7 @@ def _update_mathjax_url(self, change): @observe('mathjax_config') def _update_mathjax_config(self, change): self.log.info(_("Using MathJax configuration file: %s"), change['new']) - + quit_button = Bool(True, config=True, help="""If True, display a button in the dashboard to quit (shutdown the notebook server).""" @@ -1368,7 +1385,7 @@ def _default_info_file(self): def _default_browser_open_file(self): basename = "nbserver-%s-open.html" % os.getpid() return os.path.join(self.runtime_dir, basename) - + pylab = Unicode('disabled', config=True, help=_(""" DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib. @@ -1419,12 +1436,12 @@ def _notebook_dir_validate(self, proposal): server_extensions = List(Unicode(), config=True, help=(_("DEPRECATED use the nbserver_extensions dict instead")) ) - + @observe('server_extensions') def _update_server_extensions(self, change): self.log.warning(_("server_extensions is deprecated, use nbserver_extensions")) self.server_extensions = change['new'] - + nbserver_extensions = Dict({}, config=True, help=(_("Dict of Python modules to load as notebook server extensions." "Entry values can be used to enable and disable the loading of" @@ -1446,7 +1463,7 @@ def _update_server_extensions(self, change): Maximum rate at which stream output can be sent on iopub before they are limited.""")) - rate_limit_window = Float(3, config=True, help=_("""(sec) Time window used to + rate_limit_window = Float(3, config=True, help=_("""(sec) Time window used to check the message and data rate limits.""")) shutdown_no_activity_timeout = Integer(0, config=True, @@ -1479,7 +1496,7 @@ def parse_command_line(self, argv=None): if not os.path.exists(f): self.log.critical(_("No such file or directory: %s"), f) self.exit(1) - + # Use config here, to ensure that it takes higher priority than # anything that comes from the config dirs. c = Config() @@ -1541,7 +1558,7 @@ def init_logging(self): # self.log is a child of. The logging module dispatches log messages to a log # and all of its ancenstors until propagate is set to False. self.log.propagate = False - + for log in app_log, access_log, gen_log: # consistent log output name (NotebookApp instead of tornado.access, etc.) log.name = self.log.name @@ -1550,7 +1567,7 @@ def init_logging(self): logger.propagate = True logger.parent = self.log logger.setLevel(self.log.level) - + def init_resources(self): """initialize system resources""" if resource is None: @@ -1575,6 +1592,7 @@ def init_webapp(self): if self.allow_origin_pat: self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) self.tornado_settings['allow_credentials'] = self.allow_credentials + self.tornado_settings['autoreload'] = self.autoreload self.tornado_settings['cookie_options'] = self.cookie_options self.tornado_settings['get_secure_cookie_kwargs'] = self.get_secure_cookie_kwargs self.tornado_settings['token'] = self.token @@ -1647,7 +1665,7 @@ def init_webapp(self): ) if ssl_options.get('ca_certs', False): ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED) - + self.login_handler_class.validate_security(self, ssl_options=ssl_options) self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options, xheaders=self.trust_xheaders, @@ -1773,7 +1791,7 @@ def init_signal(self): if hasattr(signal, 'SIGINFO'): # only on BSD-based systems signal.signal(signal.SIGINFO, self._signal_info) - + def _handle_sigint(self, sig, frame): """SIGINT handler spawns confirmation dialog""" # register more forceful signal handler for ^C^C case @@ -1783,17 +1801,17 @@ def _handle_sigint(self, sig, frame): thread = threading.Thread(target=self._confirm_exit) thread.daemon = True thread.start() - + def _restore_sigint_handler(self): """callback for restoring original SIGINT handler""" signal.signal(signal.SIGINT, self._handle_sigint) - + def _confirm_exit(self): """confirm shutdown on ^C - + A second ^C, or answering 'y' within 5s will cause shutdown, otherwise original SIGINT handler will be restored. - + This doesn't work on Windows. """ info = self.log.info @@ -1820,14 +1838,14 @@ def _confirm_exit(self): # use IOLoop.add_callback because signal.signal must be called # from main thread self.io_loop.add_callback_from_signal(self._restore_sigint_handler) - + def _signal_stop(self, sig, frame): self.log.critical(_("received signal %s, stopping"), sig) self.io_loop.add_callback_from_signal(self.io_loop.stop) def _signal_info(self, sig, frame): print(self.notebook_info()) - + def init_components(self): """Check the components submodule, and warn if it's unclean""" # TODO: this should still check, but now we use bower, not git submodule @@ -1837,7 +1855,7 @@ def init_server_extension_config(self): """Consolidate server extensions specified by all configs. The resulting list is stored on self.nbserver_extensions and updates config object. - + The extension API is experimental, and may change in future releases. """ # TODO: Remove me in notebook 5.0 @@ -1858,7 +1876,7 @@ def init_server_extension_config(self): manager = ConfigManager(read_config_path=config_path) section = manager.get(self.config_file_name) extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {}) - + for modulename, enabled in sorted(extensions.items()): if modulename not in self.nbserver_extensions: self.config.NotebookApp.nbserver_extensions.update({modulename: enabled}) @@ -1869,10 +1887,10 @@ def init_server_extensions(self): Import the module, then call the load_jupyter_server_extension function, if one exists. - + The extension API is experimental, and may change in future releases. """ - + for modulename, enabled in sorted(self.nbserver_extensions.items()): if enabled: @@ -1985,7 +2003,7 @@ def initialize(self, argv=None): def cleanup_kernels(self): """Shutdown all kernels. - + The kernels will shutdown themselves when this process no longer exists, but explicit shutdown allows the KernelManagers to cleanup the connection files. """ @@ -2131,7 +2149,7 @@ def launch_browser(self): def start(self): """ Start the Notebook server app, after initialization - + This method takes no arguments so all configuration and initialization must be done prior to calling this method.""" @@ -2211,7 +2229,7 @@ def _stop(): def list_running_servers(runtime_dir=None): """Iterate over the server info files of running notebook servers. - + Given a runtime directory, find nbserver-* files in the security directory, and yield dicts of their information, each one pertaining to a currently running notebook server instance.