Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pystache_custom
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
pystache_custom
Commits
8bc8baf3
Commit
8bc8baf3
authored
May 03, 2012
by
Chris Jerdonek
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'development' into 'master': staging v0.5.2-rc
parents
cc262abf
3c839348
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
702 additions
and
221 deletions
+702
-221
HISTORY.rst
+9
-0
README.rst
+41
-32
TODO.md
+0
-1
ext/spec
+1
-1
pystache/__init__.py
+1
-1
pystache/commands/render.py
+2
-1
pystache/commands/test.py
+1
-1
pystache/common.py
+11
-1
pystache/context.py
+109
-63
pystache/locator.py
+3
-3
pystache/parsed.py
+4
-1
pystache/parser.py
+57
-17
pystache/renderengine.py
+42
-22
pystache/renderer.py
+6
-6
pystache/tests/common.py
+39
-0
pystache/tests/examples/simple.py
+1
-1
pystache/tests/main.py
+3
-1
pystache/tests/spectesting.py
+33
-8
pystache/tests/test_context.py
+110
-37
pystache/tests/test_locator.py
+5
-3
pystache/tests/test_parser.py
+26
-0
pystache/tests/test_renderengine.py
+149
-3
pystache/tests/test_renderer.py
+21
-8
pystache/tests/test_specloader.py
+4
-2
setup.py
+24
-8
No files found.
HISTORY.rst
View file @
8bc8baf3
History
History
=======
=======
0.5.2 (2012-05-03)
------------------
* Added support for dot notation and version 1.1.2 of the spec (issue #99). [rbp]
* Missing partials now render as empty string per latest version of spec (issue #115).
* Bugfix: falsey values now coerced to strings using str().
* Bugfix: lambda return values for sections no longer pushed onto context stack (issue #113).
* Bugfix: lists of lambdas for sections were not rendered (issue #114).
0.5.1 (2012-04-24)
0.5.1 (2012-04-24)
------------------
------------------
...
...
README.rst
View file @
8bc8baf3
...
@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's
...
@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's
behavior, see the official `Mustache spec`_.
behavior, see the official `Mustache spec`_.
Pystache is `semantically versioned`_ and can be found on PyPI_. This
Pystache is `semantically versioned`_ and can be found on PyPI_. This
version of Pystache passes all tests in `version 1.
0.3
`_ of the spec.
version of Pystache passes all tests in `version 1.
1.2
`_ of the spec.
Logo: `David Phillips`_
Logo: `David Phillips`_
...
@@ -23,7 +23,7 @@ Logo: `David Phillips`_
...
@@ -23,7 +23,7 @@ Logo: `David Phillips`_
Requirements
Requirements
============
============
Pystache is tested with
the following versions of Python:
Pystache is tested with
--
* Python 2.4 (requires simplejson `version 2.0.9`_ or earlier)
* Python 2.4 (requires simplejson `version 2.0.9`_ or earlier)
* Python 2.5 (requires simplejson_)
* Python 2.5 (requires simplejson_)
...
@@ -49,6 +49,9 @@ Install It
...
@@ -49,6 +49,9 @@ Install It
::
::
pip install pystache
pip install pystache
pystache-test
To install and test from source (e.g. from GitHub), see the Develop section.
Use It
Use It
...
@@ -74,7 +77,8 @@ Like so::
...
@@ -74,7 +77,8 @@ Like so::
>>> from pystache.tests.examples.readme import SayHello
>>> from pystache.tests.examples.readme import SayHello
>>> hello = SayHello()
>>> hello = SayHello()
Then your template, say_hello.mustache (by default in the same directory)::
Then your template, say_hello.mustache (in the same directory by default
as your class definition)::
Hello, {{to}}!
Hello, {{to}}!
...
@@ -95,9 +99,8 @@ more information.
...
@@ -95,9 +99,8 @@ more information.
Python 3
Python 3
========
========
As of version 0.5.1, Pystache fully supports Python 3. There are slight
Pystache has supported Python 3 since version 0.5.1. Pystache behaves
differences in behavior between Pystache running under Python 2 and 3,
slightly differently between Python 2 and 3, as follows:
as follows:
* In Python 2, the default html-escape function ``cgi.escape()`` does not
* In Python 2, the default html-escape function ``cgi.escape()`` does not
escape single quotes; whereas in Python 3, the default escape function
escape single quotes; whereas in Python 3, the default escape function
...
@@ -109,14 +112,13 @@ as follows:
...
@@ -109,14 +112,13 @@ as follows:
defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class).
defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class).
Unicode
Handling
Unicode
=======
=========
=======
This section describes Pystache's handling of unicode (e.g. strings and
This section describes how Pystache handles unicode, strings, and encodings.
encodings).
Internally, Pystache uses `only unicode strings`_ (
type
``str`` in Python 3 and
Internally, Pystache uses `only unicode strings`_ (``str`` in Python 3 and
type
``unicode`` in Python 2). For input, Pystache accepts both unicode strings
``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,
and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output,
Pystache's template rendering methods return only unicode.
Pystache's template rendering methods return only unicode.
...
@@ -143,26 +145,22 @@ attribute can be controlled on a per-view basis by subclassing the
...
@@ -143,26 +145,22 @@ attribute can be controlled on a per-view basis by subclassing the
default to values set in Pystache's ``defaults`` module.
default to values set in Pystache's ``defaults`` module.
Test It
Develop
=======
=======
From an install-- ::
To test from a source distribution (without installing)-- ::
pystache-test
From a source distribution-- ::
python test_pystache.py
python test_pystache.py
To test Pystache
source under multiple versions of Python all at once, you
To test Pystache
with multiple versions of Python (with a single command!),
can use tox_: ::
you
can use tox_: ::
pip install tox
pip install tox
tox
tox
If you do not have all Python versions listed in ``tox.ini``-- ::
If you do not have all Python versions listed in ``tox.ini``-- ::
tox -e py26,py
27
# for example
tox -e py26,py
32
# for example
The source distribution tests also include doctests and tests from the
The source distribution tests also include doctests and tests from the
Mustache spec. To include tests from the Mustache spec in your test runs: ::
Mustache spec. To include tests from the Mustache spec in your test runs: ::
...
@@ -175,23 +173,32 @@ is present. Otherwise, it parses the json files. To install PyYAML-- ::
...
@@ -175,23 +173,32 @@ is present. Otherwise, it parses the json files. To install PyYAML-- ::
pip install pyyaml
pip install pyyaml
To test Pystache from a source distribution with Python 3.x, you must use tox.
To run a subset of the tests, you can use nose_: ::
This is because the source code must first be run through 2to3_.
pip install nose
nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present
Mailing List
**Running Pystache from source with Python 3.** Pystache is written in
============
Python 2 and must be converted with 2to3_ prior to running under Python 3.
The installation process (and tox) do this conversion automatically.
To ``import pystache`` from a source distribution while using Python 3,
be sure that you are importing from a directory containing a converted
version (e.g. from your site-packages directory after manually installing)
and not from the original source directory. Otherwise, you will get a
syntax error. You can help ensure this by not running the Python IDE
from the project directory when importing Pystache.
As of November 2011, there's a mailing list, pystache@librelist.com.
Archive: http://librelist.com/browser/pystache/
Mailing List
============
Note: There's a bit of a delay in seeing the latest emails appear
There is a `mailing list`_. Note that there is a bit of a delay between
in the
archive.
posting a message and seeing it appear in the mailing list
archive.
Author
Author
s
======
======
=
::
::
...
@@ -208,9 +215,11 @@ Author
...
@@ -208,9 +215,11 @@ Author
.. _Distribute: http://pypi.python.org/pypi/distribute
.. _Distribute: http://pypi.python.org/pypi/distribute
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _json: http://docs.python.org/library/json.html
.. _json: http://docs.python.org/library/json.html
.. _mailing list: http://librelist.com/browser/pystache/
.. _Mustache: http://mustache.github.com/
.. _Mustache: http://mustache.github.com/
.. _Mustache spec: https://github.com/mustache/spec
.. _Mustache spec: https://github.com/mustache/spec
.. _mustache(5): http://mustache.github.com/mustache.5.html
.. _mustache(5): http://mustache.github.com/mustache.5.html
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _only unicode strings: http://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs
.. _PyPI: http://pypi.python.org/pypi/pystache
.. _PyPI: http://pypi.python.org/pypi/pystache
.. _Pystache: https://github.com/defunkt/pystache
.. _Pystache: https://github.com/defunkt/pystache
...
@@ -221,5 +230,5 @@ Author
...
@@ -221,5 +230,5 @@ Author
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _test: http://packages.python.org/distribute/setuptools.html#test
.. _test: http://packages.python.org/distribute/setuptools.html#test
.. _tox: http://pypi.python.org/pypi/tox
.. _tox: http://pypi.python.org/pypi/tox
.. _version 1.
0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7
.. _version 1.
1.2: https://github.com/mustache/spec/tree/v1.1.2
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
TODO.md
View file @
8bc8baf3
...
@@ -4,6 +4,5 @@ TODO
...
@@ -4,6 +4,5 @@ TODO
*
Turn the benchmarking script at pystache/tests/benchmark.py into a command in pystache/commands, or
*
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).
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.
*
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.
*
Make sure command parsing to pystache-test doesn't break with Python 2.4 and earlier.
*
Combine pystache-test with the main command.
*
Combine pystache-test with the main command.
spec
@
9b1bc7ad
Subproject commit
48c933b0bb780875acbfd15816297e263c53d6f7
Subproject commit
9b1bc7ad19247e9671304af02078f2ce30132665
pystache/__init__.py
View file @
8bc8baf3
...
@@ -10,4 +10,4 @@ from pystache.init import render, Renderer, TemplateSpec
...
@@ -10,4 +10,4 @@ from pystache.init import render, Renderer, TemplateSpec
__all__
=
[
'render'
,
'Renderer'
,
'TemplateSpec'
]
__all__
=
[
'render'
,
'Renderer'
,
'TemplateSpec'
]
__version__
=
'0.5.
1
'
# Also change in setup.py.
__version__
=
'0.5.
2-rc
'
# Also change in setup.py.
pystache/commands/render.py
View file @
8bc8baf3
...
@@ -35,6 +35,7 @@ import sys
...
@@ -35,6 +35,7 @@ import sys
#
#
# ValueError: Attempted relative import in non-package
# ValueError: Attempted relative import in non-package
#
#
from
pystache.common
import
TemplateNotFoundError
from
pystache.renderer
import
Renderer
from
pystache.renderer
import
Renderer
...
@@ -78,7 +79,7 @@ def main(sys_argv=sys.argv):
...
@@ -78,7 +79,7 @@ def main(sys_argv=sys.argv):
try
:
try
:
template
=
renderer
.
load_template
(
template
)
template
=
renderer
.
load_template
(
template
)
except
IO
Error
:
except
TemplateNotFound
Error
:
pass
pass
try
:
try
:
...
...
pystache/commands/test.py
View file @
8bc8baf3
...
@@ -7,7 +7,7 @@ This module provides a command to test pystache (unit tests, doctests, etc).
...
@@ -7,7 +7,7 @@ This module provides a command to test pystache (unit tests, doctests, etc).
import
sys
import
sys
from
pystache.tests.main
import
run_tests
from
pystache.tests.main
import
main
as
run_tests
def
main
(
sys_argv
=
sys
.
argv
):
def
main
(
sys_argv
=
sys
.
argv
):
...
...
pystache/common.py
View file @
8bc8baf3
# coding: utf-8
# coding: utf-8
"""
"""
Exposes
common functions
.
Exposes
functionality needed throughout the project
.
"""
"""
...
@@ -24,3 +24,13 @@ def read(path):
...
@@ -24,3 +24,13 @@ def read(path):
return
f
.
read
()
return
f
.
read
()
finally
:
finally
:
f
.
close
()
f
.
close
()
class
PystacheError
(
Exception
):
"""Base class for Pystache exceptions."""
pass
class
TemplateNotFoundError
(
PystacheError
):
"""An exception raised when a template is not found."""
pass
pystache/context.py
View file @
8bc8baf3
# coding: utf-8
# coding: utf-8
"""
"""
Defines a Context class to represent mustache(5)'s notion of context.
Exposes a ContextStack class.
The Mustache spec makes a special distinction between two types of context
stack elements: hashes and objects. For the purposes of interpreting the
spec, we define these categories mutually exclusively as follows:
(1) Hash: an item whose type is a subclass of dict.
(2) Object: an item that is neither a hash nor an instance of a
built-in type.
"""
"""
...
@@ -22,28 +31,23 @@ class NotFound(object):
...
@@ -22,28 +31,23 @@ class NotFound(object):
_NOT_FOUND
=
NotFound
()
_NOT_FOUND
=
NotFound
()
# TODO: share code with template.check_callable().
def
_get_value
(
context
,
key
):
def
_is_callable
(
obj
):
return
hasattr
(
obj
,
'__call__'
)
def
_get_value
(
item
,
key
):
"""
"""
Retrieve a key's value from a
n
item.
Retrieve a key's value from a
context
item.
Returns _NOT_FOUND if the key does not exist.
Returns _NOT_FOUND if the key does not exist.
The Context.get() docstring documents this function's intended behavior.
The Context
Stack
.get() docstring documents this function's intended behavior.
"""
"""
if
isinstance
(
item
,
dict
):
if
isinstance
(
context
,
dict
):
# Then we consider the argument a "hash" for the purposes of the spec.
# Then we consider the argument a "hash" for the purposes of the spec.
#
#
# We do a membership test to avoid using exceptions for flow control
# We do a membership test to avoid using exceptions for flow control
# (e.g. catching KeyError).
# (e.g. catching KeyError).
if
key
in
item
:
if
key
in
context
:
return
item
[
key
]
return
context
[
key
]
elif
type
(
item
)
.
__module__
!=
_BUILTIN_MODULE
:
elif
type
(
context
)
.
__module__
!=
_BUILTIN_MODULE
:
# Then we consider the argument an "object" for the purposes of
# Then we consider the argument an "object" for the purposes of
# the spec.
# the spec.
#
#
...
@@ -51,16 +55,18 @@ def _get_value(item, key):
...
@@ -51,16 +55,18 @@ def _get_value(item, key):
# types like integers and strings as objects (cf. issue #81).
# types like integers and strings as objects (cf. issue #81).
# Instances of user-defined classes on the other hand, for example,
# Instances of user-defined classes on the other hand, for example,
# are considered objects by the test above.
# are considered objects by the test above.
if
hasattr
(
item
,
key
):
if
hasattr
(
context
,
key
):
attr
=
getattr
(
item
,
key
)
attr
=
getattr
(
context
,
key
)
if
_is_callable
(
attr
):
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if
callable
(
attr
):
return
attr
()
return
attr
()
return
attr
return
attr
return
_NOT_FOUND
return
_NOT_FOUND
class
Context
(
object
):
class
Context
Stack
(
object
):
"""
"""
Provides dictionary-like access to a stack of zero or more items.
Provides dictionary-like access to a stack of zero or more items.
...
@@ -75,7 +81,7 @@ class Context(object):
...
@@ -75,7 +81,7 @@ class Context(object):
(last in, first out).
(last in, first out).
Caution: this class does not currently support recursive nesting in
Caution: this class does not currently support recursive nesting in
that items in the stack cannot themselves be Context instances.
that items in the stack cannot themselves be Context
Stack
instances.
See the docstrings of the methods of this class for more details.
See the docstrings of the methods of this class for more details.
...
@@ -92,7 +98,7 @@ class Context(object):
...
@@ -92,7 +98,7 @@ class Context(object):
stack in order so that, in particular, items at the end of
stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack.
the argument list are queried first when querying the stack.
Caution: items should not themselves be Context instances, as
Caution: items should not themselves be Context
Stack
instances, as
recursive nesting does not behave as one might expect.
recursive nesting does not behave as one might expect.
"""
"""
...
@@ -104,9 +110,9 @@ class Context(object):
...
@@ -104,9 +110,9 @@ class Context(object):
For example--
For example--
>>> context = Context({'alpha': 'abc'}, {'numeric': 123})
>>> context = Context
Stack
({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context)
>>> repr(context)
"Context({'alpha': 'abc'}, {'numeric': 123})"
"Context
Stack
({'alpha': 'abc'}, {'numeric': 123})"
"""
"""
return
"
%
s
%
s"
%
(
self
.
__class__
.
__name__
,
tuple
(
self
.
_stack
))
return
"
%
s
%
s"
%
(
self
.
__class__
.
__name__
,
tuple
(
self
.
_stack
))
...
@@ -114,18 +120,18 @@ class Context(object):
...
@@ -114,18 +120,18 @@ class Context(object):
@staticmethod
@staticmethod
def
create
(
*
context
,
**
kwargs
):
def
create
(
*
context
,
**
kwargs
):
"""
"""
Build a Context instance from a sequence of context-like items.
Build a Context
Stack
instance from a sequence of context-like items.
This factory-style method is more general than the Context class's
This factory-style method is more general than the Context
Stack
class's
constructor in that, unlike the constructor, the argument list
constructor in that, unlike the constructor, the argument list
can itself contain Context instances.
can itself contain Context
Stack
instances.
Here is an example illustrating various aspects of this method:
Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
>>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
>>> obj2 = Context
Stack
({'vegetable': 'spinach', 'mineral': 'silver'})
>>>
>>>
>>> context = Context.create(obj1, None, obj2, mineral='gold')
>>> context = Context
Stack
.create(obj1, None, obj2, mineral='gold')
>>>
>>>
>>> context.get('animal')
>>> context.get('animal')
'cat'
'cat'
...
@@ -136,7 +142,7 @@ class Context(object):
...
@@ -136,7 +142,7 @@ class Context(object):
Arguments:
Arguments:
*context: zero or more dictionaries, Context instances, or objects
*context: zero or more dictionaries, Context
Stack
instances, or objects
with which to populate the initial context stack. None
with which to populate the initial context stack. None
arguments will be skipped. Items in the *context list are
arguments will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument
added to the stack in order so that later items in the argument
...
@@ -152,12 +158,12 @@ class Context(object):
...
@@ -152,12 +158,12 @@ class Context(object):
"""
"""
items
=
context
items
=
context
context
=
Context
()
context
=
Context
Stack
()
for
item
in
items
:
for
item
in
items
:
if
item
is
None
:
if
item
is
None
:
continue
continue
if
isinstance
(
item
,
Context
):
if
isinstance
(
item
,
Context
Stack
):
context
.
_stack
.
extend
(
item
.
_stack
)
context
.
_stack
.
extend
(
item
.
_stack
)
else
:
else
:
context
.
push
(
item
)
context
.
push
(
item
)
...
@@ -167,9 +173,22 @@ class Context(object):
...
@@ -167,9 +173,22 @@ class Context(object):
return
context
return
context
def
get
(
self
,
key
,
default
=
None
):
# TODO: add more unit tests for this.
# TODO: update the docstring for dotted names.
def
get
(
self
,
name
,
default
=
u''
):
"""
"""
Query the stack for the given key, and return the resulting value.
Resolve a dotted name against the current context stack.
This function follows the rules outlined in the section of the
spec regarding tag interpolation. This function returns the value
as is and does not coerce the return value to a string.
Arguments:
name: a dotted or non-dotted name.
default: the value to return if name resolution fails at any point.
Defaults to the empty string per the Mustache spec.
This method queries items in the stack in order from last-added
This method queries items in the stack in order from last-added
objects to first (last in, first out). The value returned is
objects to first (last in, first out). The value returned is
...
@@ -177,30 +196,21 @@ class Context(object):
...
@@ -177,30 +196,21 @@ class Context(object):
If the key is not found in any item in the stack, then the default
If the key is not found in any item in the stack, then the default
value is returned. The default value defaults to None.
value is returned. The default value defaults to None.
When speaking about returning values from a context, the Mustache
spec distinguishes between two types of context stack elements:
hashes and objects.
In accordance with the spec, this method queries items in the
In accordance with the spec, this method queries items in the
stack for a key in the following way. For the purposes of querying,
stack for a key differently depending on whether the item is a
each item is classified into one of the following three mutually
hash, object, or neither (as defined in the module docstring):
exclusive categories: a hash, an object, or neither:
(1) Hash: if the item is a hash, then the key's value is the
(1) Hash: if the item's type is a subclass of dict, then the item
dictionary value of the key. If the dictionary doesn't contain
is considered a hash (in the terminology of the spec), and
the key, then the key is considered not found.
the key's value is the dictionary value of the key. If the
dictionary doesn't contain the key, the key is not found.
(2) Object: if the item is an an object, then the method looks for
an attribute with the same name as the key. If an attribute
(2) Object: if the item isn't a hash and isn't an instance of a
with that name exists, the value of the attribute is returned.
built-in type, then the item is considered an object (again
If the attribute is callable, however (i.e. if the attribute
using the language of the spec). In this case, the method
is a method), then the attribute is called with no arguments
looks for an attribute with the same name as the key. If an
and that value is returned. If there is no attribute with
attribute with that name exists, the value of the attribute is
the same name as the key, then the key is considered not found.
returned. If the attribute is callable, however (i.e. if the
attribute is a method), then the attribute is called with no
arguments and instead that value returned. If there is no
attribute with the same name as the key, then the key is
considered not found.
(3) Neither: if the item is neither a hash nor an object, then
(3) Neither: if the item is neither a hash nor an object, then
the key is considered not found.
the key is considered not found.
...
@@ -226,23 +236,59 @@ class Context(object):
...
@@ -226,23 +236,59 @@ class Context(object):
>>>
>>>
>>> dct['greet'] is obj.greet
>>> dct['greet'] is obj.greet
True
True
>>> Context(dct).get('greet') #doctest: +ELLIPSIS
>>> Context
Stack
(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
<function greet at 0x...>
>>> Context(obj).get('greet')
>>> Context
Stack
(obj).get('greet')
'Hi Bob!'
'Hi Bob!'
TODO: explain the rationale for this difference in treatment.
TODO: explain the rationale for this difference in treatment.
"""
"""
for
obj
in
reversed
(
self
.
_stack
):
if
name
==
'.'
:
val
=
_get_value
(
obj
,
key
)
# TODO: should we add a test case for an empty context stack?
if
val
is
_NOT_FOUND
:
return
self
.
top
()
parts
=
name
.
split
(
'.'
)
result
=
self
.
_get_simple
(
parts
[
0
])
for
part
in
parts
[
1
:]:
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if
result
is
_NOT_FOUND
:
break
# The full context stack is not used to resolve the remaining parts.
# From the spec--
#
# 5) If any name parts were retained in step 1, each should be
# resolved against a context stack containing only the result
# from the former resolution. If any part fails resolution, the
# result should be considered falsey, and should interpolate as
# the empty string.
#
# TODO: make sure we have a test case for the above point.
result
=
_get_value
(
result
,
part
)
if
result
is
_NOT_FOUND
:
return
default
return
result
def
_get_simple
(
self
,
name
):
"""
Query the stack for a non-dotted name.
"""
result
=
_NOT_FOUND
for
item
in
reversed
(
self
.
_stack
):
result
=
_get_value
(
item
,
name
)
if
result
is
_NOT_FOUND
:
continue
continue
# Otherwise, the key was found.
# Otherwise, the key was found.
return
val
break
# Otherwise, no item in the stack contained the key.
return
defa
ult
return
res
ult
def
push
(
self
,
item
):
def
push
(
self
,
item
):
"""
"""
...
@@ -270,4 +316,4 @@ class Context(object):
...
@@ -270,4 +316,4 @@ class Context(object):
Return a copy of this instance.
Return a copy of this instance.
"""
"""
return
Context
(
*
self
.
_stack
)
return
Context
Stack
(
*
self
.
_stack
)
pystache/locator.py
View file @
8bc8baf3
...
@@ -9,6 +9,7 @@ import os
...
@@ -9,6 +9,7 @@ import os
import
re
import
re
import
sys
import
sys
from
pystache.common
import
TemplateNotFoundError
from
pystache
import
defaults
from
pystache
import
defaults
...
@@ -117,9 +118,8 @@ class Locator(object):
...
@@ -117,9 +118,8 @@ class Locator(object):
path
=
self
.
_find_path
(
search_dirs
,
file_name
)
path
=
self
.
_find_path
(
search_dirs
,
file_name
)
if
path
is
None
:
if
path
is
None
:
# TODO: we should probably raise an exception of our own type.
raise
TemplateNotFoundError
(
'File
%
s not found in dirs:
%
s'
%
raise
IOError
(
'Template file
%
s not found in directories:
%
s'
%
(
repr
(
file_name
),
repr
(
search_dirs
)))
(
repr
(
file_name
),
repr
(
search_dirs
)))
return
path
return
path
...
...
pystache/parsed.py
View file @
8bc8baf3
...
@@ -17,7 +17,7 @@ class ParsedTemplate(object):
...
@@ -17,7 +17,7 @@ class ParsedTemplate(object):
parse_tree: a list, each element of which is either--
parse_tree: a list, each element of which is either--
(1) a unicode string, or
(1) a unicode string, or
(2) a "rendering" callable that accepts a Context instance
(2) a "rendering" callable that accepts a Context
Stack
instance
and returns a unicode string.
and returns a unicode string.
The possible rendering callables are the return values of the
The possible rendering callables are the return values of the
...
@@ -32,6 +32,9 @@ class ParsedTemplate(object):
...
@@ -32,6 +32,9 @@ class ParsedTemplate(object):
"""
"""
self
.
_parse_tree
=
parse_tree
self
.
_parse_tree
=
parse_tree
def
__repr__
(
self
):
return
"[
%
s]"
%
(
", "
.
join
([
repr
(
part
)
for
part
in
self
.
_parse_tree
]))
def
render
(
self
,
context
):
def
render
(
self
,
context
):
"""
"""
Returns: a string of type unicode.
Returns: a string of type unicode.
...
...
pystache/parser.py
View file @
8bc8baf3
...
@@ -9,15 +9,22 @@ This module is only meant for internal use by the renderengine module.
...
@@ -9,15 +9,22 @@ This module is only meant for internal use by the renderengine module.
import
re
import
re
from
pystache.common
import
TemplateNotFoundError
from
pystache.parsed
import
ParsedTemplate
from
pystache.parsed
import
ParsedTemplate
DEFAULT_DELIMITERS
=
(
'{{'
,
'}}'
)
DEFAULT_DELIMITERS
=
(
u'{{'
,
u
'}}'
)
END_OF_LINE_CHARACTERS
=
[
'
\r
'
,
'
\n
'
]
END_OF_LINE_CHARACTERS
=
[
u'
\r
'
,
u
'
\n
'
]
NON_BLANK_RE
=
re
.
compile
(
r'^(.)'
,
re
.
M
)
NON_BLANK_RE
=
re
.
compile
(
u
r'^(.)'
,
re
.
M
)
def
_compile_template_re
(
delimiters
):
def
_compile_template_re
(
delimiters
=
None
):
"""
Return a regular expresssion object (re.RegexObject) instance.
"""
if
delimiters
is
None
:
delimiters
=
DEFAULT_DELIMITERS
# The possible tag type characters following the opening tag,
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
# excluding "=" and "{".
...
@@ -74,19 +81,25 @@ class Parser(object):
...
@@ -74,19 +81,25 @@ class Parser(object):
self
.
_delimiters
=
delimiters
self
.
_delimiters
=
delimiters
self
.
compile_template_re
()
self
.
compile_template_re
()
def
parse
(
self
,
template
,
index
=
0
,
section_key
=
None
):
def
parse
(
self
,
template
,
start_
index
=
0
,
section_key
=
None
):
"""
"""
Parse a template string
into a ParsedTemplate instance
.
Parse a template string
starting at some index
.
This method uses the current tag delimiter.
This method uses the current tag delimiter.
Arguments:
Arguments:
template: a template string of type unicode.
template: a unicode string that is the template to parse.
index: the index at which to start parsing.
Returns:
a ParsedTemplate instance.
"""
"""
parse_tree
=
[]
parse_tree
=
[]
start_index
=
index
index
=
start_
index
while
True
:
while
True
:
match
=
self
.
_template_re
.
search
(
template
,
index
)
match
=
self
.
_template_re
.
search
(
template
,
index
)
...
@@ -133,7 +146,7 @@ class Parser(object):
...
@@ -133,7 +146,7 @@ class Parser(object):
if
tag_key
!=
section_key
:
if
tag_key
!=
section_key
:
raise
ParsingError
(
"Section end tag mismatch:
%
s !=
%
s"
%
(
tag_key
,
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
return
ParsedTemplate
(
parse_tree
),
match_index
,
end_index
index
=
self
.
_handle_tag_type
(
template
,
parse_tree
,
tag_type
,
tag_key
,
leading_whitespace
,
end_index
)
index
=
self
.
_handle_tag_type
(
template
,
parse_tree
,
tag_type
,
tag_key
,
leading_whitespace
,
end_index
)
...
@@ -142,10 +155,33 @@ class Parser(object):
...
@@ -142,10 +155,33 @@ class Parser(object):
return
ParsedTemplate
(
parse_tree
)
return
ParsedTemplate
(
parse_tree
)
def
_parse_section
(
self
,
template
,
index_start
,
section_key
):
def
_parse_section
(
self
,
template
,
start_index
,
section_key
):
parsed_template
,
template
,
index_end
=
self
.
parse
(
template
=
template
,
index
=
index_start
,
section_key
=
section_key
)
"""
Parse the contents of a template section.
Arguments:
template: a unicode template string.
start_index: the string index at which the section contents begin.
section_key: the tag key of the section.
Returns: a 3-tuple:
parsed_section: the section contents parsed as a ParsedTemplate
instance.
content_end_index: the string index after the section contents.
end_index: the string index after the closing section tag (and
including any trailing newlines).
"""
parsed_section
,
content_end_index
,
end_index
=
\
self
.
parse
(
template
=
template
,
start_index
=
start_index
,
section_key
=
section_key
)
return
parsed_
template
,
template
,
index_end
return
parsed_
section
,
template
[
start_index
:
content_end_index
],
end_index
def
_handle_tag_type
(
self
,
template
,
parse_tree
,
tag_type
,
tag_key
,
leading_whitespace
,
end_index
):
def
_handle_tag_type
(
self
,
template
,
parse_tree
,
tag_type
,
tag_key
,
leading_whitespace
,
end_index
):
...
@@ -170,20 +206,24 @@ class Parser(object):
...
@@ -170,20 +206,24 @@ class Parser(object):
elif
tag_type
==
'#'
:
elif
tag_type
==
'#'
:
parsed_section
,
template
,
end_index
=
self
.
_parse_section
(
template
,
end_index
,
tag_key
)
parsed_section
,
section_contents
,
end_index
=
self
.
_parse_section
(
template
,
end_index
,
tag_key
)
func
=
engine
.
_make_get_section
(
tag_key
,
parsed_section
,
template
,
self
.
_delimiters
)
func
=
engine
.
_make_get_section
(
tag_key
,
parsed_section
,
section_contents
,
self
.
_delimiters
)
elif
tag_type
==
'^'
:
elif
tag_type
==
'^'
:
parsed_section
,
template
,
end_index
=
self
.
_parse_section
(
template
,
end_index
,
tag_key
)
parsed_section
,
section_contents
,
end_index
=
self
.
_parse_section
(
template
,
end_index
,
tag_key
)
func
=
engine
.
_make_get_inverse
(
tag_key
,
parsed_section
)
func
=
engine
.
_make_get_inverse
(
tag_key
,
parsed_section
)
elif
tag_type
==
'>'
:
elif
tag_type
==
'>'
:
template
=
engine
.
load_partial
(
tag_key
)
try
:
# TODO: make engine.load() and test it separately.
template
=
engine
.
load_partial
(
tag_key
)
except
TemplateNotFoundError
:
template
=
u''
# Indent before rendering.
# Indent before rendering.
template
=
re
.
sub
(
NON_BLANK_RE
,
leading_whitespace
+
r'\1'
,
template
)
template
=
re
.
sub
(
NON_BLANK_RE
,
leading_whitespace
+
u
r'\1'
,
template
)
func
=
engine
.
_make_get_partial
(
template
)
func
=
engine
.
_make_get_partial
(
template
)
...
...
pystache/renderengine.py
View file @
8bc8baf3
...
@@ -35,7 +35,8 @@ class RenderEngine(object):
...
@@ -35,7 +35,8 @@ class RenderEngine(object):
load_partial: the function to call when loading a partial. The
load_partial: the function to call when loading a partial. The
function should accept a string template name and return a
function should accept a string template name and return a
template string of type unicode (not a subclass).
template string of type unicode (not a subclass). If the
template is not found, it should raise a TemplateNotFoundError.
literal: the function used to convert unescaped variable tag
literal: the function used to convert unescaped variable tag
values to unicode, e.g. the value corresponding to a tag
values to unicode, e.g. the value corresponding to a tag
...
@@ -63,6 +64,7 @@ class RenderEngine(object):
...
@@ -63,6 +64,7 @@ class RenderEngine(object):
self
.
literal
=
literal
self
.
literal
=
literal
self
.
load_partial
=
load_partial
self
.
load_partial
=
load_partial
# TODO: rename context to stack throughout this module.
def
_get_string_value
(
self
,
context
,
tag_name
):
def
_get_string_value
(
self
,
context
,
tag_name
):
"""
"""
Get a value from the given context as a basestring instance.
Get a value from the given context as a basestring instance.
...
@@ -70,15 +72,6 @@ class RenderEngine(object):
...
@@ -70,15 +72,6 @@ class RenderEngine(object):
"""
"""
val
=
context
.
get
(
tag_name
)
val
=
context
.
get
(
tag_name
)
# We use "==" rather than "is" to compare integers, as using "is"
# relies on an implementation detail of CPython. The test about
# rendering zeroes failed while using PyPy when using "is".
# See issue #34: https://github.com/defunkt/pystache/issues/34
if
not
val
and
val
!=
0
:
if
tag_name
!=
'.'
:
return
''
val
=
context
.
top
()
if
callable
(
val
):
if
callable
(
val
):
# According to the spec:
# According to the spec:
#
#
...
@@ -132,6 +125,7 @@ class RenderEngine(object):
...
@@ -132,6 +125,7 @@ class RenderEngine(object):
Returns: a string of type unicode.
Returns: a string of type unicode.
"""
"""
# TODO: the parsing should be done before calling this function.
return
self
.
_render
(
template
,
context
)
return
self
.
_render
(
template
,
context
)
return
get_partial
return
get_partial
...
@@ -142,7 +136,10 @@ class RenderEngine(object):
...
@@ -142,7 +136,10 @@ class RenderEngine(object):
Returns a string with type unicode.
Returns a string with type unicode.
"""
"""
# TODO: is there a bug because we are not using the same
# logic as in _get_string_value()?
data
=
context
.
get
(
name
)
data
=
context
.
get
(
name
)
# Per the spec, lambdas in inverted sections are considered truthy.
if
data
:
if
data
:
return
u''
return
u''
return
parsed_template
.
render
(
context
)
return
parsed_template
.
render
(
context
)
...
@@ -161,16 +158,19 @@ class RenderEngine(object):
...
@@ -161,16 +158,19 @@ class RenderEngine(object):
template
=
template_
template
=
template_
parsed_template
=
parsed_template_
parsed_template
=
parsed_template_
data
=
context
.
get
(
name
)
data
=
context
.
get
(
name
)
# From the spec:
#
# If the data is not of a list type, it is coerced into a list
# as follows: if the data is truthy (e.g. `!!data == true`),
# use a single-element list containing the data, otherwise use
# an empty list.
#
if
not
data
:
if
not
data
:
data
=
[]
data
=
[]
elif
callable
(
data
):
# TODO: should we check the arity?
template
=
data
(
template
)
parsed_template
=
self
.
_parse
(
template
,
delimiters
=
delims
)
data
=
[
data
]
else
:
else
:
# The
cleanest, least brittle way of determining whether
# The
least brittle way to determine whether something
# s
omething s
upports iteration is by trying to call iter() on it:
# supports iteration is by trying to call iter() on it:
#
#
# http://docs.python.org/library/functions.html#iter
# http://docs.python.org/library/functions.html#iter
#
#
...
@@ -184,14 +184,34 @@ class RenderEngine(object):
...
@@ -184,14 +184,34 @@ class RenderEngine(object):
# Then the value does not support iteration.
# Then the value does not support iteration.
data
=
[
data
]
data
=
[
data
]
else
:
else
:
# We treat the value as a list (but do not treat strings
# and dicts as lists).
if
isinstance
(
data
,
(
basestring
,
dict
)):
if
isinstance
(
data
,
(
basestring
,
dict
)):
# Do not treat strings and dicts (which are iterable) as lists.
data
=
[
data
]
data
=
[
data
]
# Otherwise,
leave it alone
.
# Otherwise,
treat the value as a list
.
parts
=
[]
parts
=
[]
for
element
in
data
:
for
element
in
data
:
if
callable
(
element
):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
new_template
=
element
(
template
)
parsed_template
=
self
.
_parse
(
new_template
,
delimiters
=
delims
)
parts
.
append
(
parsed_template
.
render
(
context
))
continue
context
.
push
(
element
)
context
.
push
(
element
)
parts
.
append
(
parsed_template
.
render
(
context
))
parts
.
append
(
parsed_template
.
render
(
context
))
context
.
pop
()
context
.
pop
()
...
@@ -221,7 +241,7 @@ class RenderEngine(object):
...
@@ -221,7 +241,7 @@ class RenderEngine(object):
Arguments:
Arguments:
template: a template string of type unicode.
template: a template string of type unicode.
context: a Context instance.
context: a Context
Stack
instance.
"""
"""
# We keep this type-check as an added check because this method is
# We keep this type-check as an added check because this method is
...
@@ -244,7 +264,7 @@ class RenderEngine(object):
...
@@ -244,7 +264,7 @@ class RenderEngine(object):
template: a template string of type unicode (but not a proper
template: a template string of type unicode (but not a proper
subclass of unicode).
subclass of unicode).
context: a Context instance.
context: a Context
Stack
instance.
"""
"""
# Be strict but not too strict. In other words, accept str instead
# Be strict but not too strict. In other words, accept str instead
...
...
pystache/renderer.py
View file @
8bc8baf3
...
@@ -8,7 +8,8 @@ This module provides a Renderer class to render templates.
...
@@ -8,7 +8,8 @@ This module provides a Renderer class to render templates.
import
sys
import
sys
from
pystache
import
defaults
from
pystache
import
defaults
from
pystache.context
import
Context
from
pystache.common
import
TemplateNotFoundError
from
pystache.context
import
ContextStack
from
pystache.loader
import
Loader
from
pystache.loader
import
Loader
from
pystache.renderengine
import
RenderEngine
from
pystache.renderengine
import
RenderEngine
from
pystache.specloader
import
SpecLoader
from
pystache.specloader
import
SpecLoader
...
@@ -239,9 +240,8 @@ class Renderer(object):
...
@@ -239,9 +240,8 @@ class Renderer(object):
template
=
partials
.
get
(
name
)
template
=
partials
.
get
(
name
)
if
template
is
None
:
if
template
is
None
:
# TODO: make a TemplateNotFoundException type that provides
raise
TemplateNotFoundError
(
"Name
%
s not found in partials:
%
s"
%
# the original partials as an attribute.
(
repr
(
name
),
type
(
partials
)))
raise
Exception
(
"Partial not found with name:
%
s"
%
repr
(
name
))
# RenderEngine requires that the return value be unicode.
# RenderEngine requires that the return value be unicode.
return
self
.
_to_unicode_hard
(
template
)
return
self
.
_to_unicode_hard
(
template
)
...
@@ -277,7 +277,7 @@ class Renderer(object):
...
@@ -277,7 +277,7 @@ class Renderer(object):
# RenderEngine.render() requires that the template string be unicode.
# RenderEngine.render() requires that the template string be unicode.
template
=
self
.
_to_unicode_hard
(
template
)
template
=
self
.
_to_unicode_hard
(
template
)
context
=
Context
.
create
(
*
context
,
**
kwargs
)
context
=
Context
Stack
.
create
(
*
context
,
**
kwargs
)
self
.
_context
=
context
self
.
_context
=
context
engine
=
self
.
_make_render_engine
()
engine
=
self
.
_make_render_engine
()
...
@@ -338,7 +338,7 @@ class Renderer(object):
...
@@ -338,7 +338,7 @@ class Renderer(object):
uses the passed object as the first element of the context stack
uses the passed object as the first element of the context stack
when rendering.
when rendering.
*context: zero or more dictionaries, Context instances, or objects
*context: zero or more dictionaries, Context
Stack
instances, or objects
with which to populate the initial context stack. None
with which to populate the initial context stack. None
arguments are skipped. Items in the *context list are added to
arguments are skipped. Items in the *context list are added to
the context stack in order so that later items in the argument
the context stack in order so that later items in the argument
...
...
pystache/tests/common.py
View file @
8bc8baf3
...
@@ -168,6 +168,20 @@ class AssertIsMixin:
...
@@ -168,6 +168,20 @@ class AssertIsMixin:
self
.
assertTrue
(
first
is
second
,
msg
=
"
%
s is not
%
s"
%
(
repr
(
first
),
repr
(
second
)))
self
.
assertTrue
(
first
is
second
,
msg
=
"
%
s is not
%
s"
%
(
repr
(
first
),
repr
(
second
)))
class
AssertExceptionMixin
:
"""A unittest.TestCase mixin adding assertException()."""
# unittest.assertRaisesRegexp() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertRaisesRegexp
def
assertException
(
self
,
exception_type
,
msg
,
callable
,
*
args
,
**
kwds
):
try
:
callable
(
*
args
,
**
kwds
)
raise
Exception
(
"Expected exception:
%
s:
%
s"
%
(
exception_type
,
repr
(
msg
)))
except
exception_type
,
err
:
self
.
assertEqual
(
str
(
err
),
msg
)
class
SetupDefaults
(
object
):
class
SetupDefaults
(
object
):
"""
"""
...
@@ -191,3 +205,28 @@ class SetupDefaults(object):
...
@@ -191,3 +205,28 @@ class SetupDefaults(object):
defaults
.
FILE_ENCODING
=
self
.
original_file_encoding
defaults
.
FILE_ENCODING
=
self
.
original_file_encoding
defaults
.
STRING_ENCODING
=
self
.
original_string_encoding
defaults
.
STRING_ENCODING
=
self
.
original_string_encoding
class
Attachable
(
object
):
"""
A class that attaches all constructor named parameters as attributes.
For example--
>>> obj = Attachable(foo=42, size="of the universe")
>>> repr(obj)
"Attachable(foo=42, size='of the universe')"
>>> obj.foo
42
>>> obj.size
'of the universe'
"""
def
__init__
(
self
,
**
kwargs
):
self
.
__args__
=
kwargs
for
arg
,
value
in
kwargs
.
iteritems
():
setattr
(
self
,
arg
,
value
)
def
__repr__
(
self
):
return
"
%
s(
%
s)"
%
(
self
.
__class__
.
__name__
,
", "
.
join
(
"
%
s=
%
s"
%
(
k
,
repr
(
v
))
for
k
,
v
in
self
.
__args__
.
iteritems
()))
pystache/tests/examples/simple.py
View file @
8bc8baf3
...
@@ -12,4 +12,4 @@ class Simple(TemplateSpec):
...
@@ -12,4 +12,4 @@ class Simple(TemplateSpec):
return
"pizza"
return
"pizza"
def
blank
(
self
):
def
blank
(
self
):
pass
return
''
pystache/tests/main.py
View file @
8bc8baf3
...
@@ -24,7 +24,9 @@ from pystache.tests.spectesting import get_spec_tests
...
@@ -24,7 +24,9 @@ from pystache.tests.spectesting import get_spec_tests
FROM_SOURCE_OPTION
=
"--from-source"
FROM_SOURCE_OPTION
=
"--from-source"
def
run_tests
(
sys_argv
):
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def
main
(
sys_argv
):
"""
"""
Run all tests in the project.
Run all tests in the project.
...
...
pystache/tests/spectesting.py
View file @
8bc8baf3
...
@@ -115,6 +115,37 @@ def _read_spec_tests(path):
...
@@ -115,6 +115,37 @@ def _read_spec_tests(path):
return
cases
return
cases
# TODO: simplify the implementation of this function.
def
_convert_children
(
node
):
"""
Recursively convert to functions all "code strings" below the node.
This function is needed only for the json format.
"""
if
not
isinstance
(
node
,
(
list
,
dict
)):
# Then there is nothing to iterate over and recurse.
return
if
isinstance
(
node
,
list
):
for
child
in
node
:
_convert_children
(
child
)
return
# Otherwise, node is a dict, so attempt the conversion.
for
key
in
node
.
keys
():
val
=
node
[
key
]
if
not
isinstance
(
val
,
dict
)
or
val
.
get
(
'__tag__'
)
!=
'code'
:
_convert_children
(
val
)
continue
# Otherwise, we are at a "leaf" node.
val
=
eval
(
val
[
'python'
])
node
[
key
]
=
val
continue
def
_deserialize_spec_test
(
data
,
file_path
):
def
_deserialize_spec_test
(
data
,
file_path
):
"""
"""
Return a unittest.TestCase instance representing a spec test.
Return a unittest.TestCase instance representing a spec test.
...
@@ -124,7 +155,7 @@ def _deserialize_spec_test(data, file_path):
...
@@ -124,7 +155,7 @@ def _deserialize_spec_test(data, file_path):
data: the dictionary of attributes for a single test.
data: the dictionary of attributes for a single test.
"""
"""
unconverted_
context
=
data
[
'data'
]
context
=
data
[
'data'
]
description
=
data
[
'desc'
]
description
=
data
[
'desc'
]
# PyYAML seems to leave ASCII strings as byte strings.
# PyYAML seems to leave ASCII strings as byte strings.
expected
=
unicode
(
data
[
'expected'
])
expected
=
unicode
(
data
[
'expected'
])
...
@@ -133,13 +164,7 @@ def _deserialize_spec_test(data, file_path):
...
@@ -133,13 +164,7 @@ def _deserialize_spec_test(data, file_path):
template
=
data
[
'template'
]
template
=
data
[
'template'
]
test_name
=
data
[
'name'
]
test_name
=
data
[
'name'
]
# Convert code strings to functions.
_convert_children
(
context
)
# 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
)
test_case
=
_make_spec_test
(
expected
,
template
,
context
,
partials
,
description
,
test_name
,
file_path
)
...
...
pystache/tests/test_context.py
View file @
8bc8baf3
...
@@ -10,8 +10,8 @@ import unittest
...
@@ -10,8 +10,8 @@ import unittest
from
pystache.context
import
_NOT_FOUND
from
pystache.context
import
_NOT_FOUND
from
pystache.context
import
_get_value
from
pystache.context
import
_get_value
from
pystache.context
import
Context
from
pystache.context
import
Context
Stack
from
pystache.tests.common
import
AssertIsMixin
from
pystache.tests.common
import
AssertIsMixin
,
AssertStringMixin
,
Attachable
class
SimpleObject
(
object
):
class
SimpleObject
(
object
):
...
@@ -204,10 +204,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
...
@@ -204,10 +204,10 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self
.
assertNotFound
(
item2
,
'pop'
)
self
.
assertNotFound
(
item2
,
'pop'
)
class
Context
Tests
(
unittest
.
TestCase
,
AssertIs
Mixin
):
class
Context
StackTests
(
unittest
.
TestCase
,
AssertIsMixin
,
AssertString
Mixin
):
"""
"""
Test the Context class.
Test the Context
Stack
class.
"""
"""
...
@@ -216,34 +216,34 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -216,34 +216,34 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Check that passing nothing to __init__() raises no exception.
Check that passing nothing to __init__() raises no exception.
"""
"""
context
=
Context
()
context
=
Context
Stack
()
def
test_init__many_elements
(
self
):
def
test_init__many_elements
(
self
):
"""
"""
Check that passing more than two items to __init__() raises no exception.
Check that passing more than two items to __init__() raises no exception.
"""
"""
context
=
Context
({},
{},
{})
context
=
Context
Stack
({},
{},
{})
def
test__repr
(
self
):
def
test__repr
(
self
):
context
=
Context
()
context
=
Context
Stack
()
self
.
assertEqual
(
repr
(
context
),
'Context()'
)
self
.
assertEqual
(
repr
(
context
),
'Context
Stack
()'
)
context
=
Context
({
'foo'
:
'bar'
})
context
=
Context
Stack
({
'foo'
:
'bar'
})
self
.
assertEqual
(
repr
(
context
),
"Context({'foo': 'bar'},)"
)
self
.
assertEqual
(
repr
(
context
),
"Context
Stack
({'foo': 'bar'},)"
)
context
=
Context
({
'foo'
:
'bar'
},
{
'abc'
:
123
})
context
=
Context
Stack
({
'foo'
:
'bar'
},
{
'abc'
:
123
})
self
.
assertEqual
(
repr
(
context
),
"Context({'foo': 'bar'}, {'abc': 123})"
)
self
.
assertEqual
(
repr
(
context
),
"Context
Stack
({'foo': 'bar'}, {'abc': 123})"
)
def
test__str
(
self
):
def
test__str
(
self
):
context
=
Context
()
context
=
Context
Stack
()
self
.
assertEqual
(
str
(
context
),
'Context()'
)
self
.
assertEqual
(
str
(
context
),
'Context
Stack
()'
)
context
=
Context
({
'foo'
:
'bar'
})
context
=
Context
Stack
({
'foo'
:
'bar'
})
self
.
assertEqual
(
str
(
context
),
"Context({'foo': 'bar'},)"
)
self
.
assertEqual
(
str
(
context
),
"Context
Stack
({'foo': 'bar'},)"
)
context
=
Context
({
'foo'
:
'bar'
},
{
'abc'
:
123
})
context
=
Context
Stack
({
'foo'
:
'bar'
},
{
'abc'
:
123
})
self
.
assertEqual
(
str
(
context
),
"Context({'foo': 'bar'}, {'abc': 123})"
)
self
.
assertEqual
(
str
(
context
),
"Context
Stack
({'foo': 'bar'}, {'abc': 123})"
)
## Test the static create() method.
## Test the static create() method.
...
@@ -252,7 +252,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -252,7 +252,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a dictionary.
Test passing a dictionary.
"""
"""
context
=
Context
.
create
({
'foo'
:
'bar'
})
context
=
Context
Stack
.
create
({
'foo'
:
'bar'
})
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
def
test_create__none
(
self
):
def
test_create__none
(
self
):
...
@@ -260,7 +260,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -260,7 +260,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing None.
Test passing None.
"""
"""
context
=
Context
.
create
({
'foo'
:
'bar'
},
None
)
context
=
Context
Stack
.
create
({
'foo'
:
'bar'
},
None
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
def
test_create__object
(
self
):
def
test_create__object
(
self
):
...
@@ -270,16 +270,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -270,16 +270,16 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
"""
class
Foo
(
object
):
class
Foo
(
object
):
foo
=
'bar'
foo
=
'bar'
context
=
Context
.
create
(
Foo
())
context
=
Context
Stack
.
create
(
Foo
())
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
def
test_create__context
(
self
):
def
test_create__context
(
self
):
"""
"""
Test passing a Context instance.
Test passing a Context
Stack
instance.
"""
"""
obj
=
Context
({
'foo'
:
'bar'
})
obj
=
Context
Stack
({
'foo'
:
'bar'
})
context
=
Context
.
create
(
obj
)
context
=
Context
Stack
.
create
(
obj
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
def
test_create__kwarg
(
self
):
def
test_create__kwarg
(
self
):
...
@@ -287,7 +287,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -287,7 +287,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test passing a keyword argument.
Test passing a keyword argument.
"""
"""
context
=
Context
.
create
(
foo
=
'bar'
)
context
=
Context
Stack
.
create
(
foo
=
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'bar'
)
def
test_create__precedence_positional
(
self
):
def
test_create__precedence_positional
(
self
):
...
@@ -295,7 +295,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -295,7 +295,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test precedence of positional arguments.
Test precedence of positional arguments.
"""
"""
context
=
Context
.
create
({
'foo'
:
'bar'
},
{
'foo'
:
'buzz'
})
context
=
Context
Stack
.
create
({
'foo'
:
'bar'
},
{
'foo'
:
'buzz'
})
self
.
assertEqual
(
context
.
get
(
'foo'
),
'buzz'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'buzz'
)
def
test_create__precedence_keyword
(
self
):
def
test_create__precedence_keyword
(
self
):
...
@@ -303,7 +303,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -303,7 +303,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test precedence of keyword arguments.
Test precedence of keyword arguments.
"""
"""
context
=
Context
.
create
({
'foo'
:
'bar'
},
foo
=
'buzz'
)
context
=
Context
Stack
.
create
({
'foo'
:
'bar'
},
foo
=
'buzz'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'buzz'
)
self
.
assertEqual
(
context
.
get
(
'foo'
),
'buzz'
)
def
test_get__key_present
(
self
):
def
test_get__key_present
(
self
):
...
@@ -311,7 +311,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -311,7 +311,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test getting a key.
Test getting a key.
"""
"""
context
=
Context
({
"foo"
:
"bar"
})
context
=
Context
Stack
({
"foo"
:
"bar"
})
self
.
assertEqual
(
context
.
get
(
"foo"
),
"bar"
)
self
.
assertEqual
(
context
.
get
(
"foo"
),
"bar"
)
def
test_get__key_missing
(
self
):
def
test_get__key_missing
(
self
):
...
@@ -319,15 +319,15 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -319,15 +319,15 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test getting a missing key.
Test getting a missing key.
"""
"""
context
=
Context
()
context
=
Context
Stack
()
self
.
assert
True
(
context
.
get
(
"foo"
)
is
None
)
self
.
assert
String
(
context
.
get
(
"foo"
),
u''
)
def
test_get__default
(
self
):
def
test_get__default
(
self
):
"""
"""
Test that get() respects the default value.
Test that get() respects the default value.
"""
"""
context
=
Context
()
context
=
Context
Stack
()
self
.
assertEqual
(
context
.
get
(
"foo"
,
"bar"
),
"bar"
)
self
.
assertEqual
(
context
.
get
(
"foo"
,
"bar"
),
"bar"
)
def
test_get__precedence
(
self
):
def
test_get__precedence
(
self
):
...
@@ -335,7 +335,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -335,7 +335,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Test that get() respects the order of precedence (later items first).
Test that get() respects the order of precedence (later items first).
"""
"""
context
=
Context
({
"foo"
:
"bar"
},
{
"foo"
:
"buzz"
})
context
=
Context
Stack
({
"foo"
:
"bar"
},
{
"foo"
:
"buzz"
})
self
.
assertEqual
(
context
.
get
(
"foo"
),
"buzz"
)
self
.
assertEqual
(
context
.
get
(
"foo"
),
"buzz"
)
def
test_get__fallback
(
self
):
def
test_get__fallback
(
self
):
...
@@ -343,7 +343,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -343,7 +343,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
Check that first-added stack items are queried on context misses.
Check that first-added stack items are queried on context misses.
"""
"""
context
=
Context
({
"fuzz"
:
"buzz"
},
{
"foo"
:
"bar"
})
context
=
Context
Stack
({
"fuzz"
:
"buzz"
},
{
"foo"
:
"bar"
})
self
.
assertEqual
(
context
.
get
(
"fuzz"
),
"buzz"
)
self
.
assertEqual
(
context
.
get
(
"fuzz"
),
"buzz"
)
def
test_push
(
self
):
def
test_push
(
self
):
...
@@ -352,7 +352,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -352,7 +352,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
"""
key
=
"foo"
key
=
"foo"
context
=
Context
({
key
:
"bar"
})
context
=
Context
Stack
({
key
:
"bar"
})
self
.
assertEqual
(
context
.
get
(
key
),
"bar"
)
self
.
assertEqual
(
context
.
get
(
key
),
"bar"
)
context
.
push
({
key
:
"buzz"
})
context
.
push
({
key
:
"buzz"
})
...
@@ -364,7 +364,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -364,7 +364,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
"""
"""
key
=
"foo"
key
=
"foo"
context
=
Context
({
key
:
"bar"
},
{
key
:
"buzz"
})
context
=
Context
Stack
({
key
:
"bar"
},
{
key
:
"buzz"
})
self
.
assertEqual
(
context
.
get
(
key
),
"buzz"
)
self
.
assertEqual
(
context
.
get
(
key
),
"buzz"
)
item
=
context
.
pop
()
item
=
context
.
pop
()
...
@@ -373,7 +373,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -373,7 +373,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
def
test_top
(
self
):
def
test_top
(
self
):
key
=
"foo"
key
=
"foo"
context
=
Context
({
key
:
"bar"
},
{
key
:
"buzz"
})
context
=
Context
Stack
({
key
:
"bar"
},
{
key
:
"buzz"
})
self
.
assertEqual
(
context
.
get
(
key
),
"buzz"
)
self
.
assertEqual
(
context
.
get
(
key
),
"buzz"
)
top
=
context
.
top
()
top
=
context
.
top
()
...
@@ -383,7 +383,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -383,7 +383,7 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
def
test_copy
(
self
):
def
test_copy
(
self
):
key
=
"foo"
key
=
"foo"
original
=
Context
({
key
:
"bar"
},
{
key
:
"buzz"
})
original
=
Context
Stack
({
key
:
"bar"
},
{
key
:
"buzz"
})
self
.
assertEqual
(
original
.
get
(
key
),
"buzz"
)
self
.
assertEqual
(
original
.
get
(
key
),
"buzz"
)
new
=
original
.
copy
()
new
=
original
.
copy
()
...
@@ -395,3 +395,76 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
...
@@ -395,3 +395,76 @@ class ContextTests(unittest.TestCase, AssertIsMixin):
# Confirm the original is unchanged.
# Confirm the original is unchanged.
self
.
assertEqual
(
original
.
get
(
key
),
"buzz"
)
self
.
assertEqual
(
original
.
get
(
key
),
"buzz"
)
def
test_dot_notation__dict
(
self
):
name
=
"foo.bar"
stack
=
ContextStack
({
"foo"
:
{
"bar"
:
"baz"
}})
self
.
assertEqual
(
stack
.
get
(
name
),
"baz"
)
# Works all the way down
name
=
"a.b.c.d.e.f.g"
stack
=
ContextStack
({
"a"
:
{
"b"
:
{
"c"
:
{
"d"
:
{
"e"
:
{
"f"
:
{
"g"
:
"w00t!"
}}}}}}})
self
.
assertEqual
(
stack
.
get
(
name
),
"w00t!"
)
def
test_dot_notation__user_object
(
self
):
name
=
"foo.bar"
stack
=
ContextStack
({
"foo"
:
Attachable
(
bar
=
"baz"
)})
self
.
assertEquals
(
stack
.
get
(
name
),
"baz"
)
# Works on multiple levels, too
name
=
"a.b.c.d.e.f.g"
A
=
Attachable
stack
=
ContextStack
({
"a"
:
A
(
b
=
A
(
c
=
A
(
d
=
A
(
e
=
A
(
f
=
A
(
g
=
"w00t!"
))))))})
self
.
assertEquals
(
stack
.
get
(
name
),
"w00t!"
)
def
test_dot_notation__mixed_dict_and_obj
(
self
):
name
=
"foo.bar.baz.bak"
stack
=
ContextStack
({
"foo"
:
Attachable
(
bar
=
{
"baz"
:
Attachable
(
bak
=
42
)})})
self
.
assertEquals
(
stack
.
get
(
name
),
42
)
def
test_dot_notation__missing_attr_or_key
(
self
):
name
=
"foo.bar.baz.bak"
stack
=
ContextStack
({
"foo"
:
{
"bar"
:
{}}})
self
.
assertString
(
stack
.
get
(
name
),
u''
)
stack
=
ContextStack
({
"foo"
:
Attachable
(
bar
=
Attachable
())})
self
.
assertString
(
stack
.
get
(
name
),
u''
)
def
test_dot_notation__missing_part_terminates_search
(
self
):
"""
Test that dotted name resolution terminates on a later part not found.
Check that if a later dotted name part is not found in the result from
the former resolution, then name resolution terminates rather than
starting the search over with the next element of the context stack.
From the spec (interpolation section)--
5) If any name parts were retained in step 1, each should be resolved
against a context stack containing only the result from the former
resolution. If any part fails resolution, the result should be considered
falsey, and should interpolate as the empty string.
This test case is equivalent to the test case in the following pull
request:
https://github.com/mustache/spec/pull/48
"""
stack
=
ContextStack
({
'a'
:
{
'b'
:
'A.B'
}},
{
'a'
:
'A'
})
self
.
assertEqual
(
stack
.
get
(
'a'
),
'A'
)
self
.
assertString
(
stack
.
get
(
'a.b'
),
u''
)
stack
.
pop
()
self
.
assertEqual
(
stack
.
get
(
'a.b'
),
'A.B'
)
def
test_dot_notation__autocall
(
self
):
name
=
"foo.bar.baz"
# When any element in the path is callable, it should be automatically invoked
stack
=
ContextStack
({
"foo"
:
Attachable
(
bar
=
Attachable
(
baz
=
lambda
:
"Called!"
))})
self
.
assertEquals
(
stack
.
get
(
name
),
"Called!"
)
class
Foo
(
object
):
def
bar
(
self
):
return
Attachable
(
baz
=
'Baz'
)
stack
=
ContextStack
({
"foo"
:
Foo
()})
self
.
assertEquals
(
stack
.
get
(
name
),
"Baz"
)
pystache/tests/test_locator.py
View file @
8bc8baf3
...
@@ -11,14 +11,15 @@ import sys
...
@@ -11,14 +11,15 @@ import sys
import
unittest
import
unittest
# TODO: remove this alias.
# TODO: remove this alias.
from
pystache.common
import
TemplateNotFoundError
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
,
EXAMPLES_DIR
from
pystache.tests.common
import
DATA_DIR
,
EXAMPLES_DIR
,
AssertExceptionMixin
from
pystache.tests.data.views
import
SayHello
from
pystache.tests.data.views
import
SayHello
class
LocatorTests
(
unittest
.
TestCase
):
class
LocatorTests
(
unittest
.
TestCase
,
AssertExceptionMixin
):
def
_locator
(
self
):
def
_locator
(
self
):
return
Locator
(
search_dirs
=
DATA_DIR
)
return
Locator
(
search_dirs
=
DATA_DIR
)
...
@@ -110,7 +111,8 @@ class LocatorTests(unittest.TestCase):
...
@@ -110,7 +111,8 @@ class LocatorTests(unittest.TestCase):
def
test_find_name__non_existent_template_fails
(
self
):
def
test_find_name__non_existent_template_fails
(
self
):
locator
=
Locator
()
locator
=
Locator
()
self
.
assertRaises
(
IOError
,
locator
.
find_name
,
search_dirs
=
[],
template_name
=
'doesnt_exist'
)
self
.
assertException
(
TemplateNotFoundError
,
"File 'doesnt_exist.mustache' not found in dirs: []"
,
locator
.
find_name
,
search_dirs
=
[],
template_name
=
'doesnt_exist'
)
def
test_find_object
(
self
):
def
test_find_object
(
self
):
locator
=
Locator
()
locator
=
Locator
()
...
...
pystache/tests/test_parser.py
0 → 100644
View file @
8bc8baf3
# coding: utf-8
"""
Unit tests of parser.py.
"""
import
unittest
from
pystache.parser
import
_compile_template_re
as
make_re
class
RegularExpressionTestCase
(
unittest
.
TestCase
):
"""Tests the regular expression returned by _compile_template_re()."""
def
test_re
(
self
):
"""
Test getting a key from a dictionary.
"""
re
=
make_re
()
match
=
re
.
search
(
"b {{test}}"
)
self
.
assertEqual
(
match
.
start
(),
1
)
pystache/tests/test_renderengine.py
View file @
8bc8baf3
...
@@ -7,11 +7,11 @@ Unit tests of renderengine.py.
...
@@ -7,11 +7,11 @@ Unit tests of renderengine.py.
import
unittest
import
unittest
from
pystache.context
import
Context
from
pystache.context
import
Context
Stack
from
pystache
import
defaults
from
pystache
import
defaults
from
pystache.parser
import
ParsingError
from
pystache.parser
import
ParsingError
from
pystache.renderengine
import
RenderEngine
from
pystache.renderengine
import
RenderEngine
from
pystache.tests.common
import
AssertStringMixin
from
pystache.tests.common
import
AssertStringMixin
,
Attachable
def
mock_literal
(
s
):
def
mock_literal
(
s
):
...
@@ -83,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
...
@@ -83,7 +83,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
if
partials
is
not
None
:
if
partials
is
not
None
:
engine
.
load_partial
=
lambda
key
:
unicode
(
partials
[
key
])
engine
.
load_partial
=
lambda
key
:
unicode
(
partials
[
key
])
context
=
Context
(
*
context
)
context
=
Context
Stack
(
*
context
)
actual
=
engine
.
render
(
template
,
context
)
actual
=
engine
.
render
(
template
,
context
)
...
@@ -204,6 +204,27 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
...
@@ -204,6 +204,27 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context
=
{
'test'
:
'{{#hello}}'
}
context
=
{
'test'
:
'{{#hello}}'
}
self
.
_assert_render
(
u'{{#hello}}'
,
template
,
context
)
self
.
_assert_render
(
u'{{#hello}}'
,
template
,
context
)
## Test interpolation with "falsey" values
#
# In these test cases, we test the part of the spec that says that
# "data should be coerced into a string (and escaped, if appropriate)
# before interpolation." We test this for data that is "falsey."
def
test_interpolation__falsey__zero
(
self
):
template
=
'{{.}}'
context
=
0
self
.
_assert_render
(
u'0'
,
template
,
context
)
def
test_interpolation__falsey__none
(
self
):
template
=
'{{.}}'
context
=
None
self
.
_assert_render
(
u'None'
,
template
,
context
)
def
test_interpolation__falsey__zero
(
self
):
template
=
'{{.}}'
context
=
False
self
.
_assert_render
(
u'False'
,
template
,
context
)
# Built-in types:
# Built-in types:
#
#
# Confirm that we not treat instances of built-in types as objects,
# Confirm that we not treat instances of built-in types as objects,
...
@@ -480,6 +501,56 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
...
@@ -480,6 +501,56 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context
=
{
'person'
:
'Mom'
,
'test'
:
(
lambda
text
:
text
+
" :)"
)}
context
=
{
'person'
:
'Mom'
,
'test'
:
(
lambda
text
:
text
+
" :)"
)}
self
.
_assert_render
(
u'Hi Mom :)'
,
template
,
context
)
self
.
_assert_render
(
u'Hi Mom :)'
,
template
,
context
)
def
test_section__lambda__list
(
self
):
"""
Check that lists of lambdas are processed correctly for sections.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
template
=
'<{{#lambdas}}foo{{/lambdas}}>'
context
=
{
'foo'
:
'bar'
,
'lambdas'
:
[
lambda
text
:
"~{{
%
s}}~"
%
text
,
lambda
text
:
"#{{
%
s}}#"
%
text
]}
self
.
_assert_render
(
u'<~bar~#bar#>'
,
template
,
context
)
def
test_section__lambda__not_on_context_stack
(
self
):
"""
Check that section lambdas are not pushed onto the context stack.
Even though the sections spec says that section data values should be
pushed onto the context stack prior to rendering, this does not apply
to lambdas. Lambdas obey their own special case.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
context
=
{
'foo'
:
'bar'
,
'lambda'
:
(
lambda
text
:
"{{.}}"
)}
template
=
'{{#foo}}{{#lambda}}blah{{/lambda}}{{/foo}}'
self
.
_assert_render
(
u'bar'
,
template
,
context
)
def
test_section__lambda__no_reinterpolation
(
self
):
"""
Check that section lambda return values are not re-interpolated.
This test is a sanity check that the rendered lambda return value
is not re-interpolated as could be construed by reading the
section part of the Mustache spec.
This test case is equivalent to a test submitted to the Mustache spec here:
https://github.com/mustache/spec/pull/47 .
"""
template
=
'{{#planet}}{{#lambda}}dot{{/lambda}}{{/planet}}'
context
=
{
'planet'
:
'Earth'
,
'dot'
:
'~{{.}}~'
,
'lambda'
:
(
lambda
text
:
"#{{
%
s}}#"
%
text
)}
self
.
_assert_render
(
u'#~{{.}}~#'
,
template
,
context
)
def
test_comment__multiline
(
self
):
def
test_comment__multiline
(
self
):
"""
"""
Check that multiline comments are permitted.
Check that multiline comments are permitted.
...
@@ -509,3 +580,78 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
...
@@ -509,3 +580,78 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
expected
=
u' {{foo}} '
expected
=
u' {{foo}} '
self
.
_assert_render
(
expected
,
'{{=$ $=}} {{foo}} '
)
self
.
_assert_render
(
expected
,
'{{=$ $=}} {{foo}} '
)
self
.
_assert_render
(
expected
,
'{{=$ $=}} {{foo}} $={{ }}=$'
)
# was yielding u' '.
self
.
_assert_render
(
expected
,
'{{=$ $=}} {{foo}} $={{ }}=$'
)
# was yielding u' '.
def
test_dot_notation
(
self
):
"""
Test simple dot notation cases.
Check that we can use dot notation when the variable is a dict,
user-defined object, or combination of both.
"""
template
=
'Hello, {{person.name}}. I see you are {{person.details.age}}.'
person
=
Attachable
(
name
=
'Biggles'
,
details
=
{
'age'
:
42
})
context
=
{
'person'
:
person
}
self
.
_assert_render
(
u'Hello, Biggles. I see you are 42.'
,
template
,
context
)
def
test_dot_notation__missing_attributes_or_keys
(
self
):
"""
Test dot notation with missing keys or attributes.
Check that if a key or attribute in a dotted name does not exist, then
the tag renders as the empty string.
"""
template
=
"""I cannot see {{person.name}}'s age: {{person.age}}.
Nor {{other_person.name}}'s: ."""
expected
=
u"""I cannot see Biggles's age: .
Nor Mr. Bradshaw's: ."""
context
=
{
'person'
:
{
'name'
:
'Biggles'
},
'other_person'
:
Attachable
(
name
=
'Mr. Bradshaw'
)}
self
.
_assert_render
(
expected
,
template
,
context
)
def
test_dot_notation__multiple_levels
(
self
):
"""
Test dot notation with multiple levels.
"""
template
=
"""Hello, Mr. {{person.name.lastname}}.
I see you're back from {{person.travels.last.country.city}}.
I'm missing some of your details: {{person.details.private.editor}}."""
expected
=
u"""Hello, Mr. Pither.
I see you're back from Cornwall.
I'm missing some of your details: ."""
context
=
{
'person'
:
{
'name'
:
{
'firstname'
:
'unknown'
,
'lastname'
:
'Pither'
},
'travels'
:
{
'last'
:
{
'country'
:
{
'city'
:
'Cornwall'
}}},
'details'
:
{
'public'
:
'likes cycling'
}}}
self
.
_assert_render
(
expected
,
template
,
context
)
# It should also work with user-defined objects
context
=
{
'person'
:
Attachable
(
name
=
{
'firstname'
:
'unknown'
,
'lastname'
:
'Pither'
},
travels
=
Attachable
(
last
=
Attachable
(
country
=
Attachable
(
city
=
'Cornwall'
))),
details
=
Attachable
())}
self
.
_assert_render
(
expected
,
template
,
context
)
def
test_dot_notation__missing_part_terminates_search
(
self
):
"""
Test that dotted name resolution terminates on a later part not found.
Check that if a later dotted name part is not found in the result from
the former resolution, then name resolution terminates rather than
starting the search over with the next element of the context stack.
From the spec (interpolation section)--
5) If any name parts were retained in step 1, each should be resolved
against a context stack containing only the result from the former
resolution. If any part fails resolution, the result should be considered
falsey, and should interpolate as the empty string.
This test case is equivalent to the test case in the following pull
request:
https://github.com/mustache/spec/pull/48
"""
template
=
'{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context
=
{
'a'
:
{
'b'
:
'A.B'
},
'c'
:
{
'a'
:
'A'
}
}
self
.
_assert_render
(
u'A.B :: (A :: )'
,
template
,
context
)
pystache/tests/test_renderer.py
View file @
8bc8baf3
...
@@ -13,9 +13,10 @@ import unittest
...
@@ -13,9 +13,10 @@ import unittest
from
examples.simple
import
Simple
from
examples.simple
import
Simple
from
pystache
import
Renderer
from
pystache
import
Renderer
from
pystache
import
TemplateSpec
from
pystache
import
TemplateSpec
from
pystache.common
import
TemplateNotFoundError
from
pystache.loader
import
Loader
from
pystache.loader
import
Loader
from
pystache.tests.common
import
get_data_path
,
AssertStringMixin
from
pystache.tests.common
import
get_data_path
,
AssertStringMixin
,
AssertExceptionMixin
from
pystache.tests.data.views
import
SayHello
from
pystache.tests.data.views
import
SayHello
...
@@ -405,7 +406,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
...
@@ -405,7 +406,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
# we no longer need to exercise all rendering code paths through
# we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the
# the Renderer. It suffices to test rendering paths through the
# RenderEngine for the same amount of code coverage.
# RenderEngine for the same amount of code coverage.
class
Renderer_MakeRenderEngineTests
(
unittest
.
TestCase
):
class
Renderer_MakeRenderEngineTests
(
unittest
.
TestCase
,
AssertExceptionMixin
):
"""
"""
Check the RenderEngine returned by Renderer._make_render_engine().
Check the RenderEngine returned by Renderer._make_render_engine().
...
@@ -444,7 +445,20 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
...
@@ -444,7 +445,20 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
self
.
assertEqual
(
actual
,
"abc"
)
self
.
assertEqual
(
actual
,
"abc"
)
self
.
assertEqual
(
type
(
actual
),
unicode
)
self
.
assertEqual
(
type
(
actual
),
unicode
)
def
test__load_partial__not_found
(
self
):
def
test__load_partial__not_found__default
(
self
):
"""
Check that load_partial provides a nice message when a template is not found.
"""
renderer
=
Renderer
()
engine
=
renderer
.
_make_render_engine
()
load_partial
=
engine
.
load_partial
self
.
assertException
(
TemplateNotFoundError
,
"File 'foo.mustache' not found in dirs: ['.']"
,
load_partial
,
"foo"
)
def
test__load_partial__not_found__dict
(
self
):
"""
"""
Check that load_partial provides a nice message when a template is not found.
Check that load_partial provides a nice message when a template is not found.
...
@@ -455,11 +469,10 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
...
@@ -455,11 +469,10 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase):
engine
=
renderer
.
_make_render_engine
()
engine
=
renderer
.
_make_render_engine
()
load_partial
=
engine
.
load_partial
load_partial
=
engine
.
load_partial
try
:
# Include dict directly since str(dict) is different in Python 2 and 3:
load_partial
(
"foo"
)
# <type 'dict'> versus <class 'dict'>, respectively.
raise
Exception
(
"Shouldn't get here"
)
self
.
assertException
(
TemplateNotFoundError
,
"Name 'foo' not found in partials:
%
s"
%
dict
,
except
Exception
,
err
:
load_partial
,
"foo"
)
self
.
assertEqual
(
str
(
err
),
"Partial not found with name: 'foo'"
)
## Test the engine's literal attribute.
## Test the engine's literal attribute.
...
...
pystache/tests/test_specloader.py
View file @
8bc8baf3
...
@@ -16,6 +16,7 @@ from examples.lambdas import Lambdas
...
@@ -16,6 +16,7 @@ from examples.lambdas import Lambdas
from
examples.inverted
import
Inverted
,
InvertedLists
from
examples.inverted
import
Inverted
,
InvertedLists
from
pystache
import
Renderer
from
pystache
import
Renderer
from
pystache
import
TemplateSpec
from
pystache
import
TemplateSpec
from
pystache.common
import
TemplateNotFoundError
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
...
@@ -42,7 +43,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
...
@@ -42,7 +43,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
view
=
Tagless
()
view
=
Tagless
()
renderer
=
Renderer
()
renderer
=
Renderer
()
self
.
assertRaises
(
IO
Error
,
renderer
.
render
,
view
)
self
.
assertRaises
(
TemplateNotFound
Error
,
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"
...
@@ -60,7 +61,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
...
@@ -60,7 +61,8 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin):
renderer1
=
Renderer
()
renderer1
=
Renderer
()
renderer2
=
Renderer
(
search_dirs
=
EXAMPLES_DIR
)
renderer2
=
Renderer
(
search_dirs
=
EXAMPLES_DIR
)
self
.
assertRaises
(
IOError
,
renderer1
.
render
,
spec
)
actual
=
renderer1
.
render
(
spec
)
self
.
assertString
(
actual
,
u"Partial: "
)
actual
=
renderer2
.
render
(
spec
)
actual
=
renderer2
.
render
(
spec
)
self
.
assertEqual
(
actual
,
"Partial: No tags..."
)
self
.
assertEqual
(
actual
,
"Partial: No tags..."
)
...
...
setup.py
View file @
8bc8baf3
...
@@ -4,19 +4,21 @@
...
@@ -4,19 +4,21 @@
"""
"""
This script supports publishing Pystache to PyPI.
This script supports publishing Pystache to PyPI.
Below are instructions to pystache maintainers on how to push a new
This docstring contains instructions to Pystache maintainers on how
version of pystache to PyPI--
to release a new version of Pystache.
(1) Push to PyPI. To release a new version of Pystache to PyPI--
http://pypi.python.org/pypi/pystache
http://pypi.python.org/pypi/pystache
Create a PyPI user account. The user account will need permissions to push
create a PyPI user account if you do not already have one. The user account
to PyPI. A current "Package Index Owner" of pystache can grant you those
will need permissions to push to PyPI. A current "Package Index Owner" of
permissions.
Pystache can grant you those
permissions.
When you have permissions, run the following (after preparing the release,
When you have permissions, run the following (after preparing the release,
bumping the version number in setup.py, etc):
merging to master,
bumping the version number in setup.py, etc):
>
python setup.py publish
python setup.py publish
If you get an error like the following--
If you get an error like the following--
...
@@ -33,6 +35,20 @@ as described here, for example:
...
@@ -33,6 +35,20 @@ as described here, for example:
http://docs.python.org/release/2.5.2/dist/pypirc.html
http://docs.python.org/release/2.5.2/dist/pypirc.html
(2) Tag the release on GitHub. Here are some commands for tagging.
List current tags:
git tag -l -n3
Create an annotated tag:
git tag -a -m "Version 0.5.1" "v0.5.1"
Push a tag to GitHub:
git push --tags defunkt v0.5.1
"""
"""
import
os
import
os
...
@@ -56,7 +72,7 @@ else:
...
@@ -56,7 +72,7 @@ else:
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
# print("Using: version %s of %s" % (repr(dist.__version__), repr(dist)))
VERSION
=
'0.5.
1
'
# Also change in pystache/__init__.py.
VERSION
=
'0.5.
2-rc
'
# Also change in pystache/__init__.py.
HISTORY_PATH
=
'HISTORY.rst'
HISTORY_PATH
=
'HISTORY.rst'
LICENSE_PATH
=
'LICENSE'
LICENSE_PATH
=
'LICENSE'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment