From de478f63cab6e8a1ef405c3c0ae7269170b5a3af Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Feb 2007 22:35:04 +0100 Subject: [PATCH] [svn] some small updates to make jinja performing better --HG-- branch : trunk --- ez_setup.py | 215 ++++++++++++++++++++++++++++++++++++ jinja/environment.py | 56 +++++----- jinja/loaders.py | 6 + jinja/translators/python.py | 65 +++++++---- setup.py | 100 +++++++++++++++++ tests/bigtable.py | 133 ++++++++++++++++++++++ 6 files changed, 526 insertions(+), 49 deletions(-) create mode 100644 ez_setup.py create mode 100644 setup.py create mode 100644 tests/bigtable.py diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..00dbb86 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,215 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6b3" +DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b3dev_r46791-py2.3.egg': 'e765a29566575ffac5d81cdf0c6f8db9', + 'setuptools-0.6b3dev_r46791-py2.4.egg': 'd249c022ed029ad60d134bd998adc880', +} + +import sys, os + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + from md5 import md5 + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + try: + import setuptools + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + except ImportError: + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + + import pkg_resources + try: + pkg_resources.require("setuptools>="+version) + + except pkg_resources.VersionConflict: + # XXX could we install in a subprocess here? + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first." + ) % version + sys.exit(2) + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + + try: + import setuptools + except ImportError: + import tempfile, shutil + tmpdir = tempfile.mkdtemp(prefix="easy_install-") + try: + egg = download_setuptools(version, to_dir=tmpdir, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + shutil.rmtree(tmpdir) + else: + if setuptools.__version__ == '0.0.1': + # tell the user to uninstall obsolete version + use_setuptools(version) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + + + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + from md5 import md5 + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + diff --git a/jinja/environment.py b/jinja/environment.py index 92a7618..b8d05b4 100644 --- a/jinja/environment.py +++ b/jinja/environment.py @@ -67,7 +67,7 @@ class Environment(object): def parse(self, source): """Function that creates a new parser and parses the source.""" parser = Parser(self, source) - return parser.parse_page() + return parser.parse() def from_string(self, source): """Load a template from a string.""" @@ -87,59 +87,65 @@ class Environment(object): except UnicodeError: return str(value).decode(self.charset, 'ignore') - def prepare_filter(self, name, *args): - """ - Prepare a filter. - """ - try: - return self.filters[name](*args) - except KeyError: - raise FilterNotFound(name) - - def apply_filters(self, value, context, filters): + def apply_filters(self, value, filtercache, context, filters): """ Apply a list of filters on the variable. """ - for f in filters: - value = f(self, context, value) + for key in filters: + if key in filtercache: + func = filtercache[key] + else: + filtername, args = key + if filtername not in self.filters: + raise FilterNotFound(filtername) + filtercache[key] = func = self.filters[filtername](*args) + value = func(self, context, value) return value - def perform_test(self, value, context, testname): + def perform_test(self, context, testname, value): """ Perform a test on a variable. """ - try: - test = self.tests[testname] - except KeyError: + if testname not in self.tests: raise TestNotFound(testname) - return bool(test(self, context, value)) + return bool(self.tests[testname](self, context, value)) def get_attribute(self, obj, name): """ Get the attribute name from obj. """ - try: + if name in obj: + return obj[name] + elif hasattr(obj, name): rv = getattr(obj, name) r = getattr(obj, 'jinja_allowed_attributes', None) if r is not None: if name not in r: raise AttributeError() return rv - except AttributeError: - return obj[name] - except: - return Undefined + return Undefined def call_function(self, f, args, kwargs, dyn_args, dyn_kwargs): """ - Function call helper + Function call helper. Called for all functions that are passed + any arguments. """ if dyn_args is not None: - args += dyn_args + args += tuple(dyn_args) elif dyn_kwargs is not None: kwargs.update(dyn_kwargs) + if getattr(f, 'jinja_unsafe_call', False): + raise SecurityException('unsafe function %r called' % f.__name__) return f(*args, **kwargs) + def call_function_simple(self, f): + """ + Function call without arguments. + """ + if getattr(f, 'jinja_unsafe_call', False): + raise SecurityException('unsafe function %r called' % f.__name__) + return f() + def finish_var(self, value): """ As long as no write_var function is passed to the template diff --git a/jinja/loaders.py b/jinja/loaders.py index d61e671..b2903e3 100644 --- a/jinja/loaders.py +++ b/jinja/loaders.py @@ -37,12 +37,16 @@ class LoaderWrapper(object): """ Retrieve the sourcecode of a template. """ + # just ascii chars are allowed as template names + name = str(name) return self.loader.get_source(self.environment, name, parent) def parse(self, name, parent=None): """ Retreive a template and parse it. """ + # just ascii chars are allowed as template names + name = str(name) return self.loader.parse(self.environment, name, parent) def load(self, name, translator=PythonTranslator): @@ -51,6 +55,8 @@ class LoaderWrapper(object): be a template class. The javascript translator for example will just output a string with the translated code. """ + # just ascii chars are allowed as template names + name = str(name) return self.loader.load(self.environment, name, translator) diff --git a/jinja/translators/python.py b/jinja/translators/python.py index dde6364..92ef61e 100644 --- a/jinja/translators/python.py +++ b/jinja/translators/python.py @@ -16,6 +16,16 @@ from jinja.exceptions import TemplateSyntaxError from jinja.translators import Translator +def _to_tuple(args): + """ + Return a tuple repr without nested repr. + """ + return '(%s%s)' % ( + ', '.join(args), + len(args) == 1 and ',' or '' + ) + + class Template(object): """ Represents a finished template. @@ -105,6 +115,7 @@ class PythonTranslator(Translator): def process(environment, node): translator = PythonTranslator(environment, node) source = translator.translate() + print source ns = {} exec source in ns return Template(environment, ns['generate']) @@ -155,13 +166,20 @@ class PythonTranslator(Translator): node = tmpl lines = [self.indent( - 'from jinja.datastructure import Undefined, LoopContext, CycleContext\n' - 'def generate(context, write, write_var=None):\n' + 'from jinja.datastructure import Undefined, LoopContext, CycleContext\n\n' + 'def generate(context, write):\n' + ' # BOOTSTRAPPING CODE\n' ' environment = context.environment\n' - ' if write_var is None:\n' - ' write_var = lambda x: write(environment.finish_var(x))' + ' get_attribute = environment.get_attribute\n' + ' perform_test = environment.perform_test\n' + ' apply_filters = environment.apply_filters\n' + ' call_function = environment.call_function\n' + ' call_function_simple = environment.call_function_simple\n' + ' finish_var = environment.finish_var\n' + ' write_var = lambda x: write(finish_var(x))\n' + ' filtercache = {}\n\n' + ' # TEMPLATE CODE' )] - write = lambda x: lines.append(self.indent(x)) self.indention += 1 lines.append(self.handle_node_list(node)) @@ -318,7 +336,7 @@ class PythonTranslator(Translator): """ rv = self.handle_node(node.body) if not rv: - return self.indent('# empty block from %r, line %s' % ( + return self.indent('# EMPTY BLOCK (%r:%s)' % ( node.filename or '?', node.lineno )) @@ -326,14 +344,14 @@ class PythonTranslator(Translator): buf = [] write = lambda x: buf.append(self.indent(x)) - write('# block from %r, line %s' % ( + write('# BLOCK (%r:%s)' % ( node.filename or '?', node.lineno )) write('context.push()') buf.append(self.handle_node(node.body)) write('context.pop()') - buf.append(self.indent('# end of block')) + buf.append(self.indent('# END OF BLOCK')) return '\n'.join(buf) # -- python nodes @@ -360,9 +378,9 @@ class PythonTranslator(Translator): elif node.ops[0][1].__class__ is not ast.Name: raise TemplateSyntaxError('is operator requires a test name', ' as operand', node.lineno) - return 'environment.perform_test(%s, context, %r)' % ( - self.handle_node(node.expr), - node.ops[0][1].name + return 'perform_test(context, %r, %s)' % ( + node.ops[0][1].name, + self.handle_node(node.expr) ) # normal operators @@ -395,7 +413,7 @@ class PythonTranslator(Translator): self.handle_node(node.expr), self.handle_node(node.subs[0]) ) - return 'environment.get_attribute(%s, %s)' % ( + return 'get_attribute(%s, %s)' % ( self.handle_node(node.expr), self.handle_node(node.subs[0]) ) @@ -404,7 +422,7 @@ class PythonTranslator(Translator): """ Handle hardcoded attribute access. foo.bar """ - return 'environment.get_attribute(%s, %r)' % ( + return 'get_attribute(%s, %r)' % ( self.handle_node(node.expr), node.attrname ) @@ -413,7 +431,7 @@ class PythonTranslator(Translator): """ Tuple unpacking loops. """ - return '(%s)' % ', '.join([self.handle_node(n) for n in node.nodes]) + return _to_tuple([self.handle_node(n) for n in node.nodes]) def handle_bitor(self, node): """ @@ -437,23 +455,20 @@ class PythonTranslator(Translator): if n.star_args is not None or n.dstar_args is not None: raise TemplateSynaxError('*args / **kwargs is not supported ' 'for filters', n.lineno) - if args: - args = ', ' + ', '.join(args) - filters.append('environment.prepare_filter(%r%s)' % ( + filters.append('(%r, %s)' % ( n.node.name, - args or '' + _to_tuple(args) )) elif n.__class__ is ast.Name: - filters.append('environment.prepare_filter(%r)' % - n.name) + filters.append('(%r, ())' % n.name) else: raise TemplateSyntaxError('invalid filter. filter must be a ' 'hardcoded function name from the ' 'filter namespace', n.lineno) - return 'environment.apply_filters(%s, context, [%s])' % ( + return 'apply_filters(%s, filtercache, context, %s)' % ( self.handle_node(node.nodes[0]), - ', '.join(filters) + _to_tuple(filters) ) def handle_call_func(self, node): @@ -472,9 +487,11 @@ class PythonTranslator(Translator): kwargs[arg.name] = self.handle_node(arg.expr) else: args.append(self.handle_node(arg)) - return 'environment.call_function(%s, [%s], {%s}, %s, %s)' % ( + if not (args or kwargs or star_args or dstar_args): + return 'call_function_simple(%s)' % self.handle_node(node.node) + return 'call_function(%s, %s, {%s}, %s, %s)' % ( self.handle_node(node.node), - ', '.join(args), + _to_tuple(args), ', '.join(['%r: %s' % i for i in kwargs.iteritems()]), star_args, dstar_args diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e301fae --- /dev/null +++ b/setup.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +try: + import ez_setup + ez_setup.use_setuptools() +except ImportError: + pass +from setuptools import setup + + +setup( + name = 'Jinja', + version = '0.9', + url = 'http://wsgiarea.pocoo.org/jinja/', + license = 'BSD', + author = 'Armin Ronacher', + author_email = 'armin.ronacher@active-4.com', + description = 'A small but fast and easy to use stand-alone template engine written in pure python.', + long_description = '''\ +Jinja is a small but very fast and easy to use stand-alone template engine +written in pure Python. + +Since version 0.6 it uses a new parser that increases parsing performance +a lot by caching the nodelists on disk if wanted. + +It includes multiple template inheritance and other features like simple +value escaping. + + +Template Syntax +=============== + +This is a small example template in which you can see how Jinja's syntax +looks like:: + + + + + My Webpage + + + +

My Webpage

+ {{ variable }} + + + + +Usage +===== + +Here is a small example:: + + from jinja import Template, Context, FileSystemLoader + + t = Template('mytemplate', FileSystemLoader('/path/to/the/templates')) + c = Context({ + 'navigation' [ + {'href': '#', 'caption': 'Index'}, + {'href': '#', 'caption': 'Spam'} + ], + 'variable': 'hello world' + }) + print t.render(c) + + +Unicode Support +=============== + +Jinja comes with built-in Unicode support. As a matter of fact, the return +value of ``Template.render()`` will be a Python unicode object. + +You can still output ``str`` objects as well when you encode the result:: + + s = t.render(c).encode('utf-8') + +For more examples check out the `documentation`_ on the `jinja webpage`_. + +.. _documentation: http://wsgiarea.pocoo.org/jinja/docs/ +.. _jinja webpage: http://wsgiarea.pocoo.org/jinja/ +''', + keywords = 'wsgi web templateengine templates', + packages = ['jinja'], + platforms = 'any', + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content' + ] +) diff --git a/tests/bigtable.py b/tests/bigtable.py new file mode 100644 index 0000000..01c188b --- /dev/null +++ b/tests/bigtable.py @@ -0,0 +1,133 @@ +# Template language benchmarks +# +# Objective: Generate a 1000x10 HTML table as fast as possible. +# adapted for jinja 1 +# +# Author: Jonas Borgström +# Author: Armin Ronacher + +import cgi +import sys +import timeit +from StringIO import StringIO + +from genshi.builder import tag +from genshi.template import MarkupTemplate + +from jinja import Environment + +from django.conf import settings +settings.configure() +from django.template import Context as DjangoContext +from django.template import Template as DjangoTemplate + +from Cheetah.Template import Template as CheetahTemplate + +from mako.template import Template as MakoTemplate + +table = [dict(a='1',b='2',c='3',d='4',e='5',f='6',g='7',h='8',i='9',j='10') + for x in range(1000)] + +genshi_tmpl = MarkupTemplate(""" + + + +
+
+""") + +django_tmpl = DjangoTemplate(""" + +{% for row in table %} +{% for col in row.values %}{{ col|escape }}{% endfor %} +{% endfor %} +
+""") + +jinja_tmpl = Environment().from_string(''' + +{% for row in table %} +{% for col in row.values() %}{{ col|escape }}{% endfor %} +{% endfor %} +
+''') + +cheetah_tmpl = CheetahTemplate(''' +# filter escape + +#for $row in $table + +#for $col in $row.values() +$col +#end for + +#end for +
+''', searchList=[{'table': table, 'escape': cgi.escape}]) + +mako_tmpl = MakoTemplate(''' + +% for row in table: + +% for col in row.values(): + ${col|h} +% endfor + +% endfor +
+''') + +def test_django(): + """Django Templates""" + context = DjangoContext({'table': table}) + django_tmpl.render(context) + +def test_jinja(): + """Jinja Templates""" + jinja_tmpl.render(table=table) + +def test_genshi(): + """Genshi Templates""" + stream = genshi_tmpl.generate(table=table) + stream.render('html', strip_whitespace=False) + +def test_cheetah(): + """Cheetah Templates""" + cheetah_tmpl.respond() + +def test_mako(): + """Mako Templates""" + mako_tmpl.render(table=table) + + +def run(which=None, number=10): + tests = ['test_django', 'test_jinja', 'test_genshi', 'test_cheetah', 'test_mako'] + + if which: + tests = filter(lambda n: n[5:] in which, tests) + + for test in [t for t in tests if hasattr(sys.modules[__name__], t)]: + t = timeit.Timer(setup='from __main__ import %s;' % test, + stmt='%s()' % test) + time = t.timeit(number=number) / number + + if time < 0.00001: + result = ' (not installed?)' + else: + result = '%16.2f ms' % (1000 * time) + print '%-35s %s' % (getattr(sys.modules[__name__], test).__doc__, result) + + +if __name__ == '__main__': + which = [arg for arg in sys.argv[1:] if arg[0] != '-'] + + if '-p' in sys.argv: + import hotshot, hotshot.stats + prof = hotshot.Profile("template.prof") + benchtime = prof.runcall(run, which, number=1) + stats = hotshot.stats.load("template.prof") + stats.strip_dirs() + stats.sort_stats('time', 'calls') + stats.print_stats() + else: + run(which) -- 2.26.2