diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 58362fdd1..065c63d7f 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,9 +3,9 @@ name: Testing nbclassic on: push: branches: - - master + - master pull_request: - branches: '*' + branches: "*" jobs: build: @@ -14,48 +14,43 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos, windows] - python-version: ['3.6', '3.7', '3.8', '3.9', 'pypy3'] + python-version: ["3.6", "3.7", "3.8", "3.9", "pypy-3.7-v7.3.5"] exclude: - - os: windows - python-version: pypy3 + - os: windows + python-version: pypy-3.7-v7.3.5 steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Upgrade pip, etc. - run: | - python -m pip install --user --upgrade pip setuptools wheel - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - name: Cache pip - uses: actions/cache@v1 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python-version }}- - ${{ runner.os }}-pip- - - name: Install pip dependencies - run: | - pip install -v -e ".[test]" pytest-cov - - name: Check pip environment - run: | - pip freeze - pip check - # - name: Install Jupyter Server from source - # run: | - # cd .. - # git clone https://github.com/jupyter/jupyter_server.git - # cd jupyter_server - # pip install -e . - # cd ../nbclassic - - name: Run the help command - run: | - jupyter nbclassic -h - - name: Test with pytest - run: | - pytest -vv --cov nbclassic --cov-report term-missing:skip-covered + - name: Checkout + uses: actions/checkout@v2 + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: "x64" + - name: Upgrade packaging dependencies + run: | + pip install --upgrade pip setuptools wheel --user + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Cache pip + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + - name: Install pip dependencies + run: | + pip install -v -e ".[test]" pytest-cov + - name: Check pip environment + run: | + pip freeze + pip check + - name: Run the help command + run: | + jupyter nbclassic -h + - name: Test with pytest + run: | + pytest -vv --cov nbclassic --cov-report term-missing:skip-covered diff --git a/nbclassic/nbserver.py b/nbclassic/nbserver.py index 64c3d96bb..6527d3a3a 100644 --- a/nbclassic/nbserver.py +++ b/nbclassic/nbserver.py @@ -1,10 +1,22 @@ +""" +This module contains a Jupyter Server extension that attempts to +make classic server and notebook extensions work in the new server. + +Unfortunately, you'll notice that requires some major monkey-patching. +The goal is that this extension will only be used as a temporary +patch to transition extension authors from classic notebook server to jupyter_server. +""" import os import types import inspect from functools import wraps from jupyter_core.paths import jupyter_config_path -from jupyter_server.services.config.manager import ConfigManager from traitlets.traitlets import is_trait + +import notebook + +import jupyter_server +from jupyter_server.services.config.manager import ConfigManager from .traits import NotebookAppTraits @@ -121,6 +133,43 @@ def sorted_extensions(self): ) manager.link_extension(name) + # Monkey-patch Jupyter Server's template and static path list to include + # the classic notebooks template folder. Since there are some + # redundancy in the template names between these two packages, + # this patch makes an opinionated choice to use the templates + # in the classic notebook first. This should be a *safe* choice + # because the Jupyter Server templates are simpler, more + # stripped down versions of the classic notebook templates. If + # the templates in Jupyter server eventually change, we may + # need to revisit this patch. + def template_file_path(self): + """return extra paths + the default locations""" + return self.extra_template_paths + \ + notebook.DEFAULT_TEMPLATE_PATH_LIST + \ + jupyter_server.DEFAULT_TEMPLATE_PATH_LIST + + serverapp.__class__.template_file_path = property(template_file_path) + + def static_file_path_jupyter_server(self): + """return extra paths + the default location""" + return self.extra_static_paths + [jupyter_server.DEFAULT_STATIC_FILES_PATH, notebook.DEFAULT_STATIC_FILES_PATH] + + serverapp.__class__.static_file_path = property( + static_file_path_jupyter_server) + + def static_file_path_nbclassic(self): + """return extra paths + the default location""" + # NBExtensions look for classic notebook static files under the `/static/notebook/...` + # URL. Unfortunately, this conflicts with nbclassic's new static endpoints which are + # prefixed with `/static/notebooks`, and therefore, serves these files under + # `/static/notebook/notebooks/...`. This monkey-patch places a new file-finder path + # to nbclassic's static file handlers that drops the extra "notebook". + return self.extra_static_paths + \ + [os.path.join(notebook.DEFAULT_STATIC_FILES_PATH, + "notebook"), notebook.DEFAULT_STATIC_FILES_PATH] + + nbapp.__class__.static_file_path = property(static_file_path_nbclassic) + def _load_jupyter_server_extension(serverapp): # Patch the config service manager to find the diff --git a/nbclassic/notebook/handlers.py b/nbclassic/notebook/handlers.py index 5b0eff1bb..44a6eb110 100644 --- a/nbclassic/notebook/handlers.py +++ b/nbclassic/notebook/handlers.py @@ -6,23 +6,20 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -from collections import namedtuple -import os -from tornado import web, gen -HTTPError = web.HTTPError - -from jupyter_server.base.handlers import JupyterHandler +from jupyter_server.transutils import _i18n +from jupyter_server.utils import ( + ensure_async +) +from jupyter_server.base.handlers import path_regex, FilesRedirectHandler from jupyter_server.extension.handler import ( ExtensionHandlerMixin, ExtensionHandlerJinjaMixin ) -from jupyter_server.base.handlers import path_regex, FilesRedirectHandler -from jupyter_server.utils import ( - url_path_join, - url_escape, - ensure_async -) -from jupyter_server.transutils import _i18n +from jupyter_server.base.handlers import JupyterHandler +from collections import namedtuple +import os +from tornado import web, gen +HTTPError = web.HTTPError def get_frontend_exporters(): @@ -53,7 +50,7 @@ def get_frontend_exporters(): # Ensure export_from_notebook is explicitly defined & not inherited if ux_name is not None and ux_name != super_uxname: display = _i18n('{} ({})'.format(ux_name, - exporter_instance.file_extension)) + exporter_instance.file_extension)) frontend_exporters.append(ExporterInfo(name, display)) # Ensure default_exporters are in frontend_exporters if not already @@ -77,7 +74,6 @@ def get_frontend_exporters(): class NotebookHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): - @web.authenticated @gen.coroutine def get(self, path): @@ -100,18 +96,18 @@ def get(self, path): yield FilesRedirectHandler.redirect_to_files(self, path) name = path.rsplit('/', 1)[-1] self.write(self.render_template('notebook.html', - notebook_path=path, - notebook_name=name, - kill_kernel=False, - mathjax_url=self.mathjax_url, - mathjax_config=self.mathjax_config, - get_frontend_exporters=get_frontend_exporters - ) - ) - -#----------------------------------------------------------------------------- + notebook_path=path, + notebook_name=name, + kill_kernel=False, + mathjax_url=self.mathjax_url, + mathjax_config=self.mathjax_config, + get_frontend_exporters=get_frontend_exporters + ) + ) + +# ----------------------------------------------------------------------------- # URL to handler mappings -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- default_handlers = [ diff --git a/nbclassic/notebookapp.py b/nbclassic/notebookapp.py index 02c7888a9..e5057b673 100644 --- a/nbclassic/notebookapp.py +++ b/nbclassic/notebookapp.py @@ -5,31 +5,25 @@ # Distributed under the terms of the Modified BSD License. from __future__ import absolute_import, print_function +from . import traits +from . import shim import os import gettext -import random -import sys import warnings import gettext -from jinja2 import Environment, FileSystemLoader from tornado.web import RedirectHandler import notebook from notebook import ( DEFAULT_STATIC_FILES_PATH, - DEFAULT_TEMPLATE_PATH_LIST, __version__, ) -from traitlets.config import Config -from traitlets.config.application import boolean_flag from traitlets import ( - Dict, Unicode, Integer, List, Bool, - observe, default + Unicode, List, Bool, default ) -from ipython_genutils import py3compat from jupyter_core.paths import jupyter_path from jupyter_server.base.handlers import FileFindHandler @@ -41,21 +35,17 @@ ExtensionAppJinjaMixin ) -from jupyter_server.log import log_request from jupyter_server.transutils import _i18n from jupyter_server.serverapp import ( - ServerApp, - random_ports, load_handlers ) -from jupyter_server.utils import url_path_join as ujoin from .terminal.handlers import TerminalHandler -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Module globals -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- _examples = """ jupyter nbclassic # start the notebook @@ -63,18 +53,18 @@ jupyter nbclassic password # enter a password to protect the server """ -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Aliases and Flags -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- flags = {} aliases = {} -flags['no-browser']=( - {'ServerApp' : {'open_browser' : False}}, +flags['no-browser'] = ( + {'ServerApp': {'open_browser': False}}, _i18n("Don't open the notebook in a browser after startup.") ) -flags['no-mathjax']=( - {'NotebookApp' : {'enable_mathjax' : False}}, +flags['no-mathjax'] = ( + {'NotebookApp': {'enable_mathjax': False}}, """Disable MathJax MathJax is the javascript library Jupyter uses to render math/LaTeX. It is @@ -85,8 +75,8 @@ """ ) -flags['allow-root']=( - {'ServerApp' : {'allow_root' : True}}, +flags['allow-root'] = ( + {'ServerApp': {'allow_root': True}}, _i18n("Allow the notebook to be run from root user.") ) @@ -94,21 +84,18 @@ 'ip': 'ServerApp.ip', 'port': 'ServerApp.port', 'port-retries': 'ServerApp.port_retries', - #'transport': 'KernelManager.transport', + # 'transport': 'KernelManager.transport', 'keyfile': 'ServerApp.keyfile', 'certfile': 'ServerApp.certfile', 'client-ca': 'ServerApp.client_ca', 'notebook-dir': 'ServerApp.notebook_dir', 'browser': 'ServerApp.browser', - #'gateway-url': 'GatewayClient.url', + # 'gateway-url': 'GatewayClient.url', }) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # NotebookApp -#----------------------------------------------------------------------------- - -from . import shim -from . import traits +# ----------------------------------------------------------------------------- class NotebookApp( @@ -144,8 +131,9 @@ class NotebookApp( ).tag(config=True) static_custom_path = List(Unicode(), - help=_i18n("""Path to search for custom.js, css""") - ) + help=_i18n( + """Path to search for custom.js, css""") + ) @default('static_custom_path') def _default_static_custom_path(self): @@ -156,8 +144,9 @@ def _default_static_custom_path(self): ] extra_nbextensions_path = List(Unicode(), config=True, - help=_i18n("""extra paths to look for Javascript notebook extensions""") - ) + help=_i18n( + """extra paths to look for Javascript notebook extensions""") + ) @property def nbextensions_path(self): @@ -188,19 +177,23 @@ def _prepare_templates(self): # Get translations from notebook package. base_dir = os.path.dirname(notebook.__file__) - nbui = gettext.translation('nbui', localedir=os.path.join(base_dir, 'notebook/i18n'), fallback=True) + nbui = gettext.translation('nbui', localedir=os.path.join( + base_dir, 'notebook/i18n'), fallback=True) self.jinja2_env.install_gettext_translations(nbui, newstyle=False) def initialize_settings(self): """Add settings to the tornado app.""" if self.ignore_minified_js: - self.log.warning(_i18n("""The `ignore_minified_js` flag is deprecated and no longer works.""")) - self.log.warning(_i18n("""Alternatively use `%s` when working on the notebook's Javascript and LESS""") % 'npm run build:watch') - warnings.warn(_i18n("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"), DeprecationWarning) + self.log.warning( + _i18n("""The `ignore_minified_js` flag is deprecated and no longer works.""")) + self.log.warning(_i18n( + """Alternatively use `%s` when working on the notebook's Javascript and LESS""") % 'npm run build:watch') + warnings.warn(_i18n( + "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"), DeprecationWarning) settings = dict( static_custom_path=self.static_custom_path, - static_handler_args = { + static_handler_args={ # don't cache custom.js 'no_cache_paths': [ url_path_join( @@ -220,9 +213,16 @@ def initialize_settings(self): def initialize_handlers(self): """Load the (URL pattern, handler) tuples for each component.""" + # Tornado adds two types of "Routers" to the web application, 1) the + # "wildcard" router (for all original handlers given to the __init__ method) + # and 2) the "default" router (for all handlers passed to the add_handlers + # method). The default router is called before the wildcard router. + # This is what allows the extension handlers to be matched before + # the main app handlers. + + # Default routes # Order matters. The first handler to match the URL will handle the request. handlers = [] - # Add a redirect from /notebooks to /edit # for opening non-ipynb files in edit mode. handlers.append( @@ -240,29 +240,45 @@ def initialize_handlers(self): handlers.extend(load_handlers('nbclassic.tree.handlers')) handlers.extend(load_handlers('nbclassic.notebook.handlers')) handlers.extend(load_handlers('nbclassic.edit.handlers')) + self.handlers.extend(handlers) + + # Wildcard routes + # These routes *must* be called after all extensions. To mimic + # the classic notebook server as close as possible, these routes + # need to tbe injected into the wildcard routes. + static_handlers = [] # Add terminal handlers - handlers.append( + static_handlers.append( (r"/terminals/(\w+)", TerminalHandler) ) - - handlers.append( + static_handlers.append( + # (r"/nbextensions/(?!nbextensions_configurator\/list)(.*)", FileFindHandler, { (r"/nbextensions/(.*)", FileFindHandler, { 'path': self.settings['nbextensions_path'], - 'no_cache_paths': ['/'], # don't cache anything in nbextensions + # don't cache anything in nbextensions + 'no_cache_paths': ['/'], }), ) - handlers.append( + static_handlers.append( (r"/custom/(.*)", FileFindHandler, { 'path': self.settings['static_custom_path'], - 'no_cache_paths': ['/'], # don't cache anything in nbextensions + # don't cache anything in nbextensions + 'no_cache_paths': ['/'], }), ) - # Add new handlers to Jupyter server handlers. - self.handlers.extend(handlers) - -#----------------------------------------------------------------------------- + # Get the wildcard router + router = self.serverapp.web_app.wildcard_router + # Pop out the last route... this route is the final "catch-all" 404. + last_route = router.rules.pop(-1) + # Add the handlers above + router.add_rules(static_handlers) + # Put the 404 handlers back at the end. + router.rules.append(last_route) + +# ----------------------------------------------------------------------------- # Main entry point -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- + main = launch_new_instance = NotebookApp.launch_instance diff --git a/nbclassic/shim.py b/nbclassic/shim.py index 1807e1b3f..e96edf818 100644 --- a/nbclassic/shim.py +++ b/nbclassic/shim.py @@ -4,9 +4,7 @@ from functools import wraps from copy import deepcopy from traitlets import TraitError, HasTraits -from traitlets.config.application import catch_config_error from traitlets.config.loader import ( - KVArgParseConfigLoader, Config, ) from jupyter_core.application import JupyterApp @@ -15,7 +13,7 @@ from .traits import NotebookAppTraits -NBAPP_AND_SVAPP_SHIM_MSG = lambda trait_name: ( +def NBAPP_AND_SVAPP_SHIM_MSG(trait_name): return ( "'{trait_name}' was found in both NotebookApp " "and ServerApp. This is likely a recent change. " "This config will only be set in NotebookApp. " @@ -25,7 +23,8 @@ ) ) -NBAPP_TO_SVAPP_SHIM_MSG = lambda trait_name: ( + +def NBAPP_TO_SVAPP_SHIM_MSG(trait_name): return ( "'{trait_name}' has moved from NotebookApp to " "ServerApp. This config will be passed to ServerApp. " "Be sure to update your config before " @@ -34,7 +33,8 @@ ) ) -EXTAPP_AND_NBAPP_AND_SVAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def EXTAPP_AND_NBAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' is found in {extapp_name}, NotebookApp, " "and ServerApp. This is a recent change. " "This config will only be set in {extapp_name}. " @@ -45,7 +45,8 @@ ) ) -EXTAPP_AND_SVAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def EXTAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' is found in both {extapp_name} " "and ServerApp. This is a recent change. " "This config will only be set in {extapp_name}. " @@ -56,7 +57,8 @@ ) ) -EXTAPP_AND_NBAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def EXTAPP_AND_NBAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' is found in both {extapp_name} " "and NotebookApp. This is a recent change. " "This config will only be set in {extapp_name}. " @@ -67,7 +69,8 @@ ) ) -NOT_EXTAPP_NBAPP_AND_SVAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def NOT_EXTAPP_NBAPP_AND_SVAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' is not found in {extapp_name}, but " "it was found in both NotebookApp " "and ServerApp. This is likely a recent change. " @@ -79,7 +82,8 @@ ) ) -EXTAPP_TO_SVAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def EXTAPP_TO_SVAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' has moved from {extapp_name} to " "ServerApp. Be sure to update your config before " "our next release.".format( @@ -88,7 +92,8 @@ ) ) -EXTAPP_TO_NBAPP_SHIM_MSG = lambda trait_name, extapp_name: ( + +def EXTAPP_TO_NBAPP_SHIM_MSG(trait_name, extapp_name): return ( "'{trait_name}' has moved from {extapp_name} to " "NotebookApp. Be sure to update your config before " "our next release.".format( @@ -97,6 +102,7 @@ ) ) + # A tuple of traits that shouldn't be shimmed or throw any # warnings of any kind. IGNORED_TRAITS = ("open_browser", "log_level", "log_format", "default_url") @@ -135,7 +141,8 @@ class NBClassicConfigShimMixin: @wraps(JupyterApp.update_config) def update_config(self, config): # Shim traits to handle transition from NotebookApp to ServerApp - shimmed_config = self.shim_config_from_notebook_to_jupyter_server(config) + shimmed_config = self.shim_config_from_notebook_to_jupyter_server( + config) super().update_config(shimmed_config) def shim_config_from_notebook_to_jupyter_server(self, config): diff --git a/nbclassic/traits.py b/nbclassic/traits.py index 224aaa95f..d0ad50e10 100644 --- a/nbclassic/traits.py +++ b/nbclassic/traits.py @@ -1,6 +1,6 @@ import os from traitlets import ( - HasTraits, Dict, Unicode, Integer, List, Bool, + HasTraits, Dict, Unicode, List, Bool, observe, default ) from notebook import ( @@ -16,21 +16,22 @@ class NotebookAppTraits(HasTraits): ignore_minified_js = Bool(False, - config=True, - help=_i18n('Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation'), - ) - + config=True, + help=_i18n( + 'Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation'), + ) jinja_environment_options = Dict(config=True, - help=_i18n("Supply extra arguments that will be passed to Jinja environment.")) + help=_i18n("Supply extra arguments that will be passed to Jinja environment.")) jinja_template_vars = Dict( config=True, - help=_i18n("Extra variables to supply to jinja templates when rendering."), + help=_i18n( + "Extra variables to supply to jinja templates when rendering."), ) enable_mathjax = Bool(True, config=True, - help="""Whether to enable MathJax for typesetting math/TeX + help="""Whether to enable MathJax for typesetting math/TeX 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 @@ -38,7 +39,7 @@ class NotebookAppTraits(HasTraits): When disabled, equations etc. will appear as their untransformed TeX source. """ - ) + ) @observe('enable_mathjax') def _update_enable_mathjax(self, change): @@ -47,11 +48,11 @@ def _update_enable_mathjax(self, change): self.mathjax_url = u'' extra_static_paths = List(Unicode(), config=True, - help="""Extra paths to search for serving static files. + 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): @@ -59,8 +60,9 @@ def static_file_path(self): return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH] static_custom_path = List(Unicode(), - help=_i18n("""Path to search for custom.js, css""") - ) + help=_i18n( + """Path to search for custom.js, css""") + ) @default('static_custom_path') def _default_static_custom_path(self): @@ -71,10 +73,10 @@ def _default_static_custom_path(self): ] extra_template_paths = List(Unicode(), config=True, - help=_i18n("""Extra paths to search for serving jinja templates. + help=_i18n("""Extra paths to search for serving jinja templates. Can be used to override templates from notebook.templates.""") - ) + ) @property def template_file_path(self): @@ -82,8 +84,9 @@ def template_file_path(self): return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST extra_nbextensions_path = List(Unicode(), config=True, - help=_i18n("""extra paths to look for Javascript notebook extensions""") - ) + help=_i18n( + """extra paths to look for Javascript notebook extensions""") + ) @property def nbextensions_path(self): @@ -99,11 +102,11 @@ def nbextensions_path(self): return path mathjax_url = Unicode("", config=True, - help="""A custom url for MathJax.js. + help="""A custom url for MathJax.js. Should be in the form of a case-sensitive url to MathJax, for example: /static/components/MathJax/MathJax.js """ - ) + ) @property def static_url_prefix(self): @@ -127,21 +130,23 @@ def _update_mathjax_url(self, change): self.log.info(_i18n("Using MathJax: %s"), new) mathjax_config = Unicode("TeX-AMS-MML_HTMLorMML-full,Safe", config=True, - help=_i18n("""The MathJax.js configuration file that is to be used.""") - ) + help=_i18n( + """The MathJax.js configuration file that is to be used.""") + ) @observe('mathjax_config') def _update_mathjax_config(self, change): - self.log.info(_i18n("Using MathJax configuration file: %s"), change['new']) + self.log.info( + _i18n("Using MathJax configuration file: %s"), change['new']) quit_button = Bool(True, config=True, - help="""If True, display a button in the dashboard to quit + help="""If True, display a button in the dashboard to quit (shutdown the notebook server).""" - ) + ) nbserver_extensions = Dict({}, config=True, - help=(_i18n("Dict of Python modules to load as notebook server extensions." - "Entry values can be used to enable and disable the loading of" - "the extensions. The extensions will be loaded in alphabetical " - "order.")) - ) + help=(_i18n("Dict of Python modules to load as notebook server extensions." + "Entry values can be used to enable and disable the loading of" + "the extensions. The extensions will be loaded in alphabetical " + "order.")) + ) diff --git a/tests/test_nbserver.py b/tests/test_nbserver.py new file mode 100644 index 000000000..a50261396 --- /dev/null +++ b/tests/test_nbserver.py @@ -0,0 +1,28 @@ +import pytest + + +def test_classic_notebook_templates(jp_serverapp): + classic_notebook_templates = [ + "notebook.html", + "tree.html" + ] + # Get the server's template environment. + template_env = jp_serverapp.web_app.settings.get("jinja2_env") + + for name in classic_notebook_templates: + template_env.get_template(name) + + +async def test_classic_notebook_asset_URLS(jp_fetch): + classic_notebook_paths = [ + # Some classic notebook asset paths + '/static/notebook/js/main.js', + '/static/services/contents.js', + # NBclassic asset paths work too. + '/static/notebook/notebook/js/main.js', + '/static/notebook/services/contents.js', + ] + + for url_path in classic_notebook_paths: + r = await jp_fetch(url_path) + assert r.code == 200