Commit 8dbfa9d2 by Rocky Duan

oops

parent 145bbb72
"""
TODO: add a docstring.
"""
# We keep all initialization code in a separate module.
from pystache.init import render, Renderer, TemplateSpec
__all__ = ['render', 'Renderer', 'TemplateSpec']
__version__ = '0.5.2' # Also change in setup.py.
# coding: utf-8
"""
This module provides command-line access to pystache.
Run this script using the -h option for command-line help.
"""
try:
import json
except:
# The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions.
try:
import simplejson as json
except ImportError:
# Raise an error with a type different from ImportError as a hack around
# this issue:
# http://bugs.python.org/issue7559
from sys import exc_info
ex_type, ex_value, tb = exc_info()
new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
raise new_ex.__class__, new_ex, tb
# The optparse module is deprecated in Python 2.7 in favor of argparse.
# However, argparse is not available in Python 2.6 and earlier.
from optparse import OptionParser
import sys
# We use absolute imports here to allow use of this script from its
# location in source control (e.g. for development purposes).
# Otherwise, the following error occurs:
#
# ValueError: Attempted relative import in non-package
#
from pystache.common import TemplateNotFoundError
from pystache.renderer import Renderer
USAGE = """\
%prog [-h] template context
Render a mustache template with the given context.
positional arguments:
template A filename or template string.
context A filename or JSON string."""
def parse_args(sys_argv, usage):
"""
Return an OptionParser for the script.
"""
args = sys_argv[1:]
parser = OptionParser(usage=usage)
options, args = parser.parse_args(args)
template, context = args
return template, context
# TODO: verify whether the setup() method's entry_points argument
# supports passing arguments to main:
#
# http://packages.python.org/distribute/setuptools.html#automatic-script-creation
#
def main(sys_argv=sys.argv):
template, context = parse_args(sys_argv, USAGE)
if template.endswith('.mustache'):
template = template[:-9]
renderer = Renderer()
try:
template = renderer.load_template(template)
except TemplateNotFoundError:
pass
try:
context = json.load(open(context))
except IOError:
context = json.loads(context)
rendered = renderer.render(template, context)
print rendered
if __name__=='__main__':
main()
# coding: utf-8
"""
This module provides a command to test pystache (unit tests, doctests, etc).
"""
import sys
from pystache.tests.main import main as run_tests
def main(sys_argv=sys.argv):
run_tests(sys_argv=sys_argv)
if __name__=='__main__':
main()
# coding: utf-8
"""
Exposes functionality needed throughout the project.
"""
# This function was designed to be portable across Python versions -- both
# with older versions and with Python 3 after applying 2to3.
def read(path):
"""
Return the contents of a text file as a byte string.
"""
# Opening in binary mode is necessary for compatibility across Python
# 2 and 3. In both Python 2 and 3, open() defaults to opening files in
# text mode. However, in Python 2, open() returns file objects whose
# read() method returns byte strings (strings of type `str` in Python 2),
# whereas in Python 3, the file object returns unicode strings (strings
# of type `str` in Python 3).
f = open(path, 'rb')
# We avoid use of the with keyword for Python 2.4 support.
try:
return f.read()
finally:
f.close()
class PystacheError(Exception):
"""Base class for Pystache exceptions."""
pass
class TemplateNotFoundError(PystacheError):
"""An exception raised when a template is not found."""
pass
# coding: utf-8
"""
This module provides a central location for defining default behavior.
Throughout the package, these defaults take effect only when the user
does not otherwise specify a value.
"""
try:
# Python 3.2 adds html.escape() and deprecates cgi.escape().
from html import escape
except ImportError:
from cgi import escape
import os
import sys
# How to handle encoding errors when decoding strings from str to unicode.
#
# This value is passed as the "errors" argument to Python's built-in
# unicode() function:
#
# http://docs.python.org/library/functions.html#unicode
#
DECODE_ERRORS = 'strict'
# The name of the encoding to use when converting to unicode any strings of
# type str encountered during the rendering process.
STRING_ENCODING = sys.getdefaultencoding()
# The name of the encoding to use when converting file contents to unicode.
# This default takes precedence over the STRING_ENCODING default for
# strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding()
# The starting list of directories in which to search for templates when
# loading a template by file name.
SEARCH_DIRS = [os.curdir] # i.e. ['.']
# The escape function to apply to strings that require escaping when
# rendering templates (e.g. for tags enclosed in double braces).
# Only unicode strings will be passed to this function.
#
# The quote=True argument causes double but not single quotes to be escaped
# in Python 3.1 and earlier, and both double and single quotes to be
# escaped in Python 3.2 and later:
#
# http://docs.python.org/library/cgi.html#cgi.escape
# http://docs.python.org/dev/library/html.html#html.escape
#
TAG_ESCAPE = lambda u: escape(u, quote=True)
# The default template extension.
TEMPLATE_EXTENSION = 'mustache'
# encoding: utf-8
"""
This module contains the initialization logic called by __init__.py.
"""
from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
def render(template, context=None, **kwargs):
"""
Return the given template string rendered using the given context.
"""
renderer = Renderer()
return renderer.render(template, context, **kwargs)
# coding: utf-8
"""
This module provides a Loader class for locating and reading templates.
"""
import os
import sys
from pystache import common
from pystache import defaults
from pystache.locator import Locator
# We make a function so that the current defaults take effect.
# TODO: revisit whether this is necessary.
def _make_to_unicode():
def to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
"""
if encoding is None:
encoding = defaults.STRING_ENCODING
return unicode(s, encoding, defaults.DECODE_ERRORS)
return to_unicode
class Loader(object):
"""
Loads the template associated to a name or user-defined object.
"""
def __init__(self, file_encoding=None, extension=None, to_unicode=None,
search_dirs=None):
"""
Construct a template loader instance.
Arguments:
extension: the template file extension. Pass False for no
extension (i.e. to use extensionless template files).
Defaults to the package default.
file_encoding: the name of the encoding to use when converting file
contents to unicode. Defaults to the package default.
search_dirs: the list of directories in which to search when loading
a template by name or file name. Defaults to the package default.
to_unicode: the function to use when converting strings of type
str to unicode. The function should have the signature:
to_unicode(s, encoding=None)
It should accept a string of type str and an optional encoding
name and return a string of type unicode. Defaults to calling
Python's built-in function unicode() using the package string
encoding and decode errors defaults.
"""
if extension is None:
extension = defaults.TEMPLATE_EXTENSION
if file_encoding is None:
file_encoding = defaults.FILE_ENCODING
if search_dirs is None:
search_dirs = defaults.SEARCH_DIRS
if to_unicode is None:
to_unicode = _make_to_unicode()
self.extension = extension
self.file_encoding = file_encoding
# TODO: unit test setting this attribute.
self.search_dirs = search_dirs
self.to_unicode = to_unicode
def _make_locator(self):
return Locator(extension=self.extension)
def unicode(self, s, encoding=None):
"""
Convert a string to unicode using the given encoding, and return it.
This function uses the underlying to_unicode attribute.
Arguments:
s: a basestring instance to convert to unicode. Unlike Python's
built-in unicode() function, it is okay to pass unicode strings
to this function. (Passing a unicode string to Python's unicode()
with the encoding argument throws the error, "TypeError: decoding
Unicode is not supported.")
encoding: the encoding to pass to the to_unicode attribute.
Defaults to None.
"""
if isinstance(s, unicode):
return unicode(s)
return self.to_unicode(s, encoding)
def read(self, path, encoding=None):
"""
Read the template at the given path, and return it as a unicode string.
"""
b = common.read(path)
if encoding is None:
encoding = self.file_encoding
return self.unicode(b, encoding)
# TODO: unit-test this method.
def load_name(self, name):
"""
Find and return the template with the given name.
Arguments:
name: the name of the template.
search_dirs: the list of directories in which to search.
"""
locator = self._make_locator()
path = locator.find_name(name, self.search_dirs)
return self.read(path)
# TODO: unit-test this method.
def load_object(self, obj):
"""
Find and return the template associated to the given object.
Arguments:
obj: an instance of a user-defined class.
search_dirs: the list of directories in which to search.
"""
locator = self._make_locator()
path = locator.find_object(obj, self.search_dirs)
return self.read(path)
# coding: utf-8
"""
This module provides a Locator class for finding template files.
"""
import os
import re
import sys
from pystache.common import TemplateNotFoundError
from pystache import defaults
class Locator(object):
def __init__(self, extension=None):
"""
Construct a template locator.
Arguments:
extension: the template file extension. Pass False for no
extension (i.e. to use extensionless template files).
Defaults to the package default.
"""
if extension is None:
extension = defaults.TEMPLATE_EXTENSION
self.template_extension = extension
def get_object_directory(self, obj):
"""
Return the directory containing an object's defining class.
Returns None if there is no such directory, for example if the
class was defined in an interactive Python session, or in a
doctest that appears in a text file (rather than a Python file).
"""
if not hasattr(obj, '__module__'):
return None
module = sys.modules[obj.__module__]
if not hasattr(module, '__file__'):
# TODO: add a unit test for this case.
return None
path = module.__file__
return os.path.dirname(path)
def make_template_name(self, obj):
"""
Return the canonical template name for an object instance.
This method converts Python-style class names (PEP 8's recommended
CamelCase, aka CapWords) to lower_case_with_underscords. Here
is an example with code:
>>> class HelloWorld(object):
... pass
>>> hi = HelloWorld()
>>>
>>> locator = Locator()
>>> locator.make_template_name(hi)
'hello_world'
"""
template_name = obj.__class__.__name__
def repl(match):
return '_' + match.group(0).lower()
return re.sub('[A-Z]', repl, template_name)[1:]
def make_file_name(self, template_name, template_extension=None):
"""
Generate and return the file name for the given template name.
Arguments:
template_extension: defaults to the instance's extension.
"""
file_name = template_name
if template_extension is None:
template_extension = self.template_extension
if template_extension is not False:
file_name += os.path.extsep + template_extension
return file_name
def _find_path(self, search_dirs, file_name):
"""
Search for the given file, and return the path.
Returns None if the file is not found.
"""
for dir_path in search_dirs:
file_path = os.path.join(dir_path, file_name)
if os.path.exists(file_path):
return file_path
return None
def _find_path_required(self, search_dirs, file_name):
"""
Return the path to a template with the given file name.
"""
path = self._find_path(search_dirs, file_name)
if path is None:
raise TemplateNotFoundError('File %s not found in dirs: %s' %
(repr(file_name), repr(search_dirs)))
return path
def find_name(self, template_name, search_dirs):
"""
Return the path to a template with the given name.
"""
file_name = self.make_file_name(template_name)
return self._find_path_required(search_dirs, file_name)
def find_object(self, obj, search_dirs, file_name=None):
"""
Return the path to a template associated with the given object.
"""
if file_name is None:
# TODO: should we define a make_file_name() method?
template_name = self.make_template_name(obj)
file_name = self.make_file_name(template_name)
dir_path = self.get_object_directory(obj)
if dir_path is not None:
search_dirs = [dir_path] + search_dirs
path = self._find_path_required(search_dirs, file_name)
return path
# coding: utf-8
"""
Exposes a class that represents a parsed (or compiled) template.
This module is meant only for internal use.
"""
class ParsedTemplate(object):
def __init__(self, parse_tree):
"""
Arguments:
parse_tree: a list, each element of which is either--
(1) a unicode string, or
(2) a "rendering" callable that accepts a ContextStack instance
and returns a unicode string.
The possible rendering callables are the return values of the
following functions:
* RenderEngine._make_get_escaped()
* RenderEngine._make_get_inverse()
* RenderEngine._make_get_literal()
* RenderEngine._make_get_partial()
* RenderEngine._make_get_section()
"""
self._parse_tree = parse_tree
def __repr__(self):
return "[%s]" % (", ".join([repr(part) for part in self._parse_tree]))
def render(self, context):
"""
Returns: a string of type unicode.
"""
# We avoid use of the ternary operator for Python 2.4 support.
def get_unicode(val):
if callable(val):
return val(context)
return val
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return unicode(s)
# coding: utf-8
"""
Provides a class for parsing template strings.
This module is only meant for internal use by the renderengine module.
"""
import re
from pystache.common import TemplateNotFoundError
from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
def _compile_template_re(delimiters=None):
"""
Return a regular expresssion object (re.RegexObject) instance.
"""
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
tag_types = "!>&/#^"
# TODO: are we following this in the spec?
#
# The tag's content MUST be a non-whitespace character sequence
# NOT containing the current closing delimiter.
#
tag = r"""
(?P<whitespace>[\ \t]*)
%(otag)s \s*
(?:
(?P<change>=) \s* (?P<delims>.+?) \s* = |
(?P<raw>{) \s* (?P<raw_name>.+?) \s* } |
(?P<tag>[%(tag_types)s]?) \s* (?P<tag_key>[\s\S]+?)
)
\s* %(ctag)s
""" % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}
return re.compile(tag, re.VERBOSE)
class ParsingError(Exception):
pass
class Parser(object):
_delimiters = None
_template_re = None
def __init__(self, engine, delimiters=None):
"""
Construct an instance.
Arguments:
engine: a RenderEngine instance.
"""
if delimiters is None:
delimiters = DEFAULT_DELIMITERS
self._delimiters = delimiters
self.engine = engine
def compile_template_re(self):
self._template_re = _compile_template_re(self._delimiters)
def _change_delimiters(self, delimiters):
self._delimiters = delimiters
self.compile_template_re()
def parse(self, template, start_index=0, section_key=None):
"""
Parse a template string starting at some index.
This method uses the current tag delimiter.
Arguments:
template: a unicode string that is the template to parse.
index: the index at which to start parsing.
Returns:
a ParsedTemplate instance.
"""
parse_tree = []
index = start_index
while True:
match = self._template_re.search(template, index)
if match is None:
break
match_index = match.start()
end_index = match.end()
before_tag = template[index : match_index]
parse_tree.append(before_tag)
matches = match.groupdict()
# Normalize the matches dictionary.
if matches['change'] is not None:
matches.update(tag='=', tag_key=matches['delims'])
elif matches['raw'] is not None:
matches.update(tag='&', tag_key=matches['raw_name'])
tag_type = matches['tag']
tag_key = matches['tag_key']
leading_whitespace = matches['whitespace']
# Standalone (non-interpolation) tags consume the entire line,
# both leading whitespace and trailing newline.
did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
is_tag_interpolating = tag_type in ['', '&']
if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
if end_index < len(template):
end_index += template[end_index] == '\r' and 1 or 0
if end_index < len(template):
end_index += template[end_index] == '\n' and 1 or 0
elif leading_whitespace:
parse_tree.append(leading_whitespace)
match_index += len(leading_whitespace)
leading_whitespace = ''
if tag_type == '/':
if tag_key != section_key:
raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
return ParsedTemplate(parse_tree), match_index, end_index
index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
# Save the rest of the template.
parse_tree.append(template[index:])
return ParsedTemplate(parse_tree)
def _parse_section(self, template, start_index, section_key):
"""
Parse the contents of a template section.
Arguments:
template: a unicode template string.
start_index: the string index at which the section contents begin.
section_key: the tag key of the section.
Returns: a 3-tuple:
parsed_section: the section contents parsed as a ParsedTemplate
instance.
content_end_index: the string index after the section contents.
end_index: the string index after the closing section tag (and
including any trailing newlines).
"""
parsed_section, content_end_index, end_index = \
self.parse(template=template, start_index=start_index, section_key=section_key)
return parsed_section, template[start_index:content_end_index], end_index
def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
return end_index
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
return end_index
engine = self.engine
if tag_type == '':
func = engine._make_get_escaped(tag_key)
elif tag_type == '&':
func = engine._make_get_literal(tag_key)
elif tag_type == '#':
parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
elif tag_type == '^':
parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_inverse(tag_key, parsed_section)
elif tag_type == '>':
try:
# TODO: make engine.load() and test it separately.
template = engine.load_partial(tag_key)
except TemplateNotFoundError:
template = u''
# Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
func = engine._make_get_partial(template)
else:
raise Exception("Unrecognized tag type: %s" % repr(tag_type))
parse_tree.append(func)
return end_index
# coding: utf-8
"""
This module supports customized (aka special or specified) template loading.
"""
import os.path
from pystache.loader import Loader
# TODO: add test cases for this class.
class SpecLoader(object):
"""
Supports loading custom-specified templates (from TemplateSpec instances).
"""
def __init__(self, loader=None):
if loader is None:
loader = Loader()
self.loader = loader
def _find_relative(self, spec):
"""
Return the path to the template as a relative (dir, file_name) pair.
The directory returned is relative to the directory containing the
class definition of the given object. The method returns None for
this directory if the directory is unknown without first searching
the search directories.
"""
if spec.template_rel_path is not None:
return os.path.split(spec.template_rel_path)
# Otherwise, determine the file name separately.
locator = self.loader._make_locator()
# We do not use the ternary operator for Python 2.4 support.
if spec.template_name is not None:
template_name = spec.template_name
else:
template_name = locator.make_template_name(spec)
file_name = locator.make_file_name(template_name, spec.template_extension)
return (spec.template_rel_directory, file_name)
def _find(self, spec):
"""
Find and return the path to the template associated to the instance.
"""
dir_path, file_name = self._find_relative(spec)
locator = self.loader._make_locator()
if dir_path is None:
# Then we need to search for the path.
path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name)
else:
obj_dir = locator.get_object_directory(spec)
path = os.path.join(obj_dir, dir_path, file_name)
return path
def load(self, spec):
"""
Find and return the template associated to a TemplateSpec instance.
Returns the template as a unicode string.
Arguments:
spec: a TemplateSpec instance.
"""
if spec.template is not None:
return self.loader.unicode(spec.template, spec.template_encoding)
path = self._find(spec)
return self.loader.read(path, spec.template_encoding)
# coding: utf-8
"""
Provides a class to customize template information on a per-view basis.
To customize template properties for a particular view, create that view
from a class that subclasses TemplateSpec. The "Spec" in TemplateSpec
stands for template information that is "special" or "specified".
"""
# TODO: finish the class docstring.
class TemplateSpec(object):
"""
A mixin or interface for specifying custom template information.
The "spec" in TemplateSpec can be taken to mean that the template
information is either "specified" or "special."
A view should subclass this class only if customized template loading
is needed. The following attributes allow one to customize/override
template information on a per view basis. A None value means to use
default behavior for that value and perform no customization. All
attributes are initialized to None.
Attributes:
template: the template as a string.
template_rel_path: the path to the template file, relative to the
directory containing the module defining the class.
template_rel_directory: the directory containing the template file, relative
to the directory containing the module defining the class.
template_extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files).
"""
template = None
template_rel_path = None
template_rel_directory = None
template_name = None
template_extension = None
template_encoding = None
"""
TODO: add a docstring.
"""
#!/usr/bin/env python
# coding: utf-8
"""
A rudimentary backward- and forward-compatible script to benchmark pystache.
Usage:
tests/benchmark.py 10000
"""
import sys
from timeit import Timer
import pystache
# TODO: make the example realistic.
examples = [
# Test case: 1
("""{{#person}}Hi {{name}}{{/person}}""",
{"person": {"name": "Jon"}},
"Hi Jon"),
# Test case: 2
("""\
<div class="comments">
<h3>{{header}}</h3>
<ul>
{{#comments}}<li class="comment">
<h5>{{name}}</h5><p>{{body}}</p>
</li>{{/comments}}
</ul>
</div>""",
{'header': "My Post Comments",
'comments': [
{'name': "Joe", 'body': "Thanks for this post!"},
{'name': "Sam", 'body': "Thanks for this post!"},
{'name': "Heather", 'body': "Thanks for this post!"},
{'name': "Kathy", 'body': "Thanks for this post!"},
{'name': "George", 'body': "Thanks for this post!"}]},
"""\
<div class="comments">
<h3>My Post Comments</h3>
<ul>
<li class="comment">
<h5>Joe</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Sam</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Heather</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>Kathy</h5><p>Thanks for this post!</p>
</li><li class="comment">
<h5>George</h5><p>Thanks for this post!</p>
</li>
</ul>
</div>"""),
]
def make_test_function(example):
template, context, expected = example
def test():
actual = pystache.render(template, context)
if actual != expected:
raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual))
return test
def main(sys_argv):
args = sys_argv[1:]
count = int(args[0])
print "Benchmarking: %sx" % count
print
for example in examples:
test = make_test_function(example)
t = Timer(test,)
print min(t.repeat(repeat=3, number=count))
print "Done"
if __name__ == '__main__':
main(sys.argv)
# coding: utf-8
"""
Provides test-related code that can be used by all tests.
"""
import os
import pystache
from pystache import defaults
from pystache.tests import examples
# Save a reference to the original function to avoid recursion.
_DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE
_TESTS_DIR = os.path.dirname(pystache.tests.__file__)
DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'.
EXAMPLES_DIR = os.path.dirname(examples.__file__)
PACKAGE_DIR = os.path.dirname(pystache.__file__)
PROJECT_DIR = os.path.join(PACKAGE_DIR, '..')
SPEC_TEST_DIR = os.path.join(PROJECT_DIR, 'ext', 'spec', 'specs')
# TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files)
# containing doctests. The paths should be relative to the project directory.
TEXT_DOCTEST_PATHS = ['README.rst']
UNITTEST_FILE_PREFIX = "test_"
def html_escape(u):
"""
An html escape function that behaves the same in both Python 2 and 3.
This function is needed because single quotes are escaped in Python 3
(to '&#x27;'), but not in Python 2.
The global defaults.TAG_ESCAPE can be set to this function in the
setUp() and tearDown() of unittest test cases, for example, for
consistent test results.
"""
u = _DEFAULT_TAG_ESCAPE(u)
return u.replace("'", '&#x27;')
def get_data_path(file_name):
return os.path.join(DATA_DIR, file_name)
# Functions related to get_module_names().
def _find_files(root_dir, should_include):
"""
Return a list of paths to all modules below the given directory.
Arguments:
should_include: a function that accepts a file path and returns True or False.
"""
paths = [] # Return value.
is_module = lambda path: path.endswith(".py")
# os.walk() is new in Python 2.3
# http://docs.python.org/library/os.html#os.walk
for dir_path, dir_names, file_names in os.walk(root_dir):
new_paths = [os.path.join(dir_path, file_name) for file_name in file_names]
new_paths = filter(is_module, new_paths)
new_paths = filter(should_include, new_paths)
paths.extend(new_paths)
return paths
def _make_module_names(package_dir, paths):
"""
Return a list of fully-qualified module names given a list of module paths.
"""
package_dir = os.path.abspath(package_dir)
package_name = os.path.split(package_dir)[1]
prefix_length = len(package_dir)
module_names = []
for path in paths:
path = os.path.abspath(path) # for example <path_to_package>/subpackage/module.py
rel_path = path[prefix_length:] # for example /subpackage/module.py
rel_path = os.path.splitext(rel_path)[0] # for example /subpackage/module
parts = []
while True:
(rel_path, tail) = os.path.split(rel_path)
if not tail:
break
parts.insert(0, tail)
# We now have, for example, ['subpackage', 'module'].
parts.insert(0, package_name)
module = ".".join(parts)
module_names.append(module)
return module_names
def get_module_names(package_dir=None, should_include=None):
"""
Return a list of fully-qualified module names in the given package.
"""
if package_dir is None:
package_dir = PACKAGE_DIR
if should_include is None:
should_include = lambda path: True
paths = _find_files(package_dir, should_include)
names = _make_module_names(package_dir, paths)
names.sort()
return names
class AssertStringMixin:
"""A unittest.TestCase mixin to check string equality."""
def assertString(self, actual, expected, format=None):
"""
Assert that the given strings are equal and have the same type.
Arguments:
format: a format string containing a single conversion specifier %s.
Defaults to "%s".
"""
if format is None:
format = "%s"
# Show both friendly and literal versions.
details = """String mismatch: %%s\
Expected: \"""%s\"""
Actual: \"""%s\"""
Expected: %s
Actual: %s""" % (expected, actual, repr(expected), repr(actual))
def make_message(reason):
description = details % reason
return format % description
self.assertEqual(actual, expected, make_message("different characters"))
reason = "types different: %s != %s (actual)" % (repr(type(expected)), repr(type(actual)))
self.assertEqual(type(expected), type(actual), make_message(reason))
class AssertIsMixin:
"""A unittest.TestCase mixin adding assertIs()."""
# unittest.assertIs() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
def assertIs(self, first, second):
self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
class AssertExceptionMixin:
"""A unittest.TestCase mixin adding assertException()."""
# unittest.assertRaisesRegexp() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertRaisesRegexp
def assertException(self, exception_type, msg, callable, *args, **kwds):
try:
callable(*args, **kwds)
raise Exception("Expected exception: %s: %s" % (exception_type, repr(msg)))
except exception_type, err:
self.assertEqual(str(err), msg)
class SetupDefaults(object):
"""
Mix this class in to a unittest.TestCase for standard defaults.
This class allows for consistent test results across Python 2/3.
"""
def setup_defaults(self):
self.original_decode_errors = defaults.DECODE_ERRORS
self.original_file_encoding = defaults.FILE_ENCODING
self.original_string_encoding = defaults.STRING_ENCODING
defaults.DECODE_ERRORS = 'strict'
defaults.FILE_ENCODING = 'ascii'
defaults.STRING_ENCODING = 'ascii'
def teardown_defaults(self):
defaults.DECODE_ERRORS = self.original_decode_errors
defaults.FILE_ENCODING = self.original_file_encoding
defaults.STRING_ENCODING = self.original_string_encoding
class Attachable(object):
"""
A class that attaches all constructor named parameters as attributes.
For example--
>>> obj = Attachable(foo=42, size="of the universe")
>>> repr(obj)
"Attachable(foo=42, size='of the universe')"
>>> obj.foo
42
>>> obj.size
'of the universe'
"""
def __init__(self, **kwargs):
self.__args__ = kwargs
for arg, value in kwargs.iteritems():
setattr(self, arg, value)
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__,
", ".join("%s=%s" % (k, repr(v))
for k, v in self.__args__.iteritems()))
# coding: utf-8
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class SayHello(object):
def to(self):
return "World"
class SampleView(TemplateSpec):
pass
class NonAscii(TemplateSpec):
pass
# coding: utf-8
"""
Exposes a get_doctests() function for the project's test harness.
"""
import doctest
import os
import pkgutil
import sys
import traceback
if sys.version_info >= (3,):
# Then pull in modules needed for 2to3 conversion. The modules
# below are not necessarily available in older versions of Python.
from lib2to3.main import main as lib2to3main # new in Python 2.6?
from shutil import copyfile
from pystache.tests.common import TEXT_DOCTEST_PATHS
from pystache.tests.common import get_module_names
# This module follows the guidance documented here:
#
# http://docs.python.org/library/doctest.html#unittest-api
#
def get_doctests(text_file_dir):
"""
Return a list of TestSuite instances for all doctests in the project.
Arguments:
text_file_dir: the directory in which to search for all text files
(i.e. non-module files) containing doctests.
"""
# Since module_relative is False in our calls to DocFileSuite below,
# paths should be OS-specific. See the following for more info--
#
# http://docs.python.org/library/doctest.html#doctest.DocFileSuite
#
paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS]
if sys.version_info >= (3,):
paths = _convert_paths(paths)
suites = []
for path in paths:
suite = doctest.DocFileSuite(path, module_relative=False)
suites.append(suite)
modules = get_module_names()
for module in modules:
suite = doctest.DocTestSuite(module)
suites.append(suite)
return suites
def _convert_2to3(path):
"""
Convert the given file, and return the path to the converted files.
"""
base, ext = os.path.splitext(path)
# For example, "README.temp2to3.rst".
new_path = "%s.temp2to3%s" % (base, ext)
copyfile(path, new_path)
args = ['--doctests_only', '--no-diffs', '--write', '--nobackups', new_path]
lib2to3main("lib2to3.fixes", args=args)
return new_path
def _convert_paths(paths):
"""
Convert the given files, and return the paths to the converted files.
"""
new_paths = []
for path in paths:
new_path = _convert_2to3(path)
new_paths.append(new_path)
return new_paths
"""
TODO: add a docstring.
"""
class Comments(object):
def title(self):
return "A Comedy of Errors"
"""
TODO: add a docstring.
"""
class Complex(object):
def header(self):
return "Colors"
def item(self):
items = []
items.append({ 'name': 'red', 'current': True, 'url': '#Red' })
items.append({ 'name': 'green', 'link': True, 'url': '#Green' })
items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' })
return items
def list(self):
return not self.empty()
def empty(self):
return len(self.item()) == 0
def empty_list(self):
return [];
"""
TODO: add a docstring.
"""
class Delimiters(object):
def first(self):
return "It worked the first time."
def second(self):
return "And it worked the second time."
def third(self):
return "Then, surprisingly, it worked the third time."
"""
TODO: add a docstring.
"""
class DoubleSection(object):
def t(self):
return True
def two(self):
return "second"
"""
TODO: add a docstring.
"""
class Escaped(object):
def title(self):
return "Bear > Shark"
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class Inverted(object):
def t(self):
return True
def f(self):
return False
def two(self):
return 'two'
def empty_list(self):
return []
def populated_list(self):
return ['some_value']
class InvertedLists(Inverted, TemplateSpec):
template_name = 'inverted'
def t(self):
return [0, 1, 2]
def f(self):
return []
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
def rot(s, n=13):
r = ""
for c in s:
cc = c
if cc.isalpha():
cc = cc.lower()
o = ord(cc)
ro = (o+n) % 122
if ro == 0: ro = 122
if ro < 97: ro += 96
cc = chr(ro)
r = ''.join((r,cc))
return r
def replace(subject, this='foo', with_this='bar'):
return subject.replace(this, with_this)
# This class subclasses TemplateSpec because at least one unit test
# sets the template attribute.
class Lambdas(TemplateSpec):
def replace_foo_with_bar(self, text=None):
return replace
def rot13(self, text=None):
return rot
def sort(self, text=None):
return lambda text: ''.join(sorted(text))
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class NestedContext(TemplateSpec):
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def outer_thing(self):
return "two"
def foo(self):
return {'thing1': 'one', 'thing2': 'foo'}
def derp(self):
return [{'inner': 'car'}]
def herp(self):
return [{'outer': 'car'}]
def nested_context_in_view(self):
if self._context_get('outer') == self._context_get('inner'):
return 'it works!'
return ''
"""
TODO: add a docstring.
"""
from pystache.tests.examples.lambdas import rot
class PartialsWithLambdas(object):
def rot(self):
return rot
"""
TODO: add a docstring.
"""
class SayHello(object):
def to(self):
return "Pizza"
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class Simple(TemplateSpec):
def thing(self):
return "pizza"
def blank(self):
return ''
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class TemplatePartial(TemplateSpec):
def __init__(self, renderer):
self.renderer = renderer
def _context_get(self, key):
return self.renderer.context.get(key)
def title(self):
return "Welcome"
def title_bars(self):
return '-' * len(self.title())
def looping(self):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self):
return self._context_get('prop')
"""
TODO: add a docstring.
"""
class Unescaped(object):
def title(self):
return "Bear > Shark"
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class UnicodeInput(TemplateSpec):
template_encoding = 'utf8'
def age(self):
return 156
# encoding: utf-8
"""
TODO: add a docstring.
"""
class UnicodeOutput(object):
def name(self):
return u'Henri Poincaré'
# coding: utf-8
"""
Exposes a run_tests() function that runs all tests in the project.
This module is for our test console script.
"""
import os
import sys
import unittest
from unittest import TestProgram
import pystache
from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
from pystache.tests.common import get_module_names
from pystache.tests.doctesting import get_doctests
from pystache.tests.spectesting import get_spec_tests
# If this command option is present, then the spec test and doctest directories
# will be inserted if not provided.
FROM_SOURCE_OPTION = "--from-source"
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def main(sys_argv):
"""
Run all tests in the project.
Arguments:
sys_argv: a reference to sys.argv.
"""
should_source_exist = False
spec_test_dir = None
project_dir = None
if len(sys_argv) > 1 and sys_argv[1] == FROM_SOURCE_OPTION:
should_source_exist = True
sys_argv.pop(1)
# TODO: use logging module
print "pystache: running tests: expecting source: %s" % should_source_exist
try:
# TODO: use optparse command options instead.
spec_test_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
if should_source_exist:
spec_test_dir = SPEC_TEST_DIR
try:
# TODO: use optparse command options instead.
project_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
if should_source_exist:
project_dir = PROJECT_DIR
if len(sys_argv) <= 1 or sys_argv[-1].startswith("-"):
# Then no explicit module or test names were provided, so
# auto-detect all unit tests.
module_names = _discover_test_modules(PACKAGE_DIR)
sys_argv.extend(module_names)
if project_dir is not None:
# Add the current module for unit tests contained here.
sys_argv.append(__name__)
_PystacheTestProgram._text_doctest_dir = project_dir
_PystacheTestProgram._spec_test_dir = spec_test_dir
SetupTests.project_dir = project_dir
# We pass None for the module because we do not want the unittest
# module to resolve module names relative to a given module.
# (This would require importing all of the unittest modules from
# this module.) See the loadTestsFromName() method of the
# unittest.TestLoader class for more details on this parameter.
_PystacheTestProgram(argv=sys_argv, module=None)
# No need to return since unitttest.main() exits.
def _discover_test_modules(package_dir):
"""
Discover and return a sorted list of the names of unit-test modules.
"""
def is_unittest_module(path):
file_name = os.path.basename(path)
return file_name.startswith(UNITTEST_FILE_PREFIX)
names = get_module_names(package_dir=package_dir, should_include=is_unittest_module)
# This is a sanity check to ensure that the unit-test discovery
# methods are working.
if len(names) < 1:
raise Exception("No unit-test modules found--\n in %s" % package_dir)
return names
class SetupTests(unittest.TestCase):
"""Tests about setup.py."""
project_dir = None
def test_version(self):
"""
Test that setup.py's version matches the package's version.
"""
original_path = list(sys.path)
sys.path.insert(0, self.project_dir)
try:
from setup import VERSION
self.assertEqual(VERSION, pystache.__version__)
finally:
sys.path = original_path
# The function unittest.main() is an alias for unittest.TestProgram's
# constructor. TestProgram's constructor calls self.runTests() as its
# final step, which expects self.test to be set. The constructor sets
# the self.test attribute by calling one of self.testLoader's "loadTests"
# methods prior to callint self.runTests(). Each loadTest method returns
# a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
# instance prior to calling runTests().
class _PystacheTestProgram(TestProgram):
"""
Instantiating an instance of this class runs all tests.
"""
def runTests(self):
# self.test is a unittest.TestSuite instance:
# http://docs.python.org/library/unittest.html#unittest.TestSuite
tests = self.test
if self._text_doctest_dir is not None:
doctest_suites = get_doctests(self._text_doctest_dir)
tests.addTests(doctest_suites)
if self._spec_test_dir is not None:
spec_testcases = get_spec_tests(self._spec_test_dir)
tests.addTests(spec_testcases)
TestProgram.runTests(self)
# coding: utf-8
"""
Exposes a get_spec_tests() function for the project's test harness.
Creates a unittest.TestCase for the tests defined in the mustache spec.
"""
# TODO: this module can be cleaned up somewhat.
# TODO: move all of this code to pystache/tests/spectesting.py and
# have it expose a get_spec_tests(spec_test_dir) function.
FILE_ENCODING = 'utf-8' # the encoding of the spec test files.
yaml = None
try:
# We try yaml first since it is more convenient when adding and modifying
# test cases by hand (since the YAML is human-readable and is the master
# from which the JSON format is generated).
import yaml
except ImportError:
try:
import json
except:
# The module json is not available prior to Python 2.6, whereas
# simplejson is. The simplejson package dropped support for Python 2.4
# in simplejson v2.1.0, so Python 2.4 requires a simplejson install
# older than the most recent version.
try:
import simplejson as json
except ImportError:
# Raise an error with a type different from ImportError as a hack around
# this issue:
# http://bugs.python.org/issue7559
from sys import exc_info
ex_type, ex_value, tb = exc_info()
new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
raise new_ex.__class__, new_ex, tb
file_extension = 'json'
parser = json
else:
file_extension = 'yml'
parser = yaml
import codecs
import glob
import os.path
import unittest
import pystache
from pystache import common
from pystache.renderer import Renderer
from pystache.tests.common import AssertStringMixin
def get_spec_tests(spec_test_dir):
"""
Return a list of unittest.TestCase instances.
"""
# TODO: use logging module instead.
print "pystache: spec tests: using %s" % _get_parser_info()
cases = []
# Make this absolute for easier diagnosis in case of error.
spec_test_dir = os.path.abspath(spec_test_dir)
spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension))
for path in spec_paths:
new_cases = _read_spec_tests(path)
cases.extend(new_cases)
# Store this as a value so that CheckSpecTestsFound is not checking
# a reference to cases that contains itself.
spec_test_count = len(cases)
# This test case lets us alert the user that spec tests are missing.
class CheckSpecTestsFound(unittest.TestCase):
def runTest(self):
if spec_test_count > 0:
return
raise Exception("Spec tests not found--\n in %s\n"
" Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir))
case = CheckSpecTestsFound()
cases.append(case)
return cases
def _get_parser_info():
return "%s (version %s)" % (parser.__name__, parser.__version__)
def _read_spec_tests(path):
"""
Return a list of unittest.TestCase instances.
"""
b = common.read(path)
u = unicode(b, encoding=FILE_ENCODING)
spec_data = parse(u)
tests = spec_data['tests']
cases = []
for data in tests:
case = _deserialize_spec_test(data, path)
cases.append(case)
return cases
# TODO: simplify the implementation of this function.
def _convert_children(node):
"""
Recursively convert to functions all "code strings" below the node.
This function is needed only for the json format.
"""
if not isinstance(node, (list, dict)):
# Then there is nothing to iterate over and recurse.
return
if isinstance(node, list):
for child in node:
_convert_children(child)
return
# Otherwise, node is a dict, so attempt the conversion.
for key in node.keys():
val = node[key]
if not isinstance(val, dict) or val.get('__tag__') != 'code':
_convert_children(val)
continue
# Otherwise, we are at a "leaf" node.
val = eval(val['python'])
node[key] = val
continue
def _deserialize_spec_test(data, file_path):
"""
Return a unittest.TestCase instance representing a spec test.
Arguments:
data: the dictionary of attributes for a single test.
"""
context = data['data']
description = data['desc']
# PyYAML seems to leave ASCII strings as byte strings.
expected = unicode(data['expected'])
# TODO: switch to using dict.get().
partials = data.has_key('partials') and data['partials'] or {}
template = data['template']
test_name = data['name']
_convert_children(context)
test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path)
return test_case
def _make_spec_test(expected, template, context, partials, description, test_name, file_path):
"""
Return a unittest.TestCase instance representing a spec test.
"""
file_name = os.path.basename(file_path)
test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name))
# We subclass SpecTestBase in order to control the test method name (for
# the purposes of improved reporting).
class SpecTest(SpecTestBase):
pass
def run_test(self):
self._runTest()
# TODO: should we restore this logic somewhere?
# If we don't convert unicode to str, we get the following error:
# "TypeError: __name__ must be set to a string object"
# test.__name__ = str(name)
setattr(SpecTest, test_method_name, run_test)
case = SpecTest(test_method_name)
case._context = context
case._description = description
case._expected = expected
case._file_path = file_path
case._partials = partials
case._template = template
case._test_name = test_name
return case
def parse(u):
"""
Parse the contents of a spec test file, and return a dict.
Arguments:
u: a unicode string.
"""
# TODO: find a cleaner mechanism for choosing between the two.
if yaml is None:
# Then use json.
# The only way to get the simplejson module to return unicode strings
# is to pass it unicode. See, for example--
#
# http://code.google.com/p/simplejson/issues/detail?id=40
#
# and the documentation of simplejson.loads():
#
# "If s is a str then decoded JSON strings that contain only ASCII
# characters may be parsed as str for performance and memory reasons.
# If your code expects only unicode the appropriate solution is
# decode s to unicode prior to calling loads."
#
return json.loads(u)
# Otherwise, yaml.
def code_constructor(loader, node):
value = loader.construct_mapping(node)
return eval(value['python'], {})
yaml.add_constructor(u'!code', code_constructor)
return yaml.load(u)
class SpecTestBase(unittest.TestCase, AssertStringMixin):
def _runTest(self):
context = self._context
description = self._description
expected = self._expected
file_path = self._file_path
partials = self._partials
template = self._template
test_name = self._test_name
renderer = Renderer(partials=partials)
actual = renderer.render(template, context)
# We need to escape the strings that occur in our format string because
# they can contain % symbols, for example (in delimiters.yml)--
#
# "template: '{{=<% %>=}}(<%text%>)'"
#
def escape(s):
return s.replace("%", "%%")
parser_info = _get_parser_info()
subs = [repr(test_name), description, os.path.abspath(file_path),
template, repr(context), parser_info]
subs = tuple([escape(sub) for sub in subs])
# We include the parsing module version info to help with troubleshooting
# yaml/json/simplejson issues.
message = """%s: %s
File: %s
Template: \"""%s\"""
Context: %s
%%s
[using %s]
""" % subs
self.assertString(actual, expected, format=message)
# coding: utf-8
"""
Tests of __init__.py.
"""
# Calling "import *" is allowed only at the module level.
GLOBALS_INITIAL = globals().keys()
from pystache import *
GLOBALS_PYSTACHE_IMPORTED = globals().keys()
import unittest
import pystache
class InitTests(unittest.TestCase):
def test___all__(self):
"""
Test that "from pystache import *" works as expected.
"""
actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
self.assertEqual(actual, expected)
def test_version_defined(self):
"""
Test that pystache.__version__ is set.
"""
actual_version = pystache.__version__
self.assertTrue(actual_version)
# coding: utf-8
"""
Unit tests of commands.py.
"""
import sys
import unittest
from pystache.commands.render import main
ORIGINAL_STDOUT = sys.stdout
class MockStdout(object):
def __init__(self):
self.output = ""
def write(self, str):
self.output += str
class CommandsTestCase(unittest.TestCase):
def setUp(self):
sys.stdout = MockStdout()
def callScript(self, template, context):
argv = ['pystache', template, context]
main(argv)
return sys.stdout.output
def testMainSimple(self):
"""
Test a simple command-line case.
"""
actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
self.assertEqual(actual, u"Hi world\n")
def tearDown(self):
sys.stdout = ORIGINAL_STDOUT
# encoding: utf-8
"""
TODO: add a docstring.
"""
import unittest
from examples.comments import Comments
from examples.double_section import DoubleSection
from examples.escaped import Escaped
from examples.unescaped import Unescaped
from examples.template_partial import TemplatePartial
from examples.delimiters import Delimiters
from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext
from pystache import Renderer
from pystache.tests.common import EXAMPLES_DIR
from pystache.tests.common import AssertStringMixin
class TestView(unittest.TestCase, AssertStringMixin):
def _assert(self, obj, expected):
renderer = Renderer()
actual = renderer.render(obj)
self.assertString(actual, expected)
def test_comments(self):
self._assert(Comments(), u"<h1>A Comedy of Errors</h1>")
def test_double_section(self):
self._assert(DoubleSection(), u"* first\n* second\n* third")
def test_unicode_output(self):
renderer = Renderer()
actual = renderer.render(UnicodeOutput())
self.assertString(actual, u'<p>Name: Henri Poincaré</p>')
def test_unicode_input(self):
renderer = Renderer()
actual = renderer.render(UnicodeInput())
self.assertString(actual, u'abcdé')
def test_escaping(self):
self._assert(Escaped(), u"<h1>Bear &gt; Shark</h1>")
def test_literal(self):
renderer = Renderer()
actual = renderer.render(Unescaped())
self.assertString(actual, u"<h1>Bear > Shark</h1>")
def test_template_partial(self):
renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(TemplatePartial(renderer=renderer))
self.assertString(actual, u"""<h1>Welcome</h1>
Again, Welcome!""")
def test_template_partial_extension(self):
renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
view = TemplatePartial(renderer=renderer)
actual = renderer.render(view)
self.assertString(actual, u"""Welcome
-------
## Again, Welcome! ##""")
def test_delimiters(self):
renderer = Renderer()
actual = renderer.render(Delimiters())
self.assertString(actual, u"""\
* It worked the first time.
* And it worked the second time.
* Then, surprisingly, it worked the third time.
""")
def test_nested_context(self):
renderer = Renderer()
actual = renderer.render(NestedContext(renderer))
self.assertString(actual, u"one and foo and two")
def test_nested_context_is_available_in_view(self):
renderer = Renderer()
view = NestedContext(renderer)
view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}'
actual = renderer.render(view)
self.assertString(actual, u'it works!')
def test_partial_in_partial_has_access_to_grand_parent_context(self):
renderer = Renderer(search_dirs=EXAMPLES_DIR)
view = TemplatePartial(renderer=renderer)
view.template = '''{{>partial_in_partial}}'''
actual = renderer.render(view, {'prop': 'derp'})
self.assertEqual(actual, 'Hi derp!')
if __name__ == '__main__':
unittest.main()
# encoding: utf-8
"""
Unit tests of loader.py.
"""
import os
import sys
import unittest
from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults
from pystache import defaults
from pystache.loader import Loader
class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
def setUp(self):
self.setup_defaults()
def tearDown(self):
self.teardown_defaults()
def test_init__extension(self):
loader = Loader(extension='foo')
self.assertEqual(loader.extension, 'foo')
def test_init__extension__default(self):
# Test the default value.
loader = Loader()
self.assertEqual(loader.extension, 'mustache')
def test_init__file_encoding(self):
loader = Loader(file_encoding='bar')
self.assertEqual(loader.file_encoding, 'bar')
def test_init__file_encoding__default(self):
file_encoding = defaults.FILE_ENCODING
try:
defaults.FILE_ENCODING = 'foo'
loader = Loader()
self.assertEqual(loader.file_encoding, 'foo')
finally:
defaults.FILE_ENCODING = file_encoding
def test_init__to_unicode(self):
to_unicode = lambda x: x
loader = Loader(to_unicode=to_unicode)
self.assertEqual(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self):
loader = Loader()
self.assertRaises(TypeError, loader.to_unicode, u"abc")
decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING
nonascii = u'abcdé'.encode('utf-8')
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.DECODE_ERRORS = 'ignore'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcd')
defaults.STRING_ENCODING = 'utf-8'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcdé')
def _get_path(self, filename):
return os.path.join(DATA_DIR, filename)
def test_unicode__basic__input_str(self):
"""
Test unicode(): default arguments with str input.
"""
loader = Loader()
actual = loader.unicode("foo")
self.assertString(actual, u"foo")
def test_unicode__basic__input_unicode(self):
"""
Test unicode(): default arguments with unicode input.
"""
loader = Loader()
actual = loader.unicode(u"foo")
self.assertString(actual, u"foo")
def test_unicode__basic__input_unicode_subclass(self):
"""
Test unicode(): default arguments with unicode-subclass input.
"""
class UnicodeSubclass(unicode):
pass
s = UnicodeSubclass(u"foo")
loader = Loader()
actual = loader.unicode(s)
self.assertString(actual, u"foo")
def test_unicode__to_unicode__attribute(self):
"""
Test unicode(): encoding attribute.
"""
loader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
def to_unicode(s, encoding=None):
if encoding is None:
encoding = 'utf-8'
return unicode(s, encoding)
loader.to_unicode = to_unicode
self.assertString(loader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self):
"""
Test unicode(): encoding argument.
"""
loader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
actual = loader.unicode(non_ascii, encoding='utf-8')
self.assertString(actual, u'abcdé')
# TODO: check the read() unit tests.
def test_read(self):
"""
Test read().
"""
loader = Loader()
path = self._get_path('ascii.mustache')
actual = loader.read(path)
self.assertString(actual, u'ascii: abc')
def test_read__file_encoding__attribute(self):
"""
Test read(): file_encoding attribute respected.
"""
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, loader.read, path)
loader.file_encoding = 'utf-8'
actual = loader.read(path)
self.assertString(actual, u'non-ascii: é')
def test_read__encoding__argument(self):
"""
Test read(): encoding argument respected.
"""
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, loader.read, path)
actual = loader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é')
def test_loader__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
"""
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, loader.read, path)
#loader.decode_errors = 'ignore'
#actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ')
# encoding: utf-8
"""
Unit tests for locator.py.
"""
from datetime import datetime
import os
import sys
import unittest
# TODO: remove this alias.
from pystache.common import TemplateNotFoundError
from pystache.loader import Loader as Reader
from pystache.locator import Locator
from pystache.tests.common import DATA_DIR, EXAMPLES_DIR, AssertExceptionMixin
from pystache.tests.data.views import SayHello
class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def _locator(self):
return Locator(search_dirs=DATA_DIR)
def test_init__extension(self):
# Test the default value.
locator = Locator()
self.assertEqual(locator.template_extension, 'mustache')
locator = Locator(extension='txt')
self.assertEqual(locator.template_extension, 'txt')
locator = Locator(extension=False)
self.assertTrue(locator.template_extension is False)
def _assert_paths(self, actual, expected):
"""
Assert that two paths are the same.
"""
self.assertEqual(actual, expected)
def test_get_object_directory(self):
locator = Locator()
obj = SayHello()
actual = locator.get_object_directory(obj)
self._assert_paths(actual, DATA_DIR)
def test_get_object_directory__not_hasattr_module(self):
locator = Locator()
obj = datetime(2000, 1, 1)
self.assertFalse(hasattr(obj, '__module__'))
self.assertEqual(locator.get_object_directory(obj), None)
self.assertFalse(hasattr(None, '__module__'))
self.assertEqual(locator.get_object_directory(None), None)
def test_make_file_name(self):
locator = Locator()
locator.template_extension = 'bar'
self.assertEqual(locator.make_file_name('foo'), 'foo.bar')
locator.template_extension = False
self.assertEqual(locator.make_file_name('foo'), 'foo')
locator.template_extension = ''
self.assertEqual(locator.make_file_name('foo'), 'foo.')
def test_make_file_name__template_extension_argument(self):
locator = Locator()
self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
def test_find_name(self):
locator = Locator()
path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
self.assertEqual(os.path.basename(path), 'simple.mustache')
def test_find_name__using_list_of_paths(self):
locator = Locator()
path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple')
self.assertTrue(path)
def test_find_name__precedence(self):
"""
Test the order in which find_name() searches directories.
"""
locator = Locator()
dir1 = DATA_DIR
dir2 = os.path.join(DATA_DIR, 'locator')
self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate')
dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1]
self.assertEqual(dirname, 'locator')
def test_find_name__non_existent_template_fails(self):
locator = Locator()
self.assertException(TemplateNotFoundError, "File 'doesnt_exist.mustache' not found in dirs: []",
locator.find_name, search_dirs=[], template_name='doesnt_exist')
def test_find_object(self):
locator = Locator()
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self._assert_paths(actual, expected)
def test_find_object__none_file_name(self):
locator = Locator()
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj)
expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEqual(actual, expected)
def test_find_object__none_object_directory(self):
locator = Locator()
obj = None
self.assertEqual(None, locator.get_object_directory(obj))
actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache')
expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEqual(actual, expected)
def test_make_template_name(self):
"""
Test make_template_name().
"""
locator = Locator()
class FooBar(object):
pass
foo = FooBar()
self.assertEqual(locator.make_template_name(foo), 'foo_bar')
# coding: utf-8
"""
Unit tests of parser.py.
"""
import unittest
from pystache.parser import _compile_template_re as make_re
class RegularExpressionTestCase(unittest.TestCase):
"""Tests the regular expression returned by _compile_template_re()."""
def test_re(self):
"""
Test getting a key from a dictionary.
"""
re = make_re()
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
# encoding: utf-8
import unittest
import pystache
from pystache import defaults
from pystache import renderer
from pystache.tests.common import html_escape
class PystacheTests(unittest.TestCase):
def setUp(self):
self.original_escape = defaults.TAG_ESCAPE
defaults.TAG_ESCAPE = html_escape
def tearDown(self):
defaults.TAG_ESCAPE = self.original_escape
def _assert_rendered(self, expected, template, context):
actual = pystache.render(template, context)
self.assertEqual(actual, expected)
def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEqual(ret, "Hi world!")
def test_kwargs(self):
ret = pystache.render("Hi {{thing}}!", thing='world')
self.assertEqual(ret, "Hi world!")
def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?"
context = { 'beverage': 'soda', 'person': 'Bob' }
self._assert_rendered("It's a nice day for soda, right Bob?", template, context)
def test_even_less_basic(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
context = { 'name': 'Jon', 'thing': 'racecar' }
self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context)
def test_ignores_misses(self):
template = "I think {{name}} wants a {{thing}}, right {{name}}?"
context = { 'name': 'Jon' }
self._assert_rendered("I think Jon wants a , right Jon?", template, context)
def test_render_zero(self):
template = 'My value is {{value}}.'
context = { 'value': 0 }
self._assert_rendered('My value is 0.', template, context)
def test_comments(self):
template = "What {{! the }} what?"
actual = pystache.render(template)
self.assertEqual("What what?", actual)
def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!"
context = { 'set': False }
self._assert_rendered("Ready go!", template, context)
def test_true_sections_are_shown(self):
template = "Ready {{#set}}set{{/set}} go!"
context = { 'set': True }
self._assert_rendered("Ready set go!", template, context)
non_strings_expected = """(123 & [&#x27;something&#x27;])(chris & 0.9)"""
def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
stats = []
stats.append({'key': 123, 'value': ['something']})
stats.append({'key': u"chris", 'value': 0.900})
context = { 'stats': stats }
self._assert_rendered(self.non_strings_expected, template, context)
def test_unicode(self):
template = 'Name: {{name}}; Age: {{age}}'
context = {'name': u'Henri Poincaré', 'age': 156 }
self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context)
def test_sections(self):
template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>"""
context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
self._assert_rendered(expected, template, context)
def test_implicit_iterator(self):
template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"""
context = { 'users': [ 'Chris', 'Tom','PJ' ] }
expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
self._assert_rendered(expected, template, context)
# The spec says that sections should not alter surrounding whitespace.
def test_surrounding_whitepace_not_altered(self):
template = "first{{#spacing}} second {{/spacing}}third"
context = {"spacing": True}
self._assert_rendered("first second third", template, context)
def test__section__non_false_value(self):
"""
Test when a section value is a (non-list) "non-false value".
From mustache(5):
When the value [of a section key] is non-false but not a list, it
will be used as the context for a single rendering of the block.
"""
template = """{{#person}}Hi {{name}}{{/person}}"""
context = {"person": {"name": "Jon"}}
self._assert_rendered("Hi Jon", template, context)
def test_later_list_section_with_escapable_character(self):
"""
This is a simple test case intended to cover issue #53.
The test case failed with markupsafe enabled, as follows:
AssertionError: Markup(u'foo &lt;') != 'foo <'
"""
template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}"""
context = {'s1': True, 's2': [True]}
self._assert_rendered("foo <", template, context)
import unittest
import pystache
from pystache import Renderer
from examples.nested_context import NestedContext
from examples.complex import Complex
from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial
from examples.simple import Simple
from pystache.tests.common import EXAMPLES_DIR
from pystache.tests.common import AssertStringMixin
class TestSimple(unittest.TestCase, AssertStringMixin):
def test_nested_context(self):
renderer = Renderer()
view = NestedContext(renderer)
view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}'
actual = renderer.render(view)
self.assertString(actual, u"one and foo and two")
def test_looping_and_negation_context(self):
template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}'
context = Complex()
renderer = Renderer()
actual = renderer.render(template, context)
self.assertEqual(actual, "Colors: red Colors: green Colors: blue ")
def test_empty_context(self):
template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
self.assertEqual(pystache.Renderer().render(template), "Should see me")
def test_callables(self):
view = Lambdas()
view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}'
renderer = Renderer()
actual = renderer.render(view)
self.assertString(actual, u'bar != bar. oh, it does!')
def test_rendering_partial(self):
renderer = Renderer(search_dirs=EXAMPLES_DIR)
view = TemplatePartial(renderer=renderer)
view.template = '{{>inner_partial}}'
actual = renderer.render(view)
self.assertString(actual, u'Again, Welcome!')
view.template = '{{#looping}}{{>inner_partial}} {{/looping}}'
actual = renderer.render(view)
self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ")
def test_non_existent_value_renders_blank(self):
view = Simple()
template = '{{not_set}} {{blank}}'
self.assertEqual(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self):
"""
Side note:
From the spec--
Partial tags SHOULD be treated as standalone when appropriate.
In particular, this means that trailing newlines should be removed.
"""
renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
view = TemplatePartial(renderer=renderer)
actual = renderer.render(view)
self.assertString(actual, u"""Welcome
-------
## Again, Welcome! ##""")
......@@ -9,7 +9,7 @@ to release a new version of Pystache.
(1) Push to PyPI. To release a new version of Pystache to PyPI--
http://pypi.python.org/pypi/pystache_custom_custom
http://pypi.python.org/pypi/pystache_custom
create a PyPI user account if you do not already have one. The user account
will need permissions to push to PyPI. A current "Package Index Owner" of
......@@ -72,7 +72,7 @@ else:
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
VERSION = '0.5.2' # Also change in pystache_custom_custom/__init__.py.
VERSION = '0.5.2' # Also change in pystache_custom/__init__.py.
HISTORY_PATH = 'HISTORY.rst'
LICENSE_PATH = 'LICENSE'
......@@ -170,13 +170,13 @@ INSTALL_REQUIRES = requires
# TODO: decide whether to use find_packages() instead. I'm not sure that
# find_packages() is available with distutils, for example.
PACKAGES = [
'pystache_custom_custom',
'pystache_custom_custom.commands',
'pystache_custom',
'pystache_custom.commands',
# The following packages are only for testing.
'pystache_custom_custom.tests',
'pystache_custom_custom.tests.data',
'pystache_custom_custom.tests.data.locator',
'pystache_custom_custom.tests.examples',
'pystache_custom.tests',
'pystache_custom.tests.data',
'pystache_custom.tests.data.locator',
'pystache_custom.tests.examples',
]
......@@ -185,7 +185,7 @@ def main(sys_argv):
long_description = make_long_description()
template_files = ['*.mustache', '*.txt']
setup(name='pystache_custom_custom',
setup(name='pystache_custom',
version=VERSION,
license='MIT',
description='Mustache for Python',
......@@ -193,19 +193,19 @@ def main(sys_argv):
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache_custom_custom',
url='http://github.com/defunkt/pystache_custom',
install_requires=INSTALL_REQUIRES,
packages=PACKAGES,
package_data = {
# Include template files so tests can be run.
'pystache_custom_custom.tests.data': template_files,
'pystache_custom_custom.tests.data.locator': template_files,
'pystache_custom_custom.tests.examples': template_files,
'pystache_custom.tests.data': template_files,
'pystache_custom.tests.data.locator': template_files,
'pystache_custom.tests.examples': template_files,
},
entry_points = {
'console_scripts': [
'pystache_custom_custom=pystache_custom_custom.commands.render:main',
'pystache_custom_custom-test=pystache_custom_custom.commands.test:main',
'pystache_custom=pystache_custom.commands.render:main',
'pystache_custom-test=pystache_custom.commands.test:main',
],
},
classifiers = CLASSIFIERS,
......
......@@ -6,7 +6,7 @@ Runs project tests.
This script is a substitute for running--
python -m pystache_custom_custom.commands.test
python -m pystache_custom.commands.test
It is useful in Python 2.4 because the -m flag does not accept subpackages
in Python 2.4:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment