template inheritance is strange

Issue #15 resolved
graycat
created an issue

i expected following code to have output like "a, b, c". but it prints "a, a, a". is it my fault? or bug?

# coding: utf-8

from __future__ import print_function
from wheezy.template.engine import Engine
from wheezy.template.ext.core import CoreExtension
from wheezy.template.loader import DictLoader

base_template = """
@def page_content():
@end
@page_content()
"""

page_a = """@extends("base")

@def page_content():
  a
@end
"""

page_b = """@extends("base")

@def page_content():
  b
@end
"""

page_c = """@extends("base")

@def page_content():
  c
@end
"""

page_all = """
@include("page_a")
@include("page_b")
@include("page_c")
"""

pages = {
    "base": base_template,
    "page_a": page_a,
    "page_b": page_b,
    "page_c": page_c,
    "page_all": page_all
}

template_engine = Engine(
    loader=DictLoader(pages),
    extensions=[CoreExtension()]
)
template = template_engine.get_template("page_all")
print(template.render({}))

Comments (7)

  1. Andriy Kornatskyy repo owner

    The reason you see the output as it is because include directive does not create a new context (scope) of internal inheritance scheme.

    Here is a source code generated for each page:

    page_all:

    def render(ctx, local_defs, super_defs):
         _b = []; w = _b.append
         w('\n')
         w(_r("page_a", ctx, local_defs, super_defs))
         w(_r("page_b", ctx, local_defs, super_defs))
         w(_r("page_c", ctx, local_defs, super_defs))
         return ''.join(_b)
    

    page_a:

    def render(ctx, local_defs, super_defs):
         def page_content():
             return '  a\n'
         super_defs['page_content'] = page_content; page_content = local_defs.setdefault('page_content', page_content)
         return _r("base", ctx, local_defs, super_defs)
    

    base:

    def render(ctx, local_defs, super_defs):
         _b = []; w = _b.append
         w('\n')
         def page_content():return ''
         super_defs['page_content'] = page_content; page_content = local_defs.setdefault('page_content', page_content)
         w(page_content()); w('\n')
         return ''.join(_b)
    

    When page_all is rendered a first time the local_defs and super_defs are empty dicts. Then include directive invokes page_a which overwrites placeholder for page_content in super_defs unconditionally, and local_defs only if one is not defined yet. This is how inheritance is implemented.

    However page_b attempts to "overwrite" page_content but it is not possible since local_defs already has a function to be used as defined by inclusion of page_a. So this basically explains why you see your output.

    In order to "override" this behavior you can replace the build_out builder rule with the following:

    def my_build_out(builder, lineno, token, nodes):
        assert token == 'out'
        for lineno, token, value in nodes:
            if token == 'include':
                # empty dict for local_defs
                builder.add(lineno, 'w(_r(' + value +
                            ', ctx, {}, super_defs))')
            elif token == 'var':
                var, var_filters = value
                if var_filters:
                    for f in var_filters:
                        var = known_var_filters.get(f, f) + '(' + var + ')'
                builder.add(lineno, 'w(' + var + ')')
            elif value:
                builder.add(lineno, 'w(' + value + ')')
        return True
    

    Notice that builder for include token uses an empty dictionary for local_defs.

  2. graycat reporter

    I solved this problem using inherited CoreExtension. Thank you for your advice.

    # coding: utf-8
    from wheezy.template.ext.core import (
        CoreExtension,
        known_var_filters,
        build_extends,
        build_render_single_markup,
        build_def_single_markup,
        build_render,
        build_module,
        build_import,
        build_from,
        build_require,
        build_def_syntax_check,
        build_def_empty,
        build_def,
        build_compound,
        build_comment,
        build_end
    )
    
    
    def nonlocal_build_out(builder, lineno, token, nodes):
        assert token == 'out'
        for lineno, token, value in nodes:
            if token == 'include':
                builder.add(lineno, 'w(_r(' + value +
                            ', ctx, {}, super_defs))')
            elif token == 'var':
                var, var_filters = value
                if var_filters:
                    for f in var_filters:
                        var = known_var_filters.get(f, f) + '(' + var + ')'
                builder.add(lineno, 'w(' + var + ')')
            elif value:
                builder.add(lineno, 'w(' + value + ')')
        return True
    
    
    class NonLocalBuildOutCoreExtension(CoreExtension):
        builder_rules = [
            ('render', build_extends),
            ('render', build_render_single_markup),
            ('render', build_render),
            ('module', build_module),
            ('import ', build_import),
            ('from ', build_from),
            ('require', build_require),
            ('out', nonlocal_build_out),
            ('def ', build_def_syntax_check),
            ('def ', build_def_empty),
            ('def ', build_def_single_markup),
            ('def ', build_def),
            ('if ', build_compound),
            ('elif ', build_compound),
            ('else:', build_compound),
            ('for ', build_compound),
            ('#', build_comment),
            ('end', build_end),
        ]
    
  3. Andriy Kornatskyy repo owner

    You can shorten this to the following:

    class NonLocalBuildOutExtension(object):
        builder_rules = [
            ('out', nonlocal_build_out)
        ]
    

    and include your extension just before CoreExtension so it takes effect to override builder rule for out token.

    template_engine = Engine(
        loader=DictLoader(pages),
        extensions=[NonLocalBuildOutExtension(), CoreExtension()]
    )
    

    Further Thoughts:

    It probably makes sense to allow override nested out tokens, e.g. markup, var and include (split build_out).

    def build_out(builder, lineno, token, nodes):
        assert token == 'out'
        builder.build_block(nodes)
        return True
    
    
    def build_include(builder, lineno, token, value):
        assert token == 'include'
        builder.add(lineno, 'w(_r(' + value +
                            ', ctx, local_defs, super_defs))')
        return True
    
    
    def build_var(builder, lineno, token, value):
        assert token == 'var'
        var, var_filters = value
        if var_filters:
            for f in var_filters:
                var = known_var_filters.get(f, f) + '(' + var + ')'
        builder.add(lineno, 'w(' + var + ')')
        return True
    
    
    def build_markup(builder, lineno, token, value):
        assert token == 'markup'
        if value:
            builder.add(lineno, 'w(' + value + ')')
        return True
    

    this way you will be able to override build_include only.

  4. Log in to comment