diff --git a/CHANGES b/CHANGES index 53727dc74757ac3e6a4986e4614f4af957188114..5403c806b7b9bd315ed805b43e30c97f4cb1f931 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,14 @@ file (takes filename, uri arguments). used for [ticket:14] - added optional input_encoding flag to Template, to allow sending a unicode() object with no magic encoding comment +- "expression_filter" argument in <%page> applies only to expressions +- added "default_filters" argument to Template, TemplateLookup. applies only to expressions, +gets prepended to "expression_filter" arg from <%page>. defaults to ["unicode"], so that +all expressions get stringified into u'' by default (this is what Mako already does). +By setting to [], expressions are passed through raw. +- added "imports" argument to Template, TemplateLookup. so you can predefine a list of +import statements at the top of the template. can be used in conjunction with +default_filters. 0.1.1 - buffet plugin supports string-based templates, allows ToscaWidgets to work [ticket:8] diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py index 95e1ba2eb26bd1070ff8f40968cb9ed225028c3d..ba83f1baf711e22e3c1ce43a443cd4f5baa269cb 100644 --- a/lib/mako/codegen.py +++ b/lib/mako/codegen.py @@ -14,17 +14,19 @@ from mako import util, ast, parsetree, filters MAGIC_NUMBER = 1 -def compile(node, uri, filename=None): +def compile(node, uri, filename=None, default_filters=None, imports=None): """generate module source code given a parsetree node, uri, and optional source filename""" buf = util.FastEncodingBuffer() printer = PythonPrinter(buf) - _GenerateRenderMethod(printer, _CompileContext(uri, filename), node) + _GenerateRenderMethod(printer, _CompileContext(uri, filename, default_filters, imports), node) return buf.getvalue() class _CompileContext(object): - def __init__(self, uri, filename): + def __init__(self, uri, filename, default_filters, imports): self.uri = uri self.filename = filename + self.default_filters = default_filters + self.imports = imports class _GenerateRenderMethod(object): """a template visitor object which generates the full module source for a template.""" @@ -113,10 +115,21 @@ class _GenerateRenderMethod(object): self.printer.writeline("_template_filename=%s" % repr(self.compiler.filename)) self.printer.writeline("_template_uri=%s" % repr(self.compiler.uri)) self.printer.writeline("_template_cache=cache.Cache(__name__, _modified_time)") + if self.compiler.imports: + buf = '' + for imp in self.compiler.imports: + buf += imp + "\n" + self.printer.writeline(imp) + impcode = ast.PythonCode(buf, 0, 0, 'template defined imports') + else: + impcode = None main_identifiers = module_identifiers.branch(self.node) module_identifiers.topleveldefs = module_identifiers.topleveldefs.union(main_identifiers.topleveldefs) [module_identifiers.declared.add(x) for x in ["UNDEFINED"]] + if impcode: + [module_identifiers.declared.add(x) for x in impcode.declared_identifiers] + self.compiler.identifiers = module_identifiers self.printer.writeline("_exports = %s" % repr([n.name for n in main_identifiers.topleveldefs.values()])) self.printer.write("\n\n") @@ -331,7 +344,7 @@ class _GenerateRenderMethod(object): self.printer.writeline("_buf = context.pop_buffer()") s = "_buf.getvalue()" if filtered: - s = self.create_filter_callable(node.filter_args.args, s) + s = self.create_filter_callable(node.filter_args.args, s, False) self.printer.writeline(None) if buffered or cached: self.printer.writeline("return %s" % s) @@ -376,13 +389,15 @@ class _GenerateRenderMethod(object): None ) - def create_filter_callable(self, args, target): + def create_filter_callable(self, args, target, is_expression): """write a filter-applying expression based on the filters present in the given filter names, adjusting for the global 'default' filter aliases as needed.""" - d = dict([(k, "filters." + v.func_name) for k, v in filters.DEFAULT_ESCAPES.iteritems()]) + d = dict([(k, (v is unicode and 'unicode' or "filters." + v.func_name)) for k, v in filters.DEFAULT_ESCAPES.iteritems()]) - if self.compiler.pagetag: - args += self.compiler.pagetag.filter_args.args + if is_expression and self.compiler.pagetag: + args = self.compiler.pagetag.filter_args.args + args + if is_expression and self.compiler.default_filters: + args = self.compiler.default_filters + args for e in args: # if filter given as a function, get just the identifier portion m = re.match(r'(.+?)(\(.*\))', e) @@ -397,11 +412,11 @@ class _GenerateRenderMethod(object): def visitExpression(self, node): self.write_source_comment(node) - if len(node.escapes) or (self.compiler.pagetag is not None and len(self.compiler.pagetag.filter_args.args)): - s = self.create_filter_callable(node.escapes_code.args, "unicode(%s)" % node.text) + if len(node.escapes) or (self.compiler.pagetag is not None and len(self.compiler.pagetag.filter_args.args)) or len(self.compiler.default_filters): + s = self.create_filter_callable(node.escapes_code.args, "%s" % node.text, True) self.printer.writeline("context.write(%s)" % s) else: - self.printer.writeline("context.write(unicode(%s))" % node.text) + self.printer.writeline("context.write(%s)" % node.text) def visitControlLine(self, node): if node.isend: @@ -425,7 +440,7 @@ class _GenerateRenderMethod(object): self.printer.writelines( "finally:", "_buf = context.pop_buffer()", - "context.write(%s)" % self.create_filter_callable(node.filter_args.args, "_buf.getvalue()"), + "context.write(%s)" % self.create_filter_callable(node.filter_args.args, "_buf.getvalue()", False), None ) diff --git a/lib/mako/filters.py b/lib/mako/filters.py index 5e35e340b7eeb68030d0b5a4c9f7b027a0931cfb..5d782eefb247f0fedd062ce87b45bf649a53a2be 100644 --- a/lib/mako/filters.py +++ b/lib/mako/filters.py @@ -147,6 +147,7 @@ DEFAULT_ESCAPES = { 'u':url_escape, 'trim':trim, 'entity':html_entities_escape, + 'unicode':unicode } diff --git a/lib/mako/lookup.py b/lib/mako/lookup.py index 1c910cfe0af0e90d4db7bf7d7aabed120f488119..0f6730a82bf08b288305ef3d617a6468f3fb445d 100644 --- a/lib/mako/lookup.py +++ b/lib/mako/lookup.py @@ -37,7 +37,7 @@ class TemplateCollection(object): return uri class TemplateLookup(TemplateCollection): - def __init__(self, directories=None, module_directory=None, filesystem_checks=True, collection_size=-1, format_exceptions=False, error_handler=None, output_encoding=None, cache_type=None, cache_dir=None, modulename_callable=None): + def __init__(self, directories=None, module_directory=None, filesystem_checks=True, collection_size=-1, format_exceptions=False, error_handler=None, output_encoding=None, cache_type=None, cache_dir=None, modulename_callable=None, default_filters=['unicode'], imports=None): if isinstance(directories, basestring): directories = [directories] self.directories = [posixpath.normpath(d) for d in directories or []] @@ -45,7 +45,7 @@ class TemplateLookup(TemplateCollection): self.modulename_callable = modulename_callable self.filesystem_checks = filesystem_checks self.collection_size = collection_size - self.template_args = {'format_exceptions':format_exceptions, 'error_handler':error_handler, 'output_encoding':output_encoding, 'module_directory':module_directory, 'cache_type':cache_type, 'cache_dir':cache_dir or module_directory} + self.template_args = {'format_exceptions':format_exceptions, 'error_handler':error_handler, 'output_encoding':output_encoding, 'module_directory':module_directory, 'cache_type':cache_type, 'cache_dir':cache_dir or module_directory, 'default_filters':default_filters, 'imports':imports} if collection_size == -1: self.__collection = {} self._uri_cache = {} diff --git a/lib/mako/template.py b/lib/mako/template.py index a3c4ea4d064c1b23e9dfcb84729c5db7f857e538..40a0872d4835d956b55ed7d687be50cc12c2f327 100644 --- a/lib/mako/template.py +++ b/lib/mako/template.py @@ -16,7 +16,7 @@ import imp, time, weakref, tempfile, shutil, os, stat, sys, re class Template(object): """a compiled template""" - def __init__(self, text=None, filename=None, uri=None, format_exceptions=False, error_handler=None, lookup=None, output_encoding=None, module_directory=None, cache_type=None, cache_dir=None, module_filename=None, input_encoding=None): + def __init__(self, text=None, filename=None, uri=None, format_exceptions=False, error_handler=None, lookup=None, output_encoding=None, module_directory=None, cache_type=None, cache_dir=None, module_filename=None, input_encoding=None, default_filters=['unicode'], imports=None): """construct a new Template instance using either literal template text, or a previously loaded template module text - textual template source, or None if a module is to be provided @@ -38,9 +38,13 @@ class Template(object): self.module_id = "memory:" + hex(id(self)) self.uri = self.module_id + self.default_filters = default_filters + self.input_encoding = input_encoding + self.imports = imports + # if plain text, compile code in memory only if text is not None: - (code, module) = _compile_text(text, self.module_id, filename, self.uri, input_encoding) + (code, module) = _compile_text(self, text, filename) self._code = code self._source = text ModuleInfo(module, None, self, filename, code, text) @@ -60,18 +64,18 @@ class Template(object): util.verify_directory(os.path.dirname(path)) filemtime = os.stat(filename)[stat.ST_MTIME] if not os.access(path, os.F_OK) or os.stat(path)[stat.ST_MTIME] < filemtime: - _compile_module_file(file(filename).read(), self.module_id, filename, path, self.uri, input_encoding) + _compile_module_file(self, file(filename).read(), filename, path) module = imp.load_source(self.module_id, path, file(path)) del sys.modules[self.module_id] if module._magic_number != codegen.MAGIC_NUMBER: - _compile_module_file(file(filename).read(), self.module_id, filename, path, self.uri, input_encoding) + _compile_module_file(self, file(filename).read(), filename, path) module = imp.load_source(self.module_id, path, file(path)) del sys.modules[self.module_id] ModuleInfo(module, path, self, filename, None, None) else: # template filename and no module directory, compile code # in memory - (code, module) = _compile_text(file(filename).read(), self.module_id, filename, self.uri, input_encoding) + (code, module) = _compile_text(self, file(filename).read(), filename) self._source = None self._code = code ModuleInfo(module, None, self, filename, code, None) @@ -153,19 +157,21 @@ class ModuleInfo(object): return file(self.template_filename).read() source = property(_get_source) -def _compile_text(text, identifier, filename, uri, input_encoding): - node = Lexer(text, filename, input_encoding=input_encoding).parse() - source = codegen.compile(node, uri, filename) +def _compile_text(template, text, filename): + identifier = template.module_id + node = Lexer(text, filename, input_encoding=template.input_encoding).parse() + source = codegen.compile(node, template.uri, filename, default_filters=template.default_filters, imports=template.imports) cid = identifier module = imp.new_module(cid) code = compile(source, cid, 'exec') exec code in module.__dict__, module.__dict__ return (source, module) -def _compile_module_file(text, identifier, filename, outputpath, uri, input_encoding): +def _compile_module_file(template, text, filename, outputpath): + identifier = template.module_id (dest, name) = tempfile.mkstemp() - node = Lexer(text, filename, input_encoding=input_encoding).parse() - source = codegen.compile(node, uri, filename) + node = Lexer(text, filename, input_encoding=template.input_encoding).parse() + source = codegen.compile(node, template.uri, filename, default_filters=template.default_filters, imports=template.imports) os.write(dest, source) os.close(dest) shutil.move(name, outputpath) diff --git a/test/filters.py b/test/filters.py index 8033ee4d78404965233aaa0be294aaa6ebaff515..d9ecb254c603f0e6c6d2f490babaf0f002dddc3b 100644 --- a/test/filters.py +++ b/test/filters.py @@ -46,6 +46,24 @@ class FilterTest(unittest.TestCase): assert t.render().strip()=="trim this string: some string to trim continue" + def test_import_2(self): + t = Template(""" + trim this string: ${" some string to trim " | filters.trim} continue\ + """, imports=["from mako import filters"]) + print t.code + assert t.render().strip()=="trim this string: some string to trim continue" + + def test_custom_default(self): + t = Template(""" + <%! + def myfilter(x): + return "->" + x + "<-" + %> + + hi ${'there'} + """, default_filters=['myfilter']) + assert t.render().strip()=="hi ->there<-" + def test_global(self): t = Template(""" <%page expression_filter="h"/> diff --git a/test/template.py b/test/template.py index aca42ed3ce6058870e5c0442d9ed631b7c39d9d8..88f61e384aac3034288d1242a85b78d753f3bd21 100644 --- a/test/template.py +++ b/test/template.py @@ -67,7 +67,15 @@ class EncodingTest(unittest.TestCase): res = Template(s2).render_unicode(f=lambda x:x) assert res == u"hello śląsk" - + def test_raw_strings(self): + """test that raw strings go straight thru with default_filters turned off""" + g = 'śląsk' + s = u"# -*- coding: utf-8 -*-\nhello ${x}" + t = Template(s, default_filters=[]) + y = t.render(x=g) + print t.code + assert y == "hello śląsk" + def test_encoding(self): val = u"""Alors vous imaginez ma surprise, au lever du jour, quand une drôle de petit voix m’a réveillé. Elle disait: « S’il vous plaît… dessine-moi un mouton! »""" template = Template(val, output_encoding='utf-8') @@ -101,7 +109,7 @@ class PageArgsTest(unittest.TestCase): this is page, ${x}, ${y}, ${z}, ${w} """) - + print template.code assert flatten_result(template.render(x=5, y=10, w=17)) == "this is page, 5, 10, 7, 17" def test_overrides_builtins(self):