pax_global_header 0000666 0000000 0000000 00000000064 14303667666 0014533 g ustar 00root root 0000000 0000000 52 comment=4398c04f97e9325d26d885aa79f655fc374cfc22
python-airspeed-0.5.20/ 0000775 0000000 0000000 00000000000 14303667666 0014732 5 ustar 00root root 0000000 0000000 python-airspeed-0.5.20/.github/ 0000775 0000000 0000000 00000000000 14303667666 0016272 5 ustar 00root root 0000000 0000000 python-airspeed-0.5.20/.github/FUNDING.yml 0000664 0000000 0000000 00000000023 14303667666 0020102 0 ustar 00root root 0000000 0000000 patreon: sanityinc
python-airspeed-0.5.20/.gitignore 0000664 0000000 0000000 00000001375 14303667666 0016730 0 ustar 00root root 0000000 0000000 *.pyc
*.pyo
*.egg-info
*.DS_Store
*~
build/
dist/
tests/.coverage
tests/.noseids
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
/.eggs/
python-airspeed-0.5.20/.travis.yml 0000664 0000000 0000000 00000000136 14303667666 0017043 0 ustar 00root root 0000000 0000000 language: python
python:
- "2.7"
- "3.4"
- "3.6"
- "3.7"
script: python setup.py test
python-airspeed-0.5.20/LICENCE 0000664 0000000 0000000 00000002343 14303667666 0015721 0 ustar 00root root 0000000 0000000 Copyright (c) 2004-2015 Steve Purcell & Chris Tarttelin
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
python-airspeed-0.5.20/README.md 0000664 0000000 0000000 00000010011 14303667666 0016202 0 ustar 00root root 0000000 0000000 [](https://travis-ci.org/purcell/airspeed)
[](https://pypi.org/project/airspeed/)
[](https://pypi.org/project/airspeed/)
# Airspeed - a Python template engine
## What is Airspeed?
Airspeed is a powerful and easy-to-use templating engine for Python
that aims for a high level of compatibility with the popular
[Velocity](http://velocity.apache.org/engine/devel/user-guide.html)
library for Java.
## Selling points
* Compatible with Velocity templates
* Compatible with Python 2.7 and greater, including Jython
* Features include macros definitions, conditionals, sub-templates and much more
* Airspeed is already being put to serious use
* Comprehensive set of unit tests; the entire library was written test-first
* Reasonably fast
* A single Python module of a few kilobytes, and not the 500kb of Velocity
* Liberal licence (BSD-style)
## Why another templating engine?
A number of excellent templating mechanisms already exist for Python,
including [Cheetah](http://www.cheetahtemplate.org/), which has a
syntax similar to Airspeed.
However, in making Airspeed's syntax *identical* to that of Velocity,
our goal is to allow Python programmers to prototype, replace or
extend Java code that relies on Velocity.
A simple example:
```python
t = airspeed.Template("""
Old people:
#foreach ($person in $people)
#if($person.age > 70)
$person.name
#end
#end
Third person is $people[2].name
""")
people = [{'name': 'Bill', 'age': 100}, {'name': 'Bob', 'age': 90}, {'name': 'Mark', 'age': 25}]
print t.merge(locals())
```
You can also use "Loaders" to allow templates to include each other using the `#include` or `#parse` directives:
```
% cat /tmp/1.txt
Bingo!
% cat /tmp/2.txt
#parse ("2.txt")
% python
Python 2.4.4 (#1, May 28 2007, 00:47:43)
[GCC 4.0.1 (Apple Computer, Inc. build 5367)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from airspeed import CachingFileLoader
>>> loader = CachingFileLoader("/tmp")
>>> template = loader.load_template("1.txt")
>>> template.merge({}, loader=loader)
'Bingo!\n'
```
### How compatible is Airspeed with Velocity?
All Airspeed templates should work correctly with Velocity. The vast
majority of Velocity templates will work correctly with Airspeed.
### What does and doesn't work?
Airspeed currently implements a very significant subset of the
Velocity functionality, including `$variables`, the `#if`, `#foreach`,
`#macro`, `#include` and `#parse` directives, and `"$interpolated #strings()"`. Templates are unicode-safe.
The output of templates in Airspeed is not yet 'whitespace compatible'
with Velocity's rendering of the same templates, which generally does
not matter for web applications.
### Where do I get it?
https://github.com/purcell/airspeed
### Getting started
The
[Velocity User Guide](http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html)
shows how to write templates. Our unit tests show how to use the
templates from your code.
### Reporting bugs
Please feel free to create tickets for bugs or desired features.
### Who is to blame?
Airspeed was conceived by Chris Tarttelin, and implemented jointly in
a test-driven manner by Steve Purcell and Chris Tarttelin. We can be
contacted by e-mail by using our first names (at) pythonconsulting dot
com.
Extensions for compatibility with Velocity 1.7 were kindly provided by
[Giannis Dzegoutanis](https://github.com/erasmospunk), and further modernization
has been done by [David Black](https://github.com/dbaxa/).
[💝 Support this project and my other Open Source work](https://www.patreon.com/sanityinc)
[💼 LinkedIn profile](https://uk.linkedin.com/in/stevepurcell)
[✍ sanityinc.com](http://www.sanityinc.com/)
[🐦 @sanityinc](https://twitter.com/sanityinc)
python-airspeed-0.5.20/airspeed/ 0000775 0000000 0000000 00000000000 14303667666 0016526 5 ustar 00root root 0000000 0000000 python-airspeed-0.5.20/airspeed/__init__.py 0000664 0000000 0000000 00000123543 14303667666 0020647 0 ustar 00root root 0000000 0000000 from __future__ import print_function
import re
import operator
import os
import string
import sys
import six
__all__ = [
'Template',
'TemplateError',
'TemplateExecutionError',
'TemplateSyntaxError',
'CachingFileLoader']
# A dict that maps classes to dicts of additional methods.
# This allows support for methods that are available in Java-based Velocity
# implementations, e.g., .size() of a list or .length() of a string.
# Given a method 'm' invoked with parameters '*p' on an object of type 't',
# and if __additional_methods__[t][m] exists, we will invoke and return m(t, *p)
#
# For example, given a template variable "$foo = [1,2,3]", "$foo.size()" will
# result in calling method __additional_methods__[list]['size']($foo)
__additional_methods__ = {
str: {
'length': lambda self: len(self),
'replaceAll': lambda self, pattern, repl: re.sub(pattern, repl, self),
'startsWith': lambda self, prefix: self.startswith(prefix)
},
list: {
'size': lambda self: len(self),
'get': lambda self, index: self[index],
'contains': lambda self, value: value in self,
'add': lambda self, value: self.append(value)
},
dict: {
'put': lambda self, key, value: self.update({key: value}),
'keySet': lambda self: self.keys()
}
}
try:
dict
except NameError:
from UserDict import UserDict
class dict(UserDict):
def __init__(self):
self.data = {}
try:
operator.__gt__
except AttributeError:
operator.__gt__ = lambda a, b: a > b
operator.__lt__ = lambda a, b: a < b
operator.__ge__ = lambda a, b: a >= b
operator.__le__ = lambda a, b: a <= b
operator.__eq__ = lambda a, b: a == b
operator.__ne__ = lambda a, b: a != b
operator.mod = lambda a, b: a % b
try:
basestring
def is_string(s):
return isinstance(s, basestring)
except NameError:
def is_string(s):
return isinstance(s, type(''))
###############################################################################
# Public interface
###############################################################################
def boolean_value(variable_value):
if not variable_value:
return False
return not (variable_value is None)
def is_valid_vtl_identifier(text):
return text and text[0] in set(string.ascii_letters + '_')
class Template:
def __init__(self, content, filename=""):
self.content = content
self.filename = filename
self.root_element = None
def merge(self, namespace, loader=None):
output = StoppableStream()
self.merge_to(namespace, output, loader)
return output.getvalue()
def ensure_compiled(self):
if not self.root_element:
self.root_element = TemplateBody(self.filename, self.content)
def merge_to(self, namespace, fileobj, loader=None):
if loader is None:
loader = NullLoader()
self.ensure_compiled()
self.root_element.evaluate(fileobj, namespace, loader)
class TemplateError(Exception):
pass
class TemplateExecutionError(TemplateError):
def __init__(self, element, exc_info):
cause, value, traceback = exc_info
self.__cause__ = value
self.element = element
self.start, self.end, self.filename = (element.start, element.end,
element.filename)
self.msg = "Error in template '%s' at position " \
"%d-%d in expression: %s\n%s: %s" % \
(self.filename, self.start, self.end,
element.my_text(), cause.__name__, value)
def __str__(self):
return self.msg
class TemplateSyntaxError(TemplateError):
def __init__(self, element, expected):
self.element = element
self.text_understood = element.full_text()[:element.end]
self.line = 1 + self.text_understood.count('\n')
self.column = len(
self.text_understood) - self.text_understood.rfind('\n')
got = element.next_text()
if len(got) > 40:
got = got[:36] + ' ...'
Exception.__init__(
self, "line %d, column %d: expected %s in %s, got: %s ..." %
(self.line, self.column, expected, self.element_name(), got))
def get_position_strings(self):
error_line_start = 1 + self.text_understood.rfind('\n')
if '\n' in self.element.next_text():
error_line_end = self.element.next_text().find(
'\n') + self.element.end
else:
error_line_end = len(self.element.full_text())
error_line = self.element.full_text()[error_line_start:error_line_end]
caret_pos = self.column
return [error_line, ' ' * (caret_pos - 1) + '^']
def element_name(self):
return re.sub(
'([A-Z])',
lambda m: ' ' +
m.group(1).lower(),
self.element.__class__.__name__).strip()
class NullLoader:
def load_text(self, name):
raise TemplateError("no loader available for '%s'" % name)
def load_template(self, name):
raise self.load_text(name)
class CachingFileLoader:
def __init__(self, basedir, debugging=False):
self.basedir = basedir
self.known_templates = {} # name -> (template, file_mod_time)
self.debugging = debugging
if debugging:
print("creating caching file loader with basedir:", basedir)
def filename_of(self, name):
return os.path.join(self.basedir, name)
def load_text(self, name):
if self.debugging:
print("Loading text from", self.basedir, name)
f = open(self.filename_of(name))
try:
return f.read()
finally:
f.close()
def load_template(self, name):
if self.debugging:
print("Loading template...", name,)
mtime = os.path.getmtime(self.filename_of(name))
if name in self.known_templates:
template, prev_mtime = self.known_templates[name]
if mtime <= prev_mtime:
if self.debugging:
print("loading parsed template from cache")
return template
if self.debugging:
print("loading text from disk")
template = Template(self.load_text(name), filename=name)
template.ensure_compiled()
self.known_templates[name] = (template, mtime)
return template
class StoppableStream(six.StringIO):
def __init__(self, buf=''):
self.stop = False
six.StringIO.__init__(self, buf)
def write(self, s):
if not self.stop:
six.StringIO.write(self, s)
###############################################################################
# Internals
###############################################################################
WHITESPACE_TO_END_OF_LINE = re.compile(r'[ \t\r]*\n(.*)', re.S)
class NoMatch(Exception):
pass
class LocalNamespace(dict):
def __init__(self, parent):
dict.__init__(self)
self.parent = parent
def __getitem__(self, key):
try:
return dict.__getitem__(self, key)
except KeyError:
return self.parent[key]
def find_outermost(self, key):
try:
dict.__getitem__(self, key)
return self
except KeyError:
if isinstance(self.parent, LocalNamespace):
return self.parent.find_outermost(key)
else:
return None
def set_inherited(self, key, value):
ns = self.find_outermost(key)
if ns is None:
ns = self
ns[key] = value
def top(self):
if hasattr(self.parent, "top"):
return self.parent.top()
return self.parent
def __repr__(self):
return dict.__repr__(self) + '->' + repr(self.parent)
class _Element:
def __init__(self, filename, text, start=0):
self.filename = filename
self._full_text = text
self.start = self.end = start
self.parse()
def next_text(self):
return self._full_text[self.end:]
def my_text(self):
return self._full_text[self.start:self.end]
def full_text(self):
return self._full_text
def syntax_error(self, expected):
return TemplateSyntaxError(self, expected)
def identity_match(self, pattern):
m = pattern.match(self._full_text, self.end)
if not m:
raise NoMatch()
self.end = m.start(pattern.groups)
return m.groups()[:-1]
def next_match(self, pattern):
m = pattern.match(self._full_text, self.end)
if not m:
return False
self.end = m.start(pattern.groups)
return m.groups()[:-1]
def optional_match(self, pattern):
m = pattern.match(self._full_text, self.end)
if not m:
return False
self.end = m.start(pattern.groups)
return True
def require_match(self, pattern, expected):
m = pattern.match(self._full_text, self.end)
if not m:
raise self.syntax_error(expected)
self.end = m.start(pattern.groups)
return m.groups()[:-1]
def next_element(self, element_spec):
if callable(element_spec):
element = element_spec(self.filename, self._full_text, self.end)
self.end = element.end
return element
else:
for element_class in element_spec:
try:
element = element_class(self.filename, self._full_text,
self.end)
except NoMatch:
pass
else:
self.end = element.end
return element
raise NoMatch()
def require_next_element(self, element_spec, expected):
if callable(element_spec):
try:
element = element_spec(self.filename, self._full_text,
self.end)
except NoMatch:
raise self.syntax_error(expected)
else:
self.end = element.end
return element
else:
for element_class in element_spec:
try:
element = element_class(self.filename, self._full_text,
self.end)
except NoMatch:
pass
else:
self.end = element.end
return element
expected = ', '.join([cls.__name__ for cls in element_spec])
raise self.syntax_error('one of: ' + expected)
def evaluate(self, *args):
try:
return self.evaluate_raw(*args)
except TemplateExecutionError:
raise
except:
exc_info = sys.exc_info()
six.reraise(TemplateExecutionError,
TemplateExecutionError(self, exc_info), exc_info[2])
class Text(_Element):
PLAIN = re.compile(
r'((?:[^\\\$#]+|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|#$'
r'|#[^\{\}a-zA-Z0-9#\*]+|\\.)(.*)$',
re.S +
re.I)
ESCAPED_CHAR = re.compile(r'\\([\$#]\S+)')
def parse(self):
text, = self.identity_match(self.PLAIN)
def unescape(match):
return match.group(1)
self.text = self.ESCAPED_CHAR.sub(unescape, text)
def evaluate_raw(self, stream, namespace, loader):
stream.write(self.text)
class FallthroughHashText(_Element):
""" Plain text starting with a # but which didn't match an earlier
directive or macro. The canonical example is an HTML color spec.
Note that it MUST NOT match block-ending directives.
"""
# because of earlier elements, this will always start with a hash
PLAIN = re.compile(r'(\#(?!end|else|elseif|\{(?:end|else|elseif)\}))(.*)$',
re.S)
def parse(self):
self.text, = self.identity_match(self.PLAIN)
def evaluate_raw(self, stream, namespace, loader):
stream.write(self.text)
class IntegerLiteral(_Element):
INTEGER = re.compile(r'(-?\d+)(.*)', re.S)
def parse(self):
self.value, = self.identity_match(self.INTEGER)
self.value = int(self.value)
def calculate(self, namespace, loader):
return self.value
class FloatingPointLiteral(_Element):
FLOAT = re.compile(r'(-?\d+\.\d+)(.*)', re.S)
def parse(self):
self.value, = self.identity_match(self.FLOAT)
self.value = float(self.value)
def calculate(self, namespace, loader):
return self.value
class BooleanLiteral(_Element):
BOOLEAN = re.compile(r'((?:true)|(?:false))(.*)', re.S | re.I)
def parse(self):
self.value, = self.identity_match(self.BOOLEAN)
self.value = self.value.lower() == 'true'
def calculate(self, namespace, loader):
return self.value
class StringLiteral(_Element):
STRING = re.compile(r"'((?:\\['nrbt\\\\\\$]|[^'\\])*)'(.*)", re.S)
ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])")
def parse(self):
value, = self.identity_match(self.STRING)
def unescape(match):
return {
'n': '\n',
'r': '\r',
'b': '\b',
't': '\t',
'"': '"',
'\\': '\\',
"'": "'"}.get(
match.group(1),
'\\' +
match.group(1))
self.value = self.ESCAPED_CHAR.sub(unescape, value)
def calculate(self, namespace, loader):
return self.value
class InterpolatedStringLiteral(StringLiteral):
STRING = re.compile(r'"((?:\\["nrbt\\\\\\$]|[^"\\])*)"(.*)', re.S)
ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])')
def parse(self):
StringLiteral.parse(self)
self.block = Block(self.filename, self.value, 0)
def calculate(self, namespace, loader):
output = StoppableStream()
self.block.evaluate(output, namespace, loader)
return output.getvalue()
class Range(_Element):
MIDDLE = re.compile(r'([ \t]*\.\.[ \t]*)(.*)$', re.S)
def parse(self):
self.value1 = self.next_element((FormalReference, IntegerLiteral))
self.identity_match(self.MIDDLE)
self.value2 = self.next_element((FormalReference, IntegerLiteral))
def calculate(self, namespace, loader):
value1 = self.value1.calculate(namespace, loader)
value2 = self.value2.calculate(namespace, loader)
if value2 < value1:
return range(value1, value2 - 1, -1)
return range(value1, value2 + 1)
class ValueList(_Element):
COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
def parse(self):
self.values = []
try:
value = self.next_element(Value)
except NoMatch:
pass
else:
self.values.append(value)
while self.optional_match(self.COMMA):
value = self.require_next_element(Value, 'value')
self.values.append(value)
def calculate(self, namespace, loader):
return [value.calculate(namespace, loader) for value in self.values]
class _EmptyValues:
def calculate(self, namespace, loader):
return []
class ArrayLiteral(_Element):
START = re.compile(r'\[[ \t]*(.*)$', re.S)
END = re.compile(r'[ \t]*\](.*)$', re.S)
values = _EmptyValues()
def parse(self):
self.identity_match(self.START)
try:
self.values = self.next_element((Range, ValueList))
except NoMatch:
pass
self.require_match(self.END, ']')
self.calculate = self.values.calculate
class DictionaryLiteral(_Element):
START = re.compile(r'{[ \t]*(.*)$', re.S)
END = re.compile(r'[ \t]*}(.*)$', re.S)
KEYVALSEP = re.compile(r'[ \t]*:[ \t]*(.*)$', re.S)
PAIRSEP = re.compile(r'[ \t]*,[ \t]*(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
self.local_data = {}
if self.optional_match(self.END):
# it's an empty dictionary
return
while (True):
key = self.next_element(Value)
self.require_match(self.KEYVALSEP, ':')
value = self.next_element(Value)
self.local_data[key] = value
if not self.optional_match(self.PAIRSEP):
break
self.require_match(self.END, '}')
# Note that this delays calculation of values until it's used.
# TODO confirm that that's correct.
def calculate(self, namespace, loader):
tmp = {}
for (key, val) in self.local_data.items():
tmp[key.calculate(namespace, loader)] = val.calculate(
namespace, loader)
return tmp
class Value(_Element):
def parse(self):
self.expression = self.next_element(
(FormalReference,
FloatingPointLiteral,
IntegerLiteral,
StringLiteral,
InterpolatedStringLiteral,
ArrayLiteral,
DictionaryLiteral,
ParenthesizedExpression,
UnaryOperatorValue,
BooleanLiteral))
def calculate(self, namespace, loader):
return self.expression.calculate(namespace, loader)
class NameOrCall(_Element):
NAME = re.compile(r'([a-zA-Z0-9_]+)(.*)$', re.S)
parameters = None
index = None
def parse(self):
self.name, = self.identity_match(self.NAME)
if not is_valid_vtl_identifier(self.name):
raise NoMatch('Invalid VTL identifier %s.' % self.name)
try:
self.parameters = self.next_element(ParameterList)
except NoMatch:
try:
self.index = self.next_element(ArrayIndex)
except NoMatch:
pass
def calculate(self, current_object, loader, top_namespace):
result = None
try:
result = current_object[self.name]
except (KeyError, TypeError, AttributeError):
pass
if result is None and not isinstance(current_object, LocalNamespace):
try:
result = getattr(current_object, self.name)
except AttributeError:
pass
if result is None:
methods_for_type = __additional_methods__.get(current_object.__class__)
if methods_for_type and self.name in methods_for_type:
result = lambda *args: methods_for_type[self.name](current_object, *args)
if result is None:
return None # TODO: an explicit 'not found' exception?
if isinstance(result, _FunctionDefinition):
params = self.parameters and self.parameters.calculate(top_namespace, loader) or []
stream = StoppableStream()
result.execute_function(stream, top_namespace, params, loader)
return stream.getvalue()
if self.parameters is not None:
result = result(*self.parameters.calculate(top_namespace, loader))
elif self.index is not None:
array_index = self.index.calculate(top_namespace, loader)
# If list make sure index is an integer
if isinstance(
result, list) and not isinstance(
array_index, six.integer_types):
raise ValueError(
"expected integer for array index, got '%s'" %
(array_index))
try:
result = result[array_index]
except:
result = None
return result
class SubExpression(_Element):
DOT = re.compile(r'\.(.*)', re.S)
def parse(self):
try:
self.identity_match(self.DOT)
self.expression = self.next_element(VariableExpression)
except NoMatch:
self.expression = self.next_element(ArrayIndex)
self.subexpression = None
try:
self.subexpression = self.next_element(SubExpression)
except NoMatch:
pass
def calculate(self, current_object, loader, global_namespace):
args = [current_object, loader]
if not isinstance(self.expression, ArrayIndex):
return self.expression.calculate(*(args + [global_namespace]))
index = self.expression.calculate(*args)
result = current_object[index]
if self.subexpression:
result = self.subexpression.calculate(result, loader, global_namespace)
return result
class VariableExpression(_Element):
subexpression = None
def parse(self):
self.part = self.next_element(NameOrCall)
try:
self.subexpression = self.next_element(SubExpression)
except NoMatch:
pass
def calculate(self, namespace, loader, global_namespace=None):
if global_namespace is None:
global_namespace = namespace
value = self.part.calculate(namespace, loader, global_namespace)
if self.subexpression:
value = self.subexpression.calculate(
value,
loader,
global_namespace)
return value
class ParameterList(_Element):
START = re.compile(r'\(\s*(.*)$', re.S)
COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
END = re.compile(r'\s*\)(.*)$', re.S)
values = _EmptyValues()
def parse(self):
self.identity_match(self.START)
try:
self.values = self.next_element(ValueList)
except NoMatch:
pass
self.require_match(self.END, ')')
def calculate(self, namespace, loader):
return self.values.calculate(namespace, loader)
class ArrayIndex(_Element):
START = re.compile(r'\[[ \t]*(.*)$', re.S)
END = re.compile(r'[ \t]*\](.*)$', re.S)
index = 0
def parse(self):
self.identity_match(self.START)
self.index = self.require_next_element(
(FormalReference,
IntegerLiteral,
InterpolatedStringLiteral,
ParenthesizedExpression),
'integer index or object key')
self.require_match(self.END, ']')
def calculate(self, namespace, loader):
result = self.index.calculate(namespace, loader)
return result
class AlternateValue(_Element):
START = re.compile(r'\|(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
self.expression = self.require_next_element(Value, 'expression')
self.calculate = self.expression.calculate
class FormalReference(_Element):
START = re.compile(r'\$(!?)(\{?)(.*)$', re.S)
CLOSING_BRACE = re.compile(r'\}(.*)$', re.S)
def parse(self):
self.silent, braces = self.identity_match(self.START)
try:
self.expression = self.next_element(VariableExpression)
self.calculate = self.expression.calculate
except NoMatch:
self.expression = None
self.calculate = None
self.alternate = None
if braces:
try:
self.alternate = self.next_element(AlternateValue)
except NoMatch:
pass
self.require_match(self.CLOSING_BRACE, '}')
def evaluate_raw(self, stream, namespace, loader):
value = None
if self.expression is not None:
value = self.expression.calculate(namespace, loader)
if value is None:
if self.alternate is not None:
value = self.alternate.calculate(namespace, loader)
elif self.silent and self.expression is not None:
value = ''
else:
value = self.my_text()
if is_string(value):
stream.write(value)
else:
stream.write(six.text_type(value))
class Null:
def evaluate(self, stream, namespace, loader):
pass
class Comment(_Element, Null):
COMMENT = re.compile(
'#(?:#.*?(?:\n|$)|\\*.*?\\*#(?:[ \t]*\n)?)(.*)$',
re.M +
re.S)
def parse(self):
self.identity_match(self.COMMENT)
def evaluate(self, *args):
pass
class BinaryOperator(_Element):
BINARY_OP = re.compile(
r'\s*(>=|<=|<|==|!=|>|%|\|\||&&|or|and|\+|\-|\*|\/|\%|gt|lt|ne|eq|ge'
r'|le|not)\s*(.*)$',
re.S)
OPERATORS = {'>': operator.gt, 'gt': operator.gt,
'>=': operator.ge, 'ge': operator.ge,
'<': operator.lt, 'lt': operator.lt,
'<=': operator.le, 'le': operator.le,
'==': operator.eq, 'eq': operator.eq,
'!=': operator.ne, 'ne': operator.ne,
'%': operator.mod,
'||': lambda a, b: boolean_value(a) or boolean_value(b),
'&&': lambda a, b: boolean_value(a) and boolean_value(b),
'or': lambda a, b: boolean_value(a) or boolean_value(b),
'and': lambda a, b: boolean_value(a) and boolean_value(b),
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.floordiv}
# Based on http://introcs.cs.princeton.edu/java/11precedence/
PRECEDENCE = {'>': 7, '<': 7, '==': 8, '>=': 7, '<=': 7, '!=': 9,
'||': 13, '&&': 12, 'or': 13, 'and': 12,
'+': 5, '-': 5, '*': 4, '/': 4, '%': 4,
'gt': 7, 'lt': 7, 'ne': 9, 'eq': 8, 'ge': 7, 'le': 7,
}
# In velocity, if + is applied to one string and one numeric
# argument, will convert the number into a string.
# As far as I can tell, this is undocumented.
# Note that this applies only to add, not to other operators
def parse(self):
op_string, = self.identity_match(self.BINARY_OP)
self.apply_to = self.OPERATORS[op_string]
self.precedence = self.PRECEDENCE[op_string]
# This assumes that the self operator is "to the left"
# of the argument, and thus gets higher precedence if they're
# both boolean operators.
# That is, the way this is used (see Expression.calculate)
# it should return false if the two ops have the same precedence
# that is, it's strictly greater than, not greater than or equal to
# to get proper left-to-right evaluation, it should skew towards false.
def greater_precedence_than(self, other):
return self.precedence < other.precedence
class UnaryOperatorValue(_Element):
UNARY_OP = re.compile(r'\s*(!|(?:not))\s*(.*)$', re.S)
OPERATORS = {'!': operator.__not__, 'not': operator.__not__}
def parse(self):
op_string, = self.identity_match(self.UNARY_OP)
self.value = self.next_element(Value)
self.op = self.OPERATORS[op_string]
def calculate(self, namespace, loader):
return self.op(self.value.calculate(namespace, loader))
# Note: there appears to be no way to differentiate a variable or
# value from an expression, other than context.
class Expression(_Element):
def parse(self):
self.expression = [self.next_element(Value)]
while (True):
try:
binary_operator = self.next_element(BinaryOperator)
value = self.require_next_element(Value, 'value')
self.expression.append(binary_operator)
self.expression.append(value)
except NoMatch:
break
def calculate(self, namespace, loader):
if not self.expression or len(self.expression) == 0:
return False
# TODO: how does velocity deal with an empty condition expression?
opstack = []
valuestack = [self.expression[0]]
terms = self.expression[1:]
# use top of opstack on top 2 values of valuestack
def stack_calculate(ops, values, namespace, loader):
value2 = values.pop()
if isinstance(value2, Value):
value2 = value2.calculate(namespace, loader)
value1 = values.pop()
if isinstance(value1, Value):
value1 = value1.calculate(namespace, loader)
result = ops.pop().apply_to(value1, value2)
# TODO this doesn't short circuit -- does velocity?
# also note they're eval'd out of order
values.append(result)
while terms:
# next is a binary operator
if not opstack or terms[0].greater_precedence_than(opstack[-1]):
opstack.append(terms[0])
valuestack.append(terms[1])
terms = terms[2:]
else:
stack_calculate(opstack, valuestack, namespace, loader)
# now clean out the stacks
while opstack:
stack_calculate(opstack, valuestack, namespace, loader)
if len(valuestack) != 1:
print ("evaluation of expression in Condition.calculate "
"is messed up: final length of stack is not one")
# TODO handle this officially
result = valuestack[0]
if isinstance(result, Value):
result = result.calculate(namespace, loader)
return result
class ParenthesizedExpression(_Element):
START = re.compile(r'\(\s*(.*)$', re.S)
END = re.compile(r'\s*\)(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
expression = self.next_element(Expression)
self.require_match(self.END, ')')
self.calculate = expression.calculate
class Condition(_Element):
def parse(self):
expression = self.next_element(ParenthesizedExpression)
self.optional_match(WHITESPACE_TO_END_OF_LINE)
self.calculate = expression.calculate
# TODO do I need to do anything else here?
class End(_Element):
END = re.compile(r'#(?:end|\{end\})(.*)', re.I + re.S)
def parse(self):
self.identity_match(self.END)
self.optional_match(WHITESPACE_TO_END_OF_LINE)
class ElseBlock(_Element):
START = re.compile(r'#(?:else|\{else\})(.*)$', re.S + re.I)
def parse(self):
self.identity_match(self.START)
self.block = self.require_next_element(Block, 'block')
self.evaluate = self.block.evaluate
class ElseifBlock(_Element):
START = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I)
def parse(self):
self.identity_match(self.START)
self.condition = self.require_next_element(Condition, 'condition')
self.block = self.require_next_element(Block, 'block')
self.calculate = self.condition.calculate
self.evaluate = self.block.evaluate
class IfDirective(_Element):
START = re.compile(r'#if\b\s*(.*)$', re.S + re.I)
else_block = Null()
def parse(self):
self.identity_match(self.START)
self.condition = self.next_element(Condition)
self.block = self.require_next_element(Block, "block")
self.elseifs = []
while True:
try:
self.elseifs.append(self.next_element(ElseifBlock))
except NoMatch:
break
try:
self.else_block = self.next_element(ElseBlock)
except NoMatch:
pass
self.require_next_element(End, '#else, #elseif or #end')
def evaluate_raw(self, stream, namespace, loader):
if self.condition.calculate(namespace, loader):
self.block.evaluate(stream, namespace, loader)
else:
for elseif in self.elseifs:
if elseif.calculate(namespace, loader):
elseif.evaluate(stream, namespace, loader)
return
self.else_block.evaluate(stream, namespace, loader)
# This can't deal with assignments like
# set($one.two().three = something)
# yet
class Assignment(_Element):
START = re.compile(
r'\s*\(\s*\$([a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)\s*=\s*(.*)$',
re.S +
re.I)
END = re.compile(r'\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M)
def parse(self):
var_name, = self.identity_match(self.START)
self.terms = var_name.split('.')
self.value = self.require_next_element(Expression, "expression")
self.require_match(self.END, ')')
def evaluate_raw(self, stream, namespace, loader):
val = self.value.calculate(namespace, loader)
if len(self.terms) == 1:
namespace.set_inherited(self.terms[0], val)
else:
cur = namespace
for term in self.terms[:-1]:
cur = cur[term]
cur[self.terms[-1]] = val
class EvaluateDirective(_Element):
START = re.compile(r'#evaluate\b(.*)')
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
self.require_match(self.OPEN_PAREN, '(')
self.value = self.require_next_element(Value, 'value')
self.require_match(self.CLOSE_PAREN, ')')
def evaluate_raw(self, stream, namespace, loader):
val = self.value.calculate(namespace, loader)
Template(val, "#evaluate").merge_to(namespace, stream, loader)
class _FunctionDefinition(_Element):
# Must be overridden to provide START and NAME patterns
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
ARG_NAME = re.compile(r'[, \t]+\$([a-z][a-z_0-9]*)(.*)$', re.S + re.I)
RESERVED_NAMES = []
def parse(self):
self.identity_match(self.START)
self.require_match(self.OPEN_PAREN, '(')
self.function_name, = self.require_match(self.NAME, 'function name')
if self.function_name.lower() in self.RESERVED_NAMES:
raise self.syntax_error('non-reserved name')
self.arg_names = []
while True:
m = self.next_match(self.ARG_NAME)
if not m:
break
self.arg_names.append(m[0])
self.require_match(self.CLOSE_PAREN, ') or arg name')
self.optional_match(WHITESPACE_TO_END_OF_LINE)
self.block = self.require_next_element(Block, 'block')
self.require_next_element(End, 'block')
def execute_function(self, stream, namespace, arg_values, loader):
if len(arg_values) != len(self.arg_names):
raise Exception(
"function %s expected %d arguments, got %d" %
(self.function_name, len(self.arg_names), len(arg_values)))
local_namespace = LocalNamespace(namespace)
local_namespace.update(zip(self.arg_names, arg_values))
self.block.evaluate(stream, local_namespace, loader)
class MacroDefinition(_FunctionDefinition):
START = re.compile(r'#macro\b(.*)', re.S + re.I)
NAME = re.compile(r'\s*([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
RESERVED_NAMES = (
'if',
'else',
'elseif',
'set',
'macro',
'foreach',
'parse',
'include',
'stop',
'end',
'define')
def evaluate_raw(self, stream, namespace, loader):
global_ns = namespace.top()
macro_key = '#' + self.function_name.lower()
if macro_key in global_ns:
raise Exception("cannot redefine macro {0}".format(macro_key))
global_ns[macro_key] = self
class MacroCall(_Element):
START = re.compile(r'#([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
SPACE_OR_COMMA = re.compile(r'\s*(?:,|\s)\s*(.*)$', re.S)
def parse(self):
macro_name, = self.identity_match(self.START)
self.macro_name = macro_name.lower()
self.args = []
if self.macro_name in MacroDefinition.RESERVED_NAMES:
raise NoMatch()
if not self.optional_match(self.OPEN_PAREN):
raise NoMatch() # Typically a hex colour literal
while True:
try:
self.args.append(self.next_element(Value))
except NoMatch:
break
if not self.optional_match(self.SPACE_OR_COMMA):
break
self.require_match(self.CLOSE_PAREN, 'argument value or )')
def evaluate_raw(self, stream, namespace, loader):
try:
macro = namespace['#' + self.macro_name]
except KeyError:
raise Exception('no such macro: ' + self.macro_name)
arg_values = [arg.calculate(namespace, loader) for arg in self.args]
macro.execute_function(stream, namespace, arg_values, loader)
class DefineDefinition(_FunctionDefinition):
START = re.compile(r'#define\b(.*)', re.S + re.I)
NAME = re.compile(r'\s*\$([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
def evaluate_raw(self, stream, namespace, loader):
namespace[self.function_name] = self
class IncludeDirective(_Element):
START = re.compile(r'#include\b(.*)', re.S + re.I)
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
self.require_match(self.OPEN_PAREN, '(')
self.name = self.require_next_element(
(StringLiteral,
InterpolatedStringLiteral,
FormalReference),
'template name')
self.require_match(self.CLOSE_PAREN, ')')
def evaluate_raw(self, stream, namespace, loader):
stream.write(loader.load_text(self.name.calculate(namespace, loader)))
class ParseDirective(_Element):
START = re.compile(r'#parse\b(.*)', re.S + re.I)
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
def parse(self):
self.identity_match(self.START)
self.require_match(self.OPEN_PAREN, '(')
self.name = self.require_next_element(
(StringLiteral,
InterpolatedStringLiteral,
FormalReference),
'template name')
self.require_match(self.CLOSE_PAREN, ')')
def evaluate_raw(self, stream, namespace, loader):
template = loader.load_template(self.name.calculate(namespace, loader))
# TODO: local namespace?
template.merge_to(namespace, stream, loader=loader)
class StopDirective(_Element):
STOP = re.compile(r'#stop\b(.*)', re.S + re.I)
def parse(self):
self.identity_match(self.STOP)
def evaluate_raw(self, stream, namespace, loader):
if hasattr(stream, 'stop'):
stream.stop = True
# Represents a SINGLE user-defined directive
class UserDefinedDirective(_Element):
DIRECTIVES = []
def parse(self):
self.directive = self.next_element(self.DIRECTIVES)
def evaluate_raw(self, stream, namespace, loader):
self.directive.evaluate(stream, namespace, loader)
class SetDirective(_Element):
START = re.compile(r'#set\b(.*)', re.S + re.I)
def parse(self):
self.identity_match(self.START)
self.assignment = self.require_next_element(Assignment, 'assignment')
def evaluate_raw(self, stream, namespace, loader):
self.assignment.evaluate(stream, namespace, loader)
class ForeachDirective(_Element):
START = re.compile(r'#foreach\b(.*)$', re.S + re.I)
OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
IN = re.compile(r'[ \t]+in[ \t]+(.*)$', re.S)
LOOP_VAR_NAME = re.compile(r'\$([a-z_][a-z0-9_]*)(.*)$', re.S + re.I)
CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
def parse(self):
# Could be cleaner b/c syntax error if no '('
self.identity_match(self.START)
self.require_match(self.OPEN_PAREN, '(')
self.loop_var_name, = self.require_match(
self.LOOP_VAR_NAME, 'loop var name')
self.require_match(self.IN, 'in')
self.value = self.next_element(Value)
self.require_match(self.CLOSE_PAREN, ')')
self.block = self.next_element(Block)
self.require_next_element(End, '#end')
def evaluate_raw(self, stream, namespace, loader):
iterable = self.value.calculate(namespace, loader)
counter = 1
try:
if iterable is None:
return
if hasattr(iterable, 'keys'):
iterable = iterable.keys()
try:
iter(iterable)
except TypeError:
raise ValueError(
"value for $%s is not iterable in #foreach: %s" %
(self.loop_var_name, iterable))
length = len(iterable)
for item in iterable:
localns = LocalNamespace(namespace)
localns['velocityCount'] = counter
localns['velocityHasNext'] = counter < length
localns['foreach'] = {
"count": counter,
"index": counter - 1,
"hasNext": counter < length,
"first": counter == 1,
"last": counter == length}
localns[self.loop_var_name] = item
self.block.evaluate(stream, localns, loader)
counter += 1
except TypeError:
raise
class TemplateBody(_Element):
def parse(self):
self.block = self.next_element(Block)
if self.next_text():
raise self.syntax_error('block element')
def evaluate_raw(self, stream, namespace, loader):
# Use the same namespace as the parent template, if sub-template
if not isinstance(namespace, LocalNamespace):
namespace = LocalNamespace(namespace)
self.block.evaluate(stream, namespace, loader)
class Block(_Element):
def parse(self):
self.children = []
while True:
try:
self.children.append(
self.next_element(
(Text,
FormalReference,
Comment,
IfDirective,
SetDirective,
ForeachDirective,
IncludeDirective,
ParseDirective,
MacroDefinition,
DefineDefinition,
StopDirective,
UserDefinedDirective,
EvaluateDirective,
MacroCall,
FallthroughHashText)))
except NoMatch:
break
def evaluate_raw(self, stream, namespace, loader):
for child in self.children:
child.evaluate(stream, namespace, loader)
python-airspeed-0.5.20/airspeed/api.py 0000664 0000000 0000000 00000002017 14303667666 0017651 0 ustar 00root root 0000000 0000000 # encoding: utf-8
import os
try:
from airspeed import CachingFileLoader
except ImportError:
raise ImportError('You must install the airspeed package.')
from cachetools import LRUCache
__all__ = ['Airspeed']
class Airspeed(object):
"""The airspeed templating engine.
Sample usage:
>>> from cti.core import Engines
>>> render = Engines()
>>> render('airspeed:../tests/test.txt', dict())
('text/plain', 'Bingo!')
"""
def __init__(self, cache=10, **kw):
self.loaders = LRUCache(maxsize=cache)
def __call__(self, data, template, mime_type="text/plain", **options):
basepath = os.path.dirname(template)
if basepath not in self.loaders:
loader = CachingFileLoader(basepath)
self.loaders[basepath] = loader
else:
loader = self.loaders[basepath]
template = self.loaders[basepath].load_template(
os.path.basename(template))
return mime_type, template.merge(data, loader=loader)
python-airspeed-0.5.20/setup.cfg 0000664 0000000 0000000 00000000223 14303667666 0016550 0 ustar 00root root 0000000 0000000 [egg_info]
[nosetests]
with-coverage=1
cover-package=airspeed
cover-inclusive=1
traverse-namespace=1
detailed-errors=1
where=tests
with-doctest=1
python-airspeed-0.5.20/setup.py 0000775 0000000 0000000 00000002757 14303667666 0016462 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python2.7
# encoding: utf-8
import sys
from setuptools import setup, find_packages
if sys.version_info <= (2, 6):
raise SystemExit("Python 2.6 or later is required.")
setup(
name="airspeed",
version="0.5.20",
description=("Airspeed is a powerful and easy-to-use templating engine"
" for Python that aims for a high level of compatibility "
"with the popular Velocity library for Java."),
author="Steve Purcell and Chris Tarttelin",
author_email="steve@pythonconsulting.com, chris@pythonconsulting.com",
url="https://github.com/purcell/airspeed/",
download_url="http://pypi.python.org/pypi/airspeed/",
license="BSD",
keywords='web.templating',
install_requires=[
'six',
'cachetools',
],
test_suite='tests',
tests_require=[],
classifiers=[
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules"],
packages=find_packages(
exclude=[
'examples',
'tests',
'tests.*',
'docs']),
include_package_data=False,
zip_safe=True,
entry_points={
'web.templating': [
'airspeed = airspeed.api:Airspeed',
]})
python-airspeed-0.5.20/tests/ 0000775 0000000 0000000 00000000000 14303667666 0016074 5 ustar 00root root 0000000 0000000 python-airspeed-0.5.20/tests/__init__.py 0000664 0000000 0000000 00000153603 14303667666 0020215 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import re
import sys
if sys.version_info >= (3, 0) and sys.version_info <= (3, 3):
import imp
elif sys.version_info >= (3, 4):
import importlib
from unittest import TestCase
# Make these tests runnable without needing 'nose' installed
try:
import airspeed
except ImportError:
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import airspeed
import six
class TemplateTestCase(TestCase):
def assertRaisesExecutionError(self, exctype, func, *args, **kwargs):
try:
func(*args, **kwargs)
self.fail("Expected TemplateExecutionError wrapping %s" %
(exctype,))
except airspeed.TemplateExecutionError as e:
self.assertEqual(exctype, type(e.__cause__))
def test_parser_returns_input_when_there_is_nothing_to_substitute(self):
template = airspeed.Template("")
self.assertEqual("", template.merge({}))
def test_parser_substitutes_string_added_to_the_context(self):
template = airspeed.Template("Hello $name")
self.assertEqual("Hello Chris", template.merge({"name": "Chris"}))
def test_dollar_left_untouched(self):
template = airspeed.Template("Hello $ ")
self.assertEqual("Hello $ ", template.merge({}))
template = airspeed.Template("Hello $")
self.assertEqual("Hello $", template.merge({}))
def test_unmatched_name_does_not_get_substituted(self):
template = airspeed.Template("Hello $name")
self.assertEqual("Hello $name", template.merge({}))
def test_silent_substitution_for_unmatched_values(self):
template = airspeed.Template("Hello $!name")
self.assertEqual("Hello world", template.merge({"name": "world"}))
self.assertEqual("Hello ", template.merge({}))
def test_formal_reference_in_an_if_condition(self):
template = airspeed.Template("#if(${a.b.c})yes!#end")
# reference in an if statement used to be a problem
self.assertEqual("yes!", template.merge({'a': {'b': {'c': 'd'}}}))
self.assertEqual("", template.merge({}))
def test_silent_formal_reference_in_an_if_condition(self):
# the silent modifier shouldn't make a difference here
template = airspeed.Template("#if($!{a.b.c})yes!#end")
self.assertEqual("yes!", template.merge({'a': {'b': {'c': 'd'}}}))
self.assertEqual("", template.merge({}))
# with or without curly braces
template = airspeed.Template("#if($!a.b.c)yes!#end")
self.assertEqual("yes!", template.merge({'a': {'b': {'c': 'd'}}}))
self.assertEqual("", template.merge({}))
def test_reference_function_calls_in_if_conditions(self):
template = airspeed.Template("#if(${a.b.c('cheese')})yes!#end")
self.assertEqual(
"yes!", template.merge({'a': {'b': {'c': lambda x: "hello %s" % x}}}))
self.assertEqual(
"", template.merge({'a': {'b': {'c': lambda x: None}}}))
self.assertEqual("", template.merge({}))
def test_silent_reference_function_calls_in_if_conditions(self):
# again, this shouldn't make any difference
template = airspeed.Template("#if($!{a.b.c('cheese')})yes!#end")
self.assertEqual(
"yes!", template.merge({'a': {'b': {'c': lambda x: "hello %s" % x}}}))
self.assertEqual(
"", template.merge({'a': {'b': {'c': lambda x: None}}}))
self.assertEqual("", template.merge({}))
# with or without braces
template = airspeed.Template("#if($!a.b.c('cheese'))yes!#end")
self.assertEqual(
"yes!", template.merge({'a': {'b': {'c': lambda x: "hello %s" % x}}}))
self.assertEqual(
"", template.merge({'a': {'b': {'c': lambda x: None}}}))
self.assertEqual("", template.merge({}))
def test_embed_substitution_value_in_braces_gets_handled(self):
template = airspeed.Template("Hello ${name}.")
self.assertEqual("Hello World.", template.merge({"name": "World"}))
def test_unmatched_braces_raises_exception(self):
template = airspeed.Template("Hello ${name.")
self.assertRaises(airspeed.TemplateSyntaxError, template.merge, {})
def test_unmatched_trailing_brace_preserved(self):
template = airspeed.Template("Hello $name}.")
self.assertEqual("Hello World}.", template.merge({"name": "World"}))
def test_formal_reference_with_alternate_literal_value(self):
template = airspeed.Template("${a|'hello'}")
self.assertEqual("foo", template.merge({'a': "foo"}))
self.assertEqual("hello", template.merge({}))
def test_formal_reference_with_alternate_expression_value(self):
template = airspeed.Template("${a|$b}")
self.assertEqual("hello", template.merge({'b': "hello"}))
def test_can_return_value_from_an_attribute_of_a_context_object(self):
template = airspeed.Template("Hello $name.first_name")
class MyObj:
pass
o = MyObj()
o.first_name = 'Chris'
self.assertEqual("Hello Chris", template.merge({"name": o}))
def test_can_return_value_from_a_method_of_a_context_object(self):
template = airspeed.Template("Hello $name.first_name()")
class MyObj:
def first_name(self):
return "Chris"
self.assertEqual("Hello Chris", template.merge({"name": MyObj()}))
def test_when_if_statement_resolves_to_true_the_content_is_returned(self):
template = airspeed.Template(
"Hello #if ($name)your name is ${name}#end Good to see you")
self.assertEqual(
"Hello your name is Steve Good to see you", template.merge({"name": "Steve"}))
def test_when_if_statement_resolves_to_false_the_content_is_skipped(self):
template = airspeed.Template(
"Hello #if ($show_greeting)your name is ${name}#end Good to see you")
self.assertEqual("Hello Good to see you", template.merge(
{"name": "Steve", "show_greeting": False}))
def test_when_if_statement_is_nested_inside_a_successful_enclosing_if_it_gets_evaluated(
self):
template = airspeed.Template(
"Hello #if ($show_greeting)your name is ${name}.#if ($is_birthday) Happy Birthday.#end#end Good to see you")
namespace = {"name": "Steve", "show_greeting": False}
self.assertEqual("Hello Good to see you", template.merge(namespace))
namespace["show_greeting"] = True
self.assertEqual(
"Hello your name is Steve. Good to see you",
template.merge(namespace))
namespace["is_birthday"] = True
self.assertEqual(
"Hello your name is Steve. Happy Birthday. Good to see you",
template.merge(namespace))
def test_if_statement_considers_None_to_be_false(self):
template = airspeed.Template("#if ($some_value)hide me#end")
self.assertEqual('', template.merge({}))
self.assertEqual('', template.merge({'some_value': None}))
def test_if_statement_honours_custom_truth_value_of_objects(self):
class BooleanValue(object):
def __init__(self, value):
self.value = value
def __bool__(self):
return self.value
def __nonzero__(self):
return self.__bool__()
template = airspeed.Template("#if ($v)yes#end")
self.assertEqual('', template.merge({'v': BooleanValue(False)}))
self.assertEqual('yes', template.merge({'v': BooleanValue(True)}))
def test_understands_boolean_literal_true(self):
template = airspeed.Template("#set ($v = true)$v")
self.assertEqual('True', template.merge({}))
def test_understands_boolean_literal_false(self):
template = airspeed.Template("#set ($v = false)$v")
self.assertEqual('False', template.merge({}))
def test_new_lines_in_templates_are_permitted(self):
template = airspeed.Template(
"hello #if ($show_greeting)${name}.\n#if($is_birthday)Happy Birthday\n#end.\n#endOff out later?")
namespace = {
"name": "Steve",
"show_greeting": True,
"is_birthday": True}
self.assertEqual(
"hello Steve.\nHappy Birthday\n.\nOff out later?",
template.merge(namespace))
def test_foreach_with_plain_content_loops_correctly(self):
template = airspeed.Template(
"#foreach ($name in $names)Hello you. #end")
self.assertEqual(
"Hello you. Hello you. ", template.merge({"names": ["Chris", "Steve"]}))
def test_foreach_skipped_when_nested_in_a_failing_if(self):
template = airspeed.Template(
"#if ($false_value)#foreach ($name in $names)Hello you. #end#end")
self.assertEqual(
"", template.merge({"false_value": False, "names": ["Chris", "Steve"]}))
def test_foreach_with_expression_content_loops_correctly(self):
template = airspeed.Template(
"#foreach ($name in $names)Hello $you. #end")
self.assertEqual("Hello You. Hello You. ", template.merge(
{"you": "You", "names": ["Chris", "Steve"]}))
def test_foreach_makes_loop_variable_accessible(self):
template = airspeed.Template(
"#foreach ($name in $names)Hello $name. #end")
self.assertEqual(
"Hello Chris. Hello Steve. ", template.merge({"names": ["Chris", "Steve"]}))
def test_loop_variable_not_accessible_after_loop(self):
template = airspeed.Template(
"#foreach ($name in $names)Hello $name. #end$name")
self.assertEqual("Hello Chris. Hello Steve. $name", template.merge(
{"names": ["Chris", "Steve"]}))
def test_loop_variables_do_not_clash_in_nested_loops(self):
template = airspeed.Template(
"#foreach ($word in $greetings)$word to#foreach ($word in $names) $word#end. #end")
namespace = {
"greetings": [
"Hello", "Goodbye"], "names": [
"Chris", "Steve"]}
self.assertEqual(
"Hello to Chris Steve. Goodbye to Chris Steve. ",
template.merge(namespace))
def test_loop_counter_variable_available_in_loops(self):
template = airspeed.Template(
"#foreach ($word in $greetings)$velocityCount,#end")
namespace = {"greetings": ["Hello", "Goodbye"]}
self.assertEqual("1,2,", template.merge(namespace))
def test_loop_counter_variable_available_in_loops_new(self):
template = airspeed.Template(
"#foreach ($word in $greetings)$foreach.count,#end")
namespace = {"greetings": ["Hello", "Goodbye"]}
self.assertEqual("1,2,", template.merge(namespace))
def test_loop_index_variable_available_in_loops_new(self):
template = airspeed.Template(
"#foreach ($word in $greetings)$foreach.index,#end")
namespace = {"greetings": ["Hello", "Goodbye"]}
self.assertEqual("0,1,", template.merge(namespace))
def test_loop_counter_variables_do_not_clash_in_nested_loops(self):
template = airspeed.Template(
"#foreach ($word in $greetings)Outer $velocityCount#foreach ($word in $names), inner $velocityCount#end. #end")
namespace = {
"greetings": [
"Hello", "Goodbye"], "names": [
"Chris", "Steve"]}
self.assertEqual(
"Outer 1, inner 1, inner 2. Outer 2, inner 1, inner 2. ",
template.merge(namespace))
def test_loop_counter_variables_do_not_clash_in_nested_loops_new(self):
template = airspeed.Template(
"#foreach ($word in $greetings)Outer $foreach.count#foreach ($word in $names), inner $foreach.count#end. #end")
namespace = {
"greetings": [
"Hello", "Goodbye"], "names": [
"Chris", "Steve"]}
self.assertEqual(
"Outer 1, inner 1, inner 2. Outer 2, inner 1, inner 2. ",
template.merge(namespace))
def test_loop_index_variables_do_not_clash_in_nested_loops_new(self):
template = airspeed.Template(
"#foreach ($word in $greetings)Outer $foreach.index#foreach ($word in $names), inner $foreach.index#end. #end")
namespace = {
"greetings": [
"Hello", "Goodbye"], "names": [
"Chris", "Steve"]}
self.assertEqual(
"Outer 0, inner 0, inner 1. Outer 1, inner 0, inner 1. ",
template.merge(namespace))
def test_has_next(self):
template = airspeed.Template(
"#foreach ($i in [1, 2, 3])$i. #if ($velocityHasNext)yes#end, #end")
self.assertEqual("1. yes, 2. yes, 3. , ", template.merge({}))
def test_has_next_new(self):
template = airspeed.Template(
"#foreach ($i in [1, 2, 3])$i. #if ($foreach.hasNext)yes#end, #end")
self.assertEqual("1. yes, 2. yes, 3. , ", template.merge({}))
def test_first(self):
template = airspeed.Template(
"#foreach ($i in [1, 2, 3])$i. #if ($foreach.first)yes#end, #end")
self.assertEqual("1. yes, 2. , 3. , ", template.merge({}))
def test_last(self):
template = airspeed.Template(
"#foreach ($i in [1, 2, 3])$i. #if ($foreach.last)yes#end, #end")
self.assertEqual("1. , 2. , 3. yes, ", template.merge({}))
def test_can_use_an_integer_variable_defined_in_template(self):
template = airspeed.Template("#set ($value = 10)$value")
self.assertEqual("10", template.merge({}))
def test_passed_in_namespace_not_modified_by_set(self):
template = airspeed.Template("#set ($value = 10)$value")
namespace = {}
template.merge(namespace)
self.assertEqual({}, namespace)
def test_can_use_a_string_variable_defined_in_template(self):
template = airspeed.Template('#set ($value = "Steve")$value')
self.assertEqual("Steve", template.merge({}))
def test_can_use_a_single_quoted_string_variable_defined_in_template(self):
template = airspeed.Template("#set ($value = 'Steve')$value")
self.assertEqual("Steve", template.merge({}))
def test_single_line_comments_skipped(self):
template = airspeed.Template(
'## comment\nStuff\nMore stuff## more comments $blah')
self.assertEqual("Stuff\nMore stuff", template.merge({}))
def test_multi_line_comments_skipped(self):
template = airspeed.Template(
'Stuff#*\n more comments *#\n and more stuff')
self.assertEqual("Stuff and more stuff", template.merge({}))
def test_merge_to_stream(self):
template = airspeed.Template('Hello $name!')
output = six.StringIO()
template.merge_to({"name": "Chris"}, output)
self.assertEqual('Hello Chris!', output.getvalue())
def test_string_literal_can_contain_embedded_escaped_quotes(self):
template = airspeed.Template('#set ($name = "\\"batman\\"")$name')
self.assertEqual('"batman"', template.merge({}))
def test_string_literal_can_contain_embedded_escaped_newlines(self):
template = airspeed.Template(
'#set ($name = "\\\\batman\\nand robin")$name')
self.assertEqual('\\batman\nand robin', template.merge({}))
def test_else_block_evaluated_when_if_expression_false(self):
template = airspeed.Template('#if ($value) true #else false #end')
self.assertEqual(" false ", template.merge({}))
def test_curly_else(self):
template = airspeed.Template('#if($value)true#{else}false#end')
self.assertEqual("false", template.merge({}))
def test_curly_end(self):
template = airspeed.Template('#if($value)true#{end}monkey')
self.assertEqual("monkey", template.merge({}))
def test_too_many_end_clauses_trigger_error(self):
template = airspeed.Template('#if (1)true!#end #end ')
self.assertRaises(airspeed.TemplateSyntaxError, template.merge, {})
def test_can_call_function_with_one_parameter(self):
def squared(number):
return number * number
template = airspeed.Template('$squared(8)')
self.assertEqual("64", template.merge(locals()))
some_var = 6
template = airspeed.Template('$squared($some_var)')
self.assertEqual("36", template.merge(locals()))
template = airspeed.Template('$squared($squared($some_var))')
self.assertEqual("1296", template.merge(locals()))
def test_can_call_function_with_two_parameters(self):
def multiply(number1, number2):
return number1 * number2
template = airspeed.Template('$multiply(2, 4)')
self.assertEqual("8", template.merge(locals()))
template = airspeed.Template('$multiply( 2 , 4 )')
self.assertEqual("8", template.merge(locals()))
value1, value2 = 4, 12
template = airspeed.Template('$multiply($value1,$value2)')
self.assertEqual("48", template.merge(locals()))
def test_extract_array_index_from_function_result(self):
def get_array():
return ['p1', ['p2', 'p3']]
template = airspeed.Template('$get_array()[0]')
self.assertEqual("p1", template.merge(locals()))
template = airspeed.Template('$get_array()[1][1]')
self.assertEqual("p3", template.merge(locals()))
def test_velocity_style_escaping(self): # example from Velocity docs
template = airspeed.Template(r'''
#set( $email = "foo" )
$email
\$email
\\$email
\
\\ \# \$
\#end
\# end
\#set( $email = "foo" )
''')
self.assertEqual(r'''
foo
$email
\\foo
\
\\ \# \$
#end
\# end
#set( foo = "foo" )
''', template.merge({}))
# def test_velocity_style_escaping_when_var_unset(self): # example from Velocity docs
# template = airspeed.Template('''\
#$email
#\$email
#\\$email
#\\\$email''')
# self.assertEquals('''\
#$email
#\$email
#\\$email
#\\\$email''', template.merge({}))
def test_true_elseif_evaluated_when_if_is_false(self):
template = airspeed.Template(
'#if ($value1) one #elseif ($value2) two #end')
value1, value2 = False, True
self.assertEqual(' two ', template.merge(locals()))
def test_false_elseif_skipped_when_if_is_true(self):
template = airspeed.Template(
'#if ($value1) one #elseif ($value2) two #end')
value1, value2 = True, False
self.assertEqual(' one ', template.merge(locals()))
def test_first_true_elseif_evaluated_when_if_is_false(self):
template = airspeed.Template(
'#if ($value1) one #elseif ($value2) two #elseif($value3) three #end')
value1, value2, value3 = False, True, True
self.assertEqual(' two ', template.merge(locals()))
def test_illegal_to_have_elseif_after_else(self):
template = airspeed.Template(
'#if ($value1) one #else two #elseif($value3) three #end')
self.assertRaises(airspeed.TemplateSyntaxError, template.merge, {})
def test_else_evaluated_when_if_and_elseif_are_false(self):
template = airspeed.Template(
'#if ($value1) one #elseif ($value2) two #else three #end')
value1, value2 = False, False
self.assertEqual(' three ', template.merge(locals()))
def test_syntax_error_contains_line_and_column_pos(self):
try:
airspeed.Template('#if ( $hello )\n\n#elseif blah').merge({})
except airspeed.TemplateSyntaxError as e:
self.assertEqual((3, 9), (e.line, e.column))
else:
self.fail('expected error')
try:
airspeed.Template('#else blah').merge({})
except airspeed.TemplateSyntaxError as e:
self.assertEqual((1, 1), (e.line, e.column))
else:
self.fail('expected error')
def test_get_position_strings_in_syntax_error(self):
try:
airspeed.Template('#else whatever').merge({})
except airspeed.TemplateSyntaxError as e:
self.assertEqual(['#else whatever',
'^'], e.get_position_strings())
else:
self.fail('expected error')
def test_get_position_strings_in_syntax_error_when_newline_after_error(
self):
try:
airspeed.Template('#else whatever\n').merge({})
except airspeed.TemplateSyntaxError as e:
self.assertEqual(['#else whatever',
'^'], e.get_position_strings())
else:
self.fail('expected error')
def test_get_position_strings_in_syntax_error_when_newline_before_error(
self):
try:
airspeed.Template('foobar\n #else whatever\n').merge({})
except airspeed.TemplateSyntaxError as e:
self.assertEqual([' #else whatever',
' ^'], e.get_position_strings())
else:
self.fail('expected error')
def test_compare_greater_than_operator(self):
for operator in ['>', 'gt']:
template = airspeed.Template('#if ( $value %s 1 )yes#end' %
operator)
self.assertEqual('', template.merge({'value': 0}))
self.assertEqual('', template.merge({'value': 1}))
self.assertEqual('yes', template.merge({'value': 2}))
def test_compare_greater_than_or_equal_operator(self):
for operator in ['>=', 'ge']:
template = airspeed.Template('#if ( $value %s 1 )yes#end' %
operator)
self.assertEqual('', template.merge({'value': 0}))
self.assertEqual('yes', template.merge({'value': 1}))
self.assertEqual('yes', template.merge({'value': 2}))
def test_compare_less_than_operator(self):
for operator in ['<', 'lt']:
template = airspeed.Template('#if ( $value %s 1 )yes#end' %
operator)
self.assertEqual('yes', template.merge({'value': 0}))
self.assertEqual('', template.merge({'value': 1}))
self.assertEqual('', template.merge({'value': 2}))
def test_compare_less_than_or_equal_operator(self):
for operator in ['<=', 'le']:
template = airspeed.Template('#if ( $value %s 1 )yes#end' %
operator)
self.assertEqual('yes', template.merge({'value': 0}))
self.assertEqual('yes', template.merge({'value': 1}))
self.assertEqual('', template.merge({'value': 2}))
def test_compare_equality_operator(self):
for operator in ['==', 'eq']:
template = airspeed.Template('#if ( $value %s 1 )yes#end' %
operator)
self.assertEqual('', template.merge({'value': 0}))
self.assertEqual('yes', template.merge({'value': 1}))
self.assertEqual('', template.merge({'value': 2}))
def test_or_operator(self):
template = airspeed.Template('#if ( $value1 || $value2 )yes#end')
self.assertEqual(
'', template.merge({'value1': False, 'value2': False}))
self.assertEqual(
'yes', template.merge({'value1': True, 'value2': False}))
self.assertEqual(
'yes', template.merge({'value1': False, 'value2': True}))
def test_or_operator_otherform(self):
template = airspeed.Template('#if ( $value1 or $value2 )yes#end')
self.assertEqual(
'', template.merge({'value1': False, 'value2': False}))
self.assertEqual(
'yes', template.merge({'value1': True, 'value2': False}))
self.assertEqual(
'yes', template.merge({'value1': False, 'value2': True}))
def test_or_operator_considers_not_None_values_true(self):
class SomeClass:
pass
template = airspeed.Template('#if ( $value1 || $value2 )yes#end')
self.assertEqual('', template.merge({'value1': None, 'value2': None}))
self.assertEqual(
'yes', template.merge({'value1': SomeClass(), 'value2': False}))
self.assertEqual(
'yes', template.merge({'value1': False, 'value2': SomeClass()}))
def test_and_operator(self):
template = airspeed.Template('#if ( $value1 && $value2 )yes#end')
self.assertEqual(
'', template.merge({'value1': False, 'value2': False}))
self.assertEqual(
'', template.merge({'value1': True, 'value2': False}))
self.assertEqual(
'', template.merge({'value1': False, 'value2': True}))
self.assertEqual(
'yes', template.merge({'value1': True, 'value2': True}))
def test_and_operator_otherform(self):
template = airspeed.Template('#if ( $value1 and $value2 )yes#end')
self.assertEqual(
'', template.merge({'value1': False, 'value2': False}))
self.assertEqual(
'', template.merge({'value1': True, 'value2': False}))
self.assertEqual(
'', template.merge({'value1': False, 'value2': True}))
self.assertEqual(
'yes', template.merge({'value1': True, 'value2': True}))
def test_and_operator_considers_not_None_values_true(self):
class SomeClass:
pass
template = airspeed.Template('#if ( $value1 && $value2 )yes#end')
self.assertEqual('', template.merge({'value1': None, 'value2': None}))
self.assertEqual(
'yes', template.merge({'value1': SomeClass(), 'value2': True}))
self.assertEqual(
'yes', template.merge({'value1': True, 'value2': SomeClass()}))
def test_parenthesised_value(self):
template = airspeed.Template(
'#if ( ($value1 == 1) && ($value2 == 2) )yes#end')
self.assertEqual('', template.merge({'value1': 0, 'value2': 1}))
self.assertEqual('', template.merge({'value1': 1, 'value2': 1}))
self.assertEqual('', template.merge({'value1': 0, 'value2': 2}))
self.assertEqual('yes', template.merge({'value1': 1, 'value2': 2}))
def test_multiterm_expression(self):
template = airspeed.Template(
'#if ( $value1 == 1 && $value2 == 2 )yes#end')
self.assertEqual('', template.merge({'value1': 0, 'value2': 1}))
self.assertEqual('', template.merge({'value1': 1, 'value2': 1}))
self.assertEqual('', template.merge({'value1': 0, 'value2': 2}))
self.assertEqual('yes', template.merge({'value1': 1, 'value2': 2}))
def test_compound_condition(self):
template = airspeed.Template('#if ( ($value) )yes#end')
self.assertEqual('', template.merge({'value': False}))
self.assertEqual('yes', template.merge({'value': True}))
def test_logical_negation_operator(self):
template = airspeed.Template('#if ( !$value )yes#end')
self.assertEqual('yes', template.merge({'value': False}))
self.assertEqual('', template.merge({'value': True}))
def test_logical_alt_negation_operator(self):
template = airspeed.Template('#if ( not $value )yes#end')
self.assertEqual('yes', template.merge({'value': False}))
self.assertEqual('', template.merge({'value': True}))
def test_logical_negation_operator_yields_true_for_None(self):
template = airspeed.Template('#if ( !$value )yes#end')
self.assertEqual('yes', template.merge({'value': None}))
def test_logical_negation_operator_honours_custom_truth_values(self):
class BooleanValue(object):
def __init__(self, value):
self.value = value
def __bool__(self):
return self.value
def __nonzero__(self):
return self.__bool__()
template = airspeed.Template('#if ( !$v)yes#end')
self.assertEqual('yes', template.merge({'v': BooleanValue(False)}))
self.assertEqual('', template.merge({'v': BooleanValue(True)}))
def test_compound_binary_and_unary_operators(self):
template = airspeed.Template('#if ( !$value1 && !$value2 )yes#end')
self.assertEqual(
'', template.merge({'value1': False, 'value2': True}))
self.assertEqual(
'', template.merge({'value1': True, 'value2': False}))
self.assertEqual('', template.merge({'value1': True, 'value2': True}))
self.assertEqual(
'yes', template.merge({'value1': False, 'value2': False}))
def test_cannot_define_macro_to_override_reserved_statements(self):
for reserved in (
'if',
'else',
'elseif',
'set',
'macro',
'foreach',
'parse',
'include',
'stop',
'end',
'define'):
template = airspeed.Template(
'#macro ( %s $value) $value #end' %
reserved)
self.assertRaises(airspeed.TemplateSyntaxError, template.merge, {})
def test_cannot_call_undefined_macro(self):
template = airspeed.Template('#undefined()')
self.assertRaises(airspeed.TemplateExecutionError, template.merge, {})
def test_define_and_use_macro_with_no_parameters(self):
template = airspeed.Template('#macro ( hello)hi#end#hello ()#hello()')
self.assertEqual('hihi', template.merge({'text': 'hello'}))
def test_define_and_use_macro_with_one_parameter(self):
template = airspeed.Template(
'#macro ( bold $value)$value#end#bold ($text)')
self.assertEqual(
'hello', template.merge({'text': 'hello'}))
def test_define_and_use_macro_with_two_parameters_no_comma(self):
template = airspeed.Template(
'#macro ( bold $value $other)$value$other#end#bold ($text $monkey)')
self.assertEqual('hellocheese',
template.merge({'text': 'hello', 'monkey': 'cheese'}))
# We use commas with our macros and it seems to work
# so it's correct behavior by definition; the real
# question is whether using them without a comma is a legal variant
# or not. This should affect the above test; the following test
# should be legal by definition
def test_define_and_use_macro_with_two_parameters_with_comma(self):
template = airspeed.Template(
'#macro ( bold $value, $other)$value$other#end#bold ($text, $monkey)')
self.assertEqual('hellocheese',
template.merge({'text': 'hello', 'monkey': 'cheese'}))
def test_use_of_macro_name_is_case_insensitive(self):
template = airspeed.Template(
'#macro ( bold $value)$value#end#BoLd ($text)')
self.assertEqual(
'hello', template.merge({'text': 'hello'}))
def test_define_and_use_macro_with_two_parameter(self):
template = airspeed.Template(
'#macro (addition $value1 $value2 )$value1+$value2#end#addition (1 2)')
self.assertEqual('1+2', template.merge({}))
template = airspeed.Template(
'#macro (addition $value1 $value2 )$value1+$value2#end#addition( $one $two )')
self.assertEqual(
'ONE+TWO', template.merge({'one': 'ONE', 'two': 'TWO'}))
def test_cannot_redefine_macro(self):
template = airspeed.Template(
'#macro ( hello)hi#end#macro(hello)again#end')
self.assertRaises(
airspeed.TemplateExecutionError,
template.merge,
{}) # Should this be TemplateSyntaxError?
def test_can_call_macro_with_newline_between_args(self):
template = airspeed.Template(
'#macro (hello $value1 $value2 )hello $value1 and $value2#end\n#hello (1,\n 2)')
self.assertEqual('hello 1 and 2', template.merge({}))
def test_use_define_with_no_parameters(self):
template = airspeed.Template('#define ( $hello)hi#end$hello()$hello()')
self.assertEqual('hihi', template.merge({}))
def test_use_define_with_parameters(self):
template = airspeed.Template('#define ( $echo $v1 $v2)$v1$v2#end$echo(1,"a")$echo("b",2)')
self.assertEqual('1ab2', template.merge({'text': 'hello'}))
template = airspeed.Template('#define ( $echo $v1 $v2)$v1$v2#end$echo(1,"a")$echo($echo(2,"b"),"c")')
self.assertEqual('1a2bc', template.merge({}))
template = airspeed.Template('#define ( $echo $v1 $v2)$v1$v2#end$echo(1,"a")$echo("b",$echo(3,"c"))')
self.assertEqual('1ab3c', template.merge({}))
def test_define_with_local_namespace(self):
template = airspeed.Template("#define ( $showindex )$foreach.index#end#foreach($x in [1,2,3])$showindex#end")
self.assertEqual('012', template.merge({}))
def test_use_defined_func_multiple_times(self):
template = airspeed.Template("""
#define( $myfunc )$ctx#end
#set( $ctx = 'foo' )
$myfunc
#set( $ctx = 'bar' )
$myfunc
""")
result = template.merge({}).replace("\n", "").replace(" ", "")
self.assertEqual('foobar', result)
def test_use_defined_func_create_json_loop(self):
template = airspeed.Template("""
#define( $loop ) {
#foreach($e in $map.keySet())
#set( $k = $e )
#set( $v = $map.get($k))
"$k": "$v"
#if( $foreach.hasNext ) , #end
#end
}
#end
$loop
#set( $map = {'foo':'bar'} )
$loop
""")
context = {"map": {"test": 123, "test2": "abc"}}
result = re.sub(r"\s", "", template.merge(context), flags=re.MULTILINE)
self.assertEqual('{"test":"123","test2":"abc"}{"foo":"bar"}', result)
def test_include_directive_gives_error_if_no_loader_provided(self):
template = airspeed.Template('#include ("foo.tmpl")')
self.assertRaises(airspeed.TemplateError, template.merge, {})
def test_include_directive_yields_loader_error_if_included_content_not_found(
self):
class BrokenLoader:
def load_text(self, name):
raise IOError(name)
template = airspeed.Template('#include ("foo.tmpl")')
self.assertRaisesExecutionError(IOError, template.merge,
{}, loader=BrokenLoader())
def test_valid_include_directive_include_content(self):
class WorkingLoader:
def load_text(self, name):
if name == 'foo.tmpl':
return "howdy"
template = airspeed.Template('Message is: #include ("foo.tmpl")!')
self.assertEqual(
'Message is: howdy!',
template.merge(
{},
loader=WorkingLoader()))
def test_parse_directive_gives_error_if_no_loader_provided(self):
template = airspeed.Template('#parse ("foo.tmpl")')
self.assertRaises(airspeed.TemplateExecutionError, template.merge, {})
def test_parse_directive_yields_loader_error_if_parsed_content_not_found(
self):
class BrokenLoader:
def load_template(self, name):
raise IOError(name)
template = airspeed.Template('#parse ("foo.tmpl")')
self.assertRaisesExecutionError(IOError, template.merge,
{}, loader=BrokenLoader())
def test_valid_parse_directive_outputs_parsed_content(self):
class WorkingLoader:
def load_template(self, name):
if name == 'foo.tmpl':
return airspeed.Template("$message", name)
template = airspeed.Template('Message is: #parse ("foo.tmpl")!')
self.assertEqual('Message is: hola!',
template.merge({'message': 'hola'},
loader=WorkingLoader()))
template = airspeed.Template('Message is: #parse ($foo)!')
self.assertEqual('Message is: hola!', template.merge(
{'foo': 'foo.tmpl', 'message': 'hola'}, loader=WorkingLoader()))
def test_valid_parse_directive_merge_namespace(self):
class WorkingLoader:
def load_template(self, name):
if name == 'foo.tmpl':
return airspeed.Template("#set($message = 'hola')")
template = airspeed.Template('#parse("foo.tmpl")Message is: $message!')
self.assertEqual(
'Message is: hola!',
template.merge(
{},
loader=WorkingLoader()))
def test_assign_range_literal(self):
template = airspeed.Template(
'#set($values = [1..5])#foreach($value in $values)$value,#end')
self.assertEqual('1,2,3,4,5,', template.merge({}))
template = airspeed.Template(
'#set($values = [2..-2])#foreach($value in $values)$value,#end')
self.assertEqual('2,1,0,-1,-2,', template.merge({}))
def test_local_namespace_methods_are_not_available_in_context(self):
template = airspeed.Template('#macro(tryme)$values#end#tryme()')
self.assertEqual('$values', template.merge({}))
def test_array_literal(self):
template = airspeed.Template(
'blah\n#set($valuesInList = ["Hello ", $person, ", your lucky number is ", 7])\n#foreach($value in $valuesInList)$value#end\n\nblah')
self.assertEqual(
'blah\nHello Chris, your lucky number is 7\nblah', template.merge({'person': 'Chris'}))
# NOTE: the original version of this test incorrectly preserved
# the newline at the end of the #end line
def test_dictionary_literal(self):
template = airspeed.Template(
'#set($a = {"dog": "cat" , "horse":15})$a.dog')
self.assertEqual('cat', template.merge({}))
template = airspeed.Template('#set($a = {"dog": "$horse"})$a.dog')
self.assertEqual('cow', template.merge({'horse': 'cow'}))
def test_dictionary_literal_as_parameter(self):
template = airspeed.Template('$a({"color":"blue"})')
ns = {'a': lambda x: x['color'] + ' food'}
self.assertEqual('blue food', template.merge(ns))
def test_nested_array_literals(self):
template = airspeed.Template(
'#set($values = [["Hello ", "Steve"], ["Hello", " Chris"]])#foreach($pair in $values)#foreach($word in $pair)$word#end. #end')
self.assertEqual('Hello Steve. Hello Chris. ', template.merge({}))
def test_when_dictionary_does_not_contain_referenced_attribute_no_substitution_occurs(
self):
template = airspeed.Template(" $user.name ")
self.assertEqual(" $user.name ", template.merge({'user': self}))
def test_when_dictionary_has_same_key_as_built_in_method(self):
template = airspeed.Template(" $user.items ")
self.assertEqual(" 1;2;3 ", template.merge({'user': {'items': '1;2;3'}}))
def test_when_non_dictionary_object_does_not_contain_referenced_attribute_no_substitution_occurs(
self):
class MyObject:
pass
template = airspeed.Template(" $user.name ")
self.assertEqual(" $user.name ", template.merge({'user': MyObject()}))
def test_variables_expanded_in_double_quoted_strings(self):
template = airspeed.Template(
'#set($hello="hello, $name is my name")$hello')
self.assertEqual(
"hello, Steve is my name", template.merge({'name': 'Steve'}))
def test_escaped_variable_references_not_expanded_in_double_quoted_strings(
self):
template = airspeed.Template(
'#set($hello="hello, \\$name is my name")$hello')
self.assertEqual(
"hello, $name is my name", template.merge({'name': 'Steve'}))
def test_macros_expanded_in_double_quoted_strings(self):
template = airspeed.Template(
'#macro(hi $person)$person says hello#end#set($hello="#hi($name)")$hello')
self.assertEqual(
"Steve says hello", template.merge({'name': 'Steve'}))
def test_color_spec(self):
template = airspeed.Template('')
self.assertEqual('', template.merge({}))
# check for a plain hash outside of a context where it could be
# confused with a directive or macro call.
# this is useful for cases where someone put a hash in the target
# of a link, which is typical when javascript is associated with the link
def test_standalone_hashes(self):
template = airspeed.Template('#')
self.assertEqual('#', template.merge({}))
template = airspeed.Template('"#"')
self.assertEqual('"#"', template.merge({}))
template = airspeed.Template('bob')
self.assertEqual('bob', template.merge({}))
def test_large_areas_of_text_handled_without_error(self):
text = "qwerty uiop asdfgh jkl zxcvbnm. 1234" * 300
template = airspeed.Template(text)
self.assertEqual(text, template.merge({}))
def test_foreach_with_unset_variable_expands_to_nothing(self):
template = airspeed.Template('#foreach($value in $values)foo#end')
self.assertEqual('', template.merge({}))
def test_foreach_with_non_iterable_variable_raises_error(self):
template = airspeed.Template('#foreach($value in $values)foo#end')
self.assertRaises(airspeed.TemplateExecutionError, template.merge, {'values': 1})
def test_correct_scope_for_parameters_of_method_calls(self):
template = airspeed.Template('$obj.get_self().method($param)')
class C:
def get_self(self):
return self
def method(self, p):
if p == 'bat':
return 'monkey'
value = template.merge({'obj': C(), 'param': 'bat'})
self.assertEqual('monkey', value)
def test_preserves_unicode_strings(self):
template = airspeed.Template('$value')
value = u'Grüße'
self.assertEqual(value, template.merge(locals()))
def test_preserves_unicode_strings_objects(self):
template = airspeed.Template('$value')
class Clazz:
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
value = Clazz(u'£12,000')
self.assertEqual(six.text_type(value), template.merge(locals()))
def test_can_define_macros_in_parsed_files(self):
class Loader:
def load_template(self, name):
if name == 'foo.tmpl':
return airspeed.Template('#macro(themacro)works#end')
template = airspeed.Template('#parse("foo.tmpl")#themacro()')
self.assertEqual('works', template.merge({}, loader=Loader()))
def test_modulus_operator(self):
template = airspeed.Template('#set( $modulus = ($value % 2) )$modulus')
self.assertEqual('1', template.merge({'value': 3}))
def test_can_assign_empty_string(self):
template = airspeed.Template('#set( $v = "" )#set( $y = \'\' ).$v.$y.')
self.assertEqual('...', template.merge({}))
def test_can_loop_over_numeric_ranges(self):
# Test for bug #15
template = airspeed.Template('#foreach( $v in [1..5] )$v\n#end')
self.assertEqual('1\n2\n3\n4\n5\n', template.merge({}))
def test_can_loop_over_numeric_ranges_backwards(self):
template = airspeed.Template('#foreach( $v in [5..-2] )$v,#end')
self.assertEqual('5,4,3,2,1,0,-1,-2,', template.merge({}))
def test_ranges_over_references(self):
template = airspeed.Template(
"#set($start = 1)#set($end = 5)#foreach($i in [$start .. $end])$i-#end")
self.assertEqual('1-2-3-4-5-', template.merge({}))
def test_user_defined_directive(self):
class DummyDirective(airspeed._Element):
PLAIN = re.compile(r'#(monkey)man(.*)$', re.S + re.I)
def parse(self):
self.text, = self.identity_match(self.PLAIN)
def evaluate(self, stream, namespace, loader):
stream.write(self.text)
airspeed.UserDefinedDirective.DIRECTIVES.append(DummyDirective)
template = airspeed.Template("hello #monkeyman")
self.assertEqual('hello monkey', template.merge({}))
airspeed.UserDefinedDirective.DIRECTIVES.remove(DummyDirective)
def test_stop_directive(self):
template = airspeed.Template("hello #stop world")
self.assertEqual('hello ', template.merge({}))
def test_assignment_of_parenthesized_math_expression(self):
template = airspeed.Template('#set($a = (5 + 4))$a')
self.assertEqual('9', template.merge({}))
def test_assignment_of_parenthesized_math_expression_with_reference(self):
template = airspeed.Template('#set($b = 5)#set($a = ($b + 4))$a')
self.assertEqual('9', template.merge({}))
def test_recursive_macro(self):
template = airspeed.Template(
'#macro ( recur $number)#if ($number > 0)#set($number = $number - 1)#recur($number)X#end#end#recur(5)')
self.assertEqual('XXXXX', template.merge({}))
def test_addition_has_higher_precedence_than_comparison(self):
template = airspeed.Template('#set($a = 4 > 2 + 5)$a')
self.assertEqual('False', template.merge({}))
def test_parentheses_work(self):
template = airspeed.Template('#set($a = (5 + 4) > 2)$a')
self.assertEqual('True', template.merge({}))
def test_addition_has_higher_precedence_than_comparison_other_direction(
self):
template = airspeed.Template('#set($a = 5 + 4 > 2)$a')
self.assertEqual('True', template.merge({}))
# Note: this template:
# template = airspeed.Template('#set($a = (4 > 2) + 5)$a')
# prints 6. That's because Python automatically promotes True to 1
# and False to 0.
# This is weird, but I can't say it's wrong.
def test_multiplication_has_higher_precedence_than_subtraction(self):
template = airspeed.Template("#set($a = 5 * 4 - 2)$a")
self.assertEqual('18', template.merge({}))
def test_multiplication_has_higher_precedence_than_addition_reverse(self):
template = airspeed.Template("#set($a = 2 + 5 * 4)$a")
self.assertEqual('22', template.merge({}))
def test_parse_empty_dictionary(self):
template = airspeed.Template('#set($a = {})$a')
self.assertEqual('{}', template.merge({}))
def test_macro_whitespace_and_newlines_ignored(self):
template = airspeed.Template('''#macro ( blah )
hello##
#end
#blah()''')
self.assertEqual('hello', template.merge({}))
def test_if_whitespace_and_newlines_ignored(self):
template = airspeed.Template('''#if(true)
hello##
#end''')
self.assertEqual('hello', template.merge({}))
def test_subobject_assignment(self):
template = airspeed.Template("#set($outer.inner = 'monkey')")
x = {'outer': {}}
template.merge(x)
self.assertEqual('monkey', x['outer']['inner'])
def test_expressions_with_numbers_with_fractions(self):
template = airspeed.Template('#set($a = 100.0 / 50)$a')
self.assertEqual('2.0', template.merge({}))
# TODO: is that how Velocity would format a floating point?
def test_multiline_arguments_to_function_calls(self):
class Thing:
def func(self, arg):
return 'y'
template = airspeed.Template('''$x.func("multi
line")''')
self.assertEqual('y', template.merge({'x': Thing()}))
def test_does_not_accept_dollar_digit_identifiers(self):
template = airspeed.Template('$Something$0')
self.assertEqual("$Something$0", template.merge({'0': 'bar'}))
def test_valid_vtl_identifiers(self):
template = airspeed.Template('$_x $a $A')
self.assertEqual('bar z Z',
template.merge({'_x': 'bar', 'a': 'z', 'A': 'Z'}))
def test_invalid_vtl_identifier(self):
template = airspeed.Template('$0')
self.assertEqual("$0", template.merge({'0': 'bar'}))
def test_array_notation_int_index(self):
template = airspeed.Template('$a[1]')
self.assertEqual("bar", template.merge({"a": ["foo", "bar"]}))
def test_array_notation_nested_indexes(self):
template = airspeed.Template('$a[1][1]')
self.assertEqual("bar2", template.merge({"a": ["foo", ["bar1", "bar2"]]}))
def test_array_notation_dot(self):
template = airspeed.Template('$a[1].bar1')
self.assertEqual("bar2", template.merge({"a": ["foo", {"bar1": "bar2"}]}))
def test_array_notation_dict_index(self):
template = airspeed.Template('$a["foo"]')
self.assertEqual("bar", template.merge({"a": {"foo": "bar"}}))
def test_array_notation_empty_array_variable(self):
template = airspeed.Template('$!a[1]')
self.assertEqual("", template.merge({"a": []}))
def test_array_notation_variable_index(self):
template = airspeed.Template('#set($i = 1)$a[ $i ]')
self.assertEqual("bar", template.merge({"a": ["foo", "bar"]}))
def test_array_notation_invalid_index(self):
template = airspeed.Template('#set($i = "baz")$a[$i]')
self.assertRaises(airspeed.TemplateExecutionError, template.merge, {"a": ["foo", "bar"]})
def test_provides_helpful_error_location(self):
template = airspeed.Template(u"""
#set($flag = $country.lower())
#set($url = "$host_url/images/flags/")
#set($flagUrl = $url + ${flag} + ".png")
""", filename="mytemplate")
data = {
"model": {
"host_url": u"http://whatever.com",
"country": None
}
}
try:
template.merge(data)
self.fail("expected exception")
except airspeed.TemplateExecutionError as e:
self.assertEqual("mytemplate", e.filename)
self.assertEqual(105, e.start)
self.assertEqual(142, e.end)
self.assertTrue(isinstance(e.__cause__, TypeError))
def test_outer_variable_assignable_from_foreach_block(self):
template = airspeed.Template(
"#set($var = 1)#foreach ($i in $items)"
"$var,#set($var = $i)"
"#end$var")
self.assertEqual("1,2,3,4", template.merge({"items": [2, 3, 4]}))
def test_no_assignment_to_outer_var_if_same_varname_in_block(self):
template = airspeed.Template(
"#set($i = 1)$i,"
"#foreach ($i in [2, 3, 4])$i,#set($i = $i)#end"
"$i")
self.assertEqual("1,2,3,4,1", template.merge({}))
def test_nested_foreach_vars_are_scoped(self):
template = airspeed.Template(
"#foreach ($j in [1,2])"
"#foreach ($i in [3, 4])$foreach.count,#end"
"$foreach.count|#end")
self.assertEqual("1,2,1|1,2,2|", template.merge({}))
def test_template_cannot_modify_its_args(self):
template = airspeed.Template("#set($foo = 1)")
ns = {"foo": 2}
template.merge(ns)
self.assertEqual(2, ns["foo"])
def test_doesnt_blow_stack(self):
template = airspeed.Template("""
#foreach($i in [1..$end])
$assembly##
#end
""")
ns = {"end": 400}
template.merge(ns)
def test_array_size(self):
template = airspeed.Template("#set($foo = [1,2,3]) $foo.size()")
output = template.merge({})
self.assertEqual(output, " 3")
def test_array_contains_true(self):
template = airspeed.Template("#set($foo = [1,2,3]) #if($foo.contains(1))found#end")
output = template.merge({})
self.assertEqual(output, " found")
def test_array_contains_false(self):
template = airspeed.Template("#set($foo = [1,2,3]) #if($foo.contains(10))found#end")
output = template.merge({})
self.assertEqual(output, " ")
def test_array_get_item(self):
template = airspeed.Template("#set($foo = [1,2,3]) $foo.get(1)")
output = template.merge({})
self.assertEqual(output, " 2")
def test_array_add_item(self):
template = airspeed.Template("#set($foo = [1,2,3])"
"#set( $ignore = $foo.add('string value') )"
"#foreach($item in $foo)$item,#end")
output = template.merge({})
self.assertEqual(output, "1,2,3,string value,")
def test_string_length(self):
template = airspeed.Template("#set($foo = 'foobar123') $foo.length()")
output = template.merge({})
self.assertEqual(output, " 9")
def test_string_replace_all(self):
template = airspeed.Template("#set($foo = 'foobar123bab') $foo.replaceAll('ba.', 'foo')")
output = template.merge({})
self.assertEqual(output, " foofoo123foo")
def test_string_starts_with_true(self):
template = airspeed.Template("#set($foo = 'foobar123') #if($foo.startsWith('foo'))yes!#end")
output = template.merge({})
self.assertEqual(output, " yes!")
def test_string_starts_with_false(self):
template = airspeed.Template("#set($foo = 'nofoobar123') #if($foo.startsWith('foo'))yes!#end")
output = template.merge({})
self.assertEqual(output, " ")
def test_dict_put_item(self):
template = airspeed.Template("#set( $ignore = $test_dict.put('k', 'new value') )"
"$test_dict.k")
output = template.merge({'test_dict': {'k': 'initial value'}})
self.assertEqual(output, "new value")
def test_evaluate(self):
template = airspeed.Template('''#set($source1 = "abc")
#set($select = "1")
#set($dynamicsource = "$source$select")
## $dynamicsource is now the string '$source1'
#evaluate($dynamicsource)''')
output = template.merge({})
self.assertEqual(output, "abc")
# TODO:
#
# Report locations for template errors in files included via loaders
# Gobbling up whitespace (see WHITESPACE_TO_END_OF_LINE above, but need to apply in more places)
# Bind #macro calls at compile time?
# Scope of #set across if/elseif/else?
# there seems to be some confusion about the semantics of parameter
# passing to macros; an assignment in a macro body should persist past the
# macro call. Confirm against Velocity.
if __name__ == '__main__':
if sys.version_info >= (3, 0) and sys.version_info <= (3, 3):
imp.reload(airspeed)
elif sys.version_info >= (3, 4):
importlib.reload(airspeed)
else:
reload(airspeed)
import unittest
try:
unittest.main()
except SystemExit:
pass