Skip to content

Commit

Permalink
refactor(templates): server and notebook templates in the same direct…
Browse files Browse the repository at this point in the history
…ory, and add subdirectory to search path
  • Loading branch information
maartenbreddels committed Jun 7, 2019
1 parent a6d4308 commit deb7b9a
Show file tree
Hide file tree
Showing 21 changed files with 91 additions and 99 deletions.
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 1 addition & 1 deletion js/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down
12 changes: 6 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
]
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/app/template_cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions tests/app/template_custom_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 7 additions & 19 deletions voila/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down Expand Up @@ -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'
)
)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}),
Expand Down
2 changes: 1 addition & 1 deletion voila/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ def _default_preprocessors(self):

@traitlets.default('template_file')
def default_template_file(self):
return 'voila.tpl'
return 'index.html'
4 changes: 2 additions & 2 deletions voila/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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
)
Expand Down
103 changes: 58 additions & 45 deletions voila/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<app_name>/<template_name>[/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
17 changes: 5 additions & 12 deletions voila/server_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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),
Expand Down

0 comments on commit deb7b9a

Please sign in to comment.