From a4a6d7ceaaeca5f6b6fbc26ba5eab87e723c467c Mon Sep 17 00:00:00 2001 From: Thomas Applencourt Date: Thu, 16 Feb 2017 19:14:37 -0600 Subject: [PATCH] Move lib --- src/lib/__init__.py | 0 src/lib/__init__.pyc | Bin 0 -> 165 bytes src/lib/manager.py | 10 + src/lib/manager.pyc | Bin 0 -> 311 bytes src/lib/static_ashes.py | 2602 ++++++++++++++++++++++++++++++++++++++ src/lib/static_ashes.pyc | Bin 0 -> 99433 bytes src/lib/static_irpy.py | 161 +++ 7 files changed, 2773 insertions(+) create mode 100644 src/lib/__init__.py create mode 100644 src/lib/__init__.pyc create mode 100644 src/lib/manager.py create mode 100644 src/lib/manager.pyc create mode 100644 src/lib/static_ashes.py create mode 100644 src/lib/static_ashes.pyc create mode 100644 src/lib/static_irpy.py diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/__init__.pyc b/src/lib/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf8d3fde2d852329102bd7b9fe121b15009c39e6 GIT binary patch literal 165 zcmZSn%*%DtWm!Zr0~9a^Gl0L^uzOuvh@Rs^0QKtOY{p2&GZb6^)rhK(ku=1i;I%=_4L40PG*vR he0*kJW=VX!UO{CE2hd2H-29Z%oK!oI#l=9(007PvCV2n= literal 0 HcmV?d00001 diff --git a/src/lib/manager.py b/src/lib/manager.py new file mode 100644 index 0000000..ed54d27 --- /dev/null +++ b/src/lib/manager.py @@ -0,0 +1,10 @@ +try: + import irpy +except: + import static_irpy as irpy + +try: + import ashes +except: + import static_ashes as ashes + diff --git a/src/lib/manager.pyc b/src/lib/manager.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd188400523ad59038fbc62563152f2338fa08e3 GIT binary patch literal 311 zcmZ8b(MrQW5Zp}^idHH36M4#31VIq-#Yd$erJ*l!UQU~uKF5po#2c;RH56)a~v8?tnAyMPoa%&oW|++G8h>&jiAqvvQ!sT;>I^dP1n_~ zwSE_(tbF*AMeti^qjcA|`TR!E|8X}L4Oa4ept^dO#\/)?' + r'(?:(?P[\~\#\?\@\:\<\>\+\^\%])\s*)?' + r'(?P[a-zA-Z0-9_\$\.]+|"[^"]+")' + r'(?:\:(?P[a-zA-Z0-9\$\.]+))?' + r'(?P[\|a-z]+)*?' + r'(?P(?:\s+\w+\=(("[^"]*?")|([$\w\.]+)))*)?' + r'\s*' + r'(?P\/)?' + r'\})', + flags=re.MULTILINE) + +key_re_str = '[a-zA-Z_$][0-9a-zA-Z_$]*' +key_re = re.compile(key_re_str) +path_re = re.compile('(' + key_re_str + ')?(\.' + key_re_str + ')+') +comment_re = re.compile(r'(\{!.+?!\})|(\{`.+?`\})', flags=re.DOTALL) + + +def get_path_or_key(pork): + if pork == '.': + pk = ['path', True, []] + elif path_re.match(pork): + f_local = pork.startswith('.') + if f_local: + pork = pork[1:] + pk = ['path', f_local, pork.split('.')] + elif key_re.match(pork): + pk = ['key', pork] + else: + raise ValueError('expected a path or key, not %r' % pork) + return pk + + +def split_leading(text): + leading_stripped = text.lstrip() + leading_ws = text[:len(text) - len(leading_stripped)] + return leading_ws, leading_stripped + + +class Token(object): + def __init__(self, text): + self.text = text + + def get_line_count(self): + # returns 0 if there's only one line, because the + # token hasn't increased the number of lines. + count = len(self.text.splitlines()) - 1 + if self.text[-1] in ('\n', '\r'): + count += 1 + return count + + def __repr__(self): + cn = self.__class__.__name__ + disp = self.text + if len(disp) > 20: + disp = disp[:17] + '...' + return '%s(%r)' % (cn, disp) + + +class CommentToken(Token): + def to_dust_ast(self): + return [['comment', self.text]] + + +class RawToken(Token): + def to_dust_ast(self): + return [['raw', self.text]] + + +class BufferToken(Token): + def to_dust_ast(self): + # It is hard to simulate the PEG parsing in this case, + # especially while supporting universal newlines. + if not self.text: + return [] + rev = [] + remaining_lines = self.text.splitlines() + if self.text[-1] in ('\n', '\r'): + # kind of a bug in splitlines if you ask me. + remaining_lines.append('') + while remaining_lines: + line = remaining_lines.pop() + leading_ws, lstripped = split_leading(line) + if remaining_lines: + if lstripped: + rev.append(['buffer', lstripped]) + rev.append(['format', '\n', leading_ws]) + else: + if line: + rev.append(['buffer', line]) + ret = list(reversed(rev)) + return ret + + +ALL_ATTRS = ('closing', 'symbol', 'refpath', 'contpath', + 'filters', 'params', 'selfclosing') + + +class Tag(Token): + req_attrs = () + ill_attrs = () + + def __init__(self, text, **kw): + super(Tag, self).__init__(text) + self._attr_dict = kw + self.set_attrs(kw) + + @property + def param_list(self): + try: + return params_to_kv(self.params) + except AttributeError: + return [] + + @property + def name(self): + try: + return self.refpath.strip().lstrip('.') + except (AttributeError, TypeError): + return None + + def set_attrs(self, attr_dict, raise_exc=True): + cn = self.__class__.__name__ + all_attrs = getattr(self, 'all_attrs', ()) + if all_attrs: + req_attrs = [a for a in ALL_ATTRS if a in all_attrs] + ill_attrs = [a for a in ALL_ATTRS if a not in all_attrs] + else: + req_attrs = getattr(self, 'req_attrs', ()) + ill_attrs = getattr(self, 'ill_attrs', ()) + + opt_attrs = getattr(self, 'opt_attrs', ()) + if opt_attrs: + ill_attrs = [a for a in ill_attrs if a not in opt_attrs] + for attr in req_attrs: + if attr_dict.get(attr, None) is None: + raise ValueError('%s expected %s' % (cn, attr)) + for attr in ill_attrs: + if attr_dict.get(attr, None) is not None: + raise ValueError('%s does not take %s' % (cn, attr)) + + avail_attrs = [a for a in ALL_ATTRS if a not in ill_attrs] + for attr in avail_attrs: + setattr(self, attr, attr_dict.get(attr, '')) + return True + + @classmethod + def from_match(cls, match): + kw = dict([(str(k), v.strip()) + for k, v in match.groupdict().items() + if v is not None and v.strip()]) + obj = cls(text=match.group(0), **kw) + obj.orig_match = match + return obj + + +class ReferenceTag(Tag): + all_attrs = ('refpath',) + opt_attrs = ('filters',) + + def to_dust_ast(self): + pork = get_path_or_key(self.refpath) + filters = ['filters'] + if self.filters: + f_list = self.filters.split('|')[1:] + for f in f_list: + filters.append(f) + return [['reference', pork, filters]] + + +class SectionTag(Tag): + ill_attrs = ('closing') + + +class ClosingTag(Tag): + all_attrs = ('closing', 'refpath') + + +class SpecialTag(Tag): + all_attrs = ('symbol', 'refpath') + + def to_dust_ast(self): + return [['special', self.refpath]] + + +class BlockTag(Tag): + all_attrs = ('symbol', 'refpath') + + +class PartialTag(Tag): + req_attrs = ('symbol', 'refpath', 'selfclosing') + + def __init__(self, **kw): + super(PartialTag, self).__init__(**kw) + self.subtokens = parse_inline(self.refpath) + + def to_dust_ast(self): + """ + 2014.05.09 + This brings compatibility to the more popular fork of Dust.js + from LinkedIn (v1.0) + + Adding in `params` so `partials` function like sections. + """ + context = ['context'] + contpath = self.contpath + if contpath: + context.append(get_path_or_key(contpath)) + + params = ['params'] + param_list = self.param_list + if param_list: + try: + params.extend(params_to_dust_ast(param_list)) + except ParseError as pe: + pe.token = self + raise + + # tying to make this more standardized + inline_body = inline_to_dust_ast(self.subtokens) + return [['partial', + inline_body, + context, + params, + ]] + + +def parse_inline(source): + if not source: + raise ParseError('empty inline token') + if source.startswith('"') and source.endswith('"'): + source = source[1:-1] + if not source: + return [BufferToken("")] + tokens = tokenize(source, inline=True) + return tokens + + +def inline_to_dust_ast(tokens): + if tokens and all(isinstance(t, BufferToken) for t in tokens): + body = ['literal', ''.join(t.text for t in tokens)] + else: + body = ['body'] + for b in tokens: + body.extend(b.to_dust_ast()) + return body + + +def params_to_kv(params_str): + ret = [] + new_k, v = None, None + p_str = params_str.strip() + k, _, tail = p_str.partition('=') + while tail: + tmp, _, tail = tail.partition('=') + tail = tail.strip() + if not tail: + v = tmp + else: + v, new_k = tmp.split() + ret.append((k.strip(), v.strip())) + k = new_k + return ret + + +def params_to_dust_ast(param_kv): + ret = [] + for k, v in param_kv: + try: + v_body = get_path_or_key(v) + except ValueError: + v_body = inline_to_dust_ast(parse_inline(v)) + ret.append(['param', ['literal', k], v_body]) + return ret + + +def get_tag(match, inline=False): + groups = match.groupdict() + symbol = groups['symbol'] + closing = groups['closing'] + refpath = groups['refpath'] + if closing: + tag_type = ClosingTag + elif symbol is None and refpath is not None: + tag_type = ReferenceTag + elif symbol in '#?^<+@%': + tag_type = SectionTag + elif symbol == '~': + tag_type = SpecialTag + elif symbol == ':': + tag_type = BlockTag + elif symbol == '>': + tag_type = PartialTag + else: + raise ParseError('invalid tag symbol: %r' % symbol) + if inline and tag_type not in (ReferenceTag, SpecialTag): + raise ParseError('invalid inline tag') + return tag_type.from_match(match) + + +def tokenize(source, inline=False): + tokens = [] + com_nocom = comment_re.split(source) + line_counts = [1] + + def _add_token(t): + # i wish i had nonlocal so bad + t.start_line = sum(line_counts) + line_counts.append(t.get_line_count()) + t.end_line = sum(line_counts) + tokens.append(t) + for cnc in com_nocom: + if not cnc: + continue + elif cnc.startswith('{!') and cnc.endswith('!}'): + _add_token(CommentToken(cnc[2:-2])) + continue + elif cnc.startswith('{`') and cnc.endswith('`}'): + _add_token(RawToken(cnc[2:-2])) + continue + prev_end = 0 + start = None + end = None + for match in node_re.finditer(cnc): + start, end = match.start(1), match.end(1) + if prev_end < start: + _add_token(BufferToken(cnc[prev_end:start])) + prev_end = end + try: + _add_token(get_tag(match, inline)) + except ParseError as pe: + pe.line_no = sum(line_counts) + raise + tail = cnc[prev_end:] + if tail: + _add_token(BufferToken(tail)) + return tokens + +######### +# PARSING +######### + + +class Section(object): + def __init__(self, start_tag=None, blocks=None): + if start_tag is None: + refpath = None + name = '' + else: + refpath = start_tag.refpath + name = start_tag.name + + self.refpath = refpath + self.name = name + self.start_tag = start_tag + self.blocks = blocks or [] + + def add(self, obj): + if type(obj) == Block: + self.blocks.append(obj) + else: + if not self.blocks: + self.blocks = [Block()] + self.blocks[-1].add(obj) + + def to_dict(self): + ret = {self.name: dict([(b.name, b.to_list()) for b in self.blocks])} + return ret + + def to_dust_ast(self): + symbol = self.start_tag.symbol + + pork = get_path_or_key(self.refpath) + + context = ['context'] + contpath = self.start_tag.contpath + if contpath: + context.append(get_path_or_key(contpath)) + + params = ['params'] + param_list = self.start_tag.param_list + if param_list: + try: + params.extend(params_to_dust_ast(param_list)) + except ParseError as pe: + pe.token = self + raise + + bodies = ['bodies'] + if self.blocks: + for b in reversed(self.blocks): + bodies.extend(b.to_dust_ast()) + + return [[symbol, + pork, + context, + params, + bodies]] + + +class Block(object): + def __init__(self, name='block'): + if not name: + raise ValueError('blocks need a name, not: %r' % name) + self.name = name + self.items = [] + + def add(self, item): + self.items.append(item) + + def to_list(self): + ret = [] + for i in self.items: + try: + ret.append(i.to_dict()) + except AttributeError: + ret.append(i) + return ret + + def _get_dust_body(self): + # for usage by root block in ParseTree + ret = [] + for i in self.items: + ret.extend(i.to_dust_ast()) + return ret + + def to_dust_ast(self): + name = self.name + body = ['body'] + dust_body = self._get_dust_body() + if dust_body: + body.extend(dust_body) + return [['param', + ['literal', name], + body]] + + +class ParseTree(object): + def __init__(self, root_block): + self.root_block = root_block + + def to_dust_ast(self): + ret = ['body'] + ret.extend(self.root_block._get_dust_body()) + return ret + + @classmethod + def from_tokens(cls, tokens): + root_sect = Section() + ss = [root_sect] # section stack + for token in tokens: + if type(token) == SectionTag: + new_s = Section(token) + ss[-1].add(new_s) + if not token.selfclosing: + ss.append(new_s) + elif type(token) == ClosingTag: + if len(ss) <= 1: + msg = 'closing tag before opening tag: %r' % token.text + raise ParseError(msg, token=token) + if token.name != ss[-1].name: + msg = ('improperly nested tags: %r does not close %r' % + (token.text, ss[-1].start_tag.text)) + raise ParseError(msg, token=token) + ss.pop() + elif type(token) == BlockTag: + if len(ss) <= 1: + msg = 'start block outside of a section: %r' % token.text + raise ParseError(msg, token=token) + new_b = Block(name=token.refpath) + ss[-1].add(new_b) + else: + ss[-1].add(token) + if len(ss) > 1: + raise ParseError('unclosed tag: %r' % ss[-1].start_tag.text, + token=ss[-1].start_tag) + return cls(root_sect.blocks[0]) + + @classmethod + def from_source(cls, src): + tokens = tokenize(src) + return cls.from_tokens(tokens) + + +############## +# Optimize AST +############## +DEFAULT_SPECIAL_CHARS = {'s': ' ', + 'n': '\n', + 'r': '\r', + 'lb': '{', + 'rb': '}'} + +DEFAULT_OPTIMIZERS = { + 'body': 'compact_buffers', + 'special': 'convert_special', + 'format': 'nullify', + 'comment': 'nullify'} + +for nsym in ('buffer', 'filters', 'key', 'path', 'literal', 'raw'): + DEFAULT_OPTIMIZERS[nsym] = 'noop' + +for nsym in ('#', '?', '^', '<', '+', '@', '%', 'reference', + 'partial', 'context', 'params', 'bodies', 'param'): + DEFAULT_OPTIMIZERS[nsym] = 'visit' + +UNOPT_OPTIMIZERS = dict(DEFAULT_OPTIMIZERS) +UNOPT_OPTIMIZERS.update({'format': 'noop', 'body': 'visit'}) + + +def escape(text, esc_func=json.dumps): + return esc_func(text) + + +class Optimizer(object): + def __init__(self, optimizers=None, special_chars=None): + if special_chars is None: + special_chars = DEFAULT_SPECIAL_CHARS + self.special_chars = special_chars + + if optimizers is None: + optimizers = DEFAULT_OPTIMIZERS + self.optimizers = dict(optimizers) + + def optimize(self, node): + # aka filter_node() + nsym = node[0] + optimizer_name = self.optimizers[nsym] + return getattr(self, optimizer_name)(node) + + def noop(self, node): + return node + + def nullify(self, node): + return None + + def convert_special(self, node): + return ['buffer', self.special_chars[node[1]]] + + def visit(self, node): + ret = [node[0]] + for n in node[1:]: + filtered = self.optimize(n) + if filtered: + ret.append(filtered) + return ret + + def compact_buffers(self, node): + ret = [node[0]] + memo = None + for n in node[1:]: + filtered = self.optimize(n) + if not filtered: + continue + if filtered[0] == 'buffer': + if memo is not None: + memo[1] += filtered[1] + else: + memo = filtered + ret.append(filtered) + else: + memo = None + ret.append(filtered) + return ret + + def __call__(self, node): + return self.optimize(node) + + +######### +# Compile +######### + + +ROOT_RENDER_TMPL = \ +'''def render(chk, ctx): + {body} + return {root_func_name}(chk, ctx) +''' + + +def _python_compile(source): + """ + Generates a Python `code` object (via `compile`). + + args: + source: (required) string of python code to be compiled + + this actually compiles the template to code + """ + try: + code = compile(source, '', 'single') + return code + except: + raise + + +def _python_exec(code, name, global_env=None): + """ + this loads a code object (generated via `_python_compile` + + args: + code: (required) code object (generate via `_python_compile`) + name: (required) the name of the function + + kwargs: + global_env: (default None): the environment + """ + if global_env is None: + global_env = {} + else: + global_env = dict(global_env) + if PY3: + exec(code, global_env) + else: + exec("exec code in global_env") + return global_env[name] + + +def python_string_to_code(python_string): + """ + utility function + used to compile python string functions to code object + + args: + ``python_string`` + """ + code = _python_compile(python_string) + return code + + +def python_string_to_function(python_string): + """ + utility function + used to compile python string functions for template loading/caching + + args: + ``python_string`` + """ + code = _python_compile(python_string) + function = _python_exec(code, name='render', global_env=None) + return function + + +class Compiler(object): + """ + Note: Compiler objects aren't really meant to be reused, + the class is just for namespacing and convenience. + """ + sections = {'#': 'section', + '?': 'exists', + '^': 'notexists'} + nodes = {'<': 'inline_partial', + '+': 'region', + '@': 'helper', + '%': 'pragma'} + + def __init__(self, env=None): + if env is None: + env = default_env + self.env = env + + self.bodies = {} + self.blocks = {} + self.block_str = '' + self.index = 0 + self.auto = self.env.autoescape_filter + + def compile(self, ast, name='render'): + python_source = self._gen_python(ast) + python_code = _python_compile(python_source) + python_func = _python_exec(python_code, name=name) + return (python_code, python_func) + + def _gen_python(self, ast): # ast to init? + lines = [] + c_node = self._node(ast) + + block_str = self._root_blocks() + + bodies = self._root_bodies() + lines.extend(bodies.splitlines()) + if block_str: + lines.extend(['', block_str, '']) + body = '\n '.join(lines) + + ret = ROOT_RENDER_TMPL.format(body=body, + root_func_name=c_node) + self.python_source = ret + return ret + + def _root_blocks(self): + if not self.blocks: + self.block_str = '' + return '' + self.block_str = 'ctx = ctx.shift_blocks(blocks)\n ' + pairs = ['"' + name + '": ' + fn for name, fn in self.blocks.items()] + return 'blocks = {' + ', '.join(pairs) + '}' + + def _root_bodies(self): + max_body = max(self.bodies.keys()) + ret = [''] * (max_body + 1) + for i, body in self.bodies.items(): + ret[i] = ('\ndef body_%s(chk, ctx):\n %sreturn chk%s\n' + % (i, self.block_str, body)) + return ''.join(ret) + + def _convert_special(self, node): + return ['buffer', self.special_chars[node[1]]] + + def _node(self, node): + ntype = node[0] + if ntype in self.sections: + stype = self.sections[ntype] + return self._section(node, stype) + elif ntype in self.nodes: + ntype = self.nodes[ntype] + cfunc = getattr(self, '_' + ntype, None) + if not callable(cfunc): + raise TypeError('unsupported node type: "%r"', node[0]) + return cfunc(node) + + def _body(self, node): + index = self.index + self.index += 1 # make into property, equal to len of bodies? + name = 'body_%s' % index + self.bodies[index] = self._parts(node) + return name + + def _parts(self, body): + parts = [] + for part in body[1:]: + parts.append(self._node(part)) + return ''.join(parts) + + def _raw(self, node): + return '.write(%r)' % node[1] + + def _buffer(self, node): + return '.write(%s)' % escape(node[1]) + + def _format(self, node): + return '.write(%s)' % escape(node[1] + node[2]) + + def _reference(self, node): + return '.reference(%s,ctx,%s)' % (self._node(node[1]), + self._node(node[2])) + + def _section(self, node, cmd): + return '.%s(%s,%s,%s,%s)' % (cmd, + self._node(node[1]), + self._node(node[2]), + self._node(node[4]), + self._node(node[3])) + + def _inline_partial(self, node): + bodies = node[4] + for param in bodies[1:]: + btype = param[1][1] + if btype == 'block': + self.blocks[node[1][1]] = self._node(param[2]) + return '' + return '' + + def _region(self, node): + """aka the plus sign ('+') block""" + tmpl = '.block(ctx.get_block(%s),%s,%s,%s)' + return tmpl % (escape(node[1][1]), + self._node(node[2]), + self._node(node[4]), + self._node(node[3])) + + def _helper(self, node): + return '.helper(%s,%s,%s,%s)' % (escape(node[1][1]), + self._node(node[2]), + self._node(node[4]), + self._node(node[3])) + + def _pragma(self, node): + pr_name = node[1][1] + pragma = self.env.pragmas.get(pr_name) + if not pragma or not callable(pragma): + return '' # TODO: raise? + raw_bodies = node[4] + bodies = {} + for rb in raw_bodies[1:]: + bodies[rb[1][1]] = rb[2] + + raw_params = node[3] + params = {} + for rp in raw_params[1:]: + params[rp[1][1]] = rp[2][1] + + try: + ctx = node[2][1][1] + except (IndexError, AttributeError): + ctx = None + + return pragma(self, ctx, bodies, params) + + def _partial(self, node): + """ + 2014.05.09 + This brings compatibility to the more popular fork of Dust.js + from LinkedIn (v1.0) + + Adding in `params` so `partials` function like sections. + updating call to .partial() to include the kwargs + + dust.js reference : + compile.nodes = { + partial: function(context, node) { + return '.partial(' + + compiler.compileNode(context, node[1]) + + ',' + compiler.compileNode(context, node[2]) + + ',' + compiler.compileNode(context, node[3]) + ')'; + }, + """ + if node[0] == 'body': + body_name = self._node(node[1]) + return '.partial(' + body_name + ', %s)' % self._node(node[2]) + return '.partial(%s, %s, %s)' % (self._node(node[1]), + self._node(node[2]), + self._node(node[3])) + + def _context(self, node): + contpath = node[1:] + if contpath: + return 'ctx.rebase(%s)' % (self._node(contpath[0])) + return 'ctx' + + def _params(self, node): + parts = [self._node(p) for p in node[1:]] + if parts: + return '{' + ','.join(parts) + '}' + return 'None' + + def _bodies(self, node): + parts = [self._node(p) for p in node[1:]] + return '{' + ','.join(parts) + '}' + + def _param(self, node): + return ':'.join([self._node(node[1]), self._node(node[2])]) + + def _filters(self, node): + ret = '"%s"' % self.auto + f_list = ['"%s"' % f for f in node[1:]] # repr? + if f_list: + ret += ',[%s]' % ','.join(f_list) + return ret + + def _key(self, node): + return 'ctx.get(%r)' % node[1] + + def _path(self, node): + cur = node[1] + keys = node[2] or [] + return 'ctx.get_path(%s, %s)' % (cur, keys) + + def _literal(self, node): + return escape(node[1]) + + +######### +# Runtime +######### + + +class UndefinedValueType(object): + def __repr__(self): + return self.__class__.__name__ + '()' + + def __str__(self): + return '' + + +UndefinedValue = UndefinedValueType() + +# Prerequisites for escape_url_path + + +def _make_quote_map(allowed_chars): + ret = {} + for i in range(256): + c = chr(i) + esc_c = c if c in allowed_chars else '%{0:02X}'.format(i) + ret[i] = ret[c] = esc_c + return ret + +# The unreserved URI characters (per RFC 3986) +_UNRESERVED_CHARS = (frozenset(string.ascii_letters) + | frozenset(string.digits) + | frozenset('-._~')) +_RESERVED_CHARS = frozenset(":/?#[]@!$&'()*+,;=") # not used +_PATH_RESERVED_CHARS = frozenset("?#") # not used + +_PATH_QUOTE_MAP = _make_quote_map(_UNRESERVED_CHARS | set('/?=&:#')) + +# Escapes/filters + + +def escape_uri_path(text, to_bytes=True): + # actually meant to run on path + query args + fragment + text = to_unicode(text) + if not to_bytes: + return unicode().join([_PATH_QUOTE_MAP.get(c, c) for c in text]) + try: + bytestr = text.encode('utf-8') + except UnicodeDecodeError: + bytestr = text + except: + raise ValueError('expected text or UTF-8 encoded bytes, not %r' % text) + return ''.join([_PATH_QUOTE_MAP[b] for b in bytestr]) + + +def escape_uri_component(text): + return (escape_uri_path(text) # calls to_unicode for us + .replace('/', '%2F') + .replace('?', '%3F') + .replace('=', '%3D') + .replace('&', '%26')) + + +def escape_html(text): + text = to_unicode(text) + # TODO: dust.js doesn't use this, but maybe we should: + # .replace("'", '&squot;') + return cgi.escape(text, True) + + +def escape_js(text): + text = to_unicode(text) + return (text + .replace('\\', '\\\\') + .replace('"', '\\"') + .replace("'", "\\'") + .replace('\r', '\\r') + .replace('\u2028', '\\u2028') + .replace('\u2029', '\\u2029') + .replace('\n', '\\n') + .replace('\f', '\\f') + .replace('\t', '\\t')) + + +def comma_num(val): + try: + return '{0:,}'.format(val) + except ValueError: + return to_unicode(val) + + +def pp_filter(val): + try: + return pprint.pformat(val) + except: + try: + return repr(val) + except: + return 'unreprable object %s' % object.__repr__(val) + + +JSON_PP_INDENT = 2 + + +def ppjson_filter(val): + "A best-effort pretty-printing filter, based on the JSON module" + try: + return json.dumps(val, indent=JSON_PP_INDENT, sort_keys=True) + except TypeError: + return to_unicode(val) + + +# Helpers + +def first_helper(chunk, context, bodies, params=None): + if context.stack.index > 0: + return chunk + if 'block' in bodies: + return bodies['block'](chunk, context) + return chunk + + +def last_helper(chunk, context, bodies, params=None): + if context.stack.index < context.stack.of - 1: + return chunk + if 'block' in bodies: + return bodies['block'](chunk, context) + return chunk + + +def sep_helper(chunk, context, bodies, params=None): + if context.stack.index == context.stack.of - 1: + return chunk + if 'block' in bodies: + return bodies['block'](chunk, context) + return chunk + + +def idx_helper(chunk, context, bodies, params=None): + if 'block' in bodies: + return bodies['block'](chunk, context.push(context.stack.index)) + return chunk + + +def idx_1_helper(chunk, context, bodies, params=None): + if 'block' in bodies: + return bodies['block'](chunk, context.push(context.stack.index + 1)) + return chunk + + +def size_helper(chunk, context, bodies, params): + try: + key = params['key'] + return chunk.write(unicode(len(key))) + except (KeyError, TypeError): + return chunk + + +def _sort_iterate_items(items, sort_key, direction): + if not items: + return items + reverse = False + if direction == 'desc': + reverse = True + if not sort_key: + sort_key = 0 + elif sort_key[0] == '$': + sort_key = sort_key[1:] + if sort_key == 'key': + sort_key = 0 + elif sort_key == 'value': + sort_key = 1 + else: + try: + sort_key = int(sort_key) + except: + sort_key = 0 + return sorted(items, key=lambda x: x[sort_key], reverse=reverse) + + +def iterate_helper(chunk, context, bodies, params): + params = params or {} + body = bodies.get('block') + sort = params.get('sort') + sort_key = params.get('sort_key') + target = params.get('key') + if not body or not target: + context.env.log('warn', 'helper.iterate', 'empty block or target') + return chunk + try: + iter(target) + except: + context.env.log('warn', 'helper.iterate', 'non-iterable target') + return chunk + try: + items = target.items() + is_dict = True + except: + items = target + is_dict = False + if sort: + try: + items = _sort_iterate_items(items, sort_key, direction=sort) + except: + context.env.log('warn', 'helper.iterate', 'failed to sort target') + return chunk + if is_dict: + for key, value in items: + body(chunk, context.push({'$key': key, + '$value': value, + '$type': type(value).__name__, + '$0': key, + '$1': value})) + else: + # all this is for iterating over tuples and the like + for values in items: + try: + key = values[0] + except: + key, value = None, None + else: + try: + value = values[1] + except: + value = None + new_scope = {'$key': key, + '$value': value, + '$type': type(value).__name__} + try: + for i, value in enumerate(values): + new_scope['$%s' % i] = value + except TypeError: + context.env.log('warn', 'helper.iterate', + 'unable to enumerate values') + return chunk + else: + body(chunk, context.push(new_scope)) + return chunk + + +def _do_compare(chunk, context, bodies, params, cmp_op): + "utility function used by @eq, @gt, etc." + params = params or {} + try: + body = bodies['block'] + key = params['key'] + value = params['value'] + typestr = params.get('type') + except KeyError: + context.env.log('warn', 'helper.compare', + 'comparison missing key/value') + return chunk + rkey = _resolve_value(key, chunk, context) + if not typestr: + typestr = _COERCE_REV_MAP.get(type(rkey), 'string') + rvalue = _resolve_value(value, chunk, context) + crkey, crvalue = _coerce(rkey, typestr), _coerce(rvalue, typestr) + if isinstance(crvalue, type(crkey)) and cmp_op(crkey, crvalue): + return chunk.render(body, context) + elif 'else' in bodies: + return chunk.render(bodies['else'], context) + return chunk + + +def _resolve_value(item, chunk, context): + if not callable(item): + return item + try: + return chunk.tap_render(item, context) + except TypeError: + if getattr(context, 'is_strict', None): + raise + return item + + +_COERCE_MAP = { + 'number': float, + 'string': unicode, + 'boolean': bool, +} # Not implemented: date, context +_COERCE_REV_MAP = dict([(v, k) for k, v in _COERCE_MAP.items()]) +_COERCE_REV_MAP[int] = 'number' +try: + _COERCE_REV_MAP[long] = 'number' +except NameError: + pass + + +def _coerce(value, typestr): + coerce_type = _COERCE_MAP.get(typestr.lower()) + if not coerce_type or isinstance(value, coerce_type): + return value + if isinstance(value, string_types): + try: + value = json.loads(value) + except (TypeError, ValueError): + pass + try: + return coerce_type(value) + except (TypeError, ValueError): + return value + + +def _make_compare_helpers(): + from functools import partial + from operator import eq, ne, lt, le, gt, ge + CMP_MAP = {'eq': eq, 'ne': ne, 'gt': gt, 'lt': lt, 'gte': ge, 'lte': le} + ret = {} + for name, op in CMP_MAP.items(): + ret[name] = partial(_do_compare, cmp_op=op) + return ret + + +DEFAULT_HELPERS = {'first': first_helper, + 'last': last_helper, + 'sep': sep_helper, + 'idx': idx_helper, + 'idx_1': idx_1_helper, + 'size': size_helper, + 'iterate': iterate_helper} +DEFAULT_HELPERS.update(_make_compare_helpers()) + + +def make_base(env, stack, global_vars=None): + """`make_base( env, stack, global_vars=None )` + `env` and `stack` are required by the Python implementation. + `global_vars` is optional. set to global_vars. + + 2014.05.09 + This brings compatibility to the more popular fork of Dust.js + from LinkedIn (v1.0) + + adding this to try and create compatibility with Dust + + this is used for the non-activated alternative approach of rendering a + partial with a custom context object + + dust.makeBase = function(global) { + return new Context(new Stack(), global); + }; + """ + return Context(env, stack, global_vars) + + +# Actual runtime objects + +class Context(object): + """\ + The context is a special object that handles variable lookups and + controls template behavior. It is the interface between your + application logic and your templates. The context can be + visualized as a stack of objects that grows as we descend into + nested sections. + + When looking up a key, Dust searches the context stack from the + bottom up. There is no need to merge helper functions into the + template data; instead, create a base context onto which you can + push your local template data. + """ + def __init__(self, env, stack, global_vars=None, blocks=None): + self.env = env + self.stack = stack + if global_vars is None: + global_vars = {} + self.globals = global_vars + self.blocks = blocks + + @classmethod + def wrap(cls, env, context): + if isinstance(context, cls): + return context + return cls(env, Stack(context)) + + def get(self, path, cur=False): + "Retrieves the value `path` as a key from the context stack." + if isinstance(path, (str, unicode)): + if path[0] == '.': + cur = True + path = path[1:] + path = path.split('.') + return self._get(cur, path) + + def get_path(self, cur, down): + return self._get(cur, down) + + def _get(self, cur, down): + # many thanks to jvanasco for his contribution -mh 2014 + """ + * Get a value from the context + * @method `_get` + * @param {boolean} `cur` Get only from the current context + * @param {array} `down` An array of each step in the path + * @private + * @return {string | object} + """ + ctx = self.stack + length = 0 if not down else len(down) # TODO: try/except? + + if not length: + # wants nothing? ok, send back the entire payload + return ctx.head + + first_path_element = down[0] + + value = UndefinedValue + + if cur and not length: + ctx = ctx.head + else: + if not cur: + # Search up the stack for the first_path_element value + while ctx: + if isinstance(ctx.head, dict): + if first_path_element in ctx.head: + value = ctx.head[first_path_element] + break + ctx = ctx.tail + if value is UndefinedValue: + if first_path_element in self.globals: + ctx = self.globals[first_path_element] + else: + ctx = UndefinedValue + else: + ctx = value + else: + # if scope is limited by a leading dot, don't search up tree + if first_path_element in ctx.head: + ctx = ctx.head[first_path_element] + else: + ctx = UndefinedValue + + i = 1 + while ctx and ctx is not UndefinedValue and i < length: + if down[i] in ctx: + ctx = ctx[down[i]] + else: + ctx = UndefinedValue + i += 1 + + if ctx is UndefinedValue: + return None + else: + return ctx + + def push(self, head, index=None, length=None): + """\ + Pushes an arbitrary value `head` onto the context stack and returns + a new `Context` instance. Specify `index` and/or `length` to enable + enumeration helpers.""" + return Context(self.env, + Stack(head, self.stack, index, length), + self.globals, + self.blocks) + + def rebase(self, head): + """\ + Returns a new context instance consisting only of the value at + `head`, plus any previously defined global object.""" + return Context(self.env, + Stack(head), + self.globals, + self.blocks) + + def current(self): + """Returns the head of the context stack.""" + return self.stack.head + + def get_block(self, key): + blocks = self.blocks + if not blocks: + return None + fn = None + for block in blocks[::-1]: + try: + fn = block[key] + if fn: + break + except KeyError: + continue + return fn + + def shift_blocks(self, local_vars): + blocks = self.blocks + if local_vars: + if blocks: + new_blocks = blocks + [local_vars] + else: + new_blocks = [local_vars] + return Context(self.env, self.stack, self.globals, new_blocks) + return self + + +class Stack(object): + def __init__(self, head, tail=None, index=None, length=None): + self.head = head + self.tail = tail + self.index = index or 0 + self.of = length or 1 + # self.is_object = is_scalar(head) + + def __repr__(self): + return 'Stack(%r, %r, %r, %r)' % (self.head, + self.tail, + self.index, + self.of) + + +class Stub(object): + def __init__(self, callback): + self.head = Chunk(self) + self.callback = callback + self._out = [] + + @property + def out(self): + return ''.join(self._out) + + def flush(self): + chunk = self.head + while chunk: + if chunk.flushable: + self._out.append(chunk.data) + elif chunk.error: + self.callback(chunk.error, '') + self.flush = lambda self: None + return + else: + return + self.head = chunk = chunk.next + self.callback(None, self.out) + + +class Stream(object): + def __init__(self): + self.head = Chunk(self) + self.events = {} + + def flush(self): + chunk = self.head + while chunk: + if chunk.flushable: + self.emit('data', chunk.data) + elif chunk.error: + self.emit('error', chunk.error) + self.flush = lambda self: None + return + else: + return + self.head = chunk = chunk.next + self.emit('end') + + def emit(self, etype, data=None): + try: + self.events[etype](data) + except KeyError: + pass + + def on(self, etype, callback): + self.events[etype] = callback + return self + + +def is_scalar(obj): + return not hasattr(obj, '__iter__') or isinstance(obj, string_types) + + +def is_empty(obj): + try: + return obj is None or obj is False or len(obj) == 0 + except TypeError: + return False + + +class Chunk(object): + """\ + A Chunk is a Dust primitive for controlling the flow of the + template. Depending upon the behaviors defined in the context, + templates may output one or more chunks during rendering. A + handler that writes to a chunk directly must return the modified + chunk. + """ + def __init__(self, root, next_chunk=None, taps=None): + self.root = root + self.next = next_chunk + self.taps = taps + self._data, self.data = [], '' + self.flushable = False + self.error = None + + def write(self, data): + "Writes data to this chunk's buffer" + if self.taps: + data = self.taps.go(data) + self._data.append(data) + return self + + def end(self, data=None): + """\ + Writes data to this chunk's buffer and marks it as flushable. This + method must be called on any chunks created via chunk.map. Do + not call this method on a handler's main chunk -- dust.render + and dust.stream take care of this for you. + """ + if data: + self.write(data) + self.data = ''.join(self._data) + self.flushable = True + self.root.flush() + return self + + def map(self, callback): + """\ + Creates a new chunk and passes it to `callback`. Use map to wrap + asynchronous functions and to partition the template for + streaming. chunk.map tells Dust to manufacture a new chunk, + reserving a slot in the output stream before continuing on to + render the rest of the template. You must (eventually) call + chunk.end() on a mapped chunk to weave its content back into + the stream. + """ + cursor = Chunk(self.root, self.next, self.taps) + branch = Chunk(self.root, cursor, self.taps) + self.next = branch + self.data = ''.join(self._data) + self.flushable = True + callback(branch) + return cursor + + def tap(self, tap): + "Convenience methods for applying filters to a stream." + if self.taps: + self.taps = self.taps.push(tap) + else: + self.taps = Tap(tap) + return self + + def untap(self): + "Convenience methods for applying filters to a stream." + self.taps = self.taps.tail + return self + + def render(self, body, context): + """\ + Renders a template block, such as a default block or an else + block. Basically equivalent to body(chunk, context). + """ + return body(self, context) + + def tap_render(self, body, context): + output = [] + + def tmp_tap(data): + if data: + output.append(data) + return '' + self.tap(tmp_tap) + try: + self.render(body, context) + finally: + self.untap() + return ''.join(output) + + def reference(self, elem, context, auto, filters=None): + """\ + These methods implement Dust's default behavior for keys, + sections, blocks, partials and context helpers. While it is + unlikely you'll need to modify these methods or invoke them + from within handlers, the source code may be a useful point of + reference for developers. + """ + if callable(elem): + # this whole callable thing is a quirky thing about dust + try: + elem = elem(self, context) + except TypeError: + if getattr(context, 'is_strict', None): + raise + elem = repr(elem) + else: + if isinstance(elem, Chunk): + return elem + if is_empty(elem): + return self + else: + filtered = context.env.apply_filters(elem, auto, filters) + return self.write(filtered) + + def section(self, elem, context, bodies, params=None): + """\ + These methods implement Dust's default behavior for keys, sections, + blocks, partials and context helpers. While it is unlikely you'll need + to modify these methods or invoke them from within handlers, the + source code may be a useful point of reference for developers.""" + if callable(elem): + try: + elem = elem(self, context, bodies, params) + except TypeError: + if getattr(context, 'is_strict', None): + raise + elem = repr(elem) + else: + if isinstance(elem, Chunk): + return elem + body = bodies.get('block') + else_body = bodies.get('else') + if params: + context = context.push(params) + if not elem and else_body and elem is not 0: + # breaks with dust.js; dust.js doesn't render else blocks + # on sections referencing empty lists. + return else_body(self, context) + + if not body or elem is None: + return self + if elem is True: + return body(self, context) + elif isinstance(elem, dict) or is_scalar(elem): + return body(self, context.push(elem)) + else: + chunk = self + length = len(elem) + head = context.stack.head + for i, el in enumerate(elem): + new_ctx = context.push(el, i, length) + new_ctx.globals.update({'$len': length, + '$idx': i, + '$idx_1': i + 1}) + chunk = body(chunk, new_ctx) + return chunk + + def exists(self, elem, context, bodies, params=None): + """\ + These methods implement Dust's default behavior for keys, sections, + blocks, partials and context helpers. While it is unlikely you'll need + to modify these methods or invoke them from within handlers, the + source code may be a useful point of reference for developers.""" + if not is_empty(elem): + if bodies.get('block'): + return bodies['block'](self, context) + elif bodies.get('else'): + return bodies['else'](self, context) + return self + + def notexists(self, elem, context, bodies, params=None): + """\ + These methods implement Dust's default behavior for keys, + sections, blocks, partials and context helpers. While it is + unlikely you'll need to modify these methods or invoke them + from within handlers, the source code may be a useful point of + reference for developers. + """ + if is_empty(elem): + if bodies.get('block'): + return bodies['block'](self, context) + elif bodies.get('else'): + return bodies['else'](self, context) + return self + + def block(self, elem, context, bodies, params=None): + """\ + These methods implement Dust's default behavior for keys, + sections, blocks, partials and context helpers. While it is + unlikely you'll need to modify these methods or invoke them + from within handlers, the source code may be a useful point of + reference for developers. + """ + body = bodies.get('block') + if elem: + body = elem + if body: + body(self, context) + return self + + def partial(self, elem, context, params=None): + """These methods implement Dust's default behavior for keys, sections, + blocks, partials and context helpers. While it is unlikely you'll need + to modify these methods or invoke them from within handlers, the + source code may be a useful point of reference for developers. + """ + if params: + context = context.push(params) + if callable(elem): + _env = context.env + cback = lambda name, chk: _env.load_chunk(name, chk, context).end() + return self.capture(elem, context, cback) + return context.env.load_chunk(elem, self, context) + + def helper(self, name, context, bodies, params=None): + """\ + These methods implement Dust's default behavior for keys, + sections, blocks, partials and context helpers. While it is + unlikely you'll need to modify these methods or invoke them + from within handlers, the source code may be a useful point of + reference for developers. + """ + return context.env.helpers[name](self, context, bodies, params) + + def capture(self, body, context, callback): + def map_func(chunk): + def stub_cb(err, out): + if err: + chunk.set_error(err) + else: + callback(out, chunk) + stub = Stub(stub_cb) + body(stub.head, context).end() + return self.map(map_func) + + def set_error(self, error): + "Sets an error on this chunk and immediately flushes the output." + self.error = error + self.root.flush() + return self + + +class Tap(object): + def __init__(self, head=None, tail=None): + self.head = head + self.tail = tail + + def push(self, tap): + return Tap(tap, self) + + def go(self, value): + tap = self + while tap: + value = tap.head(value) # TODO: type errors? + tap = tap.tail + return value + + def __repr__(self): + cn = self.__class__.__name__ + return '%s(%r, %r)' % (cn, self.head, self.tail) + + +def to_unicode(obj): + try: + return unicode(obj) + except UnicodeDecodeError: + return unicode(obj, encoding='utf8') + + +DEFAULT_FILTERS = { + 'h': escape_html, + 's': to_unicode, + 'j': escape_js, + 'u': escape_uri_path, + 'uc': escape_uri_component, + 'cn': comma_num, + 'pp': pp_filter, + 'ppjson': ppjson_filter} + + +######### +# Pragmas +######### + + +def esc_pragma(compiler, context, bodies, params): + old_auto = compiler.auto + if not context: + context = 'h' + if context == 's': + compiler.auto = '' + else: + compiler.auto = context + out = compiler._parts(bodies['block']) + compiler.auto = old_auto + return out + + +DEFAULT_PRAGMAS = { + 'esc': esc_pragma +} + + +########### +# Interface +########### + +def load_template_path(path, encoding='utf-8'): + """ + split off `from_path` so __init__ can use + returns a tuple of the source and adjusted absolute path + """ + abs_path = os.path.abspath(path) + if not os.path.isfile(abs_path): + raise TemplateNotFound(abs_path) + with codecs.open(abs_path, 'r', encoding) as f: + source = f.read() + return (source, abs_path) + + +class Template(object): + # no need to set defaults on __init__ + last_mtime = None + is_convertable = True + + def __init__(self, + name, + source, + source_file=None, + optimize=True, + keep_source=True, + env=None, + lazy=False, + ): + if not source and source_file: + (source, source_abs_path) = load_template_path(source_file) + self.name = name + self.source = source + self.source_file = source_file + self.time_generated = time.time() + if source_file: + self.last_mtime = os.path.getmtime(source_file) + self.optimized = optimize + if env is None: + env = default_env + self.env = env + + if lazy: # lazy is only for testing + self.render_func = None + return + (render_code, + self.render_func + ) = self._get_render_func(optimize) + if not keep_source: + self.source = None + + @classmethod + def from_path(cls, path, name=None, encoding='utf-8', **kw): + """classmethod. + Builds a template from a filepath. + args: + ``path`` + kwargs: + ``name`` default ``None``. + ``encoding`` default ``utf-8``. + """ + (source, abs_path) = load_template_path(path) + if not name: + name = path + return cls(name=name, source=source, source_file=abs_path, **kw) + + @classmethod + def from_ast(cls, ast, name=None, **kw): + """classmethod + Builds a template from an AST representation. + This is only provided as an invert to `to_ast` + args: + ``ast`` + kwargs: + ``name`` default ``None``. + """ + template = cls(name=name, source='', lazy=True, **kw) + (render_code, + render_func + ) = template._ast_to_render_func(ast) + template.render_func = render_func + template.is_convertable = False + return template + + @classmethod + def from_python_string(cls, python_string, name=None, **kw): + """classmethod + Builds a template from an python string representation. + This is only provided as an invert to `to_python_string` + args: + ``python_string`` + kwargs: + ``name`` default ``None``. + """ + template = cls(name=name, source='', lazy=True, **kw) + render_code = _python_compile(python_string) + template.render_func = _python_exec(render_code, name='render') + template.is_convertable = False + return template + + @classmethod + def from_python_code(cls, python_code, name=None, **kw): + """classmethod + Builds a template from python code object. + This is only provided as an invert to `to_python_code` + args: + ``python_code`` + kwargs: + ``name`` default ``None``. + """ + template = cls(name=name, source='', lazy=True, **kw) + template.render_func = _python_exec(python_code, name='render') + template.is_convertable = False + return template + + @classmethod + def from_python_func(cls, python_func, name=None, **kw): + """classmethod + Builds a template from an compiled python function. + This is only provided as an invert to `to_python_func` + args: + ``python_func`` + kwargs: + ``name`` default ``None``. + """ + template = cls(name=name, source='', lazy=True, **kw) + template.render_func = python_func + template.is_convertable = False + return template + + def to_ast(self, optimize=True, raw=False): + """Generates the AST for a given template. + This can be inverted with the classmethod `from_ast`. + + kwargs: + ``optimize`` default ``True``. + ``raw`` default ``False``. + + Note: this is just a public function for `_get_ast` + """ + if not self.is_convertable: + raise TemplateConversionException() + return self._get_ast(optimize=optimize, raw=raw) + + def to_python_string(self, optimize=True): + """Generates the Python string representation for a template. + This can be inverted with the classmethod `from_python_string`. + + kwargs: + ``optimize`` default ``True``. + + Note: this is just a public method for `_get_render_string` + """ + if not self.is_convertable: + raise TemplateConversionException() + python_string = self._get_render_string(optimize=optimize) + return python_string + + def to_python_code(self, optimize=True): + """Generates the Python code representation for a template. + This can be inverted with the classmethod `from_python_code`. + + kwargs: + ``optimize`` default ``True``. + + Note: this is just a public method for `_get_render_func` + """ + if not self.is_convertable: + raise TemplateConversionException() + (python_code, + python_string + ) = self._get_render_func(optimize=optimize) + return python_code + + def to_python_func(self, optimize=True): + """Makes the python render func available. + This can be inverted with the classmethod `from_python_func`. + + Note: this is just a public method for `_get_render_func` + """ + if self.render_func: + return self.render_func + if not self.is_convertable: + raise TemplateConversionException() + (render_code, render_func) = self._get_render_func(optimize=optimize) + return render_func + + def render(self, model, env=None): + env = env or self.env + rendered = [] + + def tmp_cb(err, result): + # TODO: get rid of + if err: + print('Error on template %r: %r' % (self.name, err)) + raise RenderException(err) + else: + rendered.append(result) + return result + + chunk = Stub(tmp_cb).head + self.render_chunk(chunk, Context.wrap(env, model)).end() + return rendered[0] + + def render_chunk(self, chunk, context): + if not self.render_func: + # to support laziness for testing + (render_code, + self.render_func + ) = self._get_render_func() + return self.render_func(chunk, context) + + def _get_tokens(self): + if not self.source: + return None + return tokenize(self.source) + + def _get_ast(self, optimize=False, raw=False): + if not self.source: + return None + try: + dast = ParseTree.from_source(self.source).to_dust_ast() + except ParseError as pe: + pe.source_file = self.source_file + raise + if raw: + return dast + return self.env.filter_ast(dast, optimize) + + def _get_render_string(self, optimize=True): + """ + Uses `optimize=True` by default because it makes the output easier to + read and more like dust's docs + + This was previously `_get_render_func(..., ret_str=True)` + """ + ast = self._get_ast(optimize) + if not ast: + return None + # for testing/dev purposes + return Compiler(self.env)._gen_python(ast) + + def _get_render_func(self, optimize=True, ret_str=False): + """ + Uses `optimize=True` by default because it makes the output easier to + read and more like dust's docs + + split `ret_str=True` into `_get_render_string()` + + Note that this doesn't save the render_code/render_func. + It is compiled as needed. + """ + ast = self._get_ast(optimize) + if not ast: + return (None, None) + # consolidated the original code into _ast_to_render_func as-is below + (render_code, + render_func + ) = self._ast_to_render_func(ast) + return (render_code, render_func) + + def _ast_to_render_func(self, ast): + """this was part of ``_get_render_func`` but is better implemented + as an separate function so that AST can be directly loaded. + """ + compiler = Compiler(self.env) + (python_code, + python_func + ) = compiler.compile(ast) + return (python_code, python_func) + + def __repr__(self): + cn = self.__class__.__name__ + name, source_file = self.name, self.source_file + if not source_file: + return '<%s name=%r>' % (cn, name) + return '<%s name=%r source_file=%r>' % (cn, name, source_file) + + +class AshesException(Exception): + pass + + +class TemplateNotFound(AshesException): + def __init__(self, name): + self.name = name + super(TemplateNotFound, self).__init__('could not find template: %r' + % name) + + +class RenderException(AshesException): + pass + + +class ParseError(AshesException): + token = None + source_file = None + + def __init__(self, message, line_no=None, token=None): + self.message = message + self.token = token + self._line_no = line_no + + super(ParseError, self).__init__(self.__str__()) + + @property + def line_no(self): + if self._line_no: + return self._line_no + if getattr(self.token, 'start_line', None) is not None: + return self.token.start_line + return None + + @line_no.setter + def set_line_no(self, val): + self._line_no = val + + def __str__(self): + msg = self.message + infos = [] + if self.source_file: + infos.append('in %s' % self.source_file) + if self.line_no is not None: + infos.append('line %s' % self.line_no) + if infos: + msg += ' (%s)' % ' - '.join(infos) + return msg + + +class TemplateConversionException(AshesException): + def __init__(self): + super(TemplateConversionException, self).__init__('only templates from source ' + 'are convertable') + + +class BaseAshesEnv(object): + template_type = Template + autoescape_filter = 'h' + + def __init__(self, + loaders=None, + helpers=None, + filters=None, + special_chars=None, + optimizers=None, + pragmas=None, + auto_reload=True): + self.templates = {} + self.loaders = list(loaders or []) + self.filters = dict(DEFAULT_FILTERS) + if filters: + self.filters.update(filters) + self.helpers = dict(DEFAULT_HELPERS) + if helpers: + self.helpers.update(helpers) + self.special_chars = dict(DEFAULT_SPECIAL_CHARS) + if special_chars: + self.special_chars.update(special_chars) + self.optimizers = dict(DEFAULT_OPTIMIZERS) + if optimizers: + self.optimizers.update(optimizers) + self.pragmas = dict(DEFAULT_PRAGMAS) + if pragmas: + self.pragmas.update(pragmas) + self.auto_reload = auto_reload + + def log(self, level, name, message): + return # print(level, '-', name, '-', message) + + def render(self, name, model): + tmpl = self.load(name) + return tmpl.render(model, self) + + def load(self, name): + """Loads a template. + + args: + ``name`` template name + """ + try: + template = self.templates[name] + except KeyError: + template = self._load_template(name) + self.register(template) + if self.auto_reload: + if not getattr(template, 'source_file', None): + return template + mtime = os.path.getmtime(template.source_file) + if mtime > template.last_mtime: + template = self._load_template(name) + self.register(template) + return self.templates[name] + + def _load_template(self, name): + for loader in self.loaders: + try: + source = loader.load(name, env=self) + except TemplateNotFound: + continue + else: + return source + raise TemplateNotFound(name) + + def load_all(self, do_register=True, **kw): + """Loads all templates. + + args: + ``do_register`` default ``True` + """ + all_tmpls = [] + for loader in reversed(self.loaders): + # reversed so the first loader to have a template + # will take precendence on registration + if callable(getattr(loader, 'load_all', None)): + tmpls = loader.load_all(self, **kw) + all_tmpls.extend(tmpls) + if do_register: + for t in tmpls: + self.register(t) + return all_tmpls + + def register(self, template, name=None): + if name is None: + name = template.name + self.templates[name] = template + return + + def register_path(self, path, name=None, **kw): + """\ + Reads in, compiles, and registers a single template from a specific + path to a file containing the dust source code. + """ + kw['env'] = self + ret = self.template_type.from_path(path=path, name=name, **kw) + self.register(ret) + return ret + + def register_source(self, name, source, **kw): + """\ + Compiles and registers a single template from source code + string. Assumes caller already decoded the source string. + """ + kw['env'] = self + ret = self.template_type(name=name, source=source, **kw) + self.register(ret) + return ret + + def filter_ast(self, ast, optimize=True): + if optimize: + optimizers = self.optimizers + else: + optimizers = UNOPT_OPTIMIZERS + optimizer = Optimizer(optimizers, self.special_chars) + ret = optimizer.optimize(ast) + return ret + + def apply_filters(self, string, auto, filters): + filters = filters or [] + if not filters: + if auto: + filters = ['s', auto] + else: + filters = ['s'] + elif filters[-1] != 's': + if auto and auto not in filters: + filters += ['s', auto] + else: + filters += ['s'] + for f in filters: + filt_fn = self.filters.get(f) + if filt_fn: + string = filt_fn(string) + return string + + def load_chunk(self, name, chunk, context): + try: + tmpl = self.load(name) + except TemplateNotFound as tnf: + context.env.log('error', 'load_chunk', + 'TemplateNotFound error: %r' % tnf.name) + return chunk.set_error(tnf) + return tmpl.render_chunk(chunk, context) + + def __iter__(self): + return self.templates.itervalues() + + +class AshesEnv(BaseAshesEnv): + """ + A slightly more accessible Ashes environment, with more + user-friendly options exposed. + """ + def __init__(self, paths=None, keep_whitespace=True, *a, **kw): + if isinstance(paths, string_types): + paths = [paths] + self.paths = list(paths or []) + self.keep_whitespace = keep_whitespace + self.is_strict = kw.pop('is_strict', False) + exts = list(kw.pop('exts', DEFAULT_EXTENSIONS)) + + super(AshesEnv, self).__init__(*a, **kw) + + for path in self.paths: + tpl = TemplatePathLoader(path, exts) + self.loaders.append(tpl) + + def filter_ast(self, ast, optimize=None): + optimize = not self.keep_whitespace # preferences override + return super(AshesEnv, self).filter_ast(ast, optimize) + + +def iter_find_files(directory, patterns, ignored=None): + """\ + Finds files under a `directory`, matching `patterns` using "glob" + syntax (e.g., "*.txt"). It's also possible to ignore patterns with + the `ignored` argument, which uses the same format as `patterns. + + (from osutils.py in the boltons package) + """ + if isinstance(patterns, string_types): + patterns = [patterns] + pats_re = re.compile('|'.join([fnmatch.translate(p) for p in patterns])) + + if not ignored: + ignored = [] + elif isinstance(ignored, string_types): + ignored = [ignored] + ign_re = re.compile('|'.join([fnmatch.translate(p) for p in ignored])) + for root, dirs, files in os.walk(directory): + for basename in files: + if pats_re.match(basename): + if ignored and ign_re.match(basename): + continue + filename = os.path.join(root, basename) + yield filename + return + + +def walk_ext_matches(path, exts=None, ignored=None): + if exts is None: + exts = DEFAULT_EXTENSIONS + if ignored is None: + ignored = DEFAULT_IGNORED_PATTERNS + patterns = list(['*.' + e.lstrip('*.') for e in exts]) + + return sorted(iter_find_files(directory=path, + patterns=patterns, + ignored=ignored)) + + +class TemplatePathLoader(object): + def __init__(self, root_path, exts=None, encoding='utf-8'): + self.root_path = os.path.normpath(root_path) + self.encoding = encoding + self.exts = exts or list(DEFAULT_EXTENSIONS) + + def load(self, path, env=None): + env = env or default_env + norm_path = os.path.normpath(path) + if path.startswith('../'): + raise ValueError('no traversal above loader root path: %r' % path) + if not path.startswith(self.root_path): + norm_path = os.path.join(self.root_path, norm_path) + abs_path = os.path.abspath(norm_path) + template_name = os.path.relpath(abs_path, self.root_path) + template_type = env.template_type + return template_type.from_path(name=template_name, + path=abs_path, + encoding=self.encoding, + env=env) + + def load_all(self, env, exts=None, **kw): + ret = [] + exts = exts or self.exts + tmpl_paths = walk_ext_matches(self.root_path, exts) + for tmpl_path in tmpl_paths: + ret.append(self.load(tmpl_path, env)) + return ret + + +class FlatteningPathLoader(TemplatePathLoader): + """ + I've seen this mode of using dust templates in a couple places, + but really it's lazy and too ambiguous. It increases the chances + of silent conflicts and makes it hard to tell which templates refer + to which just by looking at the template code. + """ + def __init__(self, *a, **kw): + self.keep_ext = kw.pop('keep_ext', True) + super(FlatteningPathLoader, self).__init__(*a, **kw) + + def load(self, *a, **kw): + tmpl = super(FlatteningPathLoader, self).load(*a, **kw) + name = os.path.basename(tmpl.name) + if not self.keep_ext: + name, ext = os.path.splitext(name) + tmpl.name = name + return tmpl + +try: + import bottle +except ImportError: + pass +else: + class AshesBottleTemplate(bottle.BaseTemplate): + extensions = list(bottle.BaseTemplate.extensions) + extensions.extend(['ash', 'ashes', 'dust']) + + def prepare(self, **options): + if not self.source: + self.source = self._load_source(self.name) + if self.source is None: + raise TemplateNotFound(self.name) + + options['name'] = self.name + options['source'] = self.source + options['source_file'] = self.filename + for key in ('optimize', 'keep_source', 'env'): + if key in self.settings: + options.setdefault(key, self.settings[key]) + env = self.settings.get('env', default_env) + # I truly despise 2.6.4's unicode kwarg bug + options = dict([(str(k), v) for k, v in options.iteritems()]) + self.tpl = env.register_source(**options) + + def _load_source(self, name): + fname = self.search(name, self.lookup) + if not fname: + return + with codecs.open(fname, "rb", self.encoding) as f: + return f.read() + + def render(self, *a, **kw): + for dictarg in a: + kw.update(dictarg) + context = self.defaults.copy() + context.update(kw) + return self.tpl.render(context) + + from functools import partial as _fp + ashes_bottle_template = _fp(bottle.template, + template_adapter=AshesBottleTemplate) + ashes_bottle_view = _fp(bottle.view, + template_adapter=AshesBottleTemplate) + del bottle + del _fp + + +ashes = default_env = AshesEnv() + + +def _main(): + # TODO: accidentally unclosed tags may consume + # trailing buffers without warning + try: + tmpl = ('{@eq key=hello value="True" type="boolean"}' + '{hello}, world' + '{:else}' + 'oh well, world' + '{/eq}' + ', {@size key=hello/} characters') + ashes.register_source('hi', tmpl) + print(ashes.render('hi', {'hello': 'ayy'})) + except Exception as e: + import pdb;pdb.post_mortem() + raise + + ae = AshesEnv(filters={'cn': comma_num}) + ae.register_source('cn_tmpl', 'comma_numd: {thing|cn}') + # print(ae.render('cn_tmpl', {'thing': 21000})) + ae.register_source('tmpl', '{`{ok}thing`}') + print(ae.render('tmpl', {'thing': 21000})) + + ae.register_source('tmpl2', '{test|s}') + out = ae.render('tmpl2', {'test': [''] * 10}) + print(out) + + ae.register_source('tmpl3', '{@iterate sort="desc" sort_key=1 key=lol}' + '{$idx} - {$0}: {$1}{~n}{/iterate}') + out = ae.render('tmpl3', {'lol': {'uno': 1, 'dos': 2}}) + print(out) + out = ae.render('tmpl3', {'lol': [(1, 2, 3), (4, 5, 6)]}) + print(out) + + print(escape_uri_path("https://en.wikipedia.org/wiki/Asia's_Next_Top_Model_(cycle_3)")) + print(escape_uri_component("https://en.wikipedia.org/wiki/Asia's_Next_Top_Model_(cycle_3)")) + print('') + ae.register_source('tmpl4', '{#iterable}{$idx_1}/{$len}: {.}{@sep}, {/sep}{/iterable}') + out = ae.render('tmpl4', {'iterable': range(100, 108)}) + print(out) + + tmpl = '''\ + {#.} + row{~n} + {#.} + {.}{~n} + {/.} + {/.}''' + ashes.keep_whitespace = False + ashes.autoescape_filter = '' + ashes.register_source('nested_lists', tmpl) + print(ashes.render('nested_lists', [[1, 2], [3, 4]])) + + +class CLIError(ValueError): + pass + + +def _simple_render(template_path, template_literal, env_path_list, + model_path, model_literal, + trim_whitespace, filter, no_filter, + output_path, output_encoding, verbose): + # TODO: default value (placeholder for missing values) + env = AshesEnv(env_path_list) + env.keep_whitespace = not trim_whitespace + if filter in env.filters: + env.autoescape_filter = filter + else: + raise CLIError('unexpected filter %r, expected one of %r' + % (filter, env.filters)) + if no_filter: + env.autoescape_filter = '' + + if template_literal: + tmpl_obj = env.register_source('_literal_template', template_literal) + else: + if not template_path: + raise CLIError('expected template or template literal') + try: + tmpl_obj = env.load(template_path) + except (KeyError, TemplateNotFound): + tmpl_obj = env.register_path(template_path) + + if model_literal: + model = json.loads(model_literal) + elif not model_path: + raise CLIError('expected model or model literal') + elif model_path == '-': + model = json.load(sys.stdin) + else: + with open(model_path) as f: + model = json.load(f) + + output_text = tmpl_obj.render(model) + output_bytes = output_text.encode(output_encoding) + if output_path == '-': + print(output_bytes) + else: + with open(output_path, 'w') as f: + f.write(output_bytes) + return + + +def main(): + # using optparse for backwards compat with 2.6 (and earlier, maybe) + from optparse import OptionParser + + prs = OptionParser(description="render a template using a JSON input", + version='ashes %s' % (__version__,)) + ao = prs.add_option + ao('--env-path', + help="paths to search for templates, separate paths with :") + ao('--filter', default='h', + help="autoescape values with this filter, defaults to 'h' for HTML") + ao('--no-filter', action="store_true", + help="disables default HTML-escaping filter, overrides --filter") + ao('--trim-whitespace', action="store_true", + help="removes whitespace on template load") + ao('-m', '--model', dest='model_path', + help="path to the JSON model file, default - for stdin") + ao('-M', '--model-literal', + help="the literal string of the JSON model, overrides model") + ao('-o', '--output', dest='output_path', default='-', + help="path to the output file, default - for stdout") + ao('--output-encoding', default='utf-8', + help="encoding for the output, default utf-8") + ao('-t', '--template', dest='template_path', + help="path of template to render, absolute or relative to env-path") + ao('-T', '--template-literal', + help="the literal string of the template, overrides template") + ao('--verbose', help="emit extra output on stderr") + + opts, _ = prs.parse_args() + kwargs = dict(opts.__dict__) + + kwargs['env_path_list'] = (kwargs.pop('env_path') or '').split(':') + try: + _simple_render(**kwargs) + except CLIError as clie: + err_msg = '%s; use --help option for more info.' % (clie.args[0],) + prs.error(err_msg) + return + + +if __name__ == '__main__': + main() diff --git a/src/lib/static_ashes.pyc b/src/lib/static_ashes.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14f6f7567af1e3e906a3389aaf95ddad04bb065d GIT binary patch literal 99433 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