pfalcon / utemplate

Micro template engine in Python with low memory usage, designed for Pycopy, a minimalist Python dialect, but also compatible with other Pythons.
https://github.com/pfalcon/pycopy
76 stars 8 forks source link

Feature Request - Parameterising Template Names in Include Directives #7

Closed cefn closed 6 years ago

cefn commented 7 years ago

Hi again.

Been using your templating framework with great success, but have hit a brick wall on a specific feature which is the last I need to resolve to control memory overhead for our application.

Hopefully the feature I'm proposing makes some sense. I get very lost in the utemplate code so I'm not optimistic being able to translate the feature request into a patch without some help.

I think it cashes out as needing the names of included templates to be able to be computed themselves by runtime values. It's not the full power of 'eval' (which would be incompatible with the design intent of utemplate), but it would allow runtime state to parameterise the call to the template resolver, e.g. when iterating over lists or dicts.

This is not eval, every include would still end up backed by a named template from the resolver, with the potential that it hits a cache (e.g. a pre-compiled frozen module)). However, my workaround DOES effectively use eval, and is horrible.

Roughly speaking, instead of being limited to just...

{% include 'subpage' %}

...you could have extra expressive power by having computable template names...

{% for key in keys %}{% include 'page{{key}}' %}{% endfor %}

BACKGROUND

Our templating strategy is illustrated by this line from https://github.com/cefn/avatap/blob/master/python/milecastles.py which defines arguments available to all render functions signature = "engine, story, box, node, card, sack"

This signature provides access to the text adventure engine, the whole story, the box the player has tapped on (these are RFID-sensing boxes in a museum), the node the player is at in the story, the card information of the player, and the player's sack (an inventory 'dict' loaded from their RFID)...

REQUIREMENT

The application requirement is illustrated by the (working) templates in the code-fragment under EVIL NESTED EVAL CODE below. To provide template authors with the needed features for our text adventure game, I have been forced to 'step outside' the template logic, and call the templating engine directly from within the render function. This implies a bit of an explosion of memory overhead, since it means eval is being called within eval! It also means that the embedded template is always evaled based on a runtime string and therefore can't be cached.

The motivation for the case is that authors write readable text used to render each choice you can make in the adventure (of one-to-many choices named by uid). However, these texts in turn use templating features, so they can't merely be static text, they should be loaded through the resolver/compiler with include.

Typically these parameterised template names will depend on a key from a Jinja2-style iteration. Our issue is that while the resolver can be augmented to handle special names like 'choices[choiceUid]' or 'choices[2]' or just 'choices_7' and can grab different backing templates in each parameterised case, there's no way to pass these values through include currently. Consequently I am using a reference to the templating engine directly within the template, and I am simply passing the template text (which can be accessed via Jinja2) to the engine from within the render function!

EVIL NESTED EVAL CODE

    choiceList = " {% for choiceUid in node.choiceNodeUids %}{% if not(node.isHidden(engine, choiceUid)) %}{% include 'choiceItem' choiceUid %}\n{% endif %}{% endfor %}"
    choiceItem = " {% args choiceUid %}{{ engine.concatenateGeneratedStrings(node, node.choices[choiceUid]) }} : {{story.lookupNode(choiceUid).getGoalBox(story).label}}"

This is so wrong, but it's ended up unavoidably unpacking this way. I wonder whether parameterised includes might offer important extra power without surrendering the efficiency/low overhead of cached generators. This would certainly fix our immediate problem.

DESIGN ISSUE

Of course, all of this implies that the resolver plays a part in the runtime execution of a render() function. I don't know if this is currently the case, or whether all includes are actually expanded by simply inlining at compile time. If all includes are always inlined, could it make sense to reverse this design decision, and keep the resolver in the loop even at runtime to get this extra power? It would also minimise duplication from embedding the same templates inline in multiple places.

pfalcon commented 6 years ago

Dynamic include names:

{% include {{inc_name}} %}

were implemented some time ago: https://github.com/pfalcon/utemplate/commit/24c5ceb827707db2a69d053327ec42c6520c7e79 .

That's the amount in which I can support dynamic include names, I apologize if it doesn't cover all the usecases above. I intend to close this ticket. If you really think that there're some important points not covered, and you'd like to keep it open, feel free to reopen.

cefn commented 6 years ago

Thanks for that work, @pfalcon. I think it will make a big difference when/if I get back to working with these templated pages.