pystache icon indicating copy to clipboard operation
pystache copied to clipboard

Cannot render template tag within lambda

Open micahlmartin opened this issue 11 years ago • 5 comments

I have a case where I'm rendering a dictionary that also includes a lambda.

# Data
{
    'money': '12345',
    'format_currency': format_currency
}

# Template
{{/format_currency}}{{money}}{{/format_currency}}

def format_currency(value):
    # value={{money}}
    # how can I render the value?

The problem here is that I can't actually extract the value of {{money}} because I don't have access to a rendering engine and the context. While trying to figure out a solution I came across this http://stackoverflow.com/a/8851957/17744. This essentially indicates that I should instead be writing something like this:

def lambda_renderer(f):
    @wraps(f)
    def wrapper(text, engine, context):
        value = engine.render(text, context)
        return f(value)
    return wrapper

def format_currency(value):
    # return value formatted as currency

{
    'money': '12345',
    'format_currency': lambda_renderer(format_currency)
}

What is everyones thoughts on this? Am I doing something wrong? How else would you extract the value from the template?

micahlmartin avatar Nov 05 '13 06:11 micahlmartin

This is just a quick thought, but have you played around with using a view class as discussed in the README? You might be able to store references to the objects you need more easily there (e.g. the renderer instance). There's also this code comment about accessing the current context via the renderer, so that might also help.

cjerdonek avatar Nov 05 '13 22:11 cjerdonek

I started with the view classes at first, but I felt like it was a little bit of unnecessary overhead. Also, In some cases I'm rendering mustache views within a django view using a custom template tag like this. This also allows me to create a custom django context processor that will attach global variables and settings to any context being rendered. This is how I'm actually using the format_currency method. It's attached via the custom context processor.

micahlmartin avatar Nov 05 '13 23:11 micahlmartin

For now I've just monkey patched the method to suite my needs but I think it's something that should be added.

def _new_render(self, engine, context):
    """
    Monkey patching this method so that lambda methods can be passed
    the rendering engine and the context to properly render values
    """
    values = engine.fetch_section_data(context, self.key)

    parts = []
    for val in values:
        if callable(val):
            # START HACK #
            template_part = self.template[self.index_begin:self.index_end]
            args = inspect.getargspec(val).args
            result = None
            if len(args) == 3:
                result = val(self.template[self.index_begin:self.index_end], engine, context)
            else:
                result = val(self.template[self.index_begin:self.index_end])
            # END HACK #
            val = engine._render_value(result, context, delimiters=self.delimiters)
            parts.append(val)
            continue

        context.push(val)
        parts.append(self.parsed.render(engine, context))
        context.pop()

    return unicode(''.join(parts))

_SectionNode.render = _new_render

micahlmartin avatar Nov 05 '13 23:11 micahlmartin

I agree that Pystache should be enhanced to support this use case more easily. Issue #158 is from someone trying to do something similar. I'm not sure about the right API yet though. Any thoughts on a cleaner approach?

cjerdonek avatar Dec 19 '13 00:12 cjerdonek

Why not to provide an API similar to one described in official mustache docs ?

Lambdas

When the value is a callable object, such as a function or lambda, the object will be invoked and passed the block of text. The text passed is the literal block, unrendered. {{tags}} will not have been expanded - the lambda should do that on its own. In this way you can implement filters or caching.

Template:

{{#wrapped}}
  {{name}} is awesome.
{{/wrapped}}
Hash:

{
  "name": "Willy",
  "wrapped": function() {
    return function(text, render) {
      return "<b>" + render(text) + "</b>"
    }
  }
}
Output:

<b>Willy is awesome.</b>

i.e. this should be just

# in context of renderer:
...
if instanceof(callable,val):
    template_part = self.template[self.index_begin:self.index_end]
    render = lambda text: self.engine.render(text, context)
    result = val(template_part, render)
...

and API should declare requirement for lambda/function of 2 arguments

dikderoy avatar Jul 20 '17 15:07 dikderoy