From deb7b9a026ca2dcb586acaa9e7babdadf93b9c44 Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Fri, 7 Jun 2019 16:44:25 +0200 Subject: [PATCH] refactor(templates): server and notebook templates in the same directory, and add subdirectory to search path --- .gitignore | 10 +- js/webpack.config.js | 2 +- setup.py | 12 +- .../voila/default}/404.html | 0 .../voila/default}/base.tpl | 0 .../voila/default}/browser-open.html | 0 .../voila/default}/error.html | 0 .../voila/default/index.html} | 0 .../voila/default}/lab.tpl | 0 .../voila/default}/page.html | 0 .../voila/default/resources}/jquery.min.js | 0 .../voila/default/resources}/main.js | 0 .../voila/default/resources}/require.min.js | 0 .../voila/default}/tree.html | 0 tests/app/template_cli_test.py | 6 +- tests/app/template_custom_test.py | 8 +- voila/app.py | 26 ++--- voila/exporter.py | 2 +- voila/handler.py | 4 +- voila/paths.py | 103 ++++++++++-------- voila/server_extension.py | 17 +-- 21 files changed, 91 insertions(+), 99 deletions(-) rename share/jupyter/{voila/templates/default/templates => templates/voila/default}/404.html (100%) rename share/jupyter/{voila/templates/default/nbconvert_templates => templates/voila/default}/base.tpl (100%) rename share/jupyter/{voila/templates/default/templates => templates/voila/default}/browser-open.html (100%) rename share/jupyter/{voila/templates/default/templates => templates/voila/default}/error.html (100%) rename share/jupyter/{voila/templates/default/nbconvert_templates/voila.tpl => templates/voila/default/index.html} (100%) rename share/jupyter/{voila/templates/default/nbconvert_templates => templates/voila/default}/lab.tpl (100%) rename share/jupyter/{voila/templates/default/templates => templates/voila/default}/page.html (100%) rename share/jupyter/{voila/templates/default/static => templates/voila/default/resources}/jquery.min.js (100%) rename share/jupyter/{voila/templates/default/static => templates/voila/default/resources}/main.js (100%) rename share/jupyter/{voila/templates/default/static => templates/voila/default/resources}/require.min.js (100%) rename share/jupyter/{voila/templates/default/templates => templates/voila/default}/tree.html (100%) diff --git a/.gitignore b/.gitignore index d064b87c1..911e2d24c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,10 +28,10 @@ config.rst package-lock.json -share/jupyter/voila/templates/nbconvert/static/index.css -share/jupyter/voila/templates/nbconvert/static/theme-light.css -share/jupyter/voila/templates/nbconvert/static/theme-dark.css -share/jupyter/voila/templates/default/static/*voila.js -share/jupyter/voila/templates/default/static/*[woff|woff2|eot|svg] +share/jupyter/templates/voila/default/resources/index.css +share/jupyter/templates/voila/default/resources/theme-light.css +share/jupyter/templates/voila/default/resources/theme-dark.css +share/jupyter/templates/voila/default/resources/*voila.js +share/jupyter/templates/voila/default/resources/*[woff|woff2|eot|svg] js/lib/ diff --git a/js/webpack.config.js b/js/webpack.config.js index 1b43be027..c9f4842c4 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -14,7 +14,7 @@ var rules = [ { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?limit=10000&mimetype=image/svg+xml' } ] -var distRoot = path.resolve(__dirname, '..', 'share', 'jupyter', 'voila', 'templates', 'default', 'static') +var distRoot = path.resolve(__dirname, '..', 'share', 'jupyter', 'templates', 'voila', 'default', 'resources') module.exports = [ { diff --git a/setup.py b/setup.py index a5164c909..ee2e3b284 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ class NPM(Command): node_modules = os.path.join(node_root, 'node_modules') - template_root = os.path.join(here, 'share', 'jupyter', 'voila', 'templates', 'default', 'static') + template_root = os.path.join(here, 'share', 'jupyter', 'templates', 'voila', 'default', 'resources') targets = [ os.path.join(template_root, 'voila.js') ] @@ -180,9 +180,9 @@ def _download_pycurl(self, url): return buf.getvalue() def run(self): - css_dest = os.path.join('share', 'jupyter', 'voila', 'templates', 'default', 'static', 'index.css') - theme_light_dest = os.path.join('share', 'jupyter', 'voila', 'templates', 'default', 'static', 'theme-light.css') - theme_dark_dest = os.path.join('share', 'jupyter', 'voila', 'templates', 'default', 'static', 'theme-dark.css') + css_dest = os.path.join('share', 'jupyter', 'templates', 'voila', 'default', 'resources', 'index.css') + theme_light_dest = os.path.join('share', 'jupyter', 'templates', 'voila', 'default', 'resources', 'theme-light.css') + theme_dark_dest = os.path.join('share', 'jupyter', 'templates', 'voila', 'default', 'resources', 'theme-dark.css') try: css = self._download(css_url) @@ -197,7 +197,7 @@ def run(self): return try: - os.mkdir(os.path.join('share', 'jupyter', 'voila', 'templates', 'default', 'static')) + os.mkdir(os.path.join('share', 'jupyter', 'templates', 'voila', 'default', 'resources')) except OSError: # Use FileExistsError from python 3.3 onward. pass with open(css_dest, 'wb+') as f: @@ -248,7 +248,7 @@ def get_data_files(): ('share/jupyter/nbextensions/voila', ['voila/static/extension.js']) ] # Add all the templates - for (dirpath, dirnames, filenames) in os.walk('share/jupyter/voila/templates/'): + for (dirpath, dirnames, filenames) in os.walk('share/jupyter/templates/voila/'): if filenames: data_files.append((dirpath, [os.path.join(dirpath, filename) for filename in filenames])) return data_files diff --git a/share/jupyter/voila/templates/default/templates/404.html b/share/jupyter/templates/voila/default/404.html similarity index 100% rename from share/jupyter/voila/templates/default/templates/404.html rename to share/jupyter/templates/voila/default/404.html diff --git a/share/jupyter/voila/templates/default/nbconvert_templates/base.tpl b/share/jupyter/templates/voila/default/base.tpl similarity index 100% rename from share/jupyter/voila/templates/default/nbconvert_templates/base.tpl rename to share/jupyter/templates/voila/default/base.tpl diff --git a/share/jupyter/voila/templates/default/templates/browser-open.html b/share/jupyter/templates/voila/default/browser-open.html similarity index 100% rename from share/jupyter/voila/templates/default/templates/browser-open.html rename to share/jupyter/templates/voila/default/browser-open.html diff --git a/share/jupyter/voila/templates/default/templates/error.html b/share/jupyter/templates/voila/default/error.html similarity index 100% rename from share/jupyter/voila/templates/default/templates/error.html rename to share/jupyter/templates/voila/default/error.html diff --git a/share/jupyter/voila/templates/default/nbconvert_templates/voila.tpl b/share/jupyter/templates/voila/default/index.html similarity index 100% rename from share/jupyter/voila/templates/default/nbconvert_templates/voila.tpl rename to share/jupyter/templates/voila/default/index.html diff --git a/share/jupyter/voila/templates/default/nbconvert_templates/lab.tpl b/share/jupyter/templates/voila/default/lab.tpl similarity index 100% rename from share/jupyter/voila/templates/default/nbconvert_templates/lab.tpl rename to share/jupyter/templates/voila/default/lab.tpl diff --git a/share/jupyter/voila/templates/default/templates/page.html b/share/jupyter/templates/voila/default/page.html similarity index 100% rename from share/jupyter/voila/templates/default/templates/page.html rename to share/jupyter/templates/voila/default/page.html diff --git a/share/jupyter/voila/templates/default/static/jquery.min.js b/share/jupyter/templates/voila/default/resources/jquery.min.js similarity index 100% rename from share/jupyter/voila/templates/default/static/jquery.min.js rename to share/jupyter/templates/voila/default/resources/jquery.min.js diff --git a/share/jupyter/voila/templates/default/static/main.js b/share/jupyter/templates/voila/default/resources/main.js similarity index 100% rename from share/jupyter/voila/templates/default/static/main.js rename to share/jupyter/templates/voila/default/resources/main.js diff --git a/share/jupyter/voila/templates/default/static/require.min.js b/share/jupyter/templates/voila/default/resources/require.min.js similarity index 100% rename from share/jupyter/voila/templates/default/static/require.min.js rename to share/jupyter/templates/voila/default/resources/require.min.js diff --git a/share/jupyter/voila/templates/default/templates/tree.html b/share/jupyter/templates/voila/default/tree.html similarity index 100% rename from share/jupyter/voila/templates/default/templates/tree.html rename to share/jupyter/templates/voila/default/tree.html diff --git a/tests/app/template_cli_test.py b/tests/app/template_cli_test.py index 6d62cf7dc..6bf1ec728 100644 --- a/tests/app/template_cli_test.py +++ b/tests/app/template_cli_test.py @@ -8,9 +8,9 @@ @pytest.fixture def voila_args_extra(): - path_gridstack = os.path.abspath(os.path.join(sys.prefix, 'share/jupyter/voila/templates/gridstack/nbconvert_templates')) - path_default = os.path.abspath(os.path.join(BASE_DIR, '../../share/jupyter/voila/templates/default/nbconvert_templates')) - return ['--template=None', '--Voila.nbconvert_template_paths=[%r, %r]' % (path_gridstack, path_default)] + path_gridstack = os.path.abspath(os.path.join(sys.prefix, 'share/jupyter/templates/voila/gridstack/')) + path_default = os.path.abspath(os.path.join(BASE_DIR, '../../share/jupyter/templates/voila/default')) + return ['--template=None', '--VoilaTest.template_paths=[%r, %r]' % (path_gridstack, path_default)] @pytest.mark.gen_test diff --git a/tests/app/template_custom_test.py b/tests/app/template_custom_test.py index dcb5f5f5e..b8aa23acc 100644 --- a/tests/app/template_custom_test.py +++ b/tests/app/template_custom_test.py @@ -14,11 +14,9 @@ def voila_args_extra(): @pytest.fixture def voila_config(): def config(app): - path_gridstack = os.path.abspath(os.path.join(sys.prefix, 'share/jupyter/voila/templates/gridstack/nbconvert_templates')) - path_default = os.path.abspath(os.path.join(BASE_DIR, '../../share/jupyter/voila/templates/default/nbconvert_templates')) - app.nbconvert_template_paths = [path_gridstack, path_default] - path = os.path.abspath(os.path.join(BASE_DIR, '../../share/jupyter/voila/templates/default/templates')) - app.template_paths = [path] + path_gridstack = os.path.abspath(os.path.join(sys.prefix, 'share/jupyter/templates/voila/gridstack/')) + path_default = os.path.abspath(os.path.join(BASE_DIR, '../../share/jupyter/templates/voila/default')) + app.template_paths = [path_gridstack, path_default] return config diff --git a/voila/app.py b/voila/app.py index 117b7c15c..cb30c6117 100644 --- a/voila/app.py +++ b/voila/app.py @@ -42,7 +42,7 @@ from jupyter_core.paths import jupyter_config_path, jupyter_path from ipython_genutils.py3compat import getcwd -from .paths import ROOT, STATIC_ROOT, collect_template_paths +from .paths import ROOT, STATIC_ROOT, collect_paths from .handler import VoilaHandler from .treehandler import VoilaTreeHandler from ._version import __version__ @@ -154,20 +154,11 @@ class Voila(Application): ) ) - nbconvert_template_paths = List( - [], - config=True, - help=_( - 'path to nbconvert templates' - ) - ) - template_paths = List( [], - allow_none=True, config=True, help=_( - 'path to nbconvert templates' + 'path to jinja2 templates' ) ) @@ -330,13 +321,10 @@ def initialize(self, argv=None): def setup_template_dirs(self): if self.voila_configuration.template: - collect_template_paths( - self.nbconvert_template_paths, - self.static_paths, - self.template_paths, - self.voila_configuration.template) + template_name = self.voila_configuration.template + self.template_paths = collect_paths(['voila', 'nbconvert'], template_name) + self.static_paths = collect_paths(['voila', 'nbconvert'], template_name, 'resources', include_root_paths=False) self.log.debug('using template: %s', self.voila_configuration.template) - self.log.debug('nbconvert template paths:\n\t%s', '\n\t'.join(self.nbconvert_template_paths)) self.log.debug('template paths:\n\t%s', '\n\t'.join(self.template_paths)) self.log.debug('static paths:\n\t%s', '\n\t'.join(self.static_paths)) if self.notebook_path and not os.path.exists(self.notebook_path): @@ -429,7 +417,7 @@ def start(self): VoilaHandler, { 'notebook_path': os.path.relpath(self.notebook_path, self.root_dir), - 'nbconvert_template_paths': self.nbconvert_template_paths, + 'template_paths': self.template_paths, 'config': self.config, 'voila_configuration': self.voila_configuration } @@ -441,7 +429,7 @@ def start(self): (url_path_join(self.server_url, r'/voila/tree' + path_regex), VoilaTreeHandler), (url_path_join(self.server_url, r'/voila/render' + path_regex), VoilaHandler, { - 'nbconvert_template_paths': self.nbconvert_template_paths, + 'template_paths': self.template_paths, 'config': self.config, 'voila_configuration': self.voila_configuration }), diff --git a/voila/exporter.py b/voila/exporter.py index 816815b89..192f2e060 100644 --- a/voila/exporter.py +++ b/voila/exporter.py @@ -66,4 +66,4 @@ def _default_preprocessors(self): @traitlets.default('template_file') def default_template_file(self): - return 'voila.tpl' + return 'index.html' diff --git a/voila/handler.py b/voila/handler.py index f230c65a5..8ea4153e0 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -21,7 +21,7 @@ class VoilaHandler(JupyterHandler): def initialize(self, **kwargs): self.notebook_path = kwargs.pop('notebook_path', []) # should it be [] - self.nbconvert_template_paths = kwargs.pop('nbconvert_template_paths', []) + self.template_paths = kwargs.pop('template_paths', []) self.exporter_config = kwargs.pop('config', None) self.voila_configuration = kwargs['voila_configuration'] @@ -67,7 +67,7 @@ def get(self, path=None): } exporter = VoilaExporter( - template_path=self.nbconvert_template_paths, + template_path=self.template_paths, config=self.exporter_config, contents_manager=self.contents_manager # for the image inlining ) diff --git a/voila/paths.py b/voila/paths.py index 386bfa2ae..26d02b1f3 100644 --- a/voila/paths.py +++ b/voila/paths.py @@ -10,72 +10,85 @@ import json from jupyter_core.paths import jupyter_path + ROOT = os.path.dirname(__file__) STATIC_ROOT = os.path.join(ROOT, 'static') # if the directory above us contains the following paths, it means we are installed in dev mode (pip install -e .) DEV_MODE = os.path.exists(os.path.join(ROOT, '../setup.py')) and os.path.exists(os.path.join(ROOT, '../share')) -def collect_template_paths( - nbconvert_template_paths, - static_paths, - tornado_template_paths, - template_name='default'): +def collect_paths(app_names, template_name='default', subdir=None, include_root_paths=True, prune=True, root_dirs=None): """ Voila supports custom templates for rendering notebooks. + For a specified template name, `collect_paths` can be used to collects + - template paths + - resources paths (by using the subdir arg) - For a specified template name, `collect_template_paths` collects - - nbconvert template paths, - - static paths, - - tornado template paths, - by looking in the standard Jupyter data directories (PREFIX/share/jupyter/voila/templates) + by looking in the standard Jupyter data directories: + $PREFIX/share/jupyter/templates//[/subdir] with different prefix values (user directory, sys prefix, and then system prefix) which allows users to override templates locally. - The function will recursively load the base templates upon which the specified template may be based. """ - # We look at the usual jupyter locations, and for development purposes also # relative to the package directory (first entry, meaning with highest precedence) - search_directories = [] - if DEV_MODE: - search_directories.append(os.path.abspath(os.path.join(ROOT, '..', 'share', 'jupyter', 'voila', 'templates'))) - search_directories.extend(jupyter_path('voila', 'templates')) + if root_dirs is None: + root_dirs = [] + if DEV_MODE: + root_dirs.append(os.path.abspath(os.path.join(ROOT, '..', 'share', 'jupyter', 'templates'))) + root_dirs.extend(jupyter_path('templates')) found_at_least_one = False - for search_directory in search_directories: - template_directory = os.path.join(search_directory, template_name) - if os.path.exists(template_directory): - found_at_least_one = True - conf = {} - conf_file = os.path.join(template_directory, 'conf.json') - if os.path.exists(conf_file): - with open(conf_file) as f: - conf = json.load(f) + paths = [] + full_paths = [] # only used for error reporting - # For templates that are not named 'default', we assume the default base_template is 'default' - # that means that even the default template could have a base_template when explicitly given. - if template_name != 'default' or 'base_template' in conf: - collect_template_paths( - nbconvert_template_paths, - static_paths, - tornado_template_paths, - conf.get('base_template', 'default')) + for root_dir in root_dirs: + if include_root_paths: + # we include root_dir for when we want to be very explicit, e.g. + # {% extends 'nbconvert/classic/base.html' %} + paths.append(root_dir) + for app_name in app_names: + app_dir = os.path.join(root_dir, app_name) + if include_root_paths: + # we include app_dir for when we want to be explicit, but less than root_dir, e.g. + # {% extends 'classic/base.html' %} + paths.append(app_dir) + full_paths.append(app_dir) - extra_nbconvert_path = os.path.join(template_directory, 'nbconvert_templates') - nbconvert_template_paths.insert(0, extra_nbconvert_path) + template_dir = os.path.join(app_dir, template_name) + if os.path.exists(template_dir): + # if we are intested in a subdirectory instead + if subdir: + paths.append(os.path.join(template_dir, subdir)) + else: + paths.append(template_dir) - extra_static_path = os.path.join(template_directory, 'static') - static_paths.insert(0, extra_static_path) + found_at_least_one = True + conf_file = os.path.join(template_dir, 'conf.json') + if os.path.exists(conf_file): + with open(conf_file) as f: + conf = json.load(f) + else: + conf = {} - extra_template_path = os.path.join(template_directory, 'templates') - tornado_template_paths.insert(0, extra_template_path) + # For templates that are not named 'default', we assume the default base_template is 'default' + # that means that even the default template could have a base_template when explicitly given. + if template_name != 'default' or 'base_template' in conf: + new_template_name = conf.get('base_template', 'default') + # recursively call, but not include the root_path to avoid duplicate paths + base_paths = collect_paths( + app_names, + template_name=new_template_name, + subdir=subdir, + include_root_paths=False, + root_dirs=root_dirs, + ) + paths.extend(base_paths) - # We don't look at multiple directories, once a directory with a given name is found at a - # given level of precedence (for instance user directory), we don't look further (for instance - # in sys.prefix) - break if not found_at_least_one: - paths = "\n\t".join(search_directories) - raise ValueError('No template sub-directory with name %r found in the following paths:\n\t%s' % (template_name, paths)) + paths = "\n\t".join(full_paths) + raise ValueError( + 'No template sub-directory with name %r found in the following paths:\n\t%s' % (template_name, paths) + ) + return paths diff --git a/voila/server_extension.py b/voila/server_extension.py index da85c4d49..87606b8d4 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -15,7 +15,7 @@ from jupyter_server.base.handlers import path_regex from jupyter_server.base.handlers import FileFindHandler -from .paths import ROOT, STATIC_ROOT, collect_template_paths, jupyter_path +from .paths import ROOT, STATIC_ROOT, collect_paths, jupyter_path from .handler import VoilaHandler from .treehandler import VoilaTreeHandler from .static_file_handler import MultiStaticFileHandler @@ -25,18 +25,11 @@ def load_jupyter_server_extension(server_app): web_app = server_app.web_app - nbconvert_template_paths = [] - static_paths = [STATIC_ROOT] - template_paths = [] - # common configuration options between the server extension and the application voila_configuration = VoilaConfiguration(parent=server_app) - collect_template_paths( - nbconvert_template_paths, - static_paths, - template_paths, - voila_configuration.template - ) + template_name = voila_configuration.template + template_paths = collect_paths(['voila', 'nbconvert'], template_name) + static_paths = collect_paths(['voila', 'nbconvert'], template_name, 'resources', include_root_paths=False) jenv_opt = {"autoescape": True} env = Environment(loader=FileSystemLoader(template_paths), extensions=['jinja2.ext.i18n'], **jenv_opt) @@ -51,7 +44,7 @@ def load_jupyter_server_extension(server_app): web_app.add_handlers(host_pattern, [ (url_path_join(base_url, '/voila/render' + path_regex), VoilaHandler, { 'config': server_app.config, - 'nbconvert_template_paths': nbconvert_template_paths, + 'template_paths': template_paths, 'voila_configuration': voila_configuration }), (url_path_join(base_url, '/voila'), VoilaTreeHandler),