From ae073a1645d42cd4755abee104e9e4b892e25ab4 2014-01-09 21:03:31 From: Thomas Kluyver Date: 2014-01-09 21:03:31 Subject: [PATCH] Merge pull request #4735 from minrk/better-errors add some HTML error pages --- diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index e2f938f..888584c 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -25,7 +25,13 @@ import re import stat import sys import traceback +try: + # py3 + from http.client import responses +except ImportError: + from httplib import responses +from jinja2 import TemplateNotFound from tornado import web try: @@ -46,14 +52,7 @@ UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) #----------------------------------------------------------------------------- non_alphanum = re.compile(r'[^A-Za-z0-9]') -class RequestHandler(web.RequestHandler): - """RequestHandler with default variable setting.""" - - def render(*args, **kwargs): - kwargs.setdefault('message', '') - return web.RequestHandler.render(*args, **kwargs) - -class AuthenticatedHandler(RequestHandler): +class AuthenticatedHandler(web.RequestHandler): """A RequestHandler with an authenticated user.""" def clear_login_cookie(self): @@ -212,6 +211,45 @@ class IPythonHandler(AuthenticatedHandler): raise web.HTTPError(400, u'Invalid JSON in body of request') return model + def get_error_html(self, status_code, **kwargs): + """render custom error pages""" + exception = kwargs.get('exception') + message = '' + status_message = responses.get(status_code, 'Unknown HTTP Error') + if exception: + # get the custom message, if defined + try: + message = exception.log_message % exception.args + except Exception: + pass + + # construct the custom reason, if defined + reason = getattr(exception, 'reason', '') + if reason: + status_message = reason + + # build template namespace + ns = dict( + status_code=status_code, + status_message=status_message, + message=message, + exception=exception, + ) + + # render the template + try: + html = self.render_template('%s.html' % status_code, **ns) + except TemplateNotFound: + self.log.debug("No template for %d", status_code) + html = self.render_template('error.html', **ns) + return html + + +class Template404(IPythonHandler): + """Render our 404 template""" + def prepare(self): + raise web.HTTPError(404) + class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler): """static files should only be accessible when logged in""" diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index b7197a7..2fb6f45 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -6,7 +6,7 @@ from tornado import web from ..base.handlers import IPythonHandler, notebook_path_regex from IPython.nbformat.current import to_notebook_json -from IPython.nbconvert.exporters.export import exporter_map + from IPython.utils import tz from IPython.utils.py3compat import cast_bytes @@ -47,13 +47,33 @@ def respond_zip(handler, name, output, resources): handler.finish(buffer.getvalue()) return True +def get_exporter(format, **kwargs): + """get an exporter, raising appropriate errors""" + # if this fails, will raise 500 + try: + from IPython.nbconvert.exporters.export import exporter_map + except ImportError as e: + raise web.HTTPError(500, "Could not import nbconvert: %s" % e) + + try: + Exporter = exporter_map[format] + except KeyError: + # should this be 400? + raise web.HTTPError(404, u"No exporter for format: %s" % format) + + try: + return Exporter(**kwargs) + except Exception as e: + raise web.HTTPError(500, "Could not construct Exporter: %s" % e) + class NbconvertFileHandler(IPythonHandler): SUPPORTED_METHODS = ('GET',) @web.authenticated def get(self, format, path='', name=None): - exporter = exporter_map[format](config=self.config) + + exporter = get_exporter(format, config=self.config) path = path.strip('/') os_path = self.notebook_manager.get_os_path(name, path) @@ -62,8 +82,11 @@ class NbconvertFileHandler(IPythonHandler): info = os.stat(os_path) self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime)) - - output, resources = exporter.from_filename(os_path) + + try: + output, resources = exporter.from_filename(os_path) + except Exception as e: + raise web.HTTPError(500, "nbconvert failed: %s" % e) if respond_zip(self, name, output, resources): return @@ -86,12 +109,15 @@ class NbconvertPostHandler(IPythonHandler): @web.authenticated def post(self, format): - exporter = exporter_map[format](config=self.config) + exporter = get_exporter(format, config=self.config) model = self.get_json_body() nbnode = to_notebook_json(model['content']) - - output, resources = exporter.from_notebook_node(nbnode) + + try: + output, resources = exporter.from_notebook_node(nbnode) + except Exception as e: + raise web.HTTPError(500, "nbconvert failed: %s" % e) if respond_zip(self, nbnode.metadata.name, output, resources): return diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 0dc0dc2..85b3fc2 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -61,6 +61,7 @@ from tornado import web # Our own libraries from IPython.html import DEFAULT_STATIC_FILES_PATH +from .base.handlers import Template404 from .services.kernels.kernelmanager import MappingKernelManager from .services.notebooks.nbmanager import NotebookManager @@ -208,6 +209,8 @@ class NotebookWebApplication(web.Application): pattern = url_path_join(settings['base_project_url'], handler[0]) new_handler = tuple([pattern] + list(handler[1:])) new_handlers.append(new_handler) + # add 404 on the end, which will catch everything that falls through + new_handlers.append((r'(.*)', Template404)) return new_handlers diff --git a/IPython/html/services/nbconvert/handlers.py b/IPython/html/services/nbconvert/handlers.py index e2ced71..1c74de5 100644 --- a/IPython/html/services/nbconvert/handlers.py +++ b/IPython/html/services/nbconvert/handlers.py @@ -3,7 +3,6 @@ import json from tornado import web from ...base.handlers import IPythonHandler, json_errors -from IPython.nbconvert.exporters.export import exporter_map class NbconvertRootHandler(IPythonHandler): SUPPORTED_METHODS = ('GET',) @@ -11,6 +10,10 @@ class NbconvertRootHandler(IPythonHandler): @web.authenticated @json_errors def get(self): + try: + from IPython.nbconvert.exporters.export import exporter_map + except ImportError as e: + raise web.HTTPError(500, "Could not import nbconvert: %s" % e) res = {} for format, exporter in exporter_map.items(): res[format] = info = {} diff --git a/IPython/html/static/base/less/error.less b/IPython/html/static/base/less/error.less new file mode 100644 index 0000000..0a1eadb --- /dev/null +++ b/IPython/html/static/base/less/error.less @@ -0,0 +1,20 @@ +div.error { + margin: 2em; + text-align: center; +} + +div.error > h1 { + font-size: 500%; + line-height: normal; +} + +div.error > p { + font-size: 200%; + line-height: normal; +} + +div.traceback-wrapper { + text-align: left; + max-width: 800px; + margin: auto; +} diff --git a/IPython/html/static/base/less/style.less b/IPython/html/static/base/less/style.less index 18af195..40c5b96 100644 --- a/IPython/html/static/base/less/style.less +++ b/IPython/html/static/base/less/style.less @@ -1,4 +1,4 @@ @import "variables.less"; @import "mixins.less"; @import "flexbox.less"; - +@import "error.less"; diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index a26bd1f..00d99e2 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -18,6 +18,10 @@ .start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;} .end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;} .center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;} +div.error{margin:2em;text-align:center;} +div.error>h1{font-size:500%;line-height:normal;} +div.error>p{font-size:200%;line-height:normal;} +div.traceback-wrapper{text-align:left;max-width:800px;margin:auto;} .center-nav{display:inline-block;margin-bottom:-4px;} .alternate_upload{background-color:none;display:inline;} .alternate_upload.form{padding:0;margin:0;} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 19f14f5..46e64d5 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1385,6 +1385,10 @@ ul.icons-ul{list-style-type:none;text-indent:-0.7142857142857143em;margin-left:2 .start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;} .end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;} .center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;} +div.error{margin:2em;text-align:center;} +div.error>h1{font-size:500%;line-height:normal;} +div.error>p{font-size:200%;line-height:normal;} +div.traceback-wrapper{text-align:left;max-width:800px;margin:auto;} body{background-color:white;position:absolute;left:0px;right:0px;top:0px;bottom:0px;overflow:visible;} div#header{display:none;} #ipython_notebook{padding-left:16px;} diff --git a/IPython/html/templates/404.html b/IPython/html/templates/404.html new file mode 100644 index 0000000..7335051 --- /dev/null +++ b/IPython/html/templates/404.html @@ -0,0 +1,5 @@ +{% extends "error.html" %} +{% block error_detail %} +

You are requesting a page that does not exist!

+{% endblock %} + diff --git a/IPython/html/templates/error.html b/IPython/html/templates/error.html new file mode 100644 index 0000000..bedf06c --- /dev/null +++ b/IPython/html/templates/error.html @@ -0,0 +1,31 @@ +{% extends "page.html" %} + +{% block login_widget %} +{% endblock %} + +{% block stylesheet %} +{{super()}} + +{% endblock %} +{% block site %} + +
+ {% block h1_error %} +

{{status_code}} : {{status_message}}

+ {% endblock h1_error %} + {% block error_detail %} + {% if message %} +

The error was:

+
+
{{message}}
+
+ {% endif %} + {% endblock %} + + +{% endblock %} diff --git a/IPython/nbconvert/exporters/templateexporter.py b/IPython/nbconvert/exporters/templateexporter.py index f68bfea..98d3772 100644 --- a/IPython/nbconvert/exporters/templateexporter.py +++ b/IPython/nbconvert/exporters/templateexporter.py @@ -19,8 +19,8 @@ from __future__ import print_function, absolute_import # Stdlib imports import os -# other libs/dependencies -from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound +# other libs/dependencies are imported at runtime +# to move ImportErrors to runtime when the requirement is actually needed # IPython imports from IPython.utils.traitlets import MetaHasTraits, Unicode, List, Dict, Any @@ -164,6 +164,8 @@ class TemplateExporter(Exporter): This is triggered by various trait changes that would change the template. """ + from jinja2 import TemplateNotFound + if self.template is not None: return # called too early, do nothing @@ -274,6 +276,7 @@ class TemplateExporter(Exporter): """ Create the Jinja templating environment. """ + from jinja2 import Environment, ChoiceLoader, FileSystemLoader here = os.path.dirname(os.path.realpath(__file__)) loaders = [] if extra_loaders: diff --git a/IPython/nbconvert/filters/highlight.py b/IPython/nbconvert/filters/highlight.py index e246d7b..3cf6ff4 100644 --- a/IPython/nbconvert/filters/highlight.py +++ b/IPython/nbconvert/filters/highlight.py @@ -14,14 +14,11 @@ from within Jinja templates. # Imports #----------------------------------------------------------------------------- -from pygments import highlight as pygements_highlight -from pygments.lexers import get_lexer_by_name -from pygments.formatters import HtmlFormatter -from pygments.formatters import LatexFormatter - +# pygments must not be imported at the module level +# because errors should be raised at runtime if it's actually needed, +# not import time, when it may not be needed. # Our own imports -from IPython.nbconvert.utils.lexers import IPythonLexer from IPython.nbconvert.utils.base import NbConvertBase #----------------------------------------------------------------------------- @@ -55,10 +52,11 @@ class Highlight2Html(NbConvertBase): metadata : NotebookNode cell metadata metadata of the cell to highlight """ + from pygments.formatters import HtmlFormatter if not language: language=self.default_language - return _pygment_highlight(source, HtmlFormatter(), language, metadata) + return _pygments_highlight(source, HtmlFormatter(), language, metadata) class Highlight2Latex(NbConvertBase): @@ -78,10 +76,11 @@ class Highlight2Latex(NbConvertBase): strip_verbatim : bool remove the Verbatim environment that pygments provides by default """ + from pygments.formatters import LatexFormatter if not language: language=self.default_language - latex = _pygment_highlight(source, LatexFormatter(), language, metadata) + latex = _pygments_highlight(source, LatexFormatter(), language, metadata) if strip_verbatim: latex = latex.replace(r'\begin{Verbatim}[commandchars=\\\{\}]' + '\n', '') return latex.replace('\n\\end{Verbatim}\n', '') @@ -90,7 +89,7 @@ class Highlight2Latex(NbConvertBase): -def _pygment_highlight(source, output_formatter, language='ipython', metadata=None): +def _pygments_highlight(source, output_formatter, language='ipython', metadata=None): """ Return a syntax-highlighted version of the input source @@ -104,6 +103,9 @@ def _pygment_highlight(source, output_formatter, language='ipython', metadata=No metadata : NotebookNode cell metadata metadata of the cell to highlight """ + from pygments import highlight + from pygments.lexers import get_lexer_by_name + from IPython.nbconvert.utils.lexers import IPythonLexer # If the cell uses a magic extension language, # use the magic language instead. @@ -118,4 +120,4 @@ def _pygment_highlight(source, output_formatter, language='ipython', metadata=No else: lexer = get_lexer_by_name(language, stripall=True) - return pygements_highlight(source, lexer, output_formatter) + return highlight(source, lexer, output_formatter) diff --git a/IPython/nbconvert/preprocessors/csshtmlheader.py b/IPython/nbconvert/preprocessors/csshtmlheader.py index decae7d..2b41cca 100755 --- a/IPython/nbconvert/preprocessors/csshtmlheader.py +++ b/IPython/nbconvert/preprocessors/csshtmlheader.py @@ -15,8 +15,6 @@ import os import io -from pygments.formatters import HtmlFormatter - from IPython.utils import path from .base import Preprocessor @@ -83,6 +81,7 @@ class CSSHTMLHeaderPreprocessor(Preprocessor): Fills self.header with lines of CSS extracted from IPython and Pygments. """ + from pygments.formatters import HtmlFormatter #Clear existing header. header = []