From e56862a4bfed3aea913b91dc283773bd57ec513a Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Tue, 14 May 2019 14:01:34 +0200 Subject: [PATCH 1/5] New: open browser on launch --- .../default/templates/browser-open.html | 18 +++ tests/app/conftest.py | 2 +- voila/app.py | 146 ++++++++++++++++-- 3 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 share/jupyter/voila/template/default/templates/browser-open.html diff --git a/share/jupyter/voila/template/default/templates/browser-open.html b/share/jupyter/voila/template/default/templates/browser-open.html new file mode 100644 index 000000000..1dcfffb11 --- /dev/null +++ b/share/jupyter/voila/template/default/templates/browser-open.html @@ -0,0 +1,18 @@ +{# This template is not served, but written as a file to open in the browser, + passing the token without putting it in a command-line argument. #} + + + + + + Opening voila + + + +

+ This page should redirect you to voila. If it doesn't, + click here to go to voila. +

+ + + diff --git a/tests/app/conftest.py b/tests/app/conftest.py index a8d1eb2dc..d048d2c92 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -37,7 +37,7 @@ def voila_args(voila_notebook, voila_args_extra, voila_config_file_paths_arg): @pytest.fixture def voila_app(voila_args, voila_config): voila_app = VoilaTest.instance() - voila_app.initialize(voila_args) + voila_app.initialize(voila_args + ['--no-browser']) voila_config(voila_app) voila_app.start() yield voila_app diff --git a/voila/app.py b/voila/app.py index 5be0c1924..044448ddf 100644 --- a/voila/app.py +++ b/voila/app.py @@ -7,12 +7,22 @@ ############################################################################# from zmq.eventloop import ioloop +import gettext +import logging +import threading +import tempfile import os import shutil import signal -import tempfile -import logging -import gettext +import socket +import webbrowser + +try: + from urllib.parse import urljoin + from urllib.request import pathname2url +except ImportError: + from urllib import pathname2url + from urlparse import urljoin import jinja2 @@ -42,10 +52,19 @@ _kernel_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" +# placeholder for i18 +def _(x): x + + class Voila(Application): name = 'voila' version = __version__ examples = 'voila example.ipynb --port 8888' + + flags = { + 'no-browser': ({'Voila': {'open_browser': False}}, _('Don\'t open the notebook in a browser after startup.')) + } + description = Unicode( """voila [OPTIONS] NOTEBOOK_FILENAME @@ -134,6 +153,80 @@ class Voila(Application): ) ) + ip = Unicode('localhost', config=True, + help=_("The IP address the notebook server will listen on.") + ) + + open_browser = Bool(True, config=True, + help="""Whether to open in a browser after starting. + The specific browser used is platform dependent and + determined by the python standard library `webbrowser` + module, unless it is overridden using the --browser + (NotebookApp.browser) configuration option. + """) + + browser = Unicode(u'', config=True, + help="""Specify what command to use to invoke a web + browser when opening the notebook. If not specified, the + default browser will be determined by the `webbrowser` + standard library module, which allows setting of the + BROWSER environment variable to override it. + """) + + webbrowser_open_new = Integer(2, config=True, + help=_("""Specify Where to open the notebook on startup. This is the + `new` argument passed to the standard library method `webbrowser.open`. + The behaviour is not guaranteed, but depends on browser support. Valid + values are: + - 2 opens a new tab, + - 1 opens a new window, + - 0 opens in an existing window. + See the `webbrowser.open` documentation for details. + """)) + + custom_display_url = Unicode(u'', config=True, + help=_("""Override URL shown to users. + Replace actual URL, including protocol, address, port and base URL, + with the given value when displaying URL to the users. Do not change + the actual connection URL. If authentication token is enabled, the + token is added to the custom URL automatically. + This option is intended to be used when the URL to display to the user + cannot be determined reliably by the Jupyter notebook server (proxified + or containerized setups for example).""") + ) + + @property + def display_url(self): + if self.custom_display_url: + url = self.custom_display_url + if not url.endswith('/'): + url += '/' + else: + if self.ip in ('', '0.0.0.0'): + ip = "%s" % socket.gethostname() + else: + ip = self.ip + url = self._url(ip) + # TODO: do we want to have the token? + # if self.token: + # # Don't log full token if it came from config + # token = self.token if self._token_generated else '...' + # url = (url_concat(url, {'token': token}) + # + '\n or ' + # + url_concat(self._url('127.0.0.1'), {'token': token})) + return url + + @property + def connection_url(self): + ip = self.ip if self.ip else 'localhost' + return self._url(ip) + + def _url(self, ip): + # TODO: https / certfile + # proto = 'https' if self.certfile else 'http' + proto = 'http' + return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url) + config_file_paths = List(Unicode(), config=True, help='Paths to search for voila.(py|json)') tornado_settings = Dict( @@ -251,16 +344,17 @@ def start(self): config_manager=self.config_manager ) - base_url = self.app.settings.get('base_url', '/') + # TODO: should this be configurable + self.base_url = self.app.settings.get('base_url', '/') self.app.settings.update(self.tornado_settings) handlers = [] handlers.extend([ - (url_path_join(base_url, r'/api/kernels/%s' % _kernel_id_regex), KernelHandler), - (url_path_join(base_url, r'/api/kernels/%s/channels' % _kernel_id_regex), ZMQChannelsHandler), + (url_path_join(self.base_url, r'/api/kernels/%s' % _kernel_id_regex), KernelHandler), + (url_path_join(self.base_url, r'/api/kernels/%s/channels' % _kernel_id_regex), ZMQChannelsHandler), ( - url_path_join(base_url, r'/voila/static/(.*)'), + url_path_join(self.base_url, r'/voila/static/(.*)'), MultiStaticFileHandler, { 'paths': self.static_paths, @@ -272,7 +366,7 @@ def start(self): # this handler serves the nbextensions similar to the classical notebook handlers.append( ( - url_path_join(base_url, r'/voila/nbextensions/(.*)'), + url_path_join(self.base_url, r'/voila/nbextensions/(.*)'), FileFindHandler, { 'path': self.nbextensions_path, @@ -283,7 +377,7 @@ def start(self): if self.notebook_path: handlers.append(( - url_path_join(base_url, r'/'), + url_path_join(self.base_url, r'/'), VoilaHandler, { 'notebook_path': os.path.relpath(self.notebook_path, self.root_dir), @@ -296,9 +390,9 @@ def start(self): else: self.log.debug('serving directory: %r', self.root_dir) handlers.extend([ - (base_url, VoilaTreeHandler), - (url_path_join(base_url, r'/voila/tree' + path_regex), VoilaTreeHandler), - (url_path_join(base_url, r'/voila/render' + path_regex), VoilaHandler, + (self.base_url, VoilaTreeHandler), + (url_path_join(self.base_url, r'/voila/tree' + path_regex), VoilaTreeHandler), + (url_path_join(self.base_url, r'/voila/render' + path_regex), VoilaHandler, { 'strip_sources': self.strip_sources, 'nbconvert_template_paths': self.nbconvert_template_paths, @@ -307,6 +401,8 @@ def start(self): ]) self.app.add_handlers('.*$', handlers) + if self.open_browser: + self.launch_browser() self.listen() def listen(self): @@ -322,5 +418,31 @@ def listen(self): shutil.rmtree(self.connection_dir) self.kernel_manager.shutdown_all() + def launch_browser(self): + try: + browser = webbrowser.get(self.browser or None) + except webbrowser.Error as e: + self.log.warning(_('No web browser found: %s.') % e) + browser = None + + if not browser: + return + + uri = self.base_url + fd, open_file = tempfile.mkstemp(suffix='.html') + # Write a temporary file to open in the browser + with open(fd, 'w', encoding='utf-8') as fh: + # TODO: do we want to have the token? + # if self.token: + # url = url_concat(url, {'token': self.token}) + url = url_path_join(self.connection_url, uri) + + jinja2_env = self.app.settings['jinja2_env'] + template = jinja2_env.get_template('browser-open.html') + fh.write(template.render(open_url=url)) + + def target(): + return browser.open(urljoin('file:', pathname2url(open_file)), new=self.webbrowser_open_new) + threading.Thread(target=target).start() main = Voila.launch_instance From c87ca91561e2b36ce8e250f71347b1dada1ee99e Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Tue, 14 May 2019 14:02:18 +0200 Subject: [PATCH 2/5] Show the url voila is running at --- voila/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voila/app.py b/voila/app.py index 044448ddf..9a31a3287 100644 --- a/voila/app.py +++ b/voila/app.py @@ -407,7 +407,7 @@ def start(self): def listen(self): self.app.listen(self.port) - self.log.info('Voila listening on port %s.' % self.port) + self.log.info('Voila is running at:\n%s' % self.display_url) self.ioloop = tornado.ioloop.IOLoop.current() try: From d3c0958abe454c22d82c40dbab3cd86fadba4093 Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Tue, 14 May 2019 14:03:03 +0200 Subject: [PATCH 3/5] New: shortcut to serving a directory when the cmd line argument is a directory --- voila/app.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/voila/app.py b/voila/app.py index 9a31a3287..714cdfc40 100644 --- a/voila/app.py +++ b/voila/app.py @@ -276,7 +276,19 @@ def initialize(self, argv=None): self.log.debug("Searching path %s for config files", self.config_file_paths) # to make config_file_paths settable via cmd line, we first need to parse it super(Voila, self).initialize(argv) - self.notebook_path = self.notebook_path if self.notebook_path else self.extra_args[0] if len(self.extra_args) == 1 else None + if len(self.extra_args) == 1: + arg = self.extra_args[0] + # I am not sure why we need to check if self.notebook_path is set, can we get rid of this? + if not self.notebook_path: + if os.path.isdir(arg): + self.root_dir = arg + elif os.path.isfile(arg): + self.notebook_path = arg + else: + raise ValueError('argument is neither a file nor a directory: %r' % arg) + elif len(self.extra_args) != 0: + raise ValueError('provided more than 1 argument: %r' % self.extra_args) + # then we load the config self.load_config_file('voila', path=self.config_file_paths) # but that cli config has preference, so we overwrite with that From 474b68d3d7d108fc7fc07360c9164f3eefbca543 Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Tue, 14 May 2019 15:24:38 +0200 Subject: [PATCH 4/5] flake8 --- voila/app.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/voila/app.py b/voila/app.py index 714cdfc40..7866a0697 100644 --- a/voila/app.py +++ b/voila/app.py @@ -154,8 +154,7 @@ class Voila(Application): ) ip = Unicode('localhost', config=True, - help=_("The IP address the notebook server will listen on.") - ) + help=_("The IP address the notebook server will listen on.")) open_browser = Bool(True, config=True, help="""Whether to open in a browser after starting. @@ -174,26 +173,25 @@ class Voila(Application): """) webbrowser_open_new = Integer(2, config=True, - help=_("""Specify Where to open the notebook on startup. This is the - `new` argument passed to the standard library method `webbrowser.open`. - The behaviour is not guaranteed, but depends on browser support. Valid - values are: - - 2 opens a new tab, - - 1 opens a new window, - - 0 opens in an existing window. - See the `webbrowser.open` documentation for details. - """)) + help=_("""Specify Where to open the notebook on startup. This is the + `new` argument passed to the standard library method `webbrowser.open`. + The behaviour is not guaranteed, but depends on browser support. Valid + values are: + - 2 opens a new tab, + - 1 opens a new window, + - 0 opens in an existing window. + See the `webbrowser.open` documentation for details. + """)) custom_display_url = Unicode(u'', config=True, - help=_("""Override URL shown to users. - Replace actual URL, including protocol, address, port and base URL, - with the given value when displaying URL to the users. Do not change - the actual connection URL. If authentication token is enabled, the - token is added to the custom URL automatically. - This option is intended to be used when the URL to display to the user - cannot be determined reliably by the Jupyter notebook server (proxified - or containerized setups for example).""") - ) + help=_("""Override URL shown to users. + Replace actual URL, including protocol, address, port and base URL, + with the given value when displaying URL to the users. Do not change + the actual connection URL. If authentication token is enabled, the + token is added to the custom URL automatically. + This option is intended to be used when the URL to display to the user + cannot be determined reliably by the Jupyter notebook server (proxified + or containerized setups for example).""")) @property def display_url(self): @@ -457,4 +455,5 @@ def target(): return browser.open(urljoin('file:', pathname2url(open_file)), new=self.webbrowser_open_new) threading.Thread(target=target).start() + main = Voila.launch_instance From fef38a5a53a460c551281d83bc4d1b2efc9baef0 Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Tue, 14 May 2019 16:23:20 +0200 Subject: [PATCH 5/5] fix: help was None --- voila/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voila/app.py b/voila/app.py index 7866a0697..89ec5a369 100644 --- a/voila/app.py +++ b/voila/app.py @@ -53,7 +53,7 @@ # placeholder for i18 -def _(x): x +def _(x): return x class Voila(Application):