From 3149e8ea94a615921dd9c9fbe05af4aa85aaa190 Mon Sep 17 00:00:00 2001 From: Jay Chan Date: Fri, 10 Jul 2015 11:31:27 -0700 Subject: [PATCH] [PATCH] Performance enhancement by using string concat vs list joins. Port of Jay Chan's fix [0] [1], applied to the `0.9.1` tag. We have been using this fix for years, without any issues. [0] https://github.com/justecorruptio/pybars3/commit/a8d40273fdba00186f83f242d02823b256d0662a [1] https://github.com/wbond/pybars3/pull/21 --- pybars/_compiler.py | 106 ++++++++++++++++++++++++++------------------ setup.py | 2 +- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/pybars/_compiler.py b/pybars/_compiler.py index 611709e..94f7daf 100644 --- a/pybars/_compiler.py +++ b/pybars/_compiler.py @@ -163,31 +163,43 @@ class PybarsError(Exception): pass -class strlist(list): +class strlist(object): + __slots__ = ['value'] + def __init__(self, default=None): + self.value = u'' + if default: + self.grow(default) - """A quasi-list to let the template code avoid special casing.""" + def __str__(self): + return self.value - def __str__(self): # Python 3 - return ''.join(self) + def __unicode__(self): + return self.value - def __unicode__(self): # Python 2 - return u''.join(self) + def append(self, other): + self.value += other - def grow(self, thing): - """Make the list longer, appending for unicode, extending otherwise.""" - if type(thing) == str_class: - self.append(thing) + def extend(self, other): + if type(other) is strlist: + self.value += other.value + else: + self.value += ''.join(other) - # This will only ever match in Python 2 since str_class is str in - # Python 3. - elif type(thing) == str: - self.append(unicode(thing)) + def __iter__(self): + return iter([self]) + def __add__(self, other): + self.value += other.value + return self + + def grow(self, other): + if isinstance(other, basestring): + self.value += other + elif type(other) is strlist: + self.value += other.value else: - # Recursively expand to a flat list; may deserve a C accelerator at - # some point. - for element in thing: - self.grow(element) + for item in other: + self.grow(item) _map = { @@ -212,14 +224,15 @@ def escape(something, _escape_re=_escape_re, substitute=substitute): def pick(context, name, default=None): - if isinstance(name, str) and hasattr(context, name): + try: + return context[name] + except (AttributeError, KeyError, TypeError): + pass + if type(name) is str and hasattr(context, name): return getattr(context, name) if hasattr(context, 'get'): return context.get(name) - try: - return context[name] - except (KeyError, TypeError): - return default + return default sentinel = object() @@ -270,34 +283,40 @@ def __unicode__(self): return unicode(self.context) +ITERABLE_TYPES = (list, tuple) + def resolve(context, *segments): - carryover_data = False + context_type = type(context) # This makes sure that bare "this" paths don't return a Scope object - if segments == ('',) and isinstance(context, Scope): + if segments == ('',) and context_type is Scope: return context.get('this') + carryover_data = False for segment in segments: + if context is None: + return None + # Handle @../index syntax by popping the extra @ along the segment path if carryover_data: + segment = u'@' + segment carryover_data = False - segment = u'@%s' % segment - if len(segment) > 1 and segment[0:2] == '@@': + + if segment[:2] == '@@': segment = segment[1:] carryover_data = True - if context is None: - return None - if segment in (None, ""): + if not segment: continue - if type(context) in (list, tuple): - offset = int(segment) - context = context[offset] - elif isinstance(context, Scope): + + if context_type is Scope: context = context.get(segment) + elif context_type in ITERABLE_TYPES: + context = context[int(segment)] else: context = pick(context, segment) + context_type = type(context) return context @@ -336,7 +355,7 @@ def prepare(value, should_escape): def ensure_scope(context, root): - return context if isinstance(context, Scope) else Scope(context, context, root) + return context if type(context) is Scope else Scope(context, context, root) def _each(this, options, context): @@ -378,7 +397,7 @@ def _each(this, options, context): def _if(this, options, context): - if hasattr(context, '__call__'): + if callable(context): context = context(this) if context: return options['fn'](this) @@ -403,7 +422,7 @@ def _lookup(this, context, key): def _blockHelperMissing(this, options, context): - if hasattr(context, '__call__'): + if callable(context): context = context(this) if context != u"" and not context: return options['inverse'](this) @@ -509,8 +528,8 @@ def start(self): ]) else: self._result.grow(u"def %s(context, helpers, partials, root):\n" % function_name) - self._result.grow(u" result = strlist()\n") self._result.grow(u" context = ensure_scope(context, root)\n") + self._result.grow(u" result = strlist()\n") def finish(self): lines, ns, function_name = self.stack.pop(-1) @@ -521,7 +540,7 @@ def finish(self): self._result.grow(u" result = %s(result)\n" % str_class.__name__) self._result.grow(u" return result\n") - source = str_class(u"".join(lines)) + source = str_class(lines) self._result = self.stack and self.stack[-1][0] self._locals = self.stack and self.stack[-1][1] @@ -574,15 +593,16 @@ def add_block(self, symbol, arguments, nested, alt_nested): u" value = helper = helpers.get('%s')\n" % symbol, u" if value is None:\n" u" value = resolve(context, '%s')\n" % symbol, - u" if helper and hasattr(helper, '__call__'):\n" + u" if helper and callable(helper):\n" u" value = helper(context, options%s\n" % call, u" else:\n" u" value = helpers['blockHelperMissing'](context, options, value)\n" - u" result.grow(value or '')\n" + u" if value:\n" + u" result.grow(value)\n" ]) def add_literal(self, value): - self._result.grow(u" result.append(%s)\n" % repr(value)) + self._result.grow(u" result.value += %s\n" % (repr(value),)) def _lookup_arg(self, arg): if not arg: @@ -611,7 +631,7 @@ def find_lookup(self, path, path_type, call): realname = None self._result.grow(u" value = %s\n" % path) self._result.grow([ - u" if hasattr(value, '__call__'):\n" + u" if callable(value):\n" u" value = value(context%s\n" % call, ]) if realname: diff --git a/setup.py b/setup.py index c8fdc89..a2fdc2d 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup(name='pybars3', - version='0.9.1', + version='0.9.1+eventbrite', description='Handlebars.js templating for Python 3 and 2', long_description='Documentation is maintained at https://github.com/wbond/pybars3#readme', author='wbond, mjumbewu',