From c24ecb3971cf0524147349fdfa62caa28e0d1416 Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Tue, 13 Nov 2018 19:55:32 +0100 Subject: [PATCH 01/15] Enable kernel message filtering --- .../kernels/tests/test_kernels_api.py | 1 + jupyter_server/tree/tests/handlers.py | 77 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 jupyter_server/tree/tests/handlers.py diff --git a/jupyter_server/services/kernels/tests/test_kernels_api.py b/jupyter_server/services/kernels/tests/test_kernels_api.py index 2199a41ebe..7a593bc8b2 100644 --- a/jupyter_server/services/kernels/tests/test_kernels_api.py +++ b/jupyter_server/services/kernels/tests/test_kernels_api.py @@ -15,6 +15,7 @@ from jupyter_server.tests.launchserver import ServerTestBase, assert_http_error + class KernelAPI(object): """Wrapper for kernel REST API requests""" def __init__(self, request, base_url, headers): diff --git a/jupyter_server/tree/tests/handlers.py b/jupyter_server/tree/tests/handlers.py new file mode 100644 index 0000000000..ef42527616 --- /dev/null +++ b/jupyter_server/tree/tests/handlers.py @@ -0,0 +1,77 @@ +"""Tornado handlers for the tree view.""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import web +import os +from ..base.handlers import IPythonHandler, path_regex +from ..utils import url_path_join, url_escape + + +class TreeHandler(IPythonHandler): + """Render the tree view, listing notebooks, etc.""" + + def generate_breadcrumbs(self, path): + breadcrumbs = [(url_path_join(self.base_url, 'tree'), '')] + parts = path.split('/') + for i in range(len(parts)): + if parts[i]: + link = url_path_join(self.base_url, 'tree', + url_escape(url_path_join(*parts[:i+1])), + ) + breadcrumbs.append((link, parts[i])) + return breadcrumbs + + def generate_page_title(self, path): + comps = path.split('/') + if len(comps) > 3: + for i in range(len(comps)-2): + comps.pop(0) + page_title = url_path_join(*comps) + if page_title: + return page_title+'/' + else: + return 'Home' + + @web.authenticated + def get(self, path=''): + path = path.strip('/') + cm = self.contents_manager + + if cm.dir_exists(path=path): + if cm.is_hidden(path) and not cm.allow_hidden: + self.log.info("Refusing to serve hidden directory, via 404 Error") + raise web.HTTPError(404) + breadcrumbs = self.generate_breadcrumbs(path) + page_title = self.generate_page_title(path) + self.write(self.render_template('tree.html', + page_title=page_title, + notebook_path=path, + breadcrumbs=breadcrumbs, + terminals_available=self.settings['terminals_available'], + server_root=self.settings['server_root_dir'], + )) + elif cm.file_exists(path): + # it's not a directory, we have redirecting to do + model = cm.get(path, content=False) + # redirect to /api/notebooks if it's a notebook, otherwise /api/files + service = 'notebooks' if model['type'] == 'notebook' else 'files' + url = url_path_join( + self.base_url, service, url_escape(path), + ) + self.log.debug("Redirecting %s to %s", self.request.path, url) + self.redirect(url) + else: + raise web.HTTPError(404) + + +#----------------------------------------------------------------------------- +# URL to handler mappings +#----------------------------------------------------------------------------- + + +default_handlers = [ + (r"/tree%s" % path_regex, TreeHandler), + (r"/tree", TreeHandler), + ] From 5a7e159009f42a969fd8e930938f95aa0be58239 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Fri, 16 Nov 2018 08:43:23 -0800 Subject: [PATCH 02/15] Update session_exists() to account for invalid sessions due to culling When kernels are culled, the kernel is terminated in the background, unbeknownst to the session management. As a result, invalid sessions can be produced that appear to exist, yet cannot produce a model from the persisted row due to the associated kernel no longer being active. Prior to this change, these sessions, when encountered via a subsequent call to `get_session()`, would be deleted and a KeyError would be raised. This change updates the existence check to tolerate those kinds of sessions. It removes such sessions (as would happen previously), but rather than raise a KeyError when attempting to convert the row to a dictionary, it logs a warning and returns None, which then allows `session_exists()` to return False since the session was removed (as was ultimately the case previously). Calls to `get_session()` remain just as before and have the potential to raise `KeyError` in such cases. The difference now being that the `KeyError` is accompanied by a message indicating the cause. Fixes #4209 --- .../services/sessions/sessionmanager.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/jupyter_server/services/sessions/sessionmanager.py b/jupyter_server/services/sessions/sessionmanager.py index 8630af9c30..4cddeb866d 100644 --- a/jupyter_server/services/sessions/sessionmanager.py +++ b/jupyter_server/services/sessions/sessionmanager.py @@ -59,10 +59,17 @@ def __del__(self): def session_exists(self, path): """Check to see if the session of a given name exists""" self.cursor.execute("SELECT * FROM session WHERE path=?", (path,)) - reply = self.cursor.fetchone() - if reply is None: + row = self.cursor.fetchone() + if row is None: return False else: + # Note, although we found a row for the session, the associated kernel may have + # been culled or died unexpectedly. If that's the case, we should delete the + # row, thereby terminating the session. This can be done via a call to + # row_to_model that tolerates that condition. If row_to_model returns None, + # we'll return false, since, at that point, the session doesn't exist anyway. + if self.row_to_model(row, tolerate_culled=True) is None: + return False return True def new_session_id(self): @@ -198,15 +205,25 @@ def update_session(self, session_id, **kwargs): query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets)) self.cursor.execute(query, list(kwargs.values()) + [session_id]) - def row_to_model(self, row): + def row_to_model(self, row, tolerate_culled=False): """Takes sqlite database session row and turns it into a dictionary""" if row['kernel_id'] not in self.kernel_manager: - # The kernel was killed or died without deleting the session. + # The kernel was culled or died without deleting the session. # We can't use delete_session here because that tries to find - # and shut down the kernel. - self.cursor.execute("DELETE FROM session WHERE session_id=?", + # and shut down the kernel - so we'll delete the row directly. + # + # If caller wishes to tolerate culled kernels, log a warning + # and return None. Otherwise, raise KeyError with a similar + # message. + self.cursor.execute("DELETE FROM session WHERE session_id=?", (row['session_id'],)) - raise KeyError + msg = "Kernel '{kernel_id}' appears to have been culled or died unexpectedly, " \ + "invalidating session '{session_id}'. The session has been removed.".\ + format(kernel_id=row['kernel_id'],session_id=row['session_id']) + if tolerate_culled: + self.log.warning(msg + " Continuing...") + return None + raise KeyError(msg) model = { 'id': row['session_id'], From a243228c4fc1d4baca5b5bb7003d9f03ae82be32 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 11 Dec 2018 13:20:46 +0000 Subject: [PATCH 03/15] Launch the browser with a redirect file This avoids putting the authentication token into a command-line argument to launch the browser, where it's visible to other users. Filesystem permissions should ensure that only the user who started the notebook can use this route to authenticate. Thanks to Dr Owain Kenway for suggesting this technique. --- jupyter_server/serverapp.py | 95 +++++++++++++++++----- jupyter_server/templates/browser-open.html | 18 ++++ jupyter_server/utils.py | 6 +- 3 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 jupyter_server/templates/browser-open.html diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index c94e7793cf..edd7273c38 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -26,6 +26,7 @@ import signal import socket import sys +import tempfile import threading import time import warnings @@ -97,7 +98,7 @@ from jupyter_server._sysinfo import get_sys_info from ._tz import utcnow, utcfromtimestamp -from .utils import url_path_join, check_pid, url_escape +from .utils import url_path_join, check_pid, url_escape, urljoin, pathname2url #----------------------------------------------------------------------------- # Module globals @@ -1088,6 +1089,13 @@ def _default_info_file(self): info_file = "jpserver-%s.json" % os.getpid() return os.path.join(self.runtime_dir, info_file) + browser_open_file = Unicode() + + @default('browser_open_file') + def _default_browser_open_file(self): + basename = "jpserver-%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. @@ -1594,6 +1602,67 @@ def remove_server_info_file(self): if e.errno != errno.ENOENT: raise + def write_browser_open_file(self): + """Write an nbserver--open.html file + + This can be used to open the notebook in a browser + """ + # default_url contains base_url, but so does connection_url + open_url = self.default_url[len(self.base_url):] + + with open(self.browser_open_file, 'w', encoding='utf-8') as f: + self._write_browser_open_file(open_url, f) + + def _write_browser_open_file(self, url, fh): + if self.token: + url = url_concat(url, {'token': self.one_time_token}) + url = url_path_join(self.connection_url, url) + + jinja2_env = self.web_app.settings['jinja2_env'] + template = jinja2_env.get_template('browser-open.html') + fh.write(template.render(open_url=url)) + + def remove_browser_open_file(self): + """Remove the nbserver--open.html file created for this server. + + Ignores the error raised when the file has already been removed. + """ + try: + os.unlink(self.browser_open_file) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + 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 + + if self.file_to_run: + if not os.path.exists(self.file_to_run): + self.log.critical(_("%s does not exist") % self.file_to_run) + self.exit(1) + + relpath = os.path.relpath(self.file_to_run, self.notebook_dir) + uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep))) + + # Write a temporary file to open in the browser + fd, open_file = tempfile.mkstemp(suffix='.html') + with open(fd, 'w', encoding='utf-8') as fh: + self._write_browser_open_file(uri, fh) + else: + open_file = self.browser_open_file + + b = lambda: browser.open( + urljoin('file:', pathname2url(open_file)), + new=self.webbrowser_open_new) + threading.Thread(target=b).start() + def start(self): """ Start the Jupyter server app, after initialization @@ -1623,29 +1692,10 @@ def start(self): "resources section at https://jupyter.org/community.html.")) self.write_server_info_file() + self.write_browser_open_file() if self.open_browser or self.file_to_run: - 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 self.file_to_run: - if not os.path.exists(self.file_to_run): - self.log.critical(_("%s does not exist") % self.file_to_run) - self.exit(1) - - relpath = os.path.relpath(self.file_to_run, self.root_dir) - uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep))) - else: - uri = self.base_url - if self.one_time_token: - uri = url_concat(uri, {'token': self.one_time_token}) - if browser: - b = lambda : browser.open(url_path_join(self.connection_url, uri), - new=self.webbrowser_open_new) - threading.Thread(target=b).start() + self.launch_browser() if self.token and self._token_generated: # log full URL with generated token, so there's a copy/pasteable link @@ -1669,6 +1719,7 @@ def start(self): info(_("Interrupted...")) finally: self.remove_server_info_file() + self.remove_browser_open_file() self.cleanup_kernels() def stop(self): diff --git a/jupyter_server/templates/browser-open.html b/jupyter_server/templates/browser-open.html new file mode 100644 index 0000000000..6f277967fc --- /dev/null +++ b/jupyter_server/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 Jupyter Notebook + + + +

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

+ + + diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 5d48f72331..23ede3e7ff 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -26,8 +26,12 @@ def isawaitable(f): class ConcurrentFuture: """If concurrent.futures isn't importable, nothing will be a c.f.Future""" pass + from urllib.parse import quote, unquote, urlparse, urljoin + from urllib.request import pathname2url +except ImportError: + from urllib import quote, unquote, pathname2url + from urlparse import urlparse, urljoin -from urllib.parse import quote, unquote, urlparse # tornado.concurrent.Future is asyncio.Future # in tornado >=5 with Python 3 From 232fd3db13e7caf61dcecdfb24cd8c57edd46624 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 11 Dec 2018 15:57:02 +0000 Subject: [PATCH 04/15] Use permanent token in redirect file --- jupyter_server/serverapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index edd7273c38..12b3511ff6 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1615,7 +1615,7 @@ def write_browser_open_file(self): def _write_browser_open_file(self, url, fh): if self.token: - url = url_concat(url, {'token': self.one_time_token}) + url = url_concat(url, {'token': self.token}) url = url_path_join(self.connection_url, url) jinja2_env = self.web_app.settings['jinja2_env'] From d3814b1eb32fd0f4d18b797df70847de1428e070 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 11 Dec 2018 16:05:16 +0000 Subject: [PATCH 05/15] Point to file in terminal message --- jupyter_server/serverapp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 12b3511ff6..51fbc1a511 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1702,8 +1702,9 @@ def start(self): # with auth info. self.log.critical('\n'.join([ '\n', - 'Copy/paste this URL into your browser when you connect for the first time,', - 'to login with a token:', + 'To access the notebook, open this file in a browser:', + ' %s' % urljoin('file:', pathname2url(self.browser_open_file)), + 'Or copy and paste one of these URLs:', ' %s' % self.display_url, ])) From 697533afc2605cf147b606880fbb1a987456b76d Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 12 Dec 2018 15:22:06 +0000 Subject: [PATCH 06/15] Remove one-time token code --- jupyter_server/auth/login.py | 6 ------ jupyter_server/base/handlers.py | 7 +------ jupyter_server/serverapp.py | 9 --------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/jupyter_server/auth/login.py b/jupyter_server/auth/login.py index e276cba31a..4d93cc192d 100644 --- a/jupyter_server/auth/login.py +++ b/jupyter_server/auth/login.py @@ -199,17 +199,11 @@ def get_user_token(cls, handler): return # check login token from URL argument or Authorization header user_token = cls.get_token(handler) - one_time_token = handler.one_time_token authenticated = False if user_token == token: # token-authenticated, set the login cookie handler.log.debug("Accepting token-authenticated connection from %s", handler.request.remote_ip) authenticated = True - elif one_time_token and user_token == one_time_token: - # one-time-token-authenticated, only allow this token once - handler.settings.pop('one_time_token', None) - handler.log.info("Accepting one-time-token-authenticated connection from %s", handler.request.remote_ip) - authenticated = True if authenticated: return uuid.uuid4().hex diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 37341f5a01..35d2ec7136 100755 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -171,11 +171,6 @@ def token(self): """Return the login token for this application, if any.""" return self.settings.get('token', None) - @property - def one_time_token(self): - """Return the one-time-use token for this application, if any.""" - return self.settings.get('one_time_token', None) - @property def login_available(self): """May a user proceed to log in? @@ -458,7 +453,7 @@ def template_namespace(self): logged_in=self.logged_in, allow_password_change=self.settings.get('allow_password_change'), login_available=self.login_available, - token_available=bool(self.token or self.one_time_token), + token_available=bool(self.token), static_url=self.static_url, sys_info=json_sys_info(), contents_js_source=self.contents_js_source, diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 51fbc1a511..c6451b5de9 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -739,12 +739,6 @@ def _write_cookie_secret_file(self, secret): """) ).tag(config=True) - one_time_token = Unicode( - help=_("""One-time token used for opening a browser. - Once used, this token cannot be used again. - """) - ) - _token_generated = True @default('token') @@ -1269,9 +1263,6 @@ def init_webapp(self): 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 - if (self.open_browser or self.file_to_run) and not self.password: - self.one_time_token = binascii.hexlify(os.urandom(24)).decode('ascii') - self.tornado_settings['one_time_token'] = self.one_time_token # ensure default_url starts with base_url if not self.default_url.startswith(self.base_url): From e7da136a3d8b8d5652997e8cd60b949a120a4d0e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Sun, 16 Dec 2018 21:06:36 +0100 Subject: [PATCH 07/15] Add failing test for list_running_servers --- jupyter_server/tests/test_serverapp.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jupyter_server/tests/test_serverapp.py b/jupyter_server/tests/test_serverapp.py index 08519a186a..c53bf5e51a 100644 --- a/jupyter_server/tests/test_serverapp.py +++ b/jupyter_server/tests/test_serverapp.py @@ -23,6 +23,8 @@ ServerApp = serverapp.ServerApp +from .launchnotebook import NotebookTestBase + def test_help_output(): """jupyter server --help-all works""" @@ -181,3 +183,10 @@ def list_running_servers(runtime_dir): app.start() nt.assert_equal(exc.exception.code, 1) nt.assert_equal(len(app.servers_shut_down), 0) + + +class NotebookAppTests(NotebookTestBase): + def test_list_running_servers(self): + servers = list(notebookapp.list_running_servers()) + assert len(servers) >= 1 + assert self.port in {info['port'] for info in servers} From 41fd432e0b123b97cee8eb2166eef095bad015b9 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 2 Jan 2019 11:57:41 +0000 Subject: [PATCH 08/15] Convert multiselect test to Selenium --- .gitignore | 4 +- .../tests/selenium/test_multiselect.py | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 jupyter_server/tests/selenium/test_multiselect.py diff --git a/.gitignore b/.gitignore index a87065c1d9..28a83b48fd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ __pycache__ \#*# .#* .coverage +.pytest_cache src *.swp @@ -38,4 +39,5 @@ config.rst /.project /.pydevproject -package-lock.json \ No newline at end of file +package-lock.json +geckodriver.log diff --git a/jupyter_server/tests/selenium/test_multiselect.py b/jupyter_server/tests/selenium/test_multiselect.py new file mode 100644 index 0000000000..5ce6d49045 --- /dev/null +++ b/jupyter_server/tests/selenium/test_multiselect.py @@ -0,0 +1,65 @@ +def test_multiselect(notebook): + def extend_selection_by(delta): + notebook.browser.execute_script( + "Jupyter.notebook.extend_selection_by(arguments[0]);", delta) + + def n_selected_cells(): + return notebook.browser.execute_script( + "return Jupyter.notebook.get_selected_cells().length;") + + a = 'print("a")' + b = 'print("b")' + c = 'print("c")' + notebook.edit_cell(index=0, content=a) + notebook.append(b, c) + + notebook.focus_cell(0) + assert n_selected_cells() == 1 + + # Check that only one cell is selected according to CSS classes as well + selected_css = notebook.browser.find_elements_by_css_selector( + '.cell.jupyter-soft-selected, .cell.selected') + assert len(selected_css) == 1 + + # Extend the selection down one + extend_selection_by(1) + assert n_selected_cells() == 2 + + # Contract the selection up one + extend_selection_by(-1) + assert n_selected_cells() == 1 + + # Extend the selection up one + notebook.focus_cell(1) + extend_selection_by(-1) + assert n_selected_cells() == 2 + + # Convert selected cells to Markdown + notebook.browser.execute_script("Jupyter.notebook.cells_to_markdown();") + cell_types = notebook.browser.execute_script( + "return Jupyter.notebook.get_cells().map(c => c.cell_type)") + assert cell_types == ['markdown', 'markdown', 'code'] + # One cell left selected after conversion + assert n_selected_cells() == 1 + + # Convert selected cells to raw + notebook.focus_cell(1) + extend_selection_by(1) + assert n_selected_cells() == 2 + notebook.browser.execute_script("Jupyter.notebook.cells_to_raw();") + cell_types = notebook.browser.execute_script( + "return Jupyter.notebook.get_cells().map(c => c.cell_type)") + assert cell_types == ['markdown', 'raw', 'raw'] + # One cell left selected after conversion + assert n_selected_cells() == 1 + + # Convert selected cells to code + notebook.focus_cell(0) + extend_selection_by(2) + assert n_selected_cells() == 3 + notebook.browser.execute_script("Jupyter.notebook.cells_to_code();") + cell_types = notebook.browser.execute_script( + "return Jupyter.notebook.get_cells().map(c => c.cell_type)") + assert cell_types == ['code'] * 3 + # One cell left selected after conversion + assert n_selected_cells() == 1 From c1c393c606ce4c4e8dd96d175e711eabd93a8000 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 2 Jan 2019 12:33:50 +0000 Subject: [PATCH 09/15] Limit to tornado <6 for now Tornado 6.0a1 is causing test failures in CI --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 64e4a5ee09..d460f520c6 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ zip_safe = False, install_requires = [ 'jinja2', - 'tornado>=4', + 'tornado>=4, <6', # pyzmq>=17 is not technically necessary, # but hopefully avoids incompatibilities with Tornado 5. April 2018 'pyzmq>=17', From 91b441de8bb47e6f70dde2bf77e4c666a845c67c Mon Sep 17 00:00:00 2001 From: Will Costello <43070867+wgcostello@users.noreply.github.com> Date: Sat, 5 Jan 2019 20:16:05 +0000 Subject: [PATCH 10/15] Fix typo in introduction --- docs/source/examples/Notebook/Running Code.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/examples/Notebook/Running Code.ipynb b/docs/source/examples/Notebook/Running Code.ipynb index 02a6e87761..f0ffca9390 100644 --- a/docs/source/examples/Notebook/Running Code.ipynb +++ b/docs/source/examples/Notebook/Running Code.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefor runs Python code." + "First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefore runs Python code." ] }, { From 0883a388d0009785fdda3a12d2a20bf50c21f939 Mon Sep 17 00:00:00 2001 From: Maxime Mouchet Date: Tue, 8 Jan 2019 13:42:26 +0100 Subject: [PATCH 11/15] List hidden files if allowed (#3812) --- jupyter_server/services/contents/filemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 50b18c5b20..ebac73ce17 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -288,7 +288,7 @@ def _dir_model(self, path, content=True): self.log.debug("%s not a regular file", os_path) continue - if self.should_list(name) and not is_file_hidden(os_path, stat_res=st): + if self.should_list(name) and ((not is_file_hidden(os_path, stat_res=st)) or self.allow_hidden): contents.append(self.get( path='%s/%s' % (path, name), content=False) From 2f3d6f4fc712fa791819235e0a1cf7c8b7261ec2 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 9 Jan 2019 09:45:35 +0000 Subject: [PATCH 12/15] Upgrade pytest for docs build as well --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 98c653e8be..fd9c18cb6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ before_install: - | if [[ $GROUP == docs ]]; then pip install -r docs/doc-requirements.txt + pip install --upgrade pytest fi install: From a943e37585ef4f97321f353dbaf431a8bb9545d4 Mon Sep 17 00:00:00 2001 From: Maxime Mouchet Date: Wed, 9 Jan 2019 14:18:34 +0100 Subject: [PATCH 13/15] Split logic --- jupyter_server/services/contents/filemanager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index ebac73ce17..0a62682f92 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -288,11 +288,11 @@ def _dir_model(self, path, content=True): self.log.debug("%s not a regular file", os_path) continue - if self.should_list(name) and ((not is_file_hidden(os_path, stat_res=st)) or self.allow_hidden): - contents.append(self.get( - path='%s/%s' % (path, name), - content=False) - ) + if self.should_list(name): + if self.allow_hidden or not is_file_hidden(os_path, stat_res=st): + contents.append( + self.get(path='%s/%s' % (path, name), content=False) + ) model['format'] = 'json' From 76605cee2d54b74103f6e315f94ed603866c562b Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 25 Sep 2019 16:40:50 -0700 Subject: [PATCH 14/15] patch fixes for notebook PR porting batch 4 --- jupyter_server/tests/test_serverapp.py | 6 +++--- jupyter_server/utils.py | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/jupyter_server/tests/test_serverapp.py b/jupyter_server/tests/test_serverapp.py index c53bf5e51a..9b523bc3c8 100644 --- a/jupyter_server/tests/test_serverapp.py +++ b/jupyter_server/tests/test_serverapp.py @@ -23,7 +23,7 @@ ServerApp = serverapp.ServerApp -from .launchnotebook import NotebookTestBase +from .launchserver import ServerTestBase def test_help_output(): @@ -185,8 +185,8 @@ def list_running_servers(runtime_dir): nt.assert_equal(len(app.servers_shut_down), 0) -class NotebookAppTests(NotebookTestBase): +class ServerAppTests(ServerTestBase): def test_list_running_servers(self): - servers = list(notebookapp.list_running_servers()) + servers = list(serverapp.list_running_servers()) assert len(servers) >= 1 assert self.port in {info['port'] for info in servers} diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 23ede3e7ff..af63116de1 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -26,11 +26,9 @@ def isawaitable(f): class ConcurrentFuture: """If concurrent.futures isn't importable, nothing will be a c.f.Future""" pass - from urllib.parse import quote, unquote, urlparse, urljoin - from urllib.request import pathname2url -except ImportError: - from urllib import quote, unquote, pathname2url - from urlparse import urlparse, urljoin + +from urllib.parse import quote, unquote, urlparse, urljoin +from urllib.request import pathname2url # tornado.concurrent.Future is asyncio.Future From bb6f2448e9f20842f7ffcedfe0c92ec5b5d55a24 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 25 Sep 2019 17:09:57 -0700 Subject: [PATCH 15/15] add regex matching back to server_info --- jupyter_server/serverapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index c6451b5de9..d7472d2693 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1736,7 +1736,7 @@ def list_running_servers(runtime_dir=None): return for file_name in os.listdir(runtime_dir): - if file_name.startswith('jpserver-'): + if re.match('jpserver-(.+).json', file_name): with io.open(os.path.join(runtime_dir, file_name), encoding='utf-8') as f: info = json.load(f)