classCodeBuilder:"""Build source code conveniently."""def__init__(self,indent_level=0,indent_str="\t"):self.code=[]self.indent_level=indent_levelself.indent_str=indent_strdef__str__(self):return"".join(map(str,self.code))defadd_line(self,line):"""Add a line of source to the code."""self.code.extend((self.indent_str*self.indent_level,line,"\n"))returnselfdefadd_section(self):"""Add a section, a sub-CodeBuilder."""section=CodeBuilder(self.indent_level,self.indent_str)self.code.append(section)returnsectiondefindent(self):"""Increase the current indent for following lines."""self.indent_level+=1returnselfdefdedent(self):"""Decrease the current indent for following lines."""self.indent_level-=1returnselfdefget_render_function(self)->FunctionType:"""Execute the code, and return a dict of globals it defines."""assertself.indent_level==0python_source=str(self)global_namespace={}exec(python_source,global_namespace)returnglobal_namespace["render"]
defadd_section(self):"""Add a section, a sub-CodeBuilder."""section=CodeBuilder(self.indent_level,self.indent_str)self.code.append(section)returnsection
defget_render_function(self)->FunctionType:"""Execute the code, and return a dict of globals it defines."""assertself.indent_level==0python_source=str(self)global_namespace={}exec(python_source,global_namespace)returnglobal_namespace["render"]
classCodeBuilder:"""Build source code conveniently."""def__init__(self,indent_level=0,indent_str="\t"):self.code=[]self.indent_level=indent_levelself.indent_str=indent_strdef__str__(self):return"".join(map(str,self.code))defadd_line(self,line):"""Add a line of source to the code."""self.code.extend((self.indent_str*self.indent_level,line,"\n"))returnselfdefadd_section(self):"""Add a section, a sub-CodeBuilder."""section=CodeBuilder(self.indent_level,self.indent_str)self.code.append(section)returnsectiondefindent(self):"""Increase the current indent for following lines."""self.indent_level+=1returnselfdefdedent(self):"""Decrease the current indent for following lines."""self.indent_level-=1returnselfdefget_render_function(self)->FunctionType:"""Execute the code, and return a dict of globals it defines."""assertself.indent_level==0python_source=str(self)global_namespace={}exec(python_source,global_namespace)returnglobal_namespace["render"]
defadd_section(self):"""Add a section, a sub-CodeBuilder."""section=CodeBuilder(self.indent_level,self.indent_str)self.code.append(section)returnsection
defget_render_function(self)->FunctionType:"""Execute the code, and return a dict of globals it defines."""assertself.indent_level==0python_source=str(self)global_namespace={}exec(python_source,global_namespace)returnglobal_namespace["render"]
classTemplateCore(AutoNaming):"""A simple template compiler, for a jinja2-like syntax."""def__init__(self,text:str):"""Construct a Templite with the given `text`."""self.text=textdef_flush(self):forlineinself._buffer:self._builder.add_line(line)self._buffer.clear()@staticmethoddef_unwrap_token(token:str):returndedent(token.strip()[2:-2].strip("-")).strip()def_on_literal_token(self,token:str):self._buffer.append(f"__append__({repr(token)})")def_on_eval_token(self,token):token=self._unwrap_token(token)if"\n"intoken:mod=parse(token)[*rest,last]=mod.bodyassertisinstance(last,Expr),"{{ }} block must end with an expression, or you should use {# #} block"self._buffer.extend(unparse(rest).splitlines())# type: ignoreexp=unparse(last)else:exp=tokenself._buffer.append(f"__append__({exp})")def_on_exec_token(self,token):self._buffer.extend(self._unwrap_token(token).splitlines())def_on_special_token(self,token,sync:bool):inner=self._unwrap_token(token)ifinner.startswith("end"):last=self._ops_stack.pop()assertlast==inner.removeprefix("end")self._flush()self._builder.dedent()else:op=inner.split(" ",1)[0]ifop=="if"orop=="for"orop=="while":self._ops_stack.append(op)self._flush()self._builder.add_line(f"{inner}:")self._builder.indent()elifop=="else"orop=="elif":self._flush()self._builder.dedent()self._builder.add_line(f"{inner}:")self._builder.indent()else:params:str=self._make_context(inner)ifsync:self._buffer.append(f"__append__({op}.render({params}))")else:self._buffer.append(f"__append__(await {op}.arender({params}))")@staticmethoddef_make_context(text:str):"""generate context parameter if specified otherwise use locals() by default"""ifversion_info>=(3,13):returnf"globals() | locals() | dict({text[text.index(' ')+1:]})"if" "intextelse"globals() | locals()"returnf"locals() | dict({text[text.index(' ')+1:]})"if" "intextelse"locals()"defcompile(self,sync=True,indent_str="\t"):self._buffer=[]self._ops_stack=[]self._builder=get_base_builder(sync,indent_str)fortokeninsplit_template_tokens(self.text):ifnottoken:continues_token=token.strip()ifs_token.startswith("{{")ands_token.endswith("}}"):self._on_eval_token(token)elifs_token.startswith("{#")ands_token.endswith("#}"):self._on_exec_token(token)elifs_token.startswith("{%")ands_token.endswith("%}")and"\n"notins_token:self._on_special_token(token,sync)else:self._on_literal_token(token)ifself._ops_stack:raiseSyntaxError(self._ops_stack)self._flush()self._builder.add_line("return ''.join(map(str, __parts__))")self._builder.dedent()error_handling:Literal["linecache","tempfile","file"]ifversion_info>=(3,13):error_handling="linecache"else:error_handling="tempfile"if__debug__else"file"def_patch_for_error_handling(self,sync:bool):matchself.error_handling:case"linecache":add_linecache(self.name,partial(self.get_script,sync,"\t"))case"file"|"tempfile":file=save_tempfile(self.name,self.get_script(sync,"\t"),self.error_handling=="tempfile")sys_path.append(str(file.parent))@cached_propertydef_render_code(self):self.compile()returnself._builder.get_render_function().__code__.replace(co_filename=self.name,co_name="render")defrender(self,context:Context)->str:try:returneval(self._render_code,context)exceptException:self._patch_for_error_handling(sync=True)raise@cached_propertydef_arender_code(self):self.compile(sync=False)returnself._builder.get_render_function().__code__.replace(co_filename=self.name,co_name="arender")asyncdefarender(self,context:Context)->str:try:returnawaiteval(self._arender_code,context)exceptException:self._patch_for_error_handling(sync=False)raisedefget_script(self,sync=True,indent_str=" "):"""compile template string into python script"""self.compile(sync,indent_str)returnstr(self._builder)