Commit 5436759a by Chris Jerdonek

Merge branch 'add-test-command' into 'development':

Tox now works with Python 2.4 through 3.2 and includes doctests and
spec tests in all environments.
parents eacbdb3b 09a5c9c2
*.pyc *.pyc
.tox .tox
*.temp2to3.rst
.DS_Store
# TODO: are comments allowed in .gitignore files?
# TextMate project file
*.tmproj
# Distribution-related folders and files.
build build
MANIFEST
dist dist
MANIFEST
.DS_Store pystache.egg-info
\ No newline at end of file
...@@ -4,9 +4,10 @@ History ...@@ -4,9 +4,10 @@ History
0.5.1 (TBD) 0.5.1 (TBD)
----------- -----------
* Added support for Python 3.2 (following conversion with 2to3_). * Added support for Python 3.1 and 3.2.
* Test runner now supports both yaml and json forms of Mustache spec.
* Added tox support to test multiple Python versions. * Added tox support to test multiple Python versions.
* Test harness now supports both YAML and JSON forms of Mustache spec.
* Test harness no longer requires nose.
0.5.0 (2012-04-03) 0.5.0 (2012-04-03)
------------------ ------------------
......
include LICENSE include LICENSE
include HISTORY.rst README.rst include HISTORY.rst
include README.rst
include tox.ini include tox.ini
# You cannot use package_data, for example, to include data files in a
# source distribution when using Distribute.
recursive-include pystache/tests *.mustache *.txt
...@@ -37,6 +37,10 @@ Python's json_ module was added in Python 2.6. Moreover, we require an ...@@ -37,6 +37,10 @@ Python's json_ module was added in Python 2.6. Moreover, we require an
earlier version of simplejson for Python 2.4 since simplejson stopped earlier version of simplejson for Python 2.4 since simplejson stopped
officially supporting Python 2.4 with version 2.1.0. officially supporting Python 2.4 with version 2.1.0.
An earlier version of simplejson can be installed manually, as follows: ::
pip install 'simplejson<2.1.0'
Install It Install It
========== ==========
...@@ -66,7 +70,7 @@ Here's your view class (in examples/readme.py):: ...@@ -66,7 +70,7 @@ Here's your view class (in examples/readme.py)::
Like so:: Like so::
>>> from examples.readme import SayHello >>> from pystache.tests.examples.readme import SayHello
>>> hello = SayHello() >>> hello = SayHello()
Then your template, say_hello.mustache:: Then your template, say_hello.mustache::
...@@ -134,43 +138,33 @@ default to values set in Pystache's ``defaults`` module. ...@@ -134,43 +138,33 @@ default to values set in Pystache's ``defaults`` module.
Test It Test It
======= =======
Use tox_ to test Pystache with multiple versions of Python all at once! :: From an install-- ::
pystache-test
From a source distribution-- ::
python test_pystache.py
To test Pystache source under multiple versions of Python all at once, you
can use tox_: ::
pip install tox pip install tox
tox tox
If you do not have all Python versions listed in ``tox.in``, then If you do not have all Python versions listed in ``tox.ini``, then
tox -e py26,py27 # for example tox -e py26,py27 # for example
To include tests from the Mustache spec in your test runs: :: The source distribution tests also include doctests and tests from the
Mustache spec. To include tests from the Mustache spec in your test runs: ::
git submodule init git submodule init
git submodule update git submodule update
You can also test Pystache without tox (but with only a single version of To test a source distribution of Pystache with Python 3.x, you must use tox.
Python at a time), as below. To do this, install Distribute_ :: This is because the raw source is not Python 3 compatible and must be first
be run through 2to3_.
pip install distribute
Python 2.7 and Later
--------------------
Then run Distribute's test_: ::
python setup.py test
This runs 2to3_ when using Python 3.
Python 2.6 and Earlier
----------------------
For Python 2.6 and earlier, use nose_ instead of ``test``: ::
pip install nose
python setup.py nosetests
Mailing List Mailing List
......
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.
\ No newline at end of file
"""
TODO: add a docstring.
"""
# We keep all initialization code in a separate module. # We keep all initialization code in a separate module.
# TODO: consider doing something like this instead:
# from pystache.init import __version__, render, Renderer, TemplateSpec
from pystache.init import * from pystache.init import *
# TODO: make sure that "from pystache import *" exposes only the following:
# ['__version__', 'render', 'Renderer', 'TemplateSpec']
# and add a unit test for this.
...@@ -13,7 +13,16 @@ try: ...@@ -13,7 +13,16 @@ try:
except: except:
# The json module is new in Python 2.6, whereas simplejson is # The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions. # compatible with earlier versions.
try:
import simplejson as json 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. # The optparse module is deprecated in Python 2.7 in favor of argparse.
# However, argparse is not available in Python 2.6 and earlier. # However, argparse is not available in Python 2.6 and earlier.
...@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage): ...@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage):
return template, context 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) template, context = parse_args(sys_argv, USAGE)
if template.endswith('.mustache'): if template.endswith('.mustache'):
...@@ -77,4 +91,4 @@ def main(sys_argv): ...@@ -77,4 +91,4 @@ def main(sys_argv):
if __name__=='__main__': 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()
...@@ -13,6 +13,10 @@ _BUILTIN_MODULE = type(0).__module__ ...@@ -13,6 +13,10 @@ _BUILTIN_MODULE = type(0).__module__
# not being found on lookup. This lets us distinguish between the case # 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 -- # 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. # 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): class NotFound(object):
pass pass
_NOT_FOUND = NotFound() _NOT_FOUND = NotFound()
......
...@@ -7,10 +7,9 @@ Provides test-related code that can be used by all tests. ...@@ -7,10 +7,9 @@ Provides test-related code that can be used by all tests.
import os import os
import examples
import pystache import pystache
from pystache import defaults from pystache import defaults
from pystache.tests import examples
# Save a reference to the original function to avoid recursion. # Save a reference to the original function to avoid recursion.
_DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE _DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE
...@@ -18,9 +17,14 @@ _TESTS_DIR = os.path.dirname(pystache.tests.__file__) ...@@ -18,9 +17,14 @@ _TESTS_DIR = os.path.dirname(pystache.tests.__file__)
DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'. DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'.
EXAMPLES_DIR = os.path.dirname(examples.__file__) EXAMPLES_DIR = os.path.dirname(examples.__file__)
SOURCE_DIR = os.path.dirname(pystache.__file__) PACKAGE_DIR = os.path.dirname(pystache.__file__)
PROJECT_DIR = os.path.join(SOURCE_DIR, '..') PROJECT_DIR = os.path.join(PACKAGE_DIR, '..')
SPEC_TEST_DIR = os.path.join(PROJECT_DIR, 'ext', 'spec', 'specs') 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): def html_escape(u):
...@@ -43,6 +47,80 @@ def get_data_path(file_name): ...@@ -43,6 +47,80 @@ def get_data_path(file_name):
return os.path.join(DATA_DIR, 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: class AssertStringMixin:
"""A unittest.TestCase mixin to check string equality.""" """A unittest.TestCase mixin to check string equality."""
......
# coding: utf-8 # coding: utf-8
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class SayHello(object): 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): class Comments(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
class Complex(object): class Complex(object):
def header(self): def header(self):
......
"""
TODO: add a docstring.
"""
class Delimiters(object): class Delimiters(object):
def first(self): def first(self):
......
"""
TODO: add a docstring.
"""
class DoubleSection(object): class DoubleSection(object):
def t(self): def t(self):
......
"""
TODO: add a docstring.
"""
class Escaped(object): class Escaped(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class Inverted(object): class Inverted(object):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
def rot(s, n=13): def rot(s, n=13):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class NestedContext(TemplateSpec): class NestedContext(TemplateSpec):
......
from examples.lambdas import rot
"""
TODO: add a docstring.
"""
from pystache.tests.examples.lambdas import rot
class PartialsWithLambdas(object): class PartialsWithLambdas(object):
......
"""
TODO: add a docstring.
"""
class SayHello(object): class SayHello(object):
def to(self): def to(self):
return "Pizza" return "Pizza"
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class Simple(TemplateSpec): class Simple(TemplateSpec):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class TemplatePartial(TemplateSpec): class TemplatePartial(TemplateSpec):
......
"""
TODO: add a docstring.
"""
class Unescaped(object): class Unescaped(object):
def title(self): def title(self):
......
"""
TODO: add a docstring.
"""
from pystache import TemplateSpec from pystache import TemplateSpec
class UnicodeInput(TemplateSpec): class UnicodeInput(TemplateSpec):
......
# encoding: utf-8 # encoding: utf-8
"""
TODO: add a docstring.
"""
class UnicodeOutput(object): class UnicodeOutput(object):
def name(self): 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
from unittest import TestProgram
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
# TODO: enhance this function to create spec-test tests.
def run_tests(sys_argv):
"""
Run all tests in the project.
Arguments:
sys_argv: a reference to sys.argv.
"""
try:
# TODO: use optparse command options instead.
project_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
project_dir = PROJECT_DIR
try:
# TODO: use optparse command options instead.
spec_test_dir = sys_argv[1]
sys_argv.pop(1)
except IndexError:
spec_test_dir = SPEC_TEST_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)
_PystacheTestProgram._text_doctest_dir = project_dir
_PystacheTestProgram._spec_test_dir = spec_test_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
# 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
doctest_suites = get_doctests(self._text_doctest_dir)
tests.addTests(doctest_suites)
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.
"""
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 _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("%", "%%")
subs = [repr(test_name), description, os.path.abspath(file_path),
template, repr(context), parser.__version__, str(parser)]
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 version %s of %s)
""" % subs
self.assertString(actual, expected, format=message)
# coding: utf-8
"""
Creates unittest.TestSuite instances for the doctests in the project.
"""
# This module follows the guidance documented here:
#
# http://docs.python.org/library/doctest.html#unittest-api
#
import os
import doctest
import pkgutil
import traceback
import unittest
import pystache
from pystache.tests.common import PROJECT_DIR, SOURCE_DIR
# The paths to text files (i.e. non-module files) containing doctests.
# Paths should be OS-specific and relative to the project directory.
text_file_paths = ['README.rst']
# The following load_tests() function implements unittests's load_tests
# protocol added in Python 2.7:
#
# http://docs.python.org/library/unittest.html#load-tests-protocol
#
# Using this protocol lets us include the doctests in test runs without
# using nose, for example when using Distribute's test as in the following:
#
# python setup.py test
#
# Normally, nosetests would interpret this function as a test case (because
# its name matches the test regular expression) and call it with zero arguments
# as opposed to the required three. However, we are able to exclude it with
# an entry like the following in setup.cfg:
#
# exclude=load_tests
#
# TODO: find a substitute for the load_tests protocol for Python versions
# before version 2.7.
#
def load_tests(loader, tests, ignore):
# Since module_relative is False in our calls to DocFileSuite below,
# paths should be OS-specific. Moreover, we choose absolute paths
# so that the current working directory does not come into play.
# See the following for more info--
#
# http://docs.python.org/library/doctest.html#doctest.DocFileSuite
#
paths = [os.path.join(PROJECT_DIR, path) for path in text_file_paths]
for path in paths:
suite = doctest.DocFileSuite(path, module_relative=False)
tests.addTests(suite)
modules = _get_module_doctests()
for module in modules:
suite = doctest.DocTestSuite(module)
tests.addTests(suite)
return tests
def _get_module_doctests():
modules = []
for pkg in pkgutil.walk_packages([SOURCE_DIR]):
# The importer is a pkgutil.ImpImporter instance:
#
# http://docs.python.org/library/pkgutil.html#pkgutil.ImpImporter
#
importer, module_name, is_package = pkg
if is_package:
# Otherwise, we will get the following error when adding tests:
#
# ValueError: (<module 'tests' from '.../pystache/tests/__init__.pyc'>, 'has no tests')
#
continue
# The loader is a pkgutil.ImpLoader instance.
loader = importer.find_module(module_name)
try:
module = loader.load_module(module_name)
except ImportError, e:
# In some situations, the test harness was swallowing and/or
# suppressing the display of the stack trace when errors
# occurred here. The following code makes errors occurring here
# easier to troubleshoot.
details = "".join(traceback.format_exception(*sys.exc_info()))
raise ImportError(details)
modules.append(module)
return modules
# encoding: utf-8 # encoding: utf-8
"""
TODO: add a docstring.
"""
import unittest import unittest
from examples.comments import Comments from examples.comments import Comments
......
...@@ -14,7 +14,7 @@ import unittest ...@@ -14,7 +14,7 @@ import unittest
from pystache.loader import Loader as Reader from pystache.loader import Loader as Reader
from pystache.locator import Locator from pystache.locator import Locator
from pystache.tests.common import DATA_DIR from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from pystache.tests.data.views import SayHello from pystache.tests.data.views import SayHello
...@@ -34,13 +34,20 @@ class LocatorTests(unittest.TestCase): ...@@ -34,13 +34,20 @@ class LocatorTests(unittest.TestCase):
locator = Locator(extension=False) locator = Locator(extension=False)
self.assertTrue(locator.template_extension is 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): def test_get_object_directory(self):
locator = Locator() locator = Locator()
obj = SayHello() obj = SayHello()
actual = locator.get_object_directory(obj) actual = locator.get_object_directory(obj)
self.assertEqual(actual, os.path.abspath(DATA_DIR)) self._assert_paths(actual, DATA_DIR)
def test_get_object_directory__not_hasattr_module(self): def test_get_object_directory__not_hasattr_module(self):
locator = Locator() locator = Locator()
...@@ -71,13 +78,13 @@ class LocatorTests(unittest.TestCase): ...@@ -71,13 +78,13 @@ class LocatorTests(unittest.TestCase):
def test_find_name(self): def test_find_name(self):
locator = Locator() locator = Locator()
path = locator.find_name(search_dirs=['examples'], template_name='simple') path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
self.assertEqual(os.path.basename(path), 'simple.mustache') self.assertEqual(os.path.basename(path), 'simple.mustache')
def test_find_name__using_list_of_paths(self): def test_find_name__using_list_of_paths(self):
locator = Locator() 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) self.assertTrue(path)
...@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase): ...@@ -111,9 +118,9 @@ class LocatorTests(unittest.TestCase):
obj = SayHello() obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') 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.assertEqual(actual, expected) self._assert_paths(actual, expected)
def test_find_object__none_file_name(self): def test_find_object__none_file_name(self):
locator = Locator() locator = Locator()
...@@ -121,7 +128,7 @@ class LocatorTests(unittest.TestCase): ...@@ -121,7 +128,7 @@ class LocatorTests(unittest.TestCase):
obj = SayHello() obj = SayHello()
actual = locator.find_object(search_dirs=[], obj=obj) 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.assertEqual(actual, expected) self.assertEqual(actual, expected)
......
# coding: utf-8
"""
Creates a unittest.TestCase for the tests defined in the mustache spec.
"""
# TODO: this module can be cleaned up somewhat.
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.
import simplejson as json
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, SPEC_TEST_DIR
def parse(u):
"""
Parse
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)
# This test case lets us alert the user that spec tests are missing.
class CheckSpecTestsFound(unittest.TestCase):
def test_spec_tests_found(self):
if len(spec_paths) > 0:
return
raise Exception("Spec tests not found in: %s\n "
"Consult the README file on how to add the Mustache spec tests." % repr(SPEC_TEST_DIR))
# TODO: give this a name better than MustacheSpec.
class MustacheSpec(unittest.TestCase, AssertStringMixin):
pass
def buildTest(testData, spec_filename, parser):
"""
Arguments:
parser: the module used for parsing (e.g. yaml or json).
"""
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 {}
# PyYAML seems to leave ASCII strings as byte strings.
expected = unicode(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)
# 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("%", "%%")
subs = [description, template, parser.__version__, str(parser)]
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
Template: \"""%s\"""
%%s
(using version %s of %s)
""" % subs
self.assertString(actual, expected, format=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
spec_paths = glob.glob(os.path.join(SPEC_TEST_DIR, '*.%s' % file_extension))
for path in spec_paths:
file_name = os.path.basename(path)
b = common.read(path)
u = unicode(b, encoding=FILE_ENCODING)
spec_data = parse(u)
tests = spec_data['tests']
for test in tests:
test = buildTest(test, file_name, parser)
setattr(MustacheSpec, test.__name__, test)
# Prevent this variable from being interpreted as another test.
del(test)
if __name__ == '__main__':
unittest.main()
...@@ -19,10 +19,8 @@ from pystache import TemplateSpec ...@@ -19,10 +19,8 @@ from pystache import TemplateSpec
from pystache.locator import Locator from pystache.locator import Locator
from pystache.loader import Loader from pystache.loader import Loader
from pystache.specloader import SpecLoader from pystache.specloader import SpecLoader
from pystache.tests.common import DATA_DIR from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
from pystache.tests.common import EXAMPLES_DIR from pystache.tests.common import AssertIsMixin, AssertStringMixin
from pystache.tests.common import AssertIsMixin
from pystache.tests.common import AssertStringMixin
from pystache.tests.data.views import SampleView from pystache.tests.data.views import SampleView
from pystache.tests.data.views import NonAscii from pystache.tests.data.views import NonAscii
...@@ -47,7 +45,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): ...@@ -47,7 +45,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
self.assertRaises(IOError, renderer.render, view) self.assertRaises(IOError, renderer.render, view)
# TODO: change this test to remove the following brittle line. # TODO: change this test to remove the following brittle line.
view.template_rel_directory = "../../examples" view.template_rel_directory = "examples"
actual = renderer.render(view) actual = renderer.render(view)
self.assertEqual(actual, "No tags...") self.assertEqual(actual, "No tags...")
...@@ -358,6 +356,13 @@ class TemplateSpecTests(unittest.TestCase): ...@@ -358,6 +356,13 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = 'txt' view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.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): def test_find__with_directory(self):
""" """
Test _find() with a view that has a directory specified. Test _find() with a view that has a directory specified.
...@@ -370,9 +375,9 @@ class TemplateSpecTests(unittest.TestCase): ...@@ -370,9 +375,9 @@ class TemplateSpecTests(unittest.TestCase):
self.assertTrue(loader._find_relative(view)[0] is not None) self.assertTrue(loader._find_relative(view)[0] is not None)
actual = loader._find(view) actual = loader._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'foo/bar.txt')) expected = os.path.join(DATA_DIR, 'foo/bar.txt')
self.assertEqual(actual, expected) self._assert_paths(actual, expected)
def test_find__without_directory(self): def test_find__without_directory(self):
""" """
...@@ -385,9 +390,9 @@ class TemplateSpecTests(unittest.TestCase): ...@@ -385,9 +390,9 @@ class TemplateSpecTests(unittest.TestCase):
self.assertTrue(loader._find_relative(view)[0] is None) self.assertTrue(loader._find_relative(view)[0] is None)
actual = loader._find(view) actual = loader._find(view)
expected = os.path.abspath(os.path.join(DATA_DIR, 'sample_view.mustache')) expected = os.path.join(DATA_DIR, 'sample_view.mustache')
self.assertEqual(actual, expected) self._assert_paths(actual, expected)
def _assert_get_template(self, custom, expected): def _assert_get_template(self, custom, expected):
loader = self._make_loader() loader = self._make_loader()
......
...@@ -156,13 +156,16 @@ else: ...@@ -156,13 +156,16 @@ else:
INSTALL_REQUIRES = requires 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 = [ PACKAGES = [
'pystache', 'pystache',
'pystache.commands',
# The following packages are only for testing. # The following packages are only for testing.
'examples',
'pystache.tests', 'pystache.tests',
'pystache.tests.data', 'pystache.tests.data',
'pystache.tests.data.locator' 'pystache.tests.data.locator',
'pystache.tests.examples',
] ]
...@@ -180,21 +183,17 @@ def main(sys_argv): ...@@ -180,21 +183,17 @@ def main(sys_argv):
install_requires=INSTALL_REQUIRES, install_requires=INSTALL_REQUIRES,
packages=PACKAGES, packages=PACKAGES,
package_data = { package_data = {
# Include the README so doctests can be run.
# TODO: is there a better way to include the README?
'pystache': [
'../README.rst',
'../ext/spec/specs/*.json',
'../ext/spec/specs/*.yml',
],
# Include template files so tests can be run. # Include template files so tests can be run.
'examples': template_files,
'pystache.tests.data': template_files, 'pystache.tests.data': template_files,
'pystache.tests.data.locator': template_files, 'pystache.tests.data.locator': template_files,
'pystache.tests.examples': template_files,
}, },
test_suite='pystache.tests', test_suite='pystache.tests',
entry_points = { entry_points = {
'console_scripts': ['pystache=pystache.commands.render:main'], 'console_scripts': [
'pystache=pystache.commands.render:main',
'pystache-test=pystache.commands.test:main',
],
}, },
classifiers = CLASSIFIERS, classifiers = CLASSIFIERS,
**extra **extra
......
#!/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
"""
from pystache.commands.test import main
if __name__=='__main__':
main()
...@@ -6,27 +6,9 @@ ...@@ -6,27 +6,9 @@
envlist = py24,py25,py26,py27,py31,py32 envlist = py24,py25,py26,py27,py31,py32
[testenv] [testenv]
# Change the working directory so that we don't import the pystache located
# in the original location.
changedir =
{envbindir}
commands = commands =
python setup.py --quiet test pystache-test {toxinidir} {toxinidir}/ext/spec/specs
# We use nosetests for older versions of Python to find doctests because
# the load_tests protocol (which we use for finding doctests when using
# Distribute's `test`) was not introduced until Python 2.7.
[testenv:py26]
deps =
nose
commands =
python setup.py --quiet nosetests
[testenv:py25]
deps =
nose
commands =
python setup.py --quiet nosetests
[testenv:py24]
deps =
nose
commands =
python setup.py --quiet nosetests
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