Skip to content

Commit

Permalink
Merge pull request #4735 from minrk/better-errors
Browse files Browse the repository at this point in the history
add some HTML error pages
  • Loading branch information
takluyver committed Jan 9, 2014
2 parents 41d856c + b9d0a5f commit ae073a1
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 31 deletions.
54 changes: 46 additions & 8 deletions IPython/html/base/handlers.py
Expand Up @@ -25,7 +25,13 @@
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:
Expand All @@ -46,14 +52,7 @@
#-----------------------------------------------------------------------------
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):
Expand Down Expand Up @@ -212,6 +211,45 @@ def get_json_body(self):
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"""
Expand Down
40 changes: 33 additions & 7 deletions IPython/html/nbconvert/handlers.py
Expand Up @@ -6,7 +6,7 @@

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

Expand Down Expand Up @@ -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)
Expand All @@ -62,8 +82,11 @@ def get(self, format, path='', name=None):

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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions IPython/html/notebookapp.py
Expand Up @@ -61,6 +61,7 @@

# 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
Expand Down Expand Up @@ -208,6 +209,8 @@ def init_handlers(self, settings):
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


Expand Down
5 changes: 4 additions & 1 deletion IPython/html/services/nbconvert/handlers.py
Expand Up @@ -3,14 +3,17 @@
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',)

@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 = {}
Expand Down
20 changes: 20 additions & 0 deletions 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;
}
2 changes: 1 addition & 1 deletion IPython/html/static/base/less/style.less
@@ -1,4 +1,4 @@
@import "variables.less";
@import "mixins.less";
@import "flexbox.less";

@import "error.less";
4 changes: 4 additions & 0 deletions IPython/html/static/style/ipython.min.css
Expand Up @@ -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;}
Expand Down
4 changes: 4 additions & 0 deletions IPython/html/static/style/style.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions IPython/html/templates/404.html
@@ -0,0 +1,5 @@
{% extends "error.html" %}
{% block error_detail %}
<p>You are requesting a page that does not exist!</p>
{% endblock %}

31 changes: 31 additions & 0 deletions IPython/html/templates/error.html
@@ -0,0 +1,31 @@
{% extends "page.html" %}

{% block login_widget %}
{% endblock %}

{% block stylesheet %}
{{super()}}
<style type="text/css">
/* disable initial hide */
div#header, div#site {
display: block;
}
</style>
{% endblock %}
{% block site %}

<div class="error">
{% block h1_error %}
<h1>{{status_code}} : {{status_message}}</h1>
{% endblock h1_error %}
{% block error_detail %}
{% if message %}
<p>The error was:</p>
<div class="traceback-wrapper">
<pre class="traceback">{{message}}</pre>
</div>
{% endif %}
{% endblock %}
</header>

{% endblock %}
7 changes: 5 additions & 2 deletions IPython/nbconvert/exporters/templateexporter.py
Expand Up @@ -19,8 +19,8 @@
# 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
Expand Down Expand Up @@ -164,6 +164,8 @@ def _load_template(self):
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
Expand Down Expand Up @@ -274,6 +276,7 @@ def _init_environment(self, extra_loaders=None):
"""
Create the Jinja templating environment.
"""
from jinja2 import Environment, ChoiceLoader, FileSystemLoader
here = os.path.dirname(os.path.realpath(__file__))
loaders = []
if extra_loaders:
Expand Down
22 changes: 12 additions & 10 deletions IPython/nbconvert/filters/highlight.py
Expand Up @@ -14,14 +14,11 @@
# 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

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -55,10 +52,11 @@ def __call__(self, source, language=None, metadata=None):
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):
Expand All @@ -78,10 +76,11 @@ def __call__(self, source, language=None, metadata=None, strip_verbatim=False):
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', '')
Expand All @@ -90,7 +89,7 @@ def __call__(self, source, language=None, metadata=None, strip_verbatim=False):



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
Expand All @@ -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.
Expand All @@ -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)
3 changes: 1 addition & 2 deletions IPython/nbconvert/preprocessors/csshtmlheader.py
Expand Up @@ -15,8 +15,6 @@
import os
import io

from pygments.formatters import HtmlFormatter

from IPython.utils import path

from .base import Preprocessor
Expand Down Expand Up @@ -83,6 +81,7 @@ def _regen_header(self):
Fills self.header with lines of CSS extracted from IPython
and Pygments.
"""
from pygments.formatters import HtmlFormatter

#Clear existing header.
header = []
Expand Down

0 comments on commit ae073a1

Please sign in to comment.