Hello, I'm Andrey Zubko, student faculty of Computer Science in Zaporozhye State Engineering Academy. I'm senior chief developer of a local Internet Service Provider "Telza" which provides Internet and telephony services. My responsibilities includes enhancing, improving existent Billing system that is written by myself.
In GSOC program I want to improve template subsystem by integrating template compilation into python bytecode. Goals and challenges Integrating of template compilation has 2 goals. The first goal and the chief goal is to provide backward-compatibility to templates, custom tags, custom filters that are already written. The second goal is to minimize modifications of Django sources to provide more stability and faster integrating in trunk. Implementation Support of template compilation can be achieved by adding method 'compile' to every Node-derived class, and some other classes, functions, such as django.template.Variable, django.template.FilterExpression, django.template.NodeList, and others described further. 1. Node and Node-derived classes modifications Class Node and derived from it classes should have optional method – compile, it returns list of python bytecode commands for write in compiled template. It receives parameter parent_name that will contains name of uppermost tag/block. Parent_name is a name of uppermost tag/block that identifies entry in hash blocks_output that is used for caching all outputs. Only one case can provide parent_name to be overridden – meet of the '{% block %}' tag in a parent tag, this tag should override parent_name to its unique name. Approach for caching all output is used for providing template inheritance ability. Use of template Inheritance provides following cases : child template has one sequence of blocks, but parent template has another sequence of blocks child template has blocks that haven't specified in parent's template For solving this cases I provided list blocks_sequence_output that keeps sequence of blocks outputting. Implementation of 'compile' method in Node class : def compile(self,parent_name): self.generate_name(parent_name) compiled_code = ["t = Template('%s')" % self.template, "blocks_output['%s'] = blocks_output['%s'] + t._render(context)" % (self.parent_name,self.parent_name)] return compiled_code As you saw, in purposes of providing backward-compatibility all Node- derived classes, that haven't implemented 'compile' method, will use current rendering approach. Node-derived classes with implemented 'compile' method will be looked like : class TextNode(Node): def compile(self,parent_name): return ["blocks_output['%s'] = blocks_output['%s'] + \"\"\"%s \"\"\"" % (self.parent_name, self.parent_name, self.s)] As shown before 'compile' method is not so difficult to implement. To recursively compilation of Node-derived classes is used method 'compile' in the class NodeList : class NodeList(list): def generate_name(self,node): return id(node) def compile(self, parent_name=None): bits = [] for node in self: if isinstance(node, Node): if parent_name is None or isinstance(node, BlockNode) : if isinstance(node, BlockNode) : parent_name_new = node.name else : parent_name_new = self.generate_name(node) bits.append("blocks_output['%s']=''" % parent_name_new) else : parent_name_new = parent_name bits.append(self.compile_node(node, parent_name_new)) else: bits.append(node) return mark_safe(''.join([force_unicode(b) for b in bits])) Also this function is used for initialising blocks_output hashes for uppermost nodes. 2. Blocks and dynamical inheritance I have described before Node-derived classes improvements, but django.template.loader_tags.BlockNode, django.template.loader_tags, django.template.loader_tages.ExtendsNode and others have big influence on compiled template architecture design, that's why I have allocated whole section for them. Internal variables blocks_output, blocks_output_sequence are used for providing support of dynamic inheritance. They specify the way how template should be rendered, in what sequence, what blocks should be outputted to user. 'blocks_output' hash collects blocks or tags outputs, and blocks_output_sequence saves sequence of block outputting. Because parent template can contain its own sequence of blocks that is saved in blocks_output_sequence, we must update the child blocks_output_sequence without right of overriding it. Because parent template can contain own blocks that is not present in child template, we must update child blocks_output hash with not existing items in it. By solving this requirements class BlockNode will be looked like : class BlockNode(Node): def super_compile(self): compiled_code = ["blocks_output['%s'] = blocks_output['%s'] + blocks_output_parent['%s']" % (self.name,self.name,self.name)] return compiled_code def compile(self,parent_name): compiled_childs = self.nodelist.compile() return compiled_childs blocks_output_parent – is blocks_output hash received from parent template that contains cached parent output. For providing support of static and dynamic inheritance class ExtendsNode should have 'compile' method that will be looked like : class ExtendsNode(Node): def get_parent_compiled(self): if self.parent_name_expr: # dynamical inheritance parent = self.parent_name_expr.compile() if not parent : if self.parent_name_expr: error_msg = " Got this from the '%s' variable." % self.parent_name_expr.token raise TemplateSyntaxError(error_msg) else : # static inheritance parent = self.parent_name if not parent: error_msg = "Invalid template name in 'extends' tag: %r." % parent raise TemplateSyntaxError(error_msg) if hasattr(parent, 'compile'): return parent.compile() # parent is a Template object code = ["blocks_output, blocks_sequence_output_parent = get_compiled_template(%s,no_output=True)" % self.parent_name, "blocks_output_parent = blocks_output"] return code def compile(self,parent_name): compiled_parent = self.get_compiled_parent() return compiled_parent 3. Variables and filters modifications Support of variables and filters are implemented by django.template.FilterExpression class. It is wrapped by VariableNode class – Node-derived class that implements simple call of FilterExpression instance. For supporting compilation into python bytecode VariableNode class should contain 'compile' function that calls 'compile' method of FilterExpression instance : class VariableNode(Node): def __compile__(self,parent_name): return self.filter_expression.compile() And FilterExpression should have implemented 'compile' method in which it calls filters compiled functions and variable resolving : class FilterExpression(object): def compile(self, ignore_failures=False): if isinstance(self.var, Variable): obj = self.var.compile(ignore_failures,settings.COMPILED_TEMPLATE_STRING_IF_INVALID) else: obj = self.var for func, args in self.filters: arg_vals = "" i = 0 for lookup, arg in args: if i > 0 : arg_vals = arg_vals + "," if not lookup: arg_vals = arg_vals + "\"%s\"" % mark_safe(arg) else: arg_vals = arg_vals + "%s" % arg.compile(context) if getattr(func, 'needs_autoescape', False): new_obj = "%s(%s,autoescape=context.autoescape,%s)" % (func.__name__,obj,arg_vals) else: new_obj = "%s(%s,%s)" % (func.__name__,obj,arg_vals) if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): obj = "mark_safe(%s)" % new_obj elif isinstance(obj, EscapeData): obj = "mark_for_escaping(%s)" % new_obj else: obj = new_obj return [obj] By the way, 'compile' method FilterExpression has no parent_name param – it is omitted because FilterExpression can't produce a block – it can be called only from a block – VariableNode. And django.template.Variable receives parameters ignore_failures, and default value for variable : class Variable(object): def compile(self,ignore_failures,default_value): if self.lookups is not None: value = "resolve(context,'%s',%s,'%s')" % (self.lookups,ignore_failures,default_value) else: # We're dealing with a literal, so it's already been "resolved" value = "'%s'" % self.literal if self.translate: value = "_(%s)" % value Using the Variable class introduced us to inner compiled template function named 'resolve'. 'resolve' function implements finding entry in context with looking name, value of entry can be simple value, class, function. 'resolve' receives params : 1.context – context variable that is passed to compiled template 'render' method 2.key – name of looking entry 3.ignore_failures – if ignore_failures is true and finding entry is failed, then it will return default_value or if it is omitted – None, otherwise it will raise VariableDoesNotExist exception 4.default_value – default value Compiled Template Architecture Compiled template contains two sections : 1.Dependencies section 2.Compiled code section Dependencies section is section that has default imports, and external imports of user-written custom tags, filters. External imports are determined by calling {% load %} tag. Compiled code section is a section that contains template's python byte code. Structure of the compiled code : # -- DEPENDENCIES SECTION # ----- Default imports from django.template import Template, get_compiled_template from django.template.defaultfilters import * # ----- User custom imports # from ... import ... # -- END OF DEPENDENCIES SECTION def resolve(context,ignore_failures,default_value): """ should be looked like method _resolve_lookup in django.template.Variable """ blocks_output = {} blocks_output_sequence = [] blocks_sequence_output_parent = None def output(): for block_name in blocks_output_sequence : print blocks_output.get(block_name,'') def render(context,no_output=False): # START OF COMPILED TEMPLATE PYTHON BYTECODE # ... # END OF COMPILED TEMPLATE PYTHON BYTECODE if blocks_sequence_output_parent is not None : blocks_sequence_output = blocks_sequence_output_parent if no_output : return blocks_output, blocks_output_sequence else : output() Every compiled template has 3 functions : resolve – discussed earlier output – task of this function is to output blocks output cache in the determined sequence render – entrypoint of compiled template, that contains template's compiled python bytecode. It supports one optional parameter – 'no_output' – it is used only when called parent template – not to output, to return cached data. Examples Examples based on already written code : compiling variables and filters : from django.template import Parser, FilterExpression token = 'variable|default:"Default value"|date:"Y-m-d"' p = Parser('') fe = FilterExpression(token, p) fe.compile() => 'date(default(resolve(context,variable,None,''),"Default value"),"Y-m- d")' template compilation sample : child.html : {% extends parent %} {% block first_block %} {% if True %} some_info {% endif %} {% endblock %} {% block second_block %} {% endblock %} parent.html : TextNode {% block first_block %} {% endblock %} => Compiled child template : # -- DEPENDENCIES SECTION # ----- Default imports from django.template import Template, get_compiled_template from django.template.defaultfilters import * # ----- User custom imports # from ... import ... # -- END OF DEPENDENCIES SECTION def resolve(context,key,ignore_failures,default_value): """ should be looked like method _resolve_lookup in django.template.Variable """ blocks_output = {} blocks_output_sequence = [] blocks_sequence_output_parent = None def output(): for block_name in blocks_output_sequence : print blocks_output.get(block_name,'') def render(context,no_output=False): blocks_output, blocks_sequence_output_parent = get_compiled_template(resolve(context,'parent',None,''),no_output=True) blocks_output_parent = blocks_output blocks_output['first_block'] = '' # entering block tag in NodeList compile blocks_sequence_output.append('first_block') if True : blocks_output['first_block'] = blocks_output['first_block'] + "some_info" blocks_output['second_block'] = '' # entering block tag in NodeList compile if blocks_sequence_output_parent is not None : blocks_sequence_output = blocks_sequence_output_parent if no_output : return blocks_output, blocks_output_sequence else : output() Compiled parent template : # -- DEPENDENCIES SECTION # ----- Default imports from django.template import Template, get_compiled_template from django.template.defaultfilters import * # ----- User custom imports # from ... import ... # -- END OF DEPENDENCIES SECTION def resolve(context,key,ignore_failures,default_value): """ should be looked like method _resolve_lookup in django.template.Variable """ blocks_output = {} blocks_output_sequence = [] blocks_sequence_output_parent = None def output(): for block_name in blocks_output_sequence : print blocks_output.get(block_name,'') def render(context,no_output=False): blocks_output['first_block'] = '' # entering block tag in NodeList compile blocks_sequence_output.append('first_block') if blocks_sequence_output_parent is not None : blocks_sequence_output = blocks_sequence_output_parent if no_output : return blocks_output, blocks_output_sequence else : output() Milestones 1. Designing concept - current step 2. Coding "template compilation" subsystem 3. Writing unit-tests 4. Optimization of written code The code examples here are not final, but intermediates, to show the basic algorithms of creating and execution of compiled templates. -- You received this message because you are subscribed to the Google Groups "Django developers" group. To post to this group, send email to django-developers@googlegroups.com. To unsubscribe from this group, send email to django-developers+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.