Django

Code

Changeset 1379

Show
Ignore:
Timestamp:
11/23/05 17:10:17 (2 years ago)
Author:
adrian
Message:

Fixed #603 -- Added template debugging errors to pretty error-page output, if TEMPLATE_DEBUG setting is True. Also refactored FilterParser? for a significant speed increase and changed the template_loader interface so that it returns information about the loader. Taken from new-admin. Thanks rjwittams and crew

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/conf/global_settings.py

    r1303 r1379  
    99 
    1010DEBUG = False 
     11TEMPLATE_DEBUG = False 
    1112 
    1213# Whether to use the "Etag" header. This saves bandwidth but slows down performance. 
  • django/trunk/django/conf/project_template/settings.py

    r958 r1379  
    22 
    33DEBUG = True 
     4TEMPLATE_DEBUG = DEBUG 
    45 
    56ADMINS = ( 
  • django/trunk/django/core/template/defaulttags.py

    r1098 r1379  
    193193            grouper = resolve_variable_with_filters('var.%s' % self.expression, \ 
    194194                Context({'var': obj})) 
     195            # TODO: Is this a sensible way to determine equality? 
    195196            if output and repr(output[-1]['grouper']) == repr(grouper): 
    196197                output[-1]['list'].append(obj) 
     
    629630    try: 
    630631        LoadNode.load_taglib(taglib) 
    631     except ImportError
    632         raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib 
     632    except ImportError, e
     633        raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) 
    633634    return LoadNode(taglib) 
    634635 
  • django/trunk/django/core/template/__init__.py

    r1207 r1379  
    5656""" 
    5757import re 
    58 from django.conf.settings import DEFAULT_CHARSET 
     58from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG 
    5959 
    6060__all__ = ('Template','Context','compile_string') 
     
    7575ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' 
    7676 
     77# what to report as the origin for templates that come from non-loader sources 
     78# (e.g. strings) 
     79UNKNOWN_SOURCE="<unknown source>" 
     80 
    7781# match a variable or block tag and capture the entire tag, including start/end delimiters 
    7882tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), 
     
    102106    pass 
    103107 
     108class Origin(object): 
     109    def __init__(self, name): 
     110        self.name = name 
     111 
     112    def reload(self): 
     113        raise NotImplementedException 
     114 
     115    def __str__(self): 
     116        return self.name 
     117 
     118class StringOrigin(Origin): 
     119    def __init__(self, source): 
     120        super(StringOrigin, self).__init__(UNKNOWN_SOURCE) 
     121        self.source = source 
     122 
     123    def reload(self): 
     124        return self.source 
     125 
    104126class Template: 
    105     def __init__(self, template_string): 
     127    def __init__(self, template_string, origin=None): 
    106128        "Compilation stage" 
    107         self.nodelist = compile_string(template_string) 
     129        if TEMPLATE_DEBUG and origin == None: 
     130            origin = StringOrigin(template_string) 
     131            # Could do some crazy stack-frame stuff to record where this string 
     132            # came from... 
     133        self.nodelist = compile_string(template_string, origin) 
    108134 
    109135    def __iter__(self): 
     
    116142        return self.nodelist.render(context) 
    117143 
    118 def compile_string(template_string): 
     144def compile_string(template_string, origin): 
    119145    "Compiles template_string into NodeList ready for rendering" 
    120     tokens = tokenize(template_string
    121     parser = Parser(tokens
     146    lexer = lexer_factory(template_string, origin
     147    parser = parser_factory(lexer.tokenize()
    122148    return parser.parse() 
    123149 
     
    164190        return False 
    165191 
     192    def get(self, key, otherwise): 
     193        for dict in self.dicts: 
     194            if dict.has_key(key): 
     195                return dict[key] 
     196        return otherwise 
     197 
    166198    def update(self, other_dict): 
    167199        "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." 
     
    175207    def __str__(self): 
    176208        return '<%s token: "%s...">' % ( 
    177             {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type], 
     209            {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type], 
    178210            self.contents[:20].replace('\n', '') 
    179211            ) 
    180212 
    181 def tokenize(template_string): 
    182     "Return a list of tokens from a given template_string" 
    183     # remove all empty strings, because the regex has a tendency to add them 
    184     bits = filter(None, tag_re.split(template_string)) 
    185     return map(create_token, bits) 
    186  
    187 def create_token(token_string): 
    188     "Convert the given token string into a new Token object and return it" 
    189     if token_string.startswith(VARIABLE_TAG_START): 
    190         return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) 
    191     elif token_string.startswith(BLOCK_TAG_START): 
    192         return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) 
    193     else: 
    194         return Token(TOKEN_TEXT, token_string) 
    195  
    196 class Parser: 
     213    def __repr__(self): 
     214        return '<%s token: "%s">' % ( 
     215            {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type], 
     216            self.contents[:].replace('\n', '') 
     217            ) 
     218 
     219class Lexer(object): 
     220    def __init__(self, template_string, origin): 
     221        self.template_string = template_string 
     222        self.origin = origin 
     223 
     224    def tokenize(self): 
     225        "Return a list of tokens from a given template_string" 
     226        # remove all empty strings, because the regex has a tendency to add them 
     227        bits = filter(None, tag_re.split(self.template_string)) 
     228        return map(self.create_token, bits) 
     229 
     230    def create_token(self,token_string): 
     231        "Convert the given token string into a new Token object and return it" 
     232        if token_string.startswith(VARIABLE_TAG_START): 
     233            token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) 
     234        elif token_string.startswith(BLOCK_TAG_START): 
     235            token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) 
     236        else: 
     237            token = Token(TOKEN_TEXT, token_string) 
     238        return token 
     239 
     240class DebugLexer(Lexer): 
     241    def __init__(self, template_string, origin): 
     242        super(DebugLexer, self).__init__(template_string, origin) 
     243 
     244    def tokenize(self): 
     245        "Return a list of tokens from a given template_string" 
     246        token_tups, upto = [], 0 
     247        for match in tag_re.finditer(self.template_string): 
     248            start, end = match.span() 
     249            if start > upto: 
     250                token_tups.append( (self.template_string[upto:start], (upto, start)) ) 
     251                upto = start 
     252            token_tups.append( (self.template_string[start:end], (start,end)) ) 
     253            upto = end 
     254        last_bit = self.template_string[upto:] 
     255        if last_bit: 
     256           token_tups.append( (last_bit, (upto, upto + len(last_bit))) ) 
     257        return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups] 
     258 
     259    def create_token(self, token_string, source): 
     260        token = super(DebugLexer, self).create_token(token_string) 
     261        token.source = source 
     262        return token 
     263 
     264class Parser(object): 
    197265    def __init__(self, tokens): 
    198266        self.tokens = tokens 
    199267 
    200268    def parse(self, parse_until=[]): 
    201         nodelist = NodeList() 
     269        nodelist = self.create_nodelist() 
    202270        while self.tokens: 
    203271            token = self.next_token() 
    204272            if token.token_type == TOKEN_TEXT: 
    205                 nodelist.append(TextNode(token.contents)
     273                self.extend_nodelist(nodelist, TextNode(token.contents), token
    206274            elif token.token_type == TOKEN_VAR: 
    207275                if not token.contents: 
    208                     raise TemplateSyntaxError, "Empty variable tag" 
    209                 nodelist.append(VariableNode(token.contents)) 
     276                    self.empty_variable(token) 
     277                var_node = self.create_variable_node(token.contents) 
     278                self.extend_nodelist(nodelist, var_node,token) 
    210279            elif token.token_type == TOKEN_BLOCK: 
    211280                if token.contents in parse_until: 
     
    216285                    command = token.contents.split()[0] 
    217286                except IndexError: 
    218                     raise TemplateSyntaxError, "Empty block tag" 
     287                    self.empty_block_tag(token) 
     288                # execute callback function for this tag and append resulting node 
     289                self.enter_command(command, token) 
    219290                try: 
    220                     # execute callback function for this tag and append resulting node 
    221                     nodelist.append(registered_tags[command](self, token)) 
     291                    compile_func = registered_tags[command] 
    222292                except KeyError: 
    223                     raise TemplateSyntaxError, "Invalid block tag: '%s'" % command 
     293                    self.invalid_block_tag(token, command) 
     294                try: 
     295                    compiled_result = compile_func(self, token) 
     296                except TemplateSyntaxError, e: 
     297                    if not self.compile_function_error(token, e): 
     298                       raise 
     299                self.extend_nodelist(nodelist, compiled_result, token) 
     300                self.exit_command() 
    224301        if parse_until: 
    225             raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) 
     302            self.unclosed_block_tag(parse_until) 
    226303        return nodelist 
     304 
     305    def create_variable_node(self, contents): 
     306        return VariableNode(contents) 
     307 
     308    def create_nodelist(self): 
     309        return NodeList() 
     310 
     311    def extend_nodelist(self, nodelist, node, token): 
     312        nodelist.append(node) 
     313 
     314    def enter_command(self, command, token): 
     315        pass 
     316 
     317    def exit_command(self): 
     318        pass 
     319 
     320    def error(self, token, msg ): 
     321        return TemplateSyntaxError(msg) 
     322 
     323    def empty_variable(self, token): 
     324        raise self.error( token, "Empty variable tag") 
     325 
     326    def empty_block_tag(self, token): 
     327        raise self.error( token, "Empty block tag") 
     328 
     329    def invalid_block_tag(self, token, command): 
     330        raise self.error( token, "Invalid block tag: '%s'" % command) 
     331 
     332    def unclosed_block_tag(self, parse_until): 
     333        raise self.error(None, "Unclosed tags: %s " %  ', '.join(parse_until)) 
     334 
     335    def compile_function_error(self, token, e): 
     336        pass 
    227337 
    228338    def next_token(self): 
     
    234344    def delete_first_token(self): 
    235345        del self.tokens[0] 
     346 
     347class DebugParser(Parser): 
     348    def __init__(self, lexer): 
     349        super(DebugParser, self).__init__(lexer) 
     350        self.command_stack = [] 
     351 
     352    def enter_command(self, command, token): 
     353        self.command_stack.append( (command, token.source) ) 
     354 
     355    def exit_command(self): 
     356        self.command_stack.pop() 
     357 
     358    def error(self, token, msg): 
     359        return self.source_error(token.source, msg) 
     360 
     361    def source_error(self, source,msg): 
     362        e = TemplateSyntaxError(msg) 
     363        e.source = source 
     364        return e 
     365 
     366    def create_nodelist(self): 
     367        return DebugNodeList() 
     368 
     369    def create_variable_node(self, contents): 
     370        return DebugVariableNode(contents) 
     371 
     372    def extend_nodelist(self, nodelist, node, token): 
     373        node.source = token.source 
     374        super(DebugParser, self).extend_nodelist(nodelist, node, token) 
     375 
     376    def unclosed_block_tag(self, parse_until): 
     377        (command, source) = self.command_stack.pop() 
     378        msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until)) 
     379        raise self.source_error( source, msg) 
     380 
     381    def compile_function_error(self, token, e): 
     382        if not hasattr(e, 'source'): 
     383            e.source = token.source 
     384 
     385if TEMPLATE_DEBUG: 
     386    lexer_factory = DebugLexer 
     387    parser_factory = DebugParser 
     388else: 
     389    lexer_factory = Lexer 
     390    parser_factory = Parser 
    236391 
    237392class TokenParser: 
     
    317472            return s 
    318473 
    319 class FilterParser: 
     474 
     475 
     476 
     477filter_raw_string = r""" 
     478^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s| 
     479^"(?P<constant>%(str)s)"| 
     480^(?P<var>[%(var_chars)s]+)| 
     481 (?:%(filter_sep)s 
     482     (?P<filter_name>\w+) 
     483         (?:%(arg_sep)s 
     484             (?: 
     485              %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s| 
     486              "(?P<arg>%(str)s)" 
     487             ) 
     488         )? 
     489 )""" % { 
     490    'str': r"""[^"\\]*(?:\\.[^"\\]*)*""", 
     491    'var_chars': "A-Za-z0-9\_\." , 
     492    'filter_sep': re.escape(FILTER_SEPARATOR), 
     493    'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR), 
     494    'i18n_open' : re.escape("_("), 
     495    'i18n_close' : re.escape(")"), 
     496  } 
     497 
     498filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "") 
     499filter_re = re.compile(filter_raw_string) 
     500 
     501class FilterParser(object): 
    320502    """ 
    321503    Parses a variable token and its optional filters (all as a single string), 
     
    332514    get_filters_from_token helper function. 
    333515    """ 
    334     def __init__(self, s): 
    335         self.s = s 
    336         self.i = -1 
    337         self.current = '' 
    338         self.filters = [] 
    339         self.current_filter_name = None 
    340         self.current_filter_arg = None 
    341         # First read the variable part. Decide whether we need to parse a 
    342         # string or a variable by peeking into the stream. 
    343         if self.peek_char() in ('_', '"', "'"): 
    344             self.var = self.read_constant_string_token() 
    345         else: 
    346             self.var = self.read_alphanumeric_token() 
    347         if not self.var: 
    348             raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s 
    349         if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_': 
    350             raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var 
    351         # Have we reached the end? 
    352         if self.current is None: 
    353             return 
    354         if self.current != FILTER_SEPARATOR: 
    355             raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) 
    356         # We have a filter separator; start reading the filters 
    357         self.read_filters() 
    358  
    359     def peek_char(self): 
    360         try: 
    361             return self.s[self.i+1] 
    362         except IndexError: 
    363             return None 
    364  
    365     def next_char(self): 
    366         self.i = self.i + 1 
    367         try: 
    368             self.current = self.s[self.i] 
    369         except IndexError: 
    370             self.current = None 
    371  
    372     def read_constant_string_token(self): 
    373         """ 
    374         Reads a constant string that must be delimited by either " or ' 
    375         characters. The string is returned with its delimiters. 
    376         """ 
    377         val = '' 
    378         qchar = None 
    379         i18n = False 
    380         self.next_char() 
    381         if self.current == '_': 
    382             i18n = True 
    383             self.next_char() 
    384             if self.current != '(': 
    385                 raise TemplateSyntaxError, "Bad character (expecting '(') '%s'" % self.current 
    386             self.next_char() 
    387         if not self.current in ('"', "'"): 
    388             raise TemplateSyntaxError, "Bad character (expecting '\"' or ''') '%s'" % self.current 
    389         qchar = self.current 
    390         val += qchar 
    391         while 1: 
    392            self.next_char() 
    393            if self.current == qchar: 
    394                break 
    395            val += self.current 
    396         val += self.current 
    397         self.next_char() 
    398         if i18n: 
    399             if self.current != ')': 
    400                 raise TemplateSyntaxError, "Bad character (expecting ')') '%s'" % self.current 
    401             self.next_char() 
    402             val = qchar+_(val.strip(qchar))+qchar 
    403         return val 
    404  
    405     def read_alphanumeric_token(self): 
    406         """ 
    407         Reads a variable name or filter name, which are continuous strings of 
    408         alphanumeric characters + the underscore. 
    409         """ 
    410         var = '' 
    411         while 1: 
    412             self.next_char() 
    413             if self.current is None: 
    414                 break 
    415             if self.current not in ALLOWED_VARIABLE_CHARS: 
    416                 break 
    417             var += self.current 
    418         return var 
    419  
    420     def read_filters(self): 
    421         while 1: 
    422             filter_name, arg = self.read_filter() 
    423             if not registered_filters.has_key(filter_name): 
    424                 raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name 
    425             if registered_filters[filter_name][1] == True and arg is None: 
    426                 raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name 
    427             if registered_filters[filter_name][1] == False and arg is not None: 
    428                 raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) 
    429             self.filters.append((filter_name, arg)) 
    430             if self.current is None: 
    431                 break 
    432  
    433     def read_filter(self): 
    434         self.current_filter_name = self.read_alphanumeric_token() 
    435         self.current_filter_arg = None 
    436         # Have we reached the end? 
    437         if self.current is None: 
    438             return (self.current_filter_name, None) 
    439         # Does the filter have an argument? 
    440         if self.current == FILTER_ARGUMENT_SEPARATOR: 
    441             self.current_filter_arg = self.read_arg() 
    442             return (self.current_filter_name, self.current_filter_arg) 
    443         # Next thing MUST be a pipe 
    444         if self.current != FILTER_SEPARATOR: 
    445             raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) 
    446         return (self.current_filter_name, self.current_filter_arg) 
    447  
    448     def read_arg(self): 
    449         # First read a " or a _(" 
    450         self.next_char() 
    451         translated = False 
    452         if self.current == '_': 
    453             self.next_char() 
    454             if self.current != '(': 
    455                 raise TemplateSyntaxError, "Bad character (expecting '(') '%s'" % self.current 
    456             translated = True 
    457             self.next_char() 
    458         if self.current != '"': 
    459             raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current 
    460         self.escaped = False 
    461         arg = '' 
    462         while 1: 
    463             self.next_char() 
    464             if self.current == '"' and not self.escaped: 
    465                 break 
    466             if self.current == '\\' and not self.escaped: 
    467                 self.escaped = True 
    468                 continue 
    469             if self.current == '\\' and self.escaped: 
    470                 arg += '\\' 
    471                 self.escaped = False 
    472                 continue 
    473             if self.current == '"' and self.escaped: 
    474                 arg += '"' 
    475                 self.escaped = False 
    476                 continue 
    477             if self.escaped and self.current not in '\\"': 
    478                 raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s 
    479             if self.current is None: 
    480                 raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s 
    481             arg += self.current 
    482         # self.current must now be '"' 
    483         self.next_char() 
    484         if translated: 
    485             if self.current != ')': 
    486                 raise TemplateSyntaxError, "Bad character (expecting ')') '%s'" % self.current 
    487             self.next_char() 
    488             arg = _(arg) 
    489         return arg 
     516    def __init__(self, token): 
     517        matches = filter_re.finditer(token) 
     518        var = None 
     519        filters = [] 
     520        upto = 0 
     521        for match in matches: 
     522            start = match.start() 
     523            if upto != start: 
     524                raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s"  % \ 
     525                                           (token[:upto], token[upto:start], token[start:]) 
     526            if var == None: 
     527                var, constant, i18n_constant = match.group("var", "constant", "i18n_constant") 
     528                if i18n_constant: 
     529                    var = '"%s"' %  _(i18n_constant) 
     530                elif constant: 
     531                    var = '"%s"' % constant 
     532                upto = match.end() 
     533                if var == None: 
     534                    raise TemplateSyntaxError, "Could not find variable at start of %s" % token 
     535                elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_': 
     536                    raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var 
     537            else: 
     538                filter_name = match.group("filter_name") 
     539                arg, i18n_arg = match.group("arg","i18n_arg") 
     540                if i18n_arg: 
     541                    arg =_(i18n_arg.replace('\\', '')) 
     542                if arg: 
     543                    arg = arg.replace('\\', '') 
     544                if not registered_filters.has_key(filter_name): 
     545                    raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name 
     546                if registered_filters[filter_name][1] == True and arg is None: 
     547                    raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name 
     548                if registered_filters[filter_name][1] == False and arg is not None: 
     549                    raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) 
     550                filters.append( (filter_name,arg) ) 
     551                upto = match.end() 
     552        if upto != len(token): 
     553            raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:] 
     554        self.var , self.filters = var, filters 
    490555 
    491556def get_filters_from_token(token): 
     
    581646        for node in self: 
    582647            if isinstance(node, Node): 
    583                 bits.append(node.render(context)) 
     648                bits.append(self.render_node(node, context)) 
    584649            else: 
    585650                bits.append(node) 
     
    593658        return nodes 
    594659 
     660    def render_node(self, node, context): 
     661        return(node.render(context)) 
     662 
     663class DebugNodeList(NodeList): 
     664    def render_node(self, node, context): 
     665        try: 
     666            result = node.render(context) 
     667        except TemplateSyntaxError, e: 
     668            if not hasattr(e, 'source'): 
     669                e.source = node.source 
     670            raise 
     671        except Exception: 
     672            from sys import exc_info 
     673            wrapped = TemplateSyntaxError('Caught an exception while rendering.') 
     674            wrapped.source = node.source 
     675            wrapped.exc_info = exc_info() 
     676            raise wrapped 
     677        return result 
     678 
    595679class TextNode(Node): 
    596680    def __init__(self, s): 
     
    610694        return "<Variable Node: %s>" % self.var_string 
    611695 
     696    def encode_output(self, output): 
     697        # Check type so that we don't run str() on a Unicode object 
     698        if not isinstance(output, basestring): 
     699            return str(output) 
     700        elif isinstance(output, unicode): 
     701            return output.encode(DEFAULT_CHARSET) 
     702        else: 
     703            return output 
     704 
    612705    def render(self, context): 
    613706        output = resolve_variable_with_filters(self.var_string, context) 
    614         # Check type so that we don't run str() on a Unicode object 
    615         if not isinstance(output, basestring): 
    616             output = str(output) 
    617         elif isinstance(output, unicode): 
    618             output = output.encode(DEFAULT_CHARSET) 
    619         return output 
     707        return self.encode_output(output) 
     708 
     709class DebugVariableNode(VariableNode): 
     710    def render(self, context): 
     711        try: 
     712             output = resolve_variable_with_filters(self.var_string, context) 
     713        except TemplateSyntaxError, e: 
     714            if not hasattr(e, 'source'): 
     715                e.source = self.source 
     716            raise 
     717        return self.encode_output(output) 
    620718 
    621719def register_tag(token_command, callback_function): 
  • django/trunk/django/core/template/loader.py

    r1349 r1379  
    88# name is the template name. 
    99# dirs is an optional list of directories to search instead of TEMPLATE_DIRS. 
     10# 
     11# The loader should return a tuple of (template_source, path). The path returned 
     12# might be shown to the user for debugging purposes, so it should identify where 
     13# the template was loaded from. 
    1014# 
    1115# Each loader should have an "is_usable" attribute set. This is a boolean that 
     
    1822 
    1923from django.core.exceptions import ImproperlyConfigured 
    20 from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag 
    21 from django.conf.settings import TEMPLATE_LOADERS 
     24from django.core.template import Origin, StringOrigin, Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag 
     25from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG 
    2226 
    2327template_source_loaders = [] 
     
    3943        template_source_loaders.append(func) 
    4044 
    41 def load_template_source(name, dirs=None): 
     45class LoaderOrigin(Origin): 
     46    def __init__(self, display_name, loader, name, dirs): 
     47        super(LoaderOrigin, self).__init__(display_name) 
     48        self.loader, self.loadname, self.dirs = loader, name, dirs 
     49 
     50    def reload(self): 
     51        return self.loader(self.loadname, self.dirs)[0] 
     52 
     53def make_origin(display_name, loader, name, dirs): 
     54    if TEMPLATE_DEBUG: 
     55        return LoaderOrigin(display_name, loader, name, dirs) 
     56    else: 
     57        return None 
     58 
     59def find_template_source(name, dirs=None): 
    4260    for loader in template_source_loaders: 
    4361        try: 
    44             return loader(name, dirs) 
     62            source, display_name  = loader(name, dirs) 
     63            return (source, make_origin(display_name, loader, name, dirs)) 
    4564        except TemplateDoesNotExist: 
    4665            pass 
    4766    raise TemplateDoesNotExist, name 
    4867 
     68def load_template_source(name, dirs=None): 
     69    find_template_source(name, dirs)[0] 
     70 
    4971class ExtendsError(Exception): 
    5072    pass 
     
    5577    handling template inheritance recursively. 
    5678    """ 
    57     return get_template_from_string(load_template_source(template_name)) 
    58  
    59 def get_template_from_string(source): 
     79    return get_template_from_string(*find_template_source(template_name)) 
     80 
     81def get_template_from_string(source, origin=None ): 
    6082    """ 
    6183    Returns a compiled Template object for the given template code, 
    6284    handling template inheritance recursively. 
    6385    """ 
    64     return Template(source
     86    return Template(source, origin
    6587 
    6688def render_to_string(template_name, dictionary=None, context_instance=None): 
     
    135157            raise TemplateSyntaxError, error_msg 
    136158        try: 
    137             return get_template_from_string(load_template_source(parent, self.template_dirs)) 
     159            return get_template_from_string(*find_template_source(parent, self.template_dirs)) 
    138160        except TemplateDoesNotExist: 
    139161            raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent 
     
    166188            t = get_template(template_path) 
    167189            self.template = t 
    168         except: 
     190        except Exception, e: 
     191            if TEMPLATE_DEBUG: 
     192                raise 
    169193            self.template = None 
    170194 
     
    184208             t = get_template(template_name) 
    185209             return t.render(context) 
     210         except TemplateSyntaxError, e: 
     211             if TEMPLATE_DEBUG: 
     212                 raise 
     213             return '' 
    186214         except: 
    187215             return '' # Fail silently for invalid included templates. 
     
    237265        {% include "foo/some_include" %} 
    238266    """ 
     267 
    239268    bits = token.contents.split() 
    240269    if len(bits) != 2: 
  • django/trunk/django/core/template/loaders/app_directories.py

    r985 r1379  
    3232        filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION 
    3333        try: 
    34             return open(filepath).read(
     34            return (open(filepath).read(), filepath
    3535        except IOError: 
    3636            pass 
  • django/trunk/django/core/template/loaders/eggs.py

    r889 r1379  
    1919        for app in INSTALLED_APPS: 
    2020            try: 
    21                 return resource_string(app, pkg_name
     21                return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name)
    2222            except: 
    2323                pass 
  • django/trunk/django/core/template/loaders/filesystem.py

    r890 r1379  
    1212        filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION 
    1313        try: 
    14             return open(filepath).read(
     14            return (open(filepath).read(), filepath
    1515        except IOError: 
    1616            tried.append(filepath) 
  • django/trunk/django/views/debug.py

    r1351 r1379  
    1 import re 
    2 import os 
    3 import sys 
    4 import inspect 
    51from django.conf import settings 
     2from django.core.template import Template, Context 
     3from django.utils.html import escape 
     4from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound 
     5import inspect, os, re, sys 
     6from itertools import count, izip 
    67from os.path import dirname, join as pathjoin 
    7 from django.core.template import Template, Context 
    8 from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound 
    98 
    109HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD') 
     10 
     11def linebreak_iter(template_source): 
     12    newline_re = re.compile("^", re.M) 
     13    for match in newline_re.finditer(template_source): 
     14        yield match.start() 
     15    yield len(template_source) + 1 
     16 
     17def get_template_exception_info(exc_type, exc_value, tb): 
     18    origin, (start, end) = exc_value.source 
     19    template_source = origin.reload() 
     20    context_lines = 10 
     21    line = 0 
     22    upto = 0 
     23    source_lines = [] 
     24    linebreaks = izip(count(0), linebreak_iter(template_source)) 
     25    linebreaks.next() # skip the nothing before initial line start 
     26    for num, next in linebreaks: 
     27        if start >= upto and end <= next: 
     28            line = num 
     29            before = escape(template_source[upto:start]) 
     30            during = escape(template_source[start:end]) 
     31            after = escape(template_source[end:next - 1]) 
     32        source_lines.append( (num, escape(template_source[upto:next - 1])) ) 
     33        upto = next 
     34    total = len(source_lines) 
     35 
     36    top = max(0, line - context_lines) 
     37    bottom = min(total, line + 1 + context_lines) 
     38 
     39    template_info = { 
     40        'message': exc_value.args[0], 
     41        'source_lines': source_lines[top:bottom], 
     42        'before': before, 
     43        'during': during, 
     44        'after': after, 
     45        'top': top , 
     46        'bottom': bottom , 
     47        'total': total, 
     48        'line': line, 
     49        'name': origin.name, 
     50    } 
     51    exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb) 
     52    return exc_info + (template_info,) 
    1153 
    1254def technical_500_response(request, exc_type, exc_value, tb): 
     
    1557    the values returned from sys.exc_info() and friends. 
    1658    """ 
     59    template_info = None 
     60    if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'): 
     61        exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) 
    1762    frames = [] 
    1863    while tb is not None: 
     
    2267        pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7) 
    2368        frames.append({ 
    24             'tb' : tb, 
    25             'filename' : filename, 
    26             'function' : function, 
    27             'lineno' : lineno, 
    28             'vars' : tb.tb_frame.f_locals.items(), 
    29             'id' : id(tb), 
    30             'pre_context' : pre_context, 
    31             'context_line' : context_line, 
    32             'post_context' : post_context, 
    33             'pre_context_lineno' : pre_context_lineno, 
     69            'tb': tb, 
     70            'filename': filename, 
     71            'function': function, 
     72            'lineno': lineno, 
     73            'vars': tb.tb_frame.f_locals.items(), 
     74            'id': id(tb), 
     75            'pre_context': pre_context, 
     76            'context_line': context_line, 
     77            'post_context': post_context, 
     78            'pre_context_lineno': pre_context_lineno, 
    3479        }) 
    3580        tb = tb.tb_next 
     
    4792    t = Template(TECHNICAL_500_TEMPLATE) 
    4893    c = Context({ 
    49         'exception_type' : exc_type.__name__, 
    50         'exception_value' : exc_value, 
    51         'frames' : frames, 
    52         'lastframe' : frames[-1], 
    53         'request' : request, 
    54         'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", 
    55         'settings' : settings_dict, 
    56  
     94        'exception_type': exc_type.__name__, 
     95        'exception_value': exc_value, 
     96        'frames': frames, 
     97        'lastframe': frames[-1], 
     98        'request': request, 
     99        'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http", 
     100        'settings': settings_dict, 
     101        'template_info': template_info, 
    57102    }) 
    58103    return HttpResponseServerError(t.render(c), mimetype='text/html') 
     
    70115    t = Template(TECHNICAL_404_TEMPLATE) 
    71116    c = Context({ 
    72         'root_urlconf' : settings.ROOT_URLCONF, 
    73         'urlpatterns' : tried, 
    74         'reason' : str(exception), 
    75         'request' : request, 
    76         'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", 
    77         'settings' : dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]), 
     117        'root_urlconf': settings.ROOT_URLCONF, 
     118        'urlpatterns': tried, 
     119        'reason': str(exception), 
     120        'request': request, 
     121        'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http", 
     122        'settings': dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]), 
    78123    }) 
    79124    return HttpResponseNotFound(t.render(c), mimetype='text/html') 
     
    145190    #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; } 
    146191    #requestinfo h3 { margin-bottom:-1em; } 
     192    table.source td { font-family: monospace; white-space: pre; } 
     193    span.specific { background:#ffcab7; } 
     194    .error { background: #ffc; } 
    147195  </style> 
    148196  <script type="text/javascript"> 
     
    222270  </table> 
    223271</div> 
    224  
     272{% if template_info %} 
     273<div id="template"> 
     274   <h2>Template</h2> 
     275   In template {{ template_info.name }}, error at line {{ template_info.line }} 
     276   <div>{{ template_info.message|escape }}</div> 
     277   <table class="source{% if template_info.top %} cut-top{% endif %}{% ifnotequal template_info.bottom template_info.total %} cut-bottom{% endifnotequal %}"> 
     278   {% for source_line