changeset 128:28444014ce7d

Fix error reporting and counting of lines.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 14 Nov 2014 22:49:50 +0100
parents bc63dc20baa0
children 3080b6d02f40
files piecrust/page.py piecrust/rendering.py piecrust/serving.py piecrust/templating/base.py piecrust/templating/jinjaengine.py
diffstat 5 files changed, 143 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/page.py	Fri Nov 14 22:47:18 2014 +0100
+++ b/piecrust/page.py	Fri Nov 14 22:49:50 2014 +0100
@@ -252,7 +252,16 @@
         re.M)
 
 
+def _count_lines(s):
+    return len(s.split('\n'))
+
+
 def parse_segments(raw, offset=0):
+    # Get the number of lines in the header.
+    header_lines = _count_lines(raw[:offset].rstrip())
+    current_line = header_lines
+
+    # Start parsing.
     matches = list(segment_pattern.finditer(raw, offset))
     num_matches = len(matches)
     if num_matches > 0:
@@ -262,59 +271,65 @@
         if first_offset > 0:
             # There's some default content segment at the beginning.
             seg = ContentSegment()
-            seg.parts = parse_segment_parts(raw, offset, first_offset)
+            seg.parts, current_line = parse_segment_parts(
+                    raw, offset, first_offset, current_line)
             contents['content'] = seg
 
         for i in range(1, num_matches):
             m1 = matches[i - 1]
             m2 = matches[i]
             seg = ContentSegment()
-            seg.parts = parse_segment_parts(raw, m1.end() + 1,
-                    m2.start(), m1.group('fmt'))
+            seg.parts, current_line = parse_segment_parts(
+                    raw, m1.end() + 1, m2.start(), current_line,
+                    m1.group('fmt'))
             contents[m1.group('name')] = seg
 
         # Handle text past the last match.
         lastm = matches[-1]
         seg = ContentSegment()
-        seg.parts = parse_segment_parts(raw, lastm.end() + 1,
-                len(raw), lastm.group('fmt'))
+        seg.parts, current_line = parse_segment_parts(
+                raw, lastm.end() + 1, len(raw), current_line,
+                lastm.group('fmt'))
         contents[lastm.group('name')] = seg
 
         return contents
     else:
         # No segments, just content.
         seg = ContentSegment()
-        seg.parts = parse_segment_parts(raw, offset, len(raw))
+        seg.parts, current_line = parse_segment_parts(
+                raw, offset, len(raw), current_line)
         return {'content': seg}
 
 
-def parse_segment_parts(raw, start, end, first_part_fmt=None):
+def parse_segment_parts(raw, start, end, line_offset, first_part_fmt=None):
     matches = list(part_pattern.finditer(raw, start, end))
     num_matches = len(matches)
     if num_matches > 0:
         parts = []
 
         # First part, before the first format change.
+        part_text = raw[start:matches[0].start()]
         parts.append(
-                ContentSegmentPart(raw[start:matches[0].start()],
-                    first_part_fmt,
-                    start))
+                ContentSegmentPart(part_text, first_part_fmt, line_offset))
+        line_offset += _count_lines(part_text)
 
         for i in range(1, num_matches):
             m1 = matches[i - 1]
             m2 = matches[i]
+            part_text = raw[m1.end() + 1:m2.start()]
             parts.append(
                     ContentSegmentPart(
-                        raw[m1.end() + 1:m2.start()],
-                        m1.group('fmt'),
-                        m1.end() + 1))
+                        part_text, m1.group('fmt'), line_offset))
+            line_offset += _count_lines(part_text)
 
         lastm = matches[-1]
-        parts.append(ContentSegmentPart(raw[lastm.end() + 1:end],
-                lastm.group('fmt'),
-                lastm.end() + 1))
+        part_text = raw[lastm.end() + 1:end]
+        parts.append(ContentSegmentPart(
+                part_text, lastm.group('fmt'), line_offset))
 
-        return parts
+        return parts, line_offset
     else:
-        return [ContentSegmentPart(raw[start:end], first_part_fmt)]
+        part_text = raw[start:end]
+        parts = [ContentSegmentPart(part_text, first_part_fmt, line_offset)]
+        return parts, line_offset
 
--- a/piecrust/rendering.py	Fri Nov 14 22:47:18 2014 +0100
+++ b/piecrust/rendering.py	Fri Nov 14 22:49:50 2014 +0100
@@ -4,6 +4,7 @@
 from piecrust.data.builder import (DataBuildingContext, build_page_data,
         build_layout_data)
 from piecrust.sources.base import PageSource
+from piecrust.templating.base import TemplatingError
 from piecrust.uriutil import get_slug
 
 
@@ -164,8 +165,14 @@
         seg_text = ''
         for seg_part in seg.parts:
             part_format = seg_part.fmt or format_name
-            part_text = engine.renderString(seg_part.content, page_data,
-                    filename=page.path, line_offset=seg_part.line)
+            try:
+                part_text = engine.renderString(
+                        seg_part.content, page_data,
+                        filename=page.path)
+            except TemplatingError as err:
+                err.lineno += seg_part.line
+                raise err
+
             part_text = format_text(app, part_format, part_text)
             seg_text += part_text
         formatted_content[seg_name] = seg_text
@@ -211,12 +218,19 @@
             return engine
     return None
 
-def format_text(app, format_name, txt):
+def format_text(app, format_name, txt, exact_format=False):
+    if exact_format and not format_name:
+        raise Exception("You need to specify a format name.")
+
+    format_count = 0
     format_name = format_name or app.config.get('site/default_format')
     for fmt in app.plugin_loader.getFormatters():
         if fmt.FORMAT_NAMES is None or format_name in fmt.FORMAT_NAMES:
             txt = fmt.render(format_name, txt)
+            format_count += 1
             if fmt.OUTPUT_FORMAT is not None:
                 format_name = fmt.OUTPUT_FORMAT
+    if exact_format and format_count == 0:
+        raise Exception("No such format: %s" % format_name)
     return txt
 
--- a/piecrust/serving.py	Fri Nov 14 22:47:18 2014 +0100
+++ b/piecrust/serving.py	Fri Nov 14 22:49:50 2014 +0100
@@ -7,7 +7,7 @@
 import logging
 import io
 from werkzeug.exceptions import (NotFound, MethodNotAllowed,
-        InternalServerError)
+        InternalServerError, HTTPException)
 from werkzeug.serving import run_simple
 from werkzeug.wrappers import Request, Response
 from werkzeug.wsgi import wrap_file
@@ -129,19 +129,23 @@
         try:
             response = self._try_serve_page(app, environ, request)
             return response(environ, start_response)
+        except HTTPException as ex:
+            raise
         except (RouteNotFoundError, SourceNotFoundError) as ex:
             logger.exception(ex)
             raise NotFound()
         except Exception as ex:
-            logger.exception(ex)
             if app.debug:
+                logger.exception(ex)
                 raise
-            raise InternalServerError()
+            msg = str(ex)
+            logger.error(msg)
+            raise InternalServerError(msg)
 
     def _try_serve_asset(self, app, environ, request):
         logger.debug("Searching for asset with path: %s" % request.path)
         rel_req_path = request.path.lstrip('/').replace('/', os.sep)
-        entry = self._asset_record.findEntry(rel_req_path)
+        entry = self._asset_record.previous.findEntry(rel_req_path)
         if entry is None:
             return None
 
@@ -246,6 +250,15 @@
         rendered_page = render_page(render_ctx)
         rp_content = rendered_page.content
 
+        if taxonomy is not None:
+            paginator = rendered_page.data.get('pagination')
+            if (paginator and paginator.is_loaded and
+                    len(paginator.items) == 0):
+                message = ("This URL matched a route for taxonomy '%s' but "
+                           "no pages have been found to have it. This page "
+                           "won't be generated by a bake." % taxonomy.name)
+                raise NotFound(message)
+
         if entry is None:
             entry = ServeRecordPageEntry(req_path, page_num)
             self._page_record.addEntry(entry)
@@ -308,10 +321,13 @@
     def _handle_error(self, exception, environ, start_response):
         path = 'error'
         if isinstance(exception, NotFound):
-            path = '404'
+            path += '404'
+        description = str(exception)
+        if isinstance(exception, HTTPException):
+            description = exception.description
         env = Environment(loader=ErrorMessageLoader())
         template = env.get_template(path)
-        context = {'details': str(exception)}
+        context = {'details': description}
         response = Response(template.render(context), mimetype='text/html')
         return response(environ, start_response)
 
--- a/piecrust/templating/base.py	Fri Nov 14 22:47:18 2014 +0100
+++ b/piecrust/templating/base.py	Fri Nov 14 22:49:50 2014 +0100
@@ -4,6 +4,23 @@
     pass
 
 
+class TemplatingError(Exception):
+    def __init__(self, message, filename=None, lineno=-1):
+        super(TemplatingError, self).__init__()
+        self.message = message
+        self.filename = filename
+        self.lineno = lineno
+
+    def __str__(self):
+        msg = ''
+        if self.filename:
+            msg += self.filename
+        if self.lineno >= 0:
+            msg += ', line %d' % self.lineno
+        msg += ': ' + self.message
+        return msg
+
+
 class TemplateEngine(object):
     EXTENSIONS = []
 
--- a/piecrust/templating/jinjaengine.py	Fri Nov 14 22:47:18 2014 +0100
+++ b/piecrust/templating/jinjaengine.py	Fri Nov 14 22:49:50 2014 +0100
@@ -1,5 +1,6 @@
 import re
 import time
+import os.path
 import logging
 import threading
 import strict_rfc3339
@@ -15,7 +16,8 @@
 from piecrust.data.paginator import Paginator
 from piecrust.rendering import format_text
 from piecrust.routing import CompositeRouteFunction
-from piecrust.templating.base import TemplateEngine, TemplateNotFoundError
+from piecrust.templating.base import (TemplateEngine, TemplateNotFoundError,
+                                      TemplatingError)
 from piecrust.uriutil import multi_replace, get_first_sub_uri
 
 
@@ -30,18 +32,20 @@
     def __init__(self):
         self.env = None
 
-    def renderString(self, txt, data, filename=None, line_offset=0):
+    def renderString(self, txt, data, filename=None):
         self._ensureLoaded()
-        tpl = self.env.from_string(txt)
+
+        try:
+            tpl = self.env.from_string(txt)
+        except TemplateSyntaxError as tse:
+            raise self._getTemplatingError(tse, filename=filename)
+        except TemplateNotFound:
+            raise TemplateNotFoundError()
+
         try:
             return tpl.render(data)
         except TemplateSyntaxError as tse:
-            tse.lineno += line_offset
-            if filename:
-                tse.filename = filename
-            import sys
-            _, __, traceback = sys.exc_info()
-            raise tse.with_traceback(traceback)
+            raise self._getTemplatingError(tse)
 
     def renderFile(self, paths, data):
         self._ensureLoaded()
@@ -51,11 +55,25 @@
             try:
                 tpl = self.env.get_template(p)
                 break
+            except TemplateSyntaxError as tse:
+                raise self._getTemplatingError(tse)
             except TemplateNotFound:
                 pass
+
         if tpl is None:
             raise TemplateNotFoundError()
-        return tpl.render(data)
+
+        try:
+            return tpl.render(data)
+        except TemplateSyntaxError as tse:
+            raise self._getTemplatingError(tse)
+
+    def _getTemplatingError(self, tse, filename=None):
+        filename = tse.filename or filename
+        if filename and os.path.isabs(filename):
+            filename = os.path.relpath(filename, self.env.app.root_dir)
+        err = TemplatingError(str(tse), filename, tse.lineno)
+        raise err from tse
 
     def _ensureLoaded(self):
         if self.env:
@@ -74,6 +92,9 @@
                 PieCrustHighlightExtension,
                 PieCrustCacheExtension,
                 PieCrustSpacelessExtension]
+        twig_compatibility_mode = self.app.config.get('jinja/twig_compatibility')
+        if twig_compatibility_mode is None or twig_compatibility_mode is True:
+            extensions.append(PieCrustFormatExtension)
         if autoescape:
             extensions.append('jinja2.ext.autoescape')
         self.env = PieCrustEnvironment(
@@ -210,6 +231,28 @@
     return time.strftime(fmt, time.localtime(value))
 
 
+class PieCrustFormatExtension(Extension):
+    tags = set(['pcformat'])
+
+    def __init__(self, environment):
+        super(PieCrustFormatExtension, self).__init__(environment)
+
+    def parse(self, parser):
+        lineno = next(parser.stream).lineno
+        args = [parser.parse_expression()]
+        body = parser.parse_statements(['name:endpcformat'], drop_needle=True)
+        return CallBlock(self.call_method('_format', args),
+                         [], [], body).set_lineno(lineno)
+
+    def _format(self, format_name, caller=None):
+        body = caller()
+        text = format_text(self.environment.app,
+                           format_name,
+                           Markup(body.rstrip()).unescape(),
+                           exact_format=True)
+        return text
+
+
 class PieCrustHighlightExtension(Extension):
     tags = set(['highlight', 'geshi'])
 
@@ -240,7 +283,7 @@
                                        drop_needle=True)
 
         return CallBlock(self.call_method('_highlight', args, kwargs),
-                               [], [], body).set_lineno(lineno)
+                         [], [], body).set_lineno(lineno)
 
     def _highlight(self, lang, line_numbers=False, use_classes=False,
             css_class=None, css_id=None, caller=None):