Commit b5a68892 by Chris Jerdonek

Merge branch 'development' into 'master': staging version 0.5.1-rc for issue #109.

parents 0bf6ca1f 8d02bb25
*.pyc
.DS_Store
# Tox support. See: http://pypi.python.org/pypi/tox
.tox
# Our tox runs convert the doctests in *.rst files to Python 3 prior to
# running tests. Ignore these temporary files.
*.temp2to3.rst
# TextMate project file
*.tmproj
# Distribution-related folders and files.
build
MANIFEST
dist
.DS_Store
\ No newline at end of file
MANIFEST
pystache.egg-info
History
=======
0.5.1 (2012-04-03)
-----------
* Added support for Python 3.1 and 3.2.
* Added tox support to test multiple Python versions.
* Added test script entry point: pystache-test.
* Added __version__ package attribute.
* Test harness now supports both YAML and JSON forms of Mustache spec.
* Test harness no longer requires nose.
0.5.0 (2012-04-03)
------------------
......@@ -93,5 +103,6 @@ Bug fixes:
* First release
.. _2to3: http://docs.python.org/library/2to3.html
.. _issue #13: https://github.com/defunkt/pystache/issues/13
.. _Mustache spec: https://github.com/mustache/spec
include LICENSE
include HISTORY.rst README.rst
include HISTORY.rst
include README.rst
include tox.ini
include test_pystache.py
# You cannot use package_data, for example, to include data files in a
# source distribution when using Distribute.
recursive-include pystache/tests *.mustache *.txt
......@@ -25,16 +25,22 @@ Requirements
Pystache is tested with the following versions of Python:
* Python 2.4 (requires simplejson version 2.0.9 or earlier)
* Python 2.5 (requires simplejson)
* Python 2.4 (requires simplejson `version 2.0.9`_ or earlier)
* Python 2.5 (requires simplejson_)
* Python 2.6
* Python 2.7
* Python 3.1
* Python 3.2
JSON support is needed only for the command-line interface and to run the
spec tests. Python's json_ module is new as of Python 2.6. Python's
simplejson_ package works with earlier versions of Python. Because
simplejson stopped officially supporting Python 2.4 as of version 2.1.0,
Python 2.4 requires an earlier version.
spec tests. We require simplejson for earlier versions of Python since
Python's json_ module was added in Python 2.6.
For Python 2.4 we require an earlier version of simplejson since simplejson
stopped officially supporting Python 2.4 in simplejson version 2.1.0.
Earlier versions of simplejson can be installed manually, as follows: ::
pip install 'simplejson<2.1.0'
Install It
......@@ -51,8 +57,8 @@ Use It
::
>>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
u'Hi Mom!'
>>> print pystache.render('Hi {{person}}!', {'person': 'Mom'})
Hi Mom!
You can also create dedicated view classes to hold your view logic.
......@@ -65,18 +71,42 @@ Here's your view class (in examples/readme.py)::
Like so::
>>> from examples.readme import SayHello
>>> from pystache.tests.examples.readme import SayHello
>>> hello = SayHello()
Then your template, say_hello.mustache::
Then your template, say_hello.mustache (by default in the same directory)::
Hello, {{to}}!
Pull it together::
>>> renderer = pystache.Renderer()
>>> renderer.render(hello)
u'Hello, Pizza!'
>>> print renderer.render(hello)
Hello, Pizza!
For greater control over rendering (e.g. to specify a custom template directory),
use the ``Renderer`` class directly. One can pass attributes to the class's
constructor or set them on an instance.
To customize template loading on a per-view basis, subclass ``TemplateSpec``.
See the docstrings of the Renderer_ class and TemplateSpec_ class for
more information.
Python 3
========
As of version 0.5.1, Pystache fully supports Python 3. There are slight
differences in behavior between Pystache running under Python 2 and 3,
as follows:
* In Python 2, the default html-escape function ``cgi.escape()`` does not
escape single quotes; whereas in Python 3, the default escape function
``html.escape()`` does escape single quotes.
* In both Python 2 and 3, the string and file encodings default to
``sys.getdefaultencoding()``. However, this function can return different
values under Python 2 and 3, even when run from the same system. Check
your own system for the behavior on your system, or do not rely on the
defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class).
Unicode Handling
......@@ -85,24 +115,25 @@ Unicode Handling
This section describes Pystache's handling of unicode (e.g. strings and
encodings).
Internally, Pystache uses `only unicode strings`_. For input, Pystache accepts
both ``unicode`` and ``str`` strings. For output, Pystache's template
rendering methods return only unicode.
Internally, Pystache uses `only unicode strings`_ (type ``str`` in Python 3 and
type ``unicode`` in Python 2). For input, Pystache accepts both unicode strings
and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output,
Pystache's template rendering methods return only unicode.
Pystache's ``Renderer`` class supports a number of attributes that control how
Pystache converts ``str`` strings to unicode on input. These include the
Pystache's ``Renderer`` class supports a number of attributes to control how
Pystache converts byte strings to unicode on input. These include the
``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes.
The ``file_encoding`` attribute is the encoding the renderer uses to convert
to unicode any files read from the file system. Similarly, ``string_encoding``
is the encoding the renderer uses to convert to unicode any other strings of
type ``str`` encountered during the rendering process (e.g. context values
of type ``str``).
is the encoding the renderer uses to convert any other byte strings encountered
during the rendering process into unicode (e.g. context values that are
encoded byte strings).
The ``decode_errors`` attribute is what the renderer passes as the ``errors``
argument to Python's `built-in unicode function`_ ``unicode()`` when
converting. The valid values for this argument are ``strict``, ``ignore``,
and ``replace``.
argument to Python's built-in unicode-decoding function (``str()`` in Python 3
and ``unicode()`` in Python 2). The valid values for this argument are
``strict``, ``ignore``, and ``replace``.
Each of these attributes can be set via the ``Renderer`` class's constructor
using a keyword argument of the same name. See the Renderer class's
......@@ -115,33 +146,37 @@ default to values set in Pystache's ``defaults`` module.
Test It
=======
nose_ works great! ::
From an install-- ::
pip install nose
cd pystache
nosetests
pystache-test
Depending on your Python version and nose installation, you may need
to type, for example ::
From a source distribution-- ::
nosetests-2.4
python test_pystache.py
To include tests from the Mustache spec in your test runs: ::
To test Pystache source under multiple versions of Python all at once, you
can use tox_: ::
git submodule init
git submodule update
pip install tox
tox
To run all available tests (including doctests)::
If you do not have all Python versions listed in ``tox.ini``-- ::
nosetests --with-doctest --doctest-extension=rst
tox -e py26,py27 # for example
or alternatively (using setup.cfg)::
The source distribution tests also include doctests and tests from the
Mustache spec. To include tests from the Mustache spec in your test runs: ::
python setup.py nosetests
git submodule init
git submodule update
To run a subset of the tests, you can use this pattern, for example: ::
The test harness parses the spec's (more human-readable) yaml files if PyYAML_
is present. Otherwise, it parses the json files. To install PyYAML-- ::
nosetests --tests tests/test_context.py:GetValueTests.test_dictionary__key_present
pip install pyyaml
To test Pystache from a source distribution with Python 3.x, you must use tox.
This is because the source code must first be run through 2to3_.
Mailing List
......@@ -160,23 +195,32 @@ Author
::
>>> context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
>>> pystache.render("{{author}} :: {{email}}", context)
u'Chris Wanstrath :: chris@ozmm.org'
>>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' }
>>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context)
Author: Chris Wanstrath
Maintainer: Chris Jerdonek
.. _2to3: http://docs.python.org/library/2to3.html
.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode
.. _ctemplate: http://code.google.com/p/google-ctemplate/
.. _David Phillips: http://davidphillips.us/
.. _Distribute: http://pypi.python.org/pypi/distribute
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _json: http://docs.python.org/library/json.html
.. _Mustache: http://mustache.github.com/
.. _Mustache spec: https://github.com/mustache/spec
.. _mustache(5): http://mustache.github.com/mustache.5.html
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
.. _nose: http://readthedocs.org/docs/nose/en/latest/
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _PyPI: http://pypi.python.org/pypi/pystache
.. _Pystache: https://github.com/defunkt/pystache
.. _PyYAML: http://pypi.python.org/pypi/PyYAML
.. _Renderer: https://github.com/defunkt/pystache/blob/master/pystache/renderer.py
.. _semantically versioned: http://semver.org
.. _simplejson: http://pypi.python.org/pypi/simplejson/
.. _built-in unicode function: http://docs.python.org/library/functions.html#unicode
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _test: http://packages.python.org/distribute/setuptools.html#test
.. _tox: http://pypi.python.org/pypi/tox
.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
TODO
====
* Turn the benchmarking script at pystache/tests/benchmark.py into a command in pystache/commands, or
make it a subcommand of one of the existing commands (i.e. using a command argument).
* Provide support for logging in at least one of the commands.
* Make sure doctest text files can be converted for Python 3 when using tox.
* Make sure command parsing to pystache-test doesn't break with Python 2.4 and earlier.
* Combine pystache-test with the main command.
"""
TODO: add a docstring.
"""
# We keep all initialization code in a separate module.
from init import *
from pystache.init import render, Renderer, TemplateSpec
__all__ = ['render', 'Renderer', 'TemplateSpec']
__version__ = '0.5.1-rc' # Also change in setup.py.
"""
TODO: add a docstring.
"""
......@@ -13,7 +13,16 @@ try:
except:
# The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions.
import simplejson as json
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.
......@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage):
return template, context
def main(sys_argv):
# 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'):
......@@ -77,5 +91,4 @@ def main(sys_argv):
if __name__=='__main__':
main(sys.argv)
main()
# coding: utf-8
"""
This module provides a command to test pystache (unit tests, doctests, etc).
"""
import sys
from pystache.tests.main import run_tests
def main(sys_argv=sys.argv):
run_tests(sys_argv=sys_argv)
if __name__=='__main__':
main()
# coding: utf-8
"""
Exposes common functions.
"""
# 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()
......@@ -5,11 +5,20 @@ Defines a Context class to represent mustache(5)'s notion of context.
"""
class NotFound(object): pass
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__
# We use this private global variable as a return value to represent a key
# not being found on lookup. This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
#
# TODO: eliminate the need for a private global variable, e.g. by using the
# preferred Python approach of "easier to ask for forgiveness than permission":
# http://docs.python.org/glossary.html#term-eafp
class NotFound(object):
pass
_NOT_FOUND = NotFound()
......@@ -34,7 +43,7 @@ def _get_value(item, key):
# (e.g. catching KeyError).
if key in item:
return item[key]
elif type(item).__module__ != '__builtin__':
elif type(item).__module__ != _BUILTIN_MODULE:
# Then we consider the argument an "object" for the purposes of
# the spec.
#
......
......@@ -8,7 +8,12 @@ does not otherwise specify a value.
"""
import cgi
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
......@@ -39,12 +44,14 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.']
# 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 quotes to be escaped,
# but not single quotes:
# 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: cgi.escape(u, quote=True)
TAG_ESCAPE = lambda u: escape(u, quote=True)
# The default template extension.
TEMPLATE_EXTENSION = 'mustache'
......@@ -9,11 +9,6 @@ from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
__all__ = ['__version__', 'render', 'Renderer', 'TemplateSpec']
__version__ = '0.5.0'
def render(template, context=None, **kwargs):
"""
Return the given template string rendered using the given context.
......
......@@ -8,18 +8,24 @@ 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
def _to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
# We make a function so that the current defaults take effect.
# TODO: revisit whether this is necessary.
"""
if encoding is None:
encoding = defaults.STRING_ENCODING
return unicode(s, encoding, defaults.DECODE_ERRORS)
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):
......@@ -67,7 +73,7 @@ class Loader(object):
search_dirs = defaults.SEARCH_DIRS
if to_unicode is None:
to_unicode = _to_unicode
to_unicode = _make_to_unicode()
self.extension = extension
self.file_encoding = file_encoding
......@@ -106,17 +112,12 @@ class Loader(object):
Read the template at the given path, and return it as a unicode string.
"""
# We avoid use of the with keyword for Python 2.4 support.
f = open(path, 'r')
try:
text = f.read()
finally:
f.close()
b = common.read(path)
if encoding is None:
encoding = self.file_encoding
return self.unicode(text, encoding)
return self.unicode(b, encoding)
# TODO: unit-test this method.
def load_name(self, name):
......
......@@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module.
import re
from parsed import ParsedTemplate
from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}')
......@@ -131,7 +131,7 @@ class Parser(object):
if tag_type == '/':
if tag_key != section_key:
raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(section_key)))
raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
return ParsedTemplate(parse_tree), template[start_index:match_index], end_index
......
......@@ -7,7 +7,7 @@ Defines a class responsible for rendering logic.
import re
from parser import Parser
from pystache.parser import Parser
class RenderEngine(object):
......@@ -55,7 +55,7 @@ class RenderEngine(object):
this class will not pass tag values to literal prior to passing
them to this function. This allows for more flexibility,
for example using a custom escape function that handles
incoming strings of type markupssafe.Markup differently
incoming strings of type markupsafe.Markup differently
from plain unicode strings.
"""
......@@ -167,9 +167,28 @@ class RenderEngine(object):
# TODO: should we check the arity?
template = data(template)
parsed_template = self._parse(template, delimiters=delims)
data = [ data ]
elif not hasattr(data, '__iter__') or isinstance(data, dict):
data = [ data ]
data = [data]
else:
# The cleanest, least brittle way of determining whether
# something supports iteration is by trying to call iter() on it:
#
# http://docs.python.org/library/functions.html#iter
#
# It is not sufficient, for example, to check whether the item
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try:
iter(data)
except TypeError:
# Then the value does not support iteration.
data = [data]
else:
# We treat the value as a list (but do not treat strings
# and dicts as lists).
if isinstance(data, (basestring, dict)):
data = [data]
# Otherwise, leave it alone.
parts = []
for element in data:
......
......@@ -5,14 +5,27 @@ This module provides a Renderer class to render templates.
"""
import sys
from pystache import defaults
from pystache.context import Context
from pystache.loader import Loader
from pystache.renderengine import RenderEngine
from pystache.spec_loader import SpecLoader
from pystache.specloader import SpecLoader
from pystache.template_spec import TemplateSpec
# TODO: come up with a better solution for this. One of the issues here
# is that in Python 3 there is no common base class for unicode strings
# and byte strings, and 2to3 seems to convert all of "str", "unicode",
# and "basestring" to Python 3's "str".
if sys.version_info < (3, ):
_STRING_TYPES = basestring
else:
# The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
_STRING_TYPES = (unicode, type(u"a".encode('utf-8')))
class Renderer(object):
"""
......@@ -27,8 +40,9 @@ class Renderer(object):
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
>>> renderer.render('{{>partial}}', {'thing': 'world'})
u'Hello, world!'
>>> # We apply print to make the test work in Python 3 after 2to3.
>>> print renderer.render('{{>partial}}', {'thing': 'world'})
Hello, world!
"""
......@@ -64,10 +78,10 @@ class Renderer(object):
this class will only pass it unicode strings. The constructor
assigns this function to the constructed instance's escape()
method.
The argument defaults to `cgi.escape(s, quote=True)`. To
disable escaping entirely, one can pass `lambda u: u` as the
escape function, for example. One may also wish to consider
using markupsafe's escape function: markupsafe.escape().
To disable escaping entirely, one can pass `lambda u: u`
as the escape function, for example. One may also wish to
consider using markupsafe's escape function: markupsafe.escape().
This argument defaults to the package default.
file_encoding: the name of the default encoding to use when reading
template files. All templates are converted to unicode prior
......@@ -160,9 +174,16 @@ class Renderer(object):
"""
return unicode(self.escape(self._to_unicode_soft(s)))
def unicode(self, s, encoding=None):
def unicode(self, b, encoding=None):
"""
Convert a string to unicode, using string_encoding and decode_errors.
Convert a byte string to unicode, using string_encoding and decode_errors.
Arguments:
b: a byte string.
encoding: the name of an encoding. Defaults to the string_encoding
attribute for this instance.
Raises:
......@@ -178,7 +199,7 @@ class Renderer(object):
# TODO: Wrap UnicodeDecodeErrors with a message about setting
# the string_encoding and decode_errors attributes.
return unicode(s, encoding, self.decode_errors)
return unicode(b, encoding, self.decode_errors)
def _make_loader(self):
"""
......@@ -329,7 +350,7 @@ class Renderer(object):
all items in the *context list.
"""
if isinstance(template, basestring):
if isinstance(template, _STRING_TYPES):
return self._render_string(template, *context, **kwargs)
# Otherwise, we assume the template is an object.
......
# coding: utf-8
"""
This module supports customized (aka special or specified) template loading.
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: add a docstring.
"""
# 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 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
"""
TODO: add a docstring.
"""
"""
TODO: add a docstring.
"""
# coding: utf-8
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class SayHello(object):
......
# 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.
"""
"""
TODO: add a docstring.
"""
class Comments(object):
def title(self):
......
"""
TODO: add a docstring.
"""
class Complex(object):
def header(self):
......
"""
TODO: add a docstring.
"""
class Delimiters(object):
def first(self):
......
"""
TODO: add a docstring.
"""
class DoubleSection(object):
def t(self):
......
"""
TODO: add a docstring.
"""
class Escaped(object):
def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class Inverted(object):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
def rot(s, n=13):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class NestedContext(TemplateSpec):
......
from examples.lambdas import rot
"""
TODO: add a docstring.
"""
from pystache.tests.examples.lambdas import rot
class PartialsWithLambdas(object):
def rot(self):
return rot
\ No newline at end of file
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):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class TemplatePartial(TemplateSpec):
......@@ -18,4 +24,4 @@ class TemplatePartial(TemplateSpec):
return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
def thing(self):
return self._context_get('prop')
\ No newline at end of file
return self._context_get('prop')
"""
TODO: add a docstring.
"""
class Unescaped(object):
def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec
class UnicodeInput(TemplateSpec):
......
# encoding: utf-8
"""
TODO: add a docstring.
"""
class UnicodeOutput(object):
def name(self):
......
# 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"
def run_tests(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
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.
"""
unconverted_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 code strings to functions.
# TODO: make this section of code easier to understand.
context = {}
for key, val in unconverted_context.iteritems():
if isinstance(val, dict) and val.get('__tag__') == 'code':
val = eval(val['python'])
context[key] = val
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)
......@@ -8,7 +8,7 @@ Unit tests of commands.py.
import sys
import unittest
from pystache.commands import main
from pystache.commands.render import main
ORIGINAL_STDOUT = sys.stdout
......@@ -39,7 +39,7 @@ class CommandsTestCase(unittest.TestCase):
"""
actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
self.assertEquals(actual, u"Hi world\n")
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
......@@ -12,8 +17,8 @@ from examples.unicode_output import UnicodeOutput
from examples.unicode_input import UnicodeInput
from examples.nested_context import NestedContext
from pystache import Renderer
from tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin
from pystache.tests.common import EXAMPLES_DIR
from pystache.tests.common import AssertStringMixin
class TestView(unittest.TestCase, AssertStringMixin):
......@@ -95,7 +100,7 @@ Again, Welcome!""")
view.template = '''{{>partial_in_partial}}'''
actual = renderer.render(view, {'prop': 'derp'})
self.assertEquals(actual, 'Hi derp!')
self.assertEqual(actual, 'Hi derp!')
if __name__ == '__main__':
unittest.main()
# encoding: utf-8
"""
Unit tests of reader.py.
Unit tests of loader.py.
"""
......@@ -9,42 +9,45 @@ import os
import sys
import unittest
from tests.common import AssertStringMixin
from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults
from pystache import defaults
from pystache.loader import Loader
DATA_DIR = 'tests/data'
class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
def setUp(self):
self.setup_defaults()
class LoaderTests(unittest.TestCase, AssertStringMixin):
def tearDown(self):
self.teardown_defaults()
def test_init__extension(self):
loader = Loader(extension='foo')
self.assertEquals(loader.extension, 'foo')
self.assertEqual(loader.extension, 'foo')
def test_init__extension__default(self):
# Test the default value.
loader = Loader()
self.assertEquals(loader.extension, 'mustache')
self.assertEqual(loader.extension, 'mustache')
def test_init__file_encoding(self):
loader = Loader(file_encoding='bar')
self.assertEquals(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.assertEquals(loader.file_encoding, 'foo')
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.assertEquals(loader.to_unicode, to_unicode)
self.assertEqual(loader.to_unicode, to_unicode)
def test_init__to_unicode__default(self):
loader = Loader()
......@@ -53,25 +56,19 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
decode_errors = defaults.DECODE_ERRORS
string_encoding = defaults.STRING_ENCODING
nonascii = 'abcdé'
nonascii = u'abcdé'.encode('utf-8')
try:
defaults.DECODE_ERRORS = 'strict'
defaults.STRING_ENCODING = 'ascii'
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
loader = Loader()
self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
defaults.DECODE_ERRORS = 'ignore'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcd')
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é')
defaults.STRING_ENCODING = 'utf-8'
loader = Loader()
self.assertString(loader.to_unicode(nonascii), u'abcdé')
finally:
defaults.DECODE_ERRORS = decode_errors
defaults.STRING_ENCODING = string_encoding
def _get_path(self, filename):
return os.path.join(DATA_DIR, filename)
......@@ -81,8 +78,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with str input.
"""
reader = Loader()
actual = reader.unicode("foo")
loader = Loader()
actual = loader.unicode("foo")
self.assertString(actual, u"foo")
......@@ -91,8 +88,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): default arguments with unicode input.
"""
reader = Loader()
actual = reader.unicode(u"foo")
loader = Loader()
actual = loader.unicode(u"foo")
self.assertString(actual, u"foo")
......@@ -106,8 +103,8 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
s = UnicodeSubclass(u"foo")
reader = Loader()
actual = reader.unicode(s)
loader = Loader()
actual = loader.unicode(s)
self.assertString(actual, u"foo")
......@@ -116,32 +113,31 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test unicode(): encoding attribute.
"""
reader = Loader()
loader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
def to_unicode(s, encoding=None):
if encoding is None:
encoding = 'utf-8'
return unicode(s, encoding)
reader.to_unicode = to_unicode
self.assertString(reader.unicode(non_ascii), u"abcdé")
loader.to_unicode = to_unicode
self.assertString(loader.unicode(non_ascii), u"abcdé")
def test_unicode__encoding_argument(self):
"""
Test unicode(): encoding argument.
"""
reader = Loader()
loader = Loader()
non_ascii = u'abcdé'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, reader.unicode, non_ascii)
self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
actual = reader.unicode(non_ascii, encoding='utf-8')
actual = loader.unicode(non_ascii, encoding='utf-8')
self.assertString(actual, u'abcdé')
# TODO: check the read() unit tests.
......@@ -150,9 +146,9 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read().
"""
reader = Loader()
loader = Loader()
path = self._get_path('ascii.mustache')
actual = reader.read(path)
actual = loader.read(path)
self.assertString(actual, u'ascii: abc')
def test_read__file_encoding__attribute(self):
......@@ -174,25 +170,25 @@ class LoaderTests(unittest.TestCase, AssertStringMixin):
Test read(): encoding argument respected.
"""
reader = Loader()
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path)
self.assertRaises(UnicodeDecodeError, loader.read, path)
actual = reader.read(path, encoding='utf-8')
actual = loader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é')
def test_reader__to_unicode__attribute(self):
def test_loader__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
"""
reader = Loader()
loader = Loader()
path = self._get_path('non_ascii.mustache')
self.assertRaises(UnicodeDecodeError, reader.read, path)
self.assertRaises(UnicodeDecodeError, loader.read, path)
#reader.decode_errors = 'ignore'
#actual = reader.read(path)
#loader.decode_errors = 'ignore'
#actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ')
# encoding: utf-8
"""
Contains locator.py unit tests.
Unit tests for locator.py.
"""
......@@ -14,8 +14,8 @@ import unittest
from pystache.loader import Loader as Reader
from pystache.locator import Locator
from tests.common import DATA_DIR
from data.views import SayHello
from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from pystache.tests.data.views import SayHello
class LocatorTests(unittest.TestCase):
......@@ -26,58 +26,65 @@ class LocatorTests(unittest.TestCase):
def test_init__extension(self):
# Test the default value.
locator = Locator()
self.assertEquals(locator.template_extension, 'mustache')
self.assertEqual(locator.template_extension, 'mustache')
locator = Locator(extension='txt')
self.assertEquals(locator.template_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.assertEquals(actual, os.path.abspath(DATA_DIR))
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.assertEquals(locator.get_object_directory(obj), None)
self.assertEqual(locator.get_object_directory(obj), None)
self.assertFalse(hasattr(None, '__module__'))
self.assertEquals(locator.get_object_directory(None), None)
self.assertEqual(locator.get_object_directory(None), None)
def test_make_file_name(self):
locator = Locator()
locator.template_extension = 'bar'
self.assertEquals(locator.make_file_name('foo'), 'foo.bar')
self.assertEqual(locator.make_file_name('foo'), 'foo.bar')
locator.template_extension = False
self.assertEquals(locator.make_file_name('foo'), 'foo')
self.assertEqual(locator.make_file_name('foo'), 'foo')
locator.template_extension = ''
self.assertEquals(locator.make_file_name('foo'), 'foo.')
self.assertEqual(locator.make_file_name('foo'), 'foo.')
def test_make_file_name__template_extension_argument(self):
locator = Locator()
self.assertEquals(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
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'], template_name='simple')
path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
self.assertEquals(os.path.basename(path), 'simple.mustache')
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=['doesnt_exist', 'examples'], template_name='simple')
path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple')
self.assertTrue(path)
......@@ -98,7 +105,7 @@ class LocatorTests(unittest.TestCase):
dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1]
self.assertEquals(dirname, 'locator')
self.assertEqual(dirname, 'locator')
def test_find_name__non_existent_template_fails(self):
locator = Locator()
......@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase):
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self.assertEquals(actual, expected)
self._assert_paths(actual, expected)
def test_find_object__none_file_name(self):
locator = Locator()
......@@ -121,20 +128,20 @@ class LocatorTests(unittest.TestCase):
obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj)
expected = os.path.abspath(os.path.join(DATA_DIR, 'say_hello.mustache'))
expected = os.path.join(DATA_DIR, 'say_hello.mustache')
self.assertEquals(actual, expected)
self.assertEqual(actual, expected)
def test_find_object__none_object_directory(self):
locator = Locator()
obj = None
self.assertEquals(None, locator.get_object_directory(obj))
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.assertEquals(actual, expected)
self.assertEqual(actual, expected)
def test_make_template_name(self):
"""
......@@ -147,4 +154,4 @@ class LocatorTests(unittest.TestCase):
pass
foo = FooBar()
self.assertEquals(locator.make_template_name(foo), 'foo_bar')
self.assertEqual(locator.make_template_name(foo), 'foo_bar')
# 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.assertEquals(actual, expected)
self.assertEqual(actual, expected)
def test_basic(self):
ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
self.assertEquals(ret, "Hi world!")
self.assertEqual(ret, "Hi world!")
def test_kwargs(self):
ret = pystache.render("Hi {{thing}}!", thing='world')
self.assertEquals(ret, "Hi world!")
self.assertEqual(ret, "Hi world!")
def test_less_basic(self):
template = "It's a nice day for {{beverage}}, right {{person}}?"
......@@ -42,7 +53,7 @@ class PystacheTests(unittest.TestCase):
def test_comments(self):
template = "What {{! the }} what?"
actual = pystache.render(template)
self.assertEquals("What what?", actual)
self.assertEqual("What what?", actual)
def test_false_sections_are_hidden(self):
template = "Ready {{#set}}set {{/set}}go!"
......@@ -54,7 +65,7 @@ class PystacheTests(unittest.TestCase):
context = { 'set': True }
self._assert_rendered("Ready set go!", template, context)
non_strings_expected = """(123 & ['something'])(chris & 0.9)"""
non_strings_expected = """(123 & [&#x27;something&#x27;])(chris & 0.9)"""
def test_non_strings(self):
template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
......
......@@ -5,13 +5,34 @@ Unit tests of renderengine.py.
"""
import cgi
import unittest
from pystache.context import Context
from pystache import defaults
from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine
from tests.common import AssertStringMixin
from pystache.tests.common import AssertStringMixin
def mock_literal(s):
"""
For use as the literal keyword argument to the RenderEngine constructor.
Arguments:
s: a byte string or unicode string.
"""
if isinstance(s, unicode):
# Strip off unicode super classes, if present.
u = unicode(s)
else:
u = unicode(s, encoding='ascii')
# We apply upper() to make sure we are actually using our custom
# function in the tests
return u.upper()
class RenderEngineTestCase(unittest.TestCase):
......@@ -26,9 +47,9 @@ class RenderEngineTestCase(unittest.TestCase):
# In real-life, these arguments would be functions
engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
self.assertEquals(engine.escape, "escape")
self.assertEquals(engine.literal, "literal")
self.assertEquals(engine.load_partial, "foo")
self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal")
self.assertEqual(engine.load_partial, "foo")
class RenderTests(unittest.TestCase, AssertStringMixin):
......@@ -47,7 +68,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Create and return a default RenderEngine for testing.
"""
escape = lambda s: unicode(cgi.escape(s))
escape = defaults.TAG_ESCAPE
engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
return engine
......@@ -154,12 +175,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Test a context value that is not a basestring instance.
"""
# We use include upper() to make sure we are actually using
# our custom function in the tests
to_unicode = lambda s: unicode(s, encoding='ascii').upper()
engine = self._engine()
engine.escape = to_unicode
engine.literal = to_unicode
engine.escape = mock_literal
engine.literal = mock_literal
self.assertRaises(TypeError, engine.literal, 100)
......@@ -186,37 +204,64 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': '{{#hello}}'}
self._assert_render(u'{{#hello}}', template, context)
# Built-in types:
#
# Confirm that we not treat instances of built-in types as objects,
# for example by calling a method on a built-in type instance when it
# has a method whose name matches the current key.
#
# Each test case puts an instance of a built-in type on top of the
# context stack before interpolating a tag whose key matches an
# attribute (method or property) of the instance.
#
def _assert_builtin_attr(self, item, attr_name, expected_attr):
self.assertTrue(hasattr(item, attr_name))
actual = getattr(item, attr_name)
if callable(actual):
actual = actual()
self.assertEqual(actual, expected_attr)
def _assert_builtin_type(self, item, attr_name, expected_attr, expected_template):
self._assert_builtin_attr(item, attr_name, expected_attr)
template = '{{#section}}{{%s}}{{/section}}' % attr_name
context = {'section': item, attr_name: expected_template}
self._assert_render(expected_template, template, context)
def test_interpolation__built_in_type__string(self):
"""
Check tag interpolation with a string on the top of the context stack.
Check tag interpolation with a built-in type: string.
"""
item = 'abc'
# item.upper() == 'ABC'
template = '{{#section}}{{upper}}{{/section}}'
context = {'section': item, 'upper': 'XYZ'}
self._assert_render(u'XYZ', template, context)
self._assert_builtin_type('abc', 'upper', 'ABC', u'xyz')
def test_interpolation__built_in_type__integer(self):
"""
Check tag interpolation with an integer on the top of the context stack.
Check tag interpolation with a built-in type: integer.
"""
item = 10
# item.real == 10
template = '{{#section}}{{real}}{{/section}}'
context = {'section': item, 'real': 1000}
self._assert_render(u'1000', template, context)
# Since public attributes weren't added to integers until Python 2.6
# (for example the "real" attribute of the numeric type hierarchy)--
#
# http://docs.python.org/library/numbers.html
#
# we need to resort to built-in attributes (double-underscored) on
# the integer type.
self._assert_builtin_type(15, '__neg__', -15, u'999')
def test_interpolation__built_in_type__list(self):
"""
Check tag interpolation with a list on the top of the context stack.
Check tag interpolation with a built-in type: list.
"""
item = [[1, 2, 3]]
# item[0].pop() == 3
template = '{{#section}}{{pop}}{{/section}}'
context = {'section': item, 'pop': 7}
attr_name = 'pop'
# Make a copy to prevent changes to item[0].
self._assert_builtin_attr(list(item[0]), attr_name, 3)
template = '{{#section}}{{%s}}{{/section}}' % attr_name
context = {'section': item, attr_name: 7}
self._assert_render(u'7', template, context)
def test_implicit_iterator__literal(self):
......@@ -288,7 +333,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try:
self._assert_render(None, template)
except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section' != None")
self.assertEqual(str(err), "Section end tag mismatch: section != None")
def test_section__end_tag_mismatch(self):
"""
......@@ -299,7 +344,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
try:
self._assert_render(None, template)
except ParsingError, err:
self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'")
self.assertEqual(str(err), "Section end tag mismatch: section_end != section_start")
def test_section__context_values(self):
"""
......@@ -349,6 +394,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
self._assert_render(u'{{planet}}: Earth', template, context)
# TODO: have this test case added to the spec.
def test_section__string_values_not_lists(self):
"""
Check that string section values are not interpreted as lists.
"""
template = '{{#section}}foo{{/section}}'
context = {'section': '123'}
# If strings were interpreted as lists, this would give "foofoofoo".
self._assert_render(u'foo', template, context)
def test_section__nested_truthy(self):
"""
Check that "nested truthy" sections get rendered.
......
......@@ -8,8 +8,8 @@ from examples.lambdas import Lambdas
from examples.template_partial import TemplatePartial
from examples.simple import Simple
from tests.common import EXAMPLES_DIR
from tests.common import AssertStringMixin
from pystache.tests.common import EXAMPLES_DIR
from pystache.tests.common import AssertStringMixin
class TestSimple(unittest.TestCase, AssertStringMixin):
......@@ -28,11 +28,11 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
renderer = Renderer()
actual = renderer.render(template, context)
self.assertEquals(actual, "Colors: red Colors: green Colors: blue ")
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.assertEquals(pystache.Renderer().render(template), "Should see me")
self.assertEqual(pystache.Renderer().render(template), "Should see me")
def test_callables(self):
view = Lambdas()
......@@ -58,7 +58,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin):
def test_non_existent_value_renders_blank(self):
view = Simple()
template = '{{not_set}} {{blank}}'
self.assertEquals(pystache.Renderer().render(template), ' ')
self.assertEqual(pystache.Renderer().render(template), ' ')
def test_template_partial_extension(self):
......
......@@ -18,13 +18,11 @@ from pystache import Renderer
from pystache import TemplateSpec
from pystache.locator import Locator
from pystache.loader import Loader
from pystache.spec_loader import SpecLoader
from tests.common import DATA_DIR
from tests.common import EXAMPLES_DIR
from tests.common import AssertIsMixin
from tests.common import AssertStringMixin
from tests.data.views import SampleView
from tests.data.views import NonAscii
from pystache.specloader import SpecLoader
from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from pystache.tests.common import AssertIsMixin, AssertStringMixin
from pystache.tests.data.views import SampleView
from pystache.tests.data.views import NonAscii
class Thing(object):
......@@ -46,9 +44,10 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer.render, view)
view.template_rel_directory = "../examples"
# TODO: change this test to remove the following brittle line.
view.template_rel_directory = "examples"
actual = renderer.render(view)
self.assertEquals(actual, "No tags...")
self.assertEqual(actual, "No tags...")
def test_template_path_for_partials(self):
"""
......@@ -64,7 +63,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer1.render, spec)
actual = renderer2.render(spec)
self.assertEquals(actual, "Partial: No tags...")
self.assertEqual(actual, "Partial: No tags...")
def test_basic_method_calls(self):
renderer = Renderer()
......@@ -78,7 +77,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer()
actual = renderer.render(view)
self.assertEquals(actual, "Hi Chris!")
self.assertEqual(actual, "Hi Chris!")
def test_complex(self):
renderer = Renderer()
......@@ -94,7 +93,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_higher_order_replace(self):
renderer = Renderer()
actual = renderer.render(Lambdas())
self.assertEquals(actual, 'bar != bar. oh, it does!')
self.assertEqual(actual, 'bar != bar. oh, it does!')
def test_higher_order_rot13(self):
view = Lambdas()
......@@ -118,7 +117,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer = Renderer(search_dirs=EXAMPLES_DIR)
actual = renderer.render(view)
self.assertEquals(actual, u'nopqrstuvwxyz')
self.assertEqual(actual, u'nopqrstuvwxyz')
def test_hierarchical_partials_with_lambdas(self):
view = Lambdas()
......@@ -151,6 +150,28 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertString(actual, u"""one, two, three, empty list""")
def _make_specloader():
"""
Return a default SpecLoader instance for testing purposes.
"""
# Python 2 and 3 have different default encodings. Thus, to have
# consistent test results across both versions, we need to specify
# the string and file encodings explicitly rather than relying on
# the defaults.
def to_unicode(s, encoding=None):
"""
Raises a TypeError exception if the given string is already unicode.
"""
if encoding is None:
encoding = 'ascii'
return unicode(s, encoding, 'strict')
loader = Loader(file_encoding='ascii', to_unicode=to_unicode)
return SpecLoader(loader=loader)
class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
......@@ -158,13 +179,16 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
def _make_specloader(self):
return _make_specloader()
def test_init__defaults(self):
custom = SpecLoader()
spec_loader = SpecLoader()
# Check the loader attribute.
loader = custom.loader
self.assertEquals(loader.extension, 'mustache')
self.assertEquals(loader.file_encoding, sys.getdefaultencoding())
loader = spec_loader.loader
self.assertEqual(loader.extension, 'mustache')
self.assertEqual(loader.file_encoding, sys.getdefaultencoding())
# TODO: finish testing the other Loader attributes.
to_unicode = loader.to_unicode
......@@ -186,7 +210,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
custom.template = "abc"
self._assert_template(SpecLoader(), custom, u"abc")
spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"abc")
def test_load__template__type_unicode(self):
"""
......@@ -196,7 +221,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
custom.template = u"abc"
self._assert_template(SpecLoader(), custom, u"abc")
spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"abc")
def test_load__template__unicode_non_ascii(self):
"""
......@@ -206,7 +232,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
custom.template = u"é"
self._assert_template(SpecLoader(), custom, u"é")
spec_loader = self._make_specloader()
self._assert_template(spec_loader, custom, u"é")
def test_load__template__with_template_encoding(self):
"""
......@@ -216,10 +243,12 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
custom = TemplateSpec()
custom.template = u'é'.encode('utf-8')
self.assertRaises(UnicodeDecodeError, self._assert_template, SpecLoader(), custom, u'é')
spec_loader = self._make_specloader()
self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, u'é')
custom.template_encoding = 'utf-8'
self._assert_template(SpecLoader(), custom, u'é')
self._assert_template(spec_loader, custom, u'é')
# TODO: make this test complete.
def test_load__template__correct_loader(self):
......@@ -254,8 +283,8 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# Check that our unicode() above was called.
self._assert_template(custom_loader, view, u'foo')
self.assertEquals(loader.s, "template-foo")
self.assertEquals(loader.encoding, "encoding-foo")
self.assertEqual(loader.s, "template-foo")
self.assertEqual(loader.encoding, "encoding-foo")
# TODO: migrate these tests into the SpecLoaderTests class.
......@@ -265,14 +294,13 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# TemplateSpec attributes or something).
class TemplateSpecTests(unittest.TestCase):
# TODO: rename this method to _make_loader().
def _make_locator(self):
return SpecLoader()
def _make_loader(self):
return _make_specloader()
def _assert_template_location(self, view, expected):
locator = self._make_locator()
actual = locator._find_relative(view)
self.assertEquals(actual, expected)
loader = self._make_loader()
actual = loader._find_relative(view)
self.assertEqual(actual, expected)
def test_find_relative(self):
"""
......@@ -328,43 +356,50 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.txt'))
def _assert_paths(self, actual, expected):
"""
Assert that two paths are the same.
"""
self.assertEqual(actual, expected)
def test_find__with_directory(self):
"""
Test _find() with a view that has a directory specified.
"""
locator = self._make_locator()
loader = self._make_loader()
view = SampleView()
view.template_rel_path = 'foo/bar.txt'
self.assertTrue(locator._find_relative(view)[0] is not None)
self.assertTrue(loader._find_relative(view)[0] is not None)
actual = locator._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt'))
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'foo/bar.txt')
self.assertEquals(actual, expected)
self._assert_paths(actual, expected)
def test_find__without_directory(self):
"""
Test _find() with a view that doesn't have a directory specified.
"""
locator = self._make_locator()
loader = self._make_loader()
view = SampleView()
self.assertTrue(locator._find_relative(view)[0] is None)
self.assertTrue(loader._find_relative(view)[0] is None)
actual = locator._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache'))
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self.assertEquals(actual, expected)
self._assert_paths(actual, expected)
def _assert_get_template(self, custom, expected):
locator = self._make_locator()
actual = locator.load(custom)
loader = self._make_loader()
actual = loader.load(custom)
self.assertEquals(type(actual), unicode)
self.assertEquals(actual, expected)
self.assertEqual(type(actual), unicode)
self.assertEqual(actual, expected)
def test_get_template(self):
"""
......
[nosetests]
with-doctest=1
doctest-extension=rst
......@@ -2,7 +2,7 @@
# coding: utf-8
"""
This script supports installing and distributing pystache.
This script supports publishing Pystache to PyPI.
Below are instructions to pystache maintainers on how to push a new
version of pystache to PyPI--
......@@ -38,11 +38,59 @@ as described here, for example:
import os
import sys
py_version = sys.version_info
# Distribute works with Python 2.3.5 and above:
# http://packages.python.org/distribute/setuptools.html#building-and-distributing-packages-with-distribute
if py_version < (2, 3, 5):
# TODO: this might not work yet.
import distutils as dist
from distutils import core
setup = core.setup
else:
import setuptools as dist
setup = dist.setup
# TODO: use the logging module instead of printing.
# TODO: include the following in a verbose mode.
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
VERSION = '0.5.1-rc' # Also change in pystache/__init__.py.
HISTORY_PATH = 'HISTORY.rst'
LICENSE_PATH = 'LICENSE'
README_PATH = 'README.rst'
CLASSIFIERS = (
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.4',
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
)
def read(path):
"""
Read and return the contents of a text file as a unicode string.
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
"""
# This function implementation was chosen to be compatible across Python 2/3.
f = open(path, 'rb')
# We avoid use of the with keyword for Python 2.4 support.
try:
b = f.read()
finally:
f.close()
return b.decode('utf-8')
def publish():
......@@ -58,37 +106,96 @@ def make_long_description():
Return the long description for the package.
"""
long_description = open('README.rst').read() + '\n\n' + open('HISTORY.rst').read()
license = """\
License
=======
return long_description
""" + read(LICENSE_PATH)
sections = [read(README_PATH), read(HISTORY_PATH), license]
return '\n\n'.join(sections)
if sys.argv[-1] == 'publish':
publish()
sys.exit()
long_description = make_long_description()
setup(name='pystache',
version='0.5.0',
description='Mustache for Python',
long_description=long_description,
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache',
packages=['pystache'],
license='MIT',
entry_points = {
'console_scripts': ['pystache=pystache.commands:main'],
},
classifiers = (
'Development Status :: 4 - Beta',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.4',
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
)
)
# We follow the guidance here for compatibility with using setuptools instead
# of Distribute under Python 2 (on the subject of new, unrecognized keyword
# arguments to setup()):
#
# http://packages.python.org/distribute/python3.html#note-on-compatibility-with-setuptools
#
if py_version < (3, ):
extra = {}
else:
extra = {
# Causes 2to3 to be run during the build step.
'use_2to3': True,
}
# We use the package simplejson for older Python versions since Python
# does not contain the module json before 2.6:
#
# http://docs.python.org/library/json.html
#
# Moreover, simplejson stopped officially support for Python 2.4 in version 2.1.0:
#
# https://github.com/simplejson/simplejson/blob/master/CHANGES.txt
#
requires = []
if py_version < (2, 5):
requires.append('simplejson<2.1')
elif py_version < (2, 6):
requires.append('simplejson')
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',
'pystache.commands',
# The following packages are only for testing.
'pystache.tests',
'pystache.tests.data',
'pystache.tests.data.locator',
'pystache.tests.examples',
]
def main(sys_argv):
long_description = make_long_description()
template_files = ['*.mustache', '*.txt']
setup(name='pystache',
version=VERSION,
license='MIT',
description='Mustache for Python',
long_description=long_description,
author='Chris Wanstrath',
author_email='chris@ozmm.org',
maintainer='Chris Jerdonek',
url='http://github.com/defunkt/pystache',
install_requires=INSTALL_REQUIRES,
packages=PACKAGES,
package_data = {
# Include template files so tests can be run.
'pystache.tests.data': template_files,
'pystache.tests.data.locator': template_files,
'pystache.tests.examples': template_files,
},
entry_points = {
'console_scripts': [
'pystache=pystache.commands.render:main',
'pystache-test=pystache.commands.test:main',
],
},
classifiers = CLASSIFIERS,
**extra
)
if __name__=='__main__':
main(sys.argv)
#!/usr/bin/env python
# coding: utf-8
"""
Runs project tests.
This script is a substitute for running--
python -m pystache.commands.test
It is useful in Python 2.4 because the -m flag does not accept subpackages
in Python 2.4:
http://docs.python.org/using/cmdline.html#cmdoption-m
"""
import sys
from pystache.commands import test
from pystache.tests.main import FROM_SOURCE_OPTION
def main(sys_argv=sys.argv):
sys.argv.insert(1, FROM_SOURCE_OPTION)
test.main()
if __name__=='__main__':
main()
# coding: utf-8
"""
Provides test-related code that can be used by all tests.
"""
import os
import examples
DATA_DIR = 'tests/data'
EXAMPLES_DIR = os.path.dirname(examples.__file__)
def get_data_path(file_name):
return os.path.join(DATA_DIR, file_name)
class AssertStringMixin:
"""A unittest.TestCase mixin to check string equality."""
def assertString(self, actual, expected):
"""
Assert that the given strings are equal and have the same type.
"""
# Show both friendly and literal versions.
message = """String mismatch: %%s\
Expected: \"""%s\"""
Actual: \"""%s\"""
Expected: %s
Actual: %s""" % (expected, actual, repr(expected), repr(actual))
self.assertEquals(actual, expected, message % "different characters")
details = "types different: %s != %s" % (repr(type(expected)), repr(type(actual)))
self.assertEquals(type(expected), type(actual), message % details)
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)))
# coding: utf-8
"""
Creates a unittest.TestCase for the tests defined in the mustache spec.
"""
# TODO: this module can be cleaned up somewhat.
try:
# We deserialize the json form rather than the yaml form because
# json libraries are available for Python 2.4.
import json
except:
# The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions.
import simplejson as json
import glob
import os.path
import unittest
from pystache.renderer import Renderer
root_path = os.path.join(os.path.dirname(__file__), '..', 'ext', 'spec', 'specs')
spec_paths = glob.glob(os.path.join(root_path, '*.json'))
class MustacheSpec(unittest.TestCase):
pass
def buildTest(testData, spec_filename):
name = testData['name']
description = testData['desc']
test_name = "%s (%s)" % (name, spec_filename)
def test(self):
template = testData['template']
partials = testData.has_key('partials') and testData['partials'] or {}
expected = testData['expected']
data = testData['data']
# Convert code strings to functions.
# TODO: make this section of code easier to understand.
new_data = {}
for key, val in data.iteritems():
if isinstance(val, dict) and val.get('__tag__') == 'code':
val = eval(val['python'])
new_data[key] = val
renderer = Renderer(partials=partials)
actual = renderer.render(template, new_data)
actual = actual.encode('utf-8')
message = """%s
Template: \"""%s\"""
Expected: %s
Actual: %s
Expected: \"""%s\"""
Actual: \"""%s\"""
""" % (description, template, repr(expected), repr(actual), expected, actual)
self.assertEquals(actual, expected, message)
# The name must begin with "test" for nosetests test discovery to work.
name = 'test: "%s"' % test_name
# 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)
return test
for spec_path in spec_paths:
file_name = os.path.basename(spec_path)
# We avoid use of the with keyword for Python 2.4 support.
f = open(spec_path, 'r')
try:
spec_data = json.load(f)
finally:
f.close()
tests = spec_data['tests']
for test in tests:
test = buildTest(test, file_name)
setattr(MustacheSpec, test.__name__, test)
# Prevent this variable from being interpreted as another test.
del(test)
if __name__ == '__main__':
unittest.main()
# A tox configuration file to test across multiple Python versions.
#
# http://pypi.python.org/pypi/tox
#
[tox]
envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32
[testenv]
# Change the working directory so that we don't import the pystache located
# in the original location.
changedir =
{envbindir}
commands =
pystache-test {toxinidir}/ext/spec/specs {toxinidir}
# Check that the spec tests work with PyYAML.
[testenv:py27-yaml]
basepython =
python2.7
deps =
PyYAML
changedir =
{envbindir}
commands =
pystache-test {toxinidir}/ext/spec/specs {toxinidir}
# Check that pystache-test works from an install with no arguments.
[testenv:py27-noargs]
basepython =
python2.7
changedir =
{envbindir}
commands =
pystache-test
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