├── MANIFEST.in
├── chevron
├── metadata.py
├── __init__.py
├── main.py
├── tokenizer.py
└── renderer.py
├── tests
├── unicode.mustache
├── partial.mustache
├── data.json
├── test-partials-disabled.rendered
├── test.rendered
└── test.mustache
├── .gitmodules
├── .coveragerc
├── __init__.py
├── tox.ini
├── .gitignore
├── LICENSE
├── setup.py
├── .travis.yml
├── benchmark.py
├── README.md
└── test_spec.py
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 |
--------------------------------------------------------------------------------
/chevron/metadata.py:
--------------------------------------------------------------------------------
1 | version = '0.13.1'
2 |
--------------------------------------------------------------------------------
/tests/unicode.mustache:
--------------------------------------------------------------------------------
1 | (╯°□°)╯︵ ┻━┻
2 |
--------------------------------------------------------------------------------
/tests/partial.mustache:
--------------------------------------------------------------------------------
1 | this is a partial{{excited}}
2 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "spec"]
2 | path = spec
3 | url = https://github.com/mustache/spec
4 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | if __name__ == .__main__.:
4 | def cli_main
5 |
6 | \# python 2
7 | \# not tested
8 |
--------------------------------------------------------------------------------
/chevron/__init__.py:
--------------------------------------------------------------------------------
1 | from .main import main, cli_main
2 | from .renderer import render
3 | from .tokenizer import ChevronError
4 |
5 | __all__ = ['main', 'render', 'cli_main', 'ChevronError']
6 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .chevron.main import main, cli_main
2 | from .chevron.renderer import render
3 | from .chevron.tokenizer import ChevronError
4 |
5 | __all__ = ['main', 'render', 'cli_main', 'ChevronError']
6 |
--------------------------------------------------------------------------------
/tests/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "test": "test",
3 | "html_escaped": "< > & \"",
4 | "true": true,
5 | "false": false,
6 | "list": [
7 | {"i": 1, "v": "one"},
8 | {"i": 2, "v": "two"},
9 | {"i": 3, "v": "three"}
10 | ],
11 |
12 | "scope": {
13 | "test": "new test"
14 | },
15 | "unicode": "(╯°□°)╯︵ ┻━┻",
16 | "unicode_html": "(╯°□°)╯︵ ┻━┻"
17 | }
18 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py26, py27, py32, py33, py34, py35, py36, pypy, flake8
3 |
4 | [testenv]
5 | deps = coverage
6 | commands =
7 | coverage run --source={toxinidir}/chevron {toxinidir}/test_spec.py
8 | coverage report -m
9 |
10 | [testenv:py32]
11 | commands = python {toxinidir}/test_spec.py
12 |
13 | [testenv:flake8]
14 | deps = flake8
15 | commands = flake8
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 |
3 | # Code coverage
4 | coverage.xml
5 |
6 | # Jython
7 | *.class
8 |
9 | # Packages
10 | *.egg
11 | *.egg-info
12 | dist
13 | build
14 | eggs
15 | parts
16 | bin
17 | var
18 | sdist
19 | develop-eggs
20 | .installed.cfg
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 |
29 | # Translations
30 | *.mo
31 |
32 | # Mr Developer
33 | .mr.developer.cfg
34 | .idea/
35 |
36 | # VSCode
37 | .vscode/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Noah Morrison
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import chevron.metadata
4 |
5 | try:
6 | from setuptools import setup
7 | except ImportError:
8 | from distutils.core import setup
9 |
10 |
11 | with open('README.md') as f:
12 | readme = f.read()
13 |
14 | setup(name='chevron',
15 | version=chevron.metadata.version,
16 | license='MIT',
17 |
18 | description='Mustache templating language renderer',
19 | long_description=readme,
20 | long_description_content_type='text/markdown',
21 |
22 | author='noah morrison',
23 | author_email='noah@morrison.ph',
24 | url='https://github.com/noahmorrison/chevron',
25 |
26 | packages=['chevron'],
27 | entry_points={
28 | 'console_scripts': ['chevron=chevron:cli_main']
29 | },
30 |
31 | classifiers=[
32 | 'Development Status :: 4 - Beta',
33 |
34 | 'License :: OSI Approved :: MIT License',
35 |
36 | 'Programming Language :: Python :: 2.6',
37 | 'Programming Language :: Python :: 2.7',
38 | 'Programming Language :: Python :: 3.2',
39 | 'Programming Language :: Python :: 3.3',
40 | 'Programming Language :: Python :: 3.4',
41 | 'Programming Language :: Python :: 3.5',
42 | 'Programming Language :: Python :: 3.6',
43 | 'Topic :: Text Processing :: Markup'
44 | ]
45 | )
46 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: python
3 |
4 | python:
5 | - 2.7
6 | - 3.4
7 | - 3.5
8 | - 3.6
9 | - 3.7
10 | - 3.8
11 | - pypy
12 |
13 | matrix:
14 | include:
15 | - python: 2.6
16 | dist: trusty
17 | - python: 3.2
18 | dist: trusty
19 | - python: 3.3
20 | dist: trusty
21 |
22 | - python: 3.6
23 | env:
24 | - TOXENV=flake8
25 | - python: 3.6
26 | env:
27 | - PUBLISH="true"
28 | install:
29 | - pip install coveralls
30 | - pip install tox-travis
31 | script:
32 | - tox
33 | after_success:
34 | - coveralls
35 |
36 | script:
37 | - python ./test_spec.py
38 |
39 | deploy:
40 | - provider: pypi
41 | server: https://upload.pypi.org/legacy/
42 | user: noahmorrison
43 | password:
44 | secure: GK+PXWWCczHMQAptgAA6umma0kP+fPsCktHzDnpb4Pa7RZBjxjS1mbRtwSfMm8gWcRYkqjO0la/t3TUP3qjfa+x5JmIwy2YTveBKmSjTxLAb9n7fCWZEGu9GWgGvfsmg2Hhi6NYUp9U7ggtoBXQ4AHgmnPyDCL2TQK32PKj5soM=
45 | distributions: sdist bdist_wheel
46 | skip_upload_docs: true
47 | on:
48 | tags: true
49 | repo: noahmorrison/chevron
50 | branch: master
51 | condition: $PUBLISH = "true"
52 | - provider: pypi
53 | server: https://test.pypi.org/legacy/
54 | user: noahmorrison
55 | password:
56 | secure: GK+PXWWCczHMQAptgAA6umma0kP+fPsCktHzDnpb4Pa7RZBjxjS1mbRtwSfMm8gWcRYkqjO0la/t3TUP3qjfa+x5JmIwy2YTveBKmSjTxLAb9n7fCWZEGu9GWgGvfsmg2Hhi6NYUp9U7ggtoBXQ4AHgmnPyDCL2TQK32PKj5soM=
57 | distributions: sdist bdist_wheel
58 | skip_upload_docs: true
59 | on:
60 | tags: true
61 | repo: noahmorrison/chevron
62 | all_branches: true
63 | condition: $PUBLISH = "true"
64 |
--------------------------------------------------------------------------------
/tests/test-partials-disabled.rendered:
--------------------------------------------------------------------------------
1 |
2 | variable test
3 | ===
4 | test
5 | ===
6 | test
7 | ===
8 |
9 | comment test
10 | ===
11 | ===
12 | ===
13 |
14 | html escape test (triple brackets)
15 | ===
16 | < > & "
17 | ===
18 | < > & "
19 | ===
20 |
21 | html escape test (ampersand)
22 | ===
23 | < > & "
24 | ===
25 | < > & "
26 | ===
27 |
28 | html escape test (normal)
29 | ===
30 | < > & "
31 | ===
32 | < > & "
33 | ===
34 |
35 | section test (truthy)
36 | ===
37 | true
38 | ===
39 | true
40 | ===
41 |
42 | section test (falsy)
43 | ===
44 | ===
45 | ===
46 |
47 | section test (list)
48 | ===
49 | number: 1
50 | name: one
51 | ---
52 | number: 2
53 | name: two
54 | ---
55 | number: 3
56 | name: three
57 | ---
58 | ===
59 | number: 1
60 | name: one
61 | ---
62 | number: 2
63 | name: two
64 | ---
65 | number: 3
66 | name: three
67 | ---
68 | ===
69 |
70 | section test (scope)
71 | ===
72 | test
73 | new test
74 | ===
75 | test
76 | new test
77 | ===
78 |
79 | inverted section test (truthy)
80 | ===
81 | ===
82 | ===
83 |
84 | inverted section test (falsy)
85 | ===
86 | false
87 | ===
88 | false
89 | ===
90 |
91 | partial test
92 | ===
93 | ===
94 | this is a partial
95 | ===
96 |
97 | delimiter test
98 | ===
99 | test
100 | test
101 | ===
102 | test
103 | test
104 | ===
105 |
106 | unicode test (basic)
107 | ===
108 | (╯°□°)╯︵ ┻━┻
109 | ===
110 | (╯°□°)╯︵ ┻━┻
111 | ===
112 |
113 | unicode test (variable)
114 | ===
115 | (╯°□°)╯︵ ┻━┻
116 | ===
117 | (╯°□°)╯︵ ┻━┻
118 | ===
119 |
120 | unicode test (partial)
121 | ===
122 | ===
123 | (╯°□°)╯︵ ┻━┻
124 | ===
125 |
126 | unicode test (no-escape)
127 | ===
128 | (╯°□°)╯︵ ┻━┻
129 | ===
130 | (╯°□°)╯︵ ┻━┻
131 | ===
132 |
--------------------------------------------------------------------------------
/tests/test.rendered:
--------------------------------------------------------------------------------
1 |
2 | variable test
3 | ===
4 | test
5 | ===
6 | test
7 | ===
8 |
9 | comment test
10 | ===
11 | ===
12 | ===
13 |
14 | html escape test (triple brackets)
15 | ===
16 | < > & "
17 | ===
18 | < > & "
19 | ===
20 |
21 | html escape test (ampersand)
22 | ===
23 | < > & "
24 | ===
25 | < > & "
26 | ===
27 |
28 | html escape test (normal)
29 | ===
30 | < > & "
31 | ===
32 | < > & "
33 | ===
34 |
35 | section test (truthy)
36 | ===
37 | true
38 | ===
39 | true
40 | ===
41 |
42 | section test (falsy)
43 | ===
44 | ===
45 | ===
46 |
47 | section test (list)
48 | ===
49 | number: 1
50 | name: one
51 | ---
52 | number: 2
53 | name: two
54 | ---
55 | number: 3
56 | name: three
57 | ---
58 | ===
59 | number: 1
60 | name: one
61 | ---
62 | number: 2
63 | name: two
64 | ---
65 | number: 3
66 | name: three
67 | ---
68 | ===
69 |
70 | section test (scope)
71 | ===
72 | test
73 | new test
74 | ===
75 | test
76 | new test
77 | ===
78 |
79 | inverted section test (truthy)
80 | ===
81 | ===
82 | ===
83 |
84 | inverted section test (falsy)
85 | ===
86 | false
87 | ===
88 | false
89 | ===
90 |
91 | partial test
92 | ===
93 | this is a partial
94 | ===
95 | this is a partial
96 | ===
97 |
98 | delimiter test
99 | ===
100 | test
101 | test
102 | ===
103 | test
104 | test
105 | ===
106 |
107 | unicode test (basic)
108 | ===
109 | (╯°□°)╯︵ ┻━┻
110 | ===
111 | (╯°□°)╯︵ ┻━┻
112 | ===
113 |
114 | unicode test (variable)
115 | ===
116 | (╯°□°)╯︵ ┻━┻
117 | ===
118 | (╯°□°)╯︵ ┻━┻
119 | ===
120 |
121 | unicode test (partial)
122 | ===
123 | (╯°□°)╯︵ ┻━┻
124 | ===
125 | (╯°□°)╯︵ ┻━┻
126 | ===
127 |
128 | unicode test (no-escape)
129 | ===
130 | (╯°□°)╯︵ ┻━┻
131 | ===
132 | (╯°□°)╯︵ ┻━┻
133 | ===
134 |
--------------------------------------------------------------------------------
/tests/test.mustache:
--------------------------------------------------------------------------------
1 | {{!
2 | mustache comment!
3 | }}
4 |
5 | variable test
6 | ===
7 | {{ test }}
8 | ===
9 | test
10 | ===
11 |
12 | comment test
13 | ===
14 | {{!
15 | mustache comment
16 | }}
17 | ===
18 | ===
19 |
20 | html escape test (triple brackets)
21 | ===
22 | {{{html_escaped}}}
23 | ===
24 | < > & "
25 | ===
26 |
27 | html escape test (ampersand)
28 | ===
29 | {{& html_escaped}}
30 | ===
31 | < > & "
32 | ===
33 |
34 | html escape test (normal)
35 | ===
36 | {{ html_escaped }}
37 | ===
38 | < > & "
39 | ===
40 |
41 | section test (truthy)
42 | ===
43 | {{# true }}
44 | true
45 | {{/ true }}
46 | ===
47 | true
48 | ===
49 |
50 | section test (falsy)
51 | ===
52 | {{# false }}
53 | ERROR
54 | {{/ false }}
55 | ===
56 | ===
57 |
58 | section test (list)
59 | ===
60 | {{# list }}
61 | number: {{i}}
62 | name: {{v}}
63 | ---
64 | {{/ list }}
65 | ===
66 | number: 1
67 | name: one
68 | ---
69 | number: 2
70 | name: two
71 | ---
72 | number: 3
73 | name: three
74 | ---
75 | ===
76 |
77 | section test (scope)
78 | ===
79 | {{ test }}
80 | {{# scope }}
81 | {{ test }}
82 | {{/ scope }}
83 | ===
84 | test
85 | new test
86 | ===
87 |
88 | inverted section test (truthy)
89 | ===
90 | {{^ true }}
91 | ERROR
92 | {{/ true }}
93 | ===
94 | ===
95 |
96 | inverted section test (falsy)
97 | ===
98 | {{^ false }}
99 | false
100 | {{/ false }}
101 | ===
102 | false
103 | ===
104 |
105 | partial test
106 | ===
107 | {{> partial}}
108 | ===
109 | this is a partial
110 | ===
111 |
112 | delimiter test
113 | ===
114 | {{=(( ))=}}
115 | (( test ))
116 | ((={{ }}=))
117 | {{ test }}
118 | ===
119 | test
120 | test
121 | ===
122 |
123 | unicode test (basic)
124 | ===
125 | (╯°□°)╯︵ ┻━┻
126 | ===
127 | (╯°□°)╯︵ ┻━┻
128 | ===
129 |
130 | unicode test (variable)
131 | ===
132 | {{ unicode }}
133 | ===
134 | (╯°□°)╯︵ ┻━┻
135 | ===
136 |
137 | unicode test (partial)
138 | ===
139 | {{> unicode }}
140 | ===
141 | (╯°□°)╯︵ ┻━┻
142 | ===
143 |
144 | unicode test (no-escape)
145 | ===
146 | {{& unicode_html }}
147 | ===
148 | (╯°□°)╯︵ ┻━┻
149 | ===
150 |
--------------------------------------------------------------------------------
/benchmark.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # coding: utf-8
3 |
4 | from sys import argv
5 | from timeit import timeit
6 |
7 | import chevron
8 |
9 |
10 | def make_test(template=None, data=None, expected=None):
11 | def test():
12 | result = chevron.render(template, data)
13 | if result != expected:
14 | error = 'Test failed:\n-- got --\n{}\n-- expected --\n{}'
15 | raise Exception(error.format(result, expected))
16 |
17 | return test
18 |
19 |
20 | def main(times):
21 | args = {
22 | 'template': """\
23 | {{# comments }}
24 |
29 | {{/ comments }}
30 | """,
31 | 'data': {
32 | 'comments': [
33 | {'user': 'tommy',
34 | 'body': 'If this gets to the front page I\'ll eat my hat!',
35 | 'vote': 625},
36 |
37 | {'user': 'trololol',
38 | 'body': 'this',
39 | 'vote': -142},
40 |
41 | {'user': 'mctom',
42 | 'body': 'I wish thinking of test phrases was easier',
43 | 'vote': 83},
44 |
45 | {'user': 'the_thinker',
46 | 'body': 'Why is /u/trololol\'s post higher than ours?',
47 | 'vote': 36}
48 | ]
49 | },
50 | 'expected': """\
51 |
56 |
61 |
66 |
71 | """
72 | }
73 |
74 | test = make_test(**args)
75 |
76 | print(timeit(test, number=times))
77 |
78 |
79 | if __name__ == '__main__':
80 | try:
81 | main(int(argv[1]))
82 | except IndexError:
83 | main(10000)
84 |
--------------------------------------------------------------------------------
/chevron/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import io
4 | import sys
5 |
6 | try:
7 | from .renderer import render
8 | from .metadata import version
9 | except (ValueError, SystemError): # python 2
10 | from renderer import render
11 | from metadata import version
12 |
13 |
14 | def main(template, data=None, **kwargs):
15 | with io.open(template, 'r', encoding='utf-8') as template_file:
16 | yaml_loader = kwargs.pop('yaml_loader', None) or 'SafeLoader'
17 |
18 | if data is not None:
19 | with io.open(data, 'r', encoding='utf-8') as data_file:
20 | data = _load_data(data_file, yaml_loader)
21 | else:
22 | data = {}
23 |
24 | args = {
25 | 'template': template_file,
26 | 'data': data
27 | }
28 |
29 | args.update(kwargs)
30 | return render(**args)
31 |
32 |
33 | def _load_data(file, yaml_loader):
34 | try:
35 | import yaml
36 | loader = getattr(yaml, yaml_loader) # not tested
37 | return yaml.load(file, Loader=loader) # not tested
38 | except ImportError:
39 | import json
40 | return json.load(file)
41 |
42 |
43 | def cli_main():
44 | """Render mustache templates using json files"""
45 | import argparse
46 | import os
47 |
48 | def is_file_or_pipe(arg):
49 | if not os.path.exists(arg) or os.path.isdir(arg):
50 | parser.error('The file {0} does not exist!'.format(arg))
51 | else:
52 | return arg
53 |
54 | def is_dir(arg):
55 | if not os.path.isdir(arg):
56 | parser.error('The directory {0} does not exist!'.format(arg))
57 | else:
58 | return arg
59 |
60 | parser = argparse.ArgumentParser(description=__doc__)
61 |
62 | parser.add_argument('-v', '--version', action='version',
63 | version=version)
64 |
65 | parser.add_argument('template', help='The mustache file',
66 | type=is_file_or_pipe)
67 |
68 | parser.add_argument('-d', '--data', dest='data',
69 | help='The json data file',
70 | type=is_file_or_pipe, default={})
71 |
72 | parser.add_argument('-y', '--yaml-loader', dest='yaml_loader',
73 | help=argparse.SUPPRESS)
74 |
75 | parser.add_argument('-p', '--path', dest='partials_path',
76 | help='The directory where your partials reside',
77 | type=is_dir, default='.')
78 |
79 | parser.add_argument('-e', '--ext', dest='partials_ext',
80 | help='The extension for your mustache\
81 | partials, \'mustache\' by default',
82 | default='mustache')
83 |
84 | parser.add_argument('-l', '--left-delimiter', dest='def_ldel',
85 | help='The default left delimiter, "{{" by default.',
86 | default='{{')
87 |
88 | parser.add_argument('-r', '--right-delimiter', dest='def_rdel',
89 | help='The default right delimiter, "}}" by default.',
90 | default='}}')
91 |
92 | parser.add_argument('-w', '--warn', dest='warn',
93 | help='Print a warning to stderr for each undefined template key encountered',
94 | action='store_true')
95 |
96 |
97 | args = vars(parser.parse_args())
98 |
99 | try:
100 | sys.stdout.write(main(**args))
101 | sys.stdout.flush()
102 | except SyntaxError as e:
103 | print('Chevron: syntax error')
104 | sys.exit(' ' + '\n '.join(e.args[0].split('\n')))
105 |
106 |
107 | if __name__ == '__main__':
108 | cli_main()
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://badge.fury.io/py/chevron)
2 | [](https://travis-ci.org/noahmorrison/chevron)
3 | [](https://coveralls.io/github/noahmorrison/chevron?branch=master)
4 |
5 | A python implementation of the [mustache templating language](http://mustache.github.io).
6 |
7 | Why chevron?
8 | ------------
9 |
10 | I'm glad you asked!
11 |
12 | ### chevron is fast ###
13 |
14 | Chevron runs in less than half the time of [pystache](http://github.com/defunkt/pystache) (Which is not even up to date on the spec).
15 | And in about 70% the time of [Stache](https://github.com/hyperturtle/Stache) (A 'trimmed' version of mustache, also not spec compliant).
16 |
17 | ### chevron is pep8 ###
18 |
19 | The flake8 command is run by [travis](https://travis-ci.org/noahmorrison/chevron) to ensure consistency.
20 |
21 | ### chevron is spec compliant ###
22 |
23 | Chevron passes all the unittests provided by the [spec](https://github.com/mustache/spec) (in every version listed below).
24 |
25 | If you find a test that chevron does not pass, please [report it.](https://github.com/noahmorrison/chevron/issues/new)
26 |
27 | ### chevron is Python 2 and 3 compatible ###
28 |
29 | Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, and 3.6 are all tested by travis.
30 |
31 |
32 |
33 | USAGE
34 | -----
35 |
36 | Commandline usage: (if installed via pypi)
37 | ```
38 | usage: chevron [-h] [-v] [-d DATA] [-p PARTIALS_PATH] [-e PARTIALS_EXT]
39 | [-l DEF_LDEL] [-r DEF_RDEL]
40 | template
41 |
42 | positional arguments:
43 | template The mustache file
44 |
45 | optional arguments:
46 | -h, --help show this help message and exit
47 | -v, --version show program's version number and exit
48 | -d DATA, --data DATA The json data file
49 | -p PARTIALS_PATH, --path PARTIALS_PATH
50 | The directory where your partials reside
51 | -e PARTIALS_EXT, --ext PARTIALS_EXT
52 | The extension for your mustache partials, 'mustache'
53 | by default
54 | -l DEF_LDEL, --left-delimiter DEF_LDEL
55 | The default left delimiter, "{{" by default.
56 | -r DEF_RDEL, --right-delimiter DEF_RDEL
57 | The default right delimiter, "}}" by default.
58 | ```
59 |
60 | Python usage with strings
61 | ```python
62 | import chevron
63 |
64 | chevron.render('Hello, {{ mustache }}!', {'mustache': 'World'})
65 | ```
66 |
67 | Python usage with file
68 | ```python
69 | import chevron
70 |
71 | with open('file.mustache', 'r') as f:
72 | chevron.render(f, {'mustache': 'World'})
73 | ```
74 |
75 | Python usage with unpacking
76 | ```python
77 | import chevron
78 |
79 | args = {
80 | 'template': 'Hello, {{ mustache }}!',
81 |
82 | 'data': {
83 | 'mustache': 'World'
84 | }
85 | }
86 |
87 | chevron.render(**args)
88 | ```
89 |
90 | chevron supports partials (via dictionaries)
91 | ```python
92 | import chevron
93 |
94 | args = {
95 | 'template': 'Hello, {{> thing }}!',
96 |
97 | 'partials_dict': {
98 | 'thing': 'World'
99 | }
100 | }
101 |
102 | chevron.render(**args)
103 | ```
104 |
105 | chevron supports partials (via the filesystem)
106 | ```python
107 | import chevron
108 |
109 | args = {
110 | 'template': 'Hello, {{> thing }}!',
111 |
112 | # defaults to .
113 | 'partials_path': 'partials/',
114 |
115 | # defaults to mustache
116 | 'partials_ext': 'ms',
117 | }
118 |
119 | # ./partials/thing.ms will be read and rendered
120 | chevron.render(**args)
121 | ```
122 |
123 | chevron supports lambdas
124 | ```python
125 | import chevron
126 |
127 | def first(text, render):
128 | # return only first occurance of items
129 | result = render(text)
130 | return [ x.strip() for x in result.split(" || ") if x.strip() ][0]
131 |
132 | def inject_x(text, render):
133 | # inject data into scope
134 | return render(text, {'x': 'data'})
135 |
136 | args = {
137 | 'template': 'Hello, {{# first}} {{x}} || {{y}} || {{z}} {{/ first}}! {{# inject_x}} {{x}} {{/ inject_x}}',
138 |
139 | 'data': {
140 | 'y': 'foo',
141 | 'z': 'bar',
142 | 'first': first,
143 | 'inject_x': inject_x
144 | }
145 | }
146 |
147 | chevron.render(**args)
148 | ```
149 |
150 | INSTALL
151 | -------
152 |
153 | - with git
154 | ```
155 | $ git clone https://github.com/noahmorrison/chevron.git
156 | ```
157 |
158 | or using submodules
159 | ```
160 | $ git submodules add https://github.com/noahmorrison/chevron.git
161 | ```
162 |
163 | Also available on pypi!
164 |
165 | - with pip
166 | ```
167 | $ pip install chevron
168 | ```
169 |
170 |
171 |
172 | TODO
173 | ---
174 |
175 | * get popular
176 | * have people complain
177 | * fix those complaints
178 |
--------------------------------------------------------------------------------
/chevron/tokenizer.py:
--------------------------------------------------------------------------------
1 | # Globals
2 | _CURRENT_LINE = 1
3 | _LAST_TAG_LINE = None
4 |
5 |
6 | class ChevronError(SyntaxError):
7 | pass
8 |
9 | #
10 | # Helper functions
11 | #
12 |
13 |
14 | def grab_literal(template, l_del):
15 | """Parse a literal from the template"""
16 |
17 | global _CURRENT_LINE
18 |
19 | try:
20 | # Look for the next tag and move the template to it
21 | literal, template = template.split(l_del, 1)
22 | _CURRENT_LINE += literal.count('\n')
23 | return (literal, template)
24 |
25 | # There are no more tags in the template?
26 | except ValueError:
27 | # Then the rest of the template is a literal
28 | return (template, '')
29 |
30 |
31 | def l_sa_check(template, literal, is_standalone):
32 | """Do a preliminary check to see if a tag could be a standalone"""
33 |
34 | # If there is a newline, or the previous tag was a standalone
35 | if literal.find('\n') != -1 or is_standalone:
36 | padding = literal.split('\n')[-1]
37 |
38 | # If all the characters since the last newline are spaces
39 | if padding.isspace() or padding == '':
40 | # Then the next tag could be a standalone
41 | return True
42 | else:
43 | # Otherwise it can't be
44 | return False
45 |
46 |
47 | def r_sa_check(template, tag_type, is_standalone):
48 | """Do a final checkto see if a tag could be a standalone"""
49 |
50 | # Check right side if we might be a standalone
51 | if is_standalone and tag_type not in ['variable', 'no escape']:
52 | on_newline = template.split('\n', 1)
53 |
54 | # If the stuff to the right of us are spaces we're a standalone
55 | if on_newline[0].isspace() or not on_newline[0]:
56 | return True
57 | else:
58 | return False
59 |
60 | # If we're a tag can't be a standalone
61 | else:
62 | return False
63 |
64 |
65 | def parse_tag(template, l_del, r_del):
66 | """Parse a tag from a template"""
67 | global _CURRENT_LINE
68 | global _LAST_TAG_LINE
69 |
70 | tag_types = {
71 | '!': 'comment',
72 | '#': 'section',
73 | '^': 'inverted section',
74 | '/': 'end',
75 | '>': 'partial',
76 | '=': 'set delimiter?',
77 | '{': 'no escape?',
78 | '&': 'no escape'
79 | }
80 |
81 | # Get the tag
82 | try:
83 | tag, template = template.split(r_del, 1)
84 | except ValueError:
85 | raise ChevronError('unclosed tag '
86 | 'at line {0}'.format(_CURRENT_LINE))
87 |
88 | # Find the type meaning of the first character
89 | tag_type = tag_types.get(tag[0], 'variable')
90 |
91 | # If the type is not a variable
92 | if tag_type != 'variable':
93 | # Then that first character is not needed
94 | tag = tag[1:]
95 |
96 | # If we might be a set delimiter tag
97 | if tag_type == 'set delimiter?':
98 | # Double check to make sure we are
99 | if tag.endswith('='):
100 | tag_type = 'set delimiter'
101 | # Remove the equal sign
102 | tag = tag[:-1]
103 |
104 | # Otherwise we should complain
105 | else:
106 | raise ChevronError('unclosed set delimiter tag\n'
107 | 'at line {0}'.format(_CURRENT_LINE))
108 |
109 | # If we might be a no html escape tag
110 | elif tag_type == 'no escape?':
111 | # And we have a third curly brace
112 | # (And are using curly braces as delimiters)
113 | if l_del == '{{' and r_del == '}}' and template.startswith('}'):
114 | # Then we are a no html escape tag
115 | template = template[1:]
116 | tag_type = 'no escape'
117 |
118 | # Strip the whitespace off the key and return
119 | return ((tag_type, tag.strip()), template)
120 |
121 |
122 | #
123 | # The main tokenizing function
124 | #
125 |
126 | def tokenize(template, def_ldel='{{', def_rdel='}}'):
127 | """Tokenize a mustache template
128 |
129 | Tokenizes a mustache template in a generator fashion,
130 | using file-like objects. It also accepts a string containing
131 | the template.
132 |
133 |
134 | Arguments:
135 |
136 | template -- a file-like object, or a string of a mustache template
137 |
138 | def_ldel -- The default left delimiter
139 | ("{{" by default, as in spec compliant mustache)
140 |
141 | def_rdel -- The default right delimiter
142 | ("}}" by default, as in spec compliant mustache)
143 |
144 |
145 | Returns:
146 |
147 | A generator of mustache tags in the form of a tuple
148 |
149 | -- (tag_type, tag_key)
150 |
151 | Where tag_type is one of:
152 | * literal
153 | * section
154 | * inverted section
155 | * end
156 | * partial
157 | * no escape
158 |
159 | And tag_key is either the key or in the case of a literal tag,
160 | the literal itself.
161 | """
162 |
163 | global _CURRENT_LINE, _LAST_TAG_LINE
164 | _CURRENT_LINE = 1
165 | _LAST_TAG_LINE = None
166 | # If the template is a file-like object then read it
167 | try:
168 | template = template.read()
169 | except AttributeError:
170 | pass
171 |
172 | is_standalone = True
173 | open_sections = []
174 | l_del = def_ldel
175 | r_del = def_rdel
176 |
177 | while template:
178 | literal, template = grab_literal(template, l_del)
179 |
180 | # If the template is completed
181 | if not template:
182 | # Then yield the literal and leave
183 | yield ('literal', literal)
184 | break
185 |
186 | # Do the first check to see if we could be a standalone
187 | is_standalone = l_sa_check(template, literal, is_standalone)
188 |
189 | # Parse the tag
190 | tag, template = parse_tag(template, l_del, r_del)
191 | tag_type, tag_key = tag
192 |
193 | # Special tag logic
194 |
195 | # If we are a set delimiter tag
196 | if tag_type == 'set delimiter':
197 | # Then get and set the delimiters
198 | dels = tag_key.strip().split(' ')
199 | l_del, r_del = dels[0], dels[-1]
200 |
201 | # If we are a section tag
202 | elif tag_type in ['section', 'inverted section']:
203 | # Then open a new section
204 | open_sections.append(tag_key)
205 | _LAST_TAG_LINE = _CURRENT_LINE
206 |
207 | # If we are an end tag
208 | elif tag_type == 'end':
209 | # Then check to see if the last opened section
210 | # is the same as us
211 | try:
212 | last_section = open_sections.pop()
213 | except IndexError:
214 | raise ChevronError('Trying to close tag "{0}"\n'
215 | 'Looks like it was not opened.\n'
216 | 'line {1}'
217 | .format(tag_key, _CURRENT_LINE + 1))
218 | if tag_key != last_section:
219 | # Otherwise we need to complain
220 | raise ChevronError('Trying to close tag "{0}"\n'
221 | 'last open tag is "{1}"\n'
222 | 'line {2}'
223 | .format(tag_key, last_section,
224 | _CURRENT_LINE + 1))
225 |
226 | # Do the second check to see if we're a standalone
227 | is_standalone = r_sa_check(template, tag_type, is_standalone)
228 |
229 | # Which if we are
230 | if is_standalone:
231 | # Remove the stuff before the newline
232 | template = template.split('\n', 1)[-1]
233 |
234 | # Partials need to keep the spaces on their left
235 | if tag_type != 'partial':
236 | # But other tags don't
237 | literal = literal.rstrip(' ')
238 |
239 | # Start yielding
240 | # Ignore literals that are empty
241 | if literal != '':
242 | yield ('literal', literal)
243 |
244 | # Ignore comments and set delimiters
245 | if tag_type not in ['comment', 'set delimiter?']:
246 | yield (tag_type, tag_key)
247 |
248 | # If there are any open sections when we're done
249 | if open_sections:
250 | # Then we need to complain
251 | raise ChevronError('Unexpected EOF\n'
252 | 'the tag "{0}" was never closed\n'
253 | 'was opened at line {1}'
254 | .format(open_sections[-1], _LAST_TAG_LINE))
255 |
--------------------------------------------------------------------------------
/chevron/renderer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import io
4 | from os import linesep, path
5 |
6 | try:
7 | from collections.abc import Sequence, Iterator, Callable
8 | except ImportError: # python 2
9 | from collections import Sequence, Iterator, Callable
10 | try:
11 | from .tokenizer import tokenize
12 | except (ValueError, SystemError): # python 2
13 | from tokenizer import tokenize
14 |
15 |
16 | import sys
17 | if sys.version_info[0] == 3:
18 | python3 = True
19 | unicode_type = str
20 | string_type = str
21 |
22 | def unicode(x, y):
23 | return x
24 |
25 | else: # python 2
26 | python3 = False
27 | unicode_type = unicode
28 | string_type = basestring # noqa: F821 (This is defined in python2)
29 |
30 |
31 | #
32 | # Helper functions
33 | #
34 |
35 | def _html_escape(string):
36 | """HTML escape all of these " & < >"""
37 |
38 | html_codes = {
39 | '"': '"',
40 | '<': '<',
41 | '>': '>',
42 | }
43 |
44 | # & must be handled first
45 | string = string.replace('&', '&')
46 | for char in html_codes:
47 | string = string.replace(char, html_codes[char])
48 | return string
49 |
50 |
51 | def _get_key(key, scopes, warn, keep, def_ldel, def_rdel):
52 | """Get a key from the current scope"""
53 |
54 | # If the key is a dot
55 | if key == '.':
56 | # Then just return the current scope
57 | return scopes[0]
58 |
59 | # Loop through the scopes
60 | for scope in scopes:
61 | try:
62 | # For every dot seperated key
63 | for child in key.split('.'):
64 | # Move into the scope
65 | try:
66 | # Try subscripting (Normal dictionaries)
67 | scope = scope[child]
68 | except (TypeError, AttributeError):
69 | try:
70 | scope = getattr(scope, child)
71 | except (TypeError, AttributeError):
72 | # Try as a list
73 | scope = scope[int(child)]
74 |
75 | # Return an empty string if falsy, with two exceptions
76 | # 0 should return 0, and False should return False
77 | if scope in (0, False):
78 | return scope
79 |
80 | try:
81 | # This allows for custom falsy data types
82 | # https://github.com/noahmorrison/chevron/issues/35
83 | if scope._CHEVRON_return_scope_when_falsy:
84 | return scope
85 | except AttributeError:
86 | return scope or ''
87 | except (AttributeError, KeyError, IndexError, ValueError):
88 | # We couldn't find the key in the current scope
89 | # We'll try again on the next pass
90 | pass
91 |
92 | # We couldn't find the key in any of the scopes
93 |
94 | if warn:
95 | sys.stderr.write("Could not find key '%s'%s" % (key, linesep))
96 |
97 | if keep:
98 | return "%s %s %s" % (def_ldel, key, def_rdel)
99 |
100 | return ''
101 |
102 |
103 | def _get_partial(name, partials_dict, partials_path, partials_ext):
104 | """Load a partial"""
105 | try:
106 | # Maybe the partial is in the dictionary
107 | return partials_dict[name]
108 | except KeyError:
109 | # Don't try loading from the file system if the partials_path is None or empty
110 | if partials_path is None or partials_path == '': return ''
111 |
112 | # Nope...
113 | try:
114 | # Maybe it's in the file system
115 | path_ext = ('.' + partials_ext if partials_ext else '')
116 | partial_path = path.join(partials_path, name + path_ext)
117 | with io.open(partial_path, 'r', encoding='utf-8') as partial:
118 | return partial.read()
119 |
120 | except IOError:
121 | # Alright I give up on you
122 | return ''
123 |
124 |
125 | #
126 | # The main rendering function
127 | #
128 | g_token_cache = {}
129 |
130 |
131 | def render(template='', data={}, partials_path='.', partials_ext='mustache',
132 | partials_dict={}, padding='', def_ldel='{{', def_rdel='}}',
133 | scopes=None, warn=False, keep=False):
134 | """Render a mustache template.
135 |
136 | Renders a mustache template with a data scope and partial capability.
137 | Given the file structure...
138 | ╷
139 | ├─╼ main.py
140 | ├─╼ main.ms
141 | └─┮ partials
142 | └── part.ms
143 |
144 | then main.py would make the following call:
145 |
146 | render(open('main.ms', 'r'), {...}, 'partials', 'ms')
147 |
148 |
149 | Arguments:
150 |
151 | template -- A file-like object or a string containing the template
152 |
153 | data -- A python dictionary with your data scope
154 |
155 | partials_path -- The path to where your partials are stored
156 | If set to None, then partials won't be loaded from the file system
157 | (defaults to '.')
158 |
159 | partials_ext -- The extension that you want the parser to look for
160 | (defaults to 'mustache')
161 |
162 | partials_dict -- A python dictionary which will be search for partials
163 | before the filesystem is. {'include': 'foo'} is the same
164 | as a file called include.mustache
165 | (defaults to {})
166 |
167 | padding -- This is for padding partials, and shouldn't be used
168 | (but can be if you really want to)
169 |
170 | def_ldel -- The default left delimiter
171 | ("{{" by default, as in spec compliant mustache)
172 |
173 | def_rdel -- The default right delimiter
174 | ("}}" by default, as in spec compliant mustache)
175 |
176 | scopes -- The list of scopes that get_key will look through
177 |
178 | warn -- Issue a warning to stderr when a template substitution isn't found in the data
179 |
180 | keep -- Keep unreplaced tags when a template substitution isn't found in the data
181 |
182 |
183 | Returns:
184 |
185 | A string containing the rendered template.
186 | """
187 |
188 | # If the template is a seqeuence but not derived from a string
189 | if isinstance(template, Sequence) and \
190 | not isinstance(template, string_type):
191 | # Then we don't need to tokenize it
192 | # But it does need to be a generator
193 | tokens = (token for token in template)
194 | else:
195 | if template in g_token_cache:
196 | tokens = (token for token in g_token_cache[template])
197 | else:
198 | # Otherwise make a generator
199 | tokens = tokenize(template, def_ldel, def_rdel)
200 |
201 | output = unicode('', 'utf-8')
202 |
203 | if scopes is None:
204 | scopes = [data]
205 |
206 | # Run through the tokens
207 | for tag, key in tokens:
208 | # Set the current scope
209 | current_scope = scopes[0]
210 |
211 | # If we're an end tag
212 | if tag == 'end':
213 | # Pop out of the latest scope
214 | del scopes[0]
215 |
216 | # If the current scope is falsy and not the only scope
217 | elif not current_scope and len(scopes) != 1:
218 | if tag in ['section', 'inverted section']:
219 | # Set the most recent scope to a falsy value
220 | # (I heard False is a good one)
221 | scopes.insert(0, False)
222 |
223 | # If we're a literal tag
224 | elif tag == 'literal':
225 | # Add padding to the key and add it to the output
226 | if not isinstance(key, unicode_type): # python 2
227 | key = unicode(key, 'utf-8')
228 | output += key.replace('\n', '\n' + padding)
229 |
230 | # If we're a variable tag
231 | elif tag == 'variable':
232 | # Add the html escaped key to the output
233 | thing = _get_key(key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel)
234 | if thing is True and key == '.':
235 | # if we've coerced into a boolean by accident
236 | # (inverted tags do this)
237 | # then get the un-coerced object (next in the stack)
238 | thing = scopes[1]
239 | if not isinstance(thing, unicode_type):
240 | thing = unicode(str(thing), 'utf-8')
241 | output += _html_escape(thing)
242 |
243 | # If we're a no html escape tag
244 | elif tag == 'no escape':
245 | # Just lookup the key and add it
246 | thing = _get_key(key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel)
247 | if not isinstance(thing, unicode_type):
248 | thing = unicode(str(thing), 'utf-8')
249 | output += thing
250 |
251 | # If we're a section tag
252 | elif tag == 'section':
253 | # Get the sections scope
254 | scope = _get_key(key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel)
255 |
256 | # If the scope is a callable (as described in
257 | # https://mustache.github.io/mustache.5.html)
258 | if isinstance(scope, Callable):
259 |
260 | # Generate template text from tags
261 | text = unicode('', 'utf-8')
262 | tags = []
263 | for tag in tokens:
264 | if tag == ('end', key):
265 | break
266 |
267 | tags.append(tag)
268 | tag_type, tag_key = tag
269 | if tag_type == 'literal':
270 | text += tag_key
271 | elif tag_type == 'no escape':
272 | text += "%s& %s %s" % (def_ldel, tag_key, def_rdel)
273 | else:
274 | text += "%s%s %s%s" % (def_ldel, {
275 | 'commment': '!',
276 | 'section': '#',
277 | 'inverted section': '^',
278 | 'end': '/',
279 | 'partial': '>',
280 | 'set delimiter': '=',
281 | 'no escape': '&',
282 | 'variable': ''
283 | }[tag_type], tag_key, def_rdel)
284 |
285 | g_token_cache[text] = tags
286 |
287 | rend = scope(text, lambda template, data=None: render(template,
288 | data={},
289 | partials_path=partials_path,
290 | partials_ext=partials_ext,
291 | partials_dict=partials_dict,
292 | padding=padding,
293 | def_ldel=def_ldel, def_rdel=def_rdel,
294 | scopes=data and [data]+scopes or scopes,
295 | warn=warn, keep=keep))
296 |
297 | if python3:
298 | output += rend
299 | else: # python 2
300 | output += rend.decode('utf-8')
301 |
302 | # If the scope is a sequence, an iterator or generator but not
303 | # derived from a string
304 | elif isinstance(scope, (Sequence, Iterator)) and \
305 | not isinstance(scope, string_type):
306 | # Then we need to do some looping
307 |
308 | # Gather up all the tags inside the section
309 | # (And don't be tricked by nested end tags with the same key)
310 | # TODO: This feels like it still has edge cases, no?
311 | tags = []
312 | tags_with_same_key = 0
313 | for tag in tokens:
314 | if tag == ('section', key):
315 | tags_with_same_key += 1
316 | if tag == ('end', key):
317 | tags_with_same_key -= 1
318 | if tags_with_same_key < 0:
319 | break
320 | tags.append(tag)
321 |
322 | # For every item in the scope
323 | for thing in scope:
324 | # Append it as the most recent scope and render
325 | new_scope = [thing] + scopes
326 | rend = render(template=tags, scopes=new_scope,
327 | padding=padding,
328 | partials_path=partials_path,
329 | partials_ext=partials_ext,
330 | partials_dict=partials_dict,
331 | def_ldel=def_ldel, def_rdel=def_rdel,
332 | warn=warn, keep=keep)
333 |
334 | if python3:
335 | output += rend
336 | else: # python 2
337 | output += rend.decode('utf-8')
338 |
339 | else:
340 | # Otherwise we're just a scope section
341 | scopes.insert(0, scope)
342 |
343 | # If we're an inverted section
344 | elif tag == 'inverted section':
345 | # Add the flipped scope to the scopes
346 | scope = _get_key(key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel)
347 | scopes.insert(0, not scope)
348 |
349 | # If we're a partial
350 | elif tag == 'partial':
351 | # Load the partial
352 | partial = _get_partial(key, partials_dict,
353 | partials_path, partials_ext)
354 |
355 | # Find what to pad the partial with
356 | left = output.rpartition('\n')[2]
357 | part_padding = padding
358 | if left.isspace():
359 | part_padding += left
360 |
361 | # Render the partial
362 | part_out = render(template=partial, partials_path=partials_path,
363 | partials_ext=partials_ext,
364 | partials_dict=partials_dict,
365 | def_ldel=def_ldel, def_rdel=def_rdel,
366 | padding=part_padding, scopes=scopes,
367 | warn=warn, keep=keep)
368 |
369 | # If the partial was indented
370 | if left.isspace():
371 | # then remove the spaces from the end
372 | part_out = part_out.rstrip(' \t')
373 |
374 | # Add the partials output to the ouput
375 | if python3:
376 | output += part_out
377 | else: # python 2
378 | output += part_out.decode('utf-8')
379 |
380 | if python3:
381 | return output
382 | else: # python 2
383 | return output.encode('utf-8')
384 |
--------------------------------------------------------------------------------
/test_spec.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | import collections
5 | import unittest
6 | import os
7 | import json
8 | import io
9 |
10 | import chevron
11 |
12 | import sys
13 | if sys.version_info[0] == 3:
14 | python3 = True
15 | else: # python 2
16 | python3 = False
17 |
18 |
19 | SPECS_PATH = os.path.join('spec', 'specs')
20 | if os.path.exists(SPECS_PATH):
21 | SPECS = [path for path in os.listdir(SPECS_PATH) if path.endswith('.json')]
22 | else:
23 | SPECS = []
24 |
25 | STACHE = chevron.render
26 |
27 |
28 | def _test_case_from_path(json_path):
29 | json_path = '%s.json' % json_path
30 |
31 | class MustacheTestCase(unittest.TestCase):
32 | """A simple yaml based test case"""
33 |
34 | def _test_from_object(obj):
35 | """Generate a unit test from a test object"""
36 |
37 | def test_case(self):
38 | result = STACHE(obj['template'], obj['data'],
39 | partials_dict=obj.get('partials', {}))
40 |
41 | self.assertEqual(result, obj['expected'])
42 |
43 | test_case.__doc__ = 'suite: {0} desc: {1}'.format(spec,
44 | obj['desc'])
45 | return test_case
46 |
47 | with io.open(json_path, 'r', encoding='utf-8') as f:
48 | yaml = json.load(f)
49 |
50 | # Generates a unit test for each test object
51 | for i, test in enumerate(yaml['tests']):
52 | vars()['test_' + str(i)] = _test_from_object(test)
53 |
54 | # Return the built class
55 | return MustacheTestCase
56 |
57 |
58 | # Create TestCase for each json file
59 | for spec in SPECS:
60 | # Ignore optional tests
61 | if spec[0] != '~':
62 | spec = spec.split('.')[0]
63 | globals()[spec] = _test_case_from_path(os.path.join(SPECS_PATH, spec))
64 |
65 |
66 | class ExpandedCoverage(unittest.TestCase):
67 |
68 | def test_unclosed_sections(self):
69 | test1 = {
70 | 'template': '{{# section }} oops {{/ wrong_section }}'
71 | }
72 |
73 | test2 = {
74 | 'template': '{{# section }} end of file'
75 | }
76 |
77 | self.assertRaises(chevron.ChevronError, chevron.render, **test1)
78 | self.assertRaises(chevron.ChevronError, chevron.render, **test2)
79 | # check SyntaxError still catches ChevronError:
80 | self.assertRaises(SyntaxError, chevron.render, **test1)
81 |
82 | def test_bad_set_delimiter_tag(self):
83 | args = {
84 | 'template': '{{= bad!}}'
85 | }
86 |
87 | self.assertRaises(SyntaxError, chevron.render, **args)
88 |
89 | def test_unicode_basic(self):
90 | args = {
91 | 'template': '(╯°□°)╯︵ ┻━┻'
92 | }
93 |
94 | result = chevron.render(**args)
95 | expected = '(╯°□°)╯︵ ┻━┻'
96 |
97 | self.assertEqual(result, expected)
98 |
99 | def test_unicode_variable(self):
100 | args = {
101 | 'template': '{{ table_flip }}',
102 | 'data': {'table_flip': '(╯°□°)╯︵ ┻━┻'}
103 | }
104 |
105 | result = chevron.render(**args)
106 | expected = '(╯°□°)╯︵ ┻━┻'
107 |
108 | self.assertEqual(result, expected)
109 |
110 | def test_unicode_partial(self):
111 | args = {
112 | 'template': '{{> table_flip }}',
113 | 'partials_dict': {'table_flip': '(╯°□°)╯︵ ┻━┻'}
114 | }
115 |
116 | result = chevron.render(**args)
117 | expected = '(╯°□°)╯︵ ┻━┻'
118 |
119 | self.assertEqual(result, expected)
120 |
121 | def test_missing_key_partial(self):
122 | args = {
123 | 'template': 'before, {{> with_missing_key }}, after',
124 | 'partials_dict': {
125 | 'with_missing_key': '{{#missing_key}}bloop{{/missing_key}}',
126 | },
127 | }
128 |
129 | result = chevron.render(**args)
130 | expected = 'before, , after'
131 |
132 | self.assertEqual(result, expected)
133 |
134 | def test_listed_data(self):
135 | args = {
136 | 'template': '{{# . }}({{ . }}){{/ . }}',
137 | 'data': [1, 2, 3, 4, 5]
138 | }
139 |
140 | result = chevron.render(**args)
141 | expected = '(1)(2)(3)(4)(5)'
142 |
143 | self.assertEqual(result, expected)
144 |
145 | def test_main(self):
146 | result = chevron.main('tests/test.mustache', 'tests/data.json',
147 | partials_path='tests')
148 |
149 | with io.open('tests/test.rendered', 'r', encoding='utf-8') as f:
150 | expected = f.read()
151 | if not python3:
152 | expected = expected.encode('utf-8')
153 |
154 | self.assertEqual(result, expected)
155 |
156 | def test_recursion(self):
157 | args = {
158 | 'template': '{{# 1.2 }}{{# data }}{{.}}{{/ data }}{{/ 1.2 }}',
159 | 'data': {'1': {'2': [{'data': ["1", "2", "3"]}]}}
160 | }
161 |
162 | result = chevron.render(**args)
163 | expected = '123'
164 |
165 | self.assertEqual(result, expected)
166 |
167 | def test_unicode_inside_list(self):
168 | args = {
169 | 'template': '{{#list}}{{.}}{{/list}}',
170 | 'data': {'list': ['☠']}
171 | }
172 |
173 | result = chevron.render(**args)
174 | expected = '☠'
175 |
176 | self.assertEqual(result, expected)
177 |
178 | def test_falsy(self):
179 | args = {
180 | 'template': '{{null}}{{false}}{{list}}{{dict}}{{zero}}',
181 | 'data': {'null': None,
182 | 'false': False,
183 | 'list': [],
184 | 'dict': {},
185 | 'zero': 0
186 | }
187 | }
188 |
189 | result = chevron.render(**args)
190 | expected = 'False0'
191 |
192 | self.assertEqual(result, expected)
193 |
194 | def test_complex(self):
195 | class Complex:
196 | def __init__(self):
197 | self.attr = 42
198 |
199 | args = {
200 | 'template': '{{comp.attr}} {{int.attr}}',
201 | 'data': {'comp': Complex(),
202 | 'int': 1
203 | }
204 | }
205 |
206 | result = chevron.render(**args)
207 | expected = '42 '
208 |
209 | self.assertEqual(result, expected)
210 |
211 | # https://github.com/noahmorrison/chevron/issues/17
212 | def test_inverted_coercion(self):
213 | args = {
214 | 'template': '{{#object}}{{^child}}{{.}}{{/child}}{{/object}}',
215 | 'data': {'object': [
216 | 'foo', 'bar', {'child': True}, 'baz'
217 | ]}
218 | }
219 |
220 | result = chevron.render(**args)
221 | expected = 'foobarbaz'
222 |
223 | self.assertEqual(result, expected)
224 |
225 | def test_closing_tag_only(self):
226 | args = {
227 | 'template': '{{ foo } bar',
228 | 'data': {'foo': 'xx'}
229 | }
230 |
231 | self.assertRaises(chevron.ChevronError, chevron.render, **args)
232 |
233 | def test_current_line_rest(self):
234 | args = {
235 | 'template': 'first line\nsecond line\n {{ foo } bar',
236 | 'data': {'foo': 'xx'}
237 | }
238 |
239 | # self.assertRaisesRegex does not exist in python2.6
240 | for _ in range(10):
241 | try:
242 | chevron.render(**args)
243 | except chevron.ChevronError as error:
244 | self.assertEqual(error.msg, 'unclosed tag at line 3')
245 |
246 | def test_no_opening_tag(self):
247 | args = {
248 | 'template': 'oops, no opening tag {{/ closing_tag }}',
249 | 'data': {'foo': 'xx'}
250 | }
251 |
252 | try:
253 | chevron.render(**args)
254 | except chevron.ChevronError as error:
255 | self.assertEqual(error.msg, 'Trying to close tag "closing_tag"\n'
256 | 'Looks like it was not opened.\n'
257 | 'line 2')
258 |
259 | # https://github.com/noahmorrison/chevron/issues/17
260 | def test_callable_1(self):
261 | args_passed = {}
262 |
263 | def first(content, render):
264 | args_passed['content'] = content
265 | args_passed['render'] = render
266 |
267 | return "not implemented"
268 |
269 | args = {
270 | 'template': '{{{postcode}}} {{#first}} {{{city}}} || {{{town}}} '
271 | '|| {{{village}}} || {{{state}}} {{/first}}',
272 | 'data': {
273 | "postcode": "1234",
274 | "city": "Mustache City",
275 | "state": "Nowhere",
276 | "first": first,
277 | }
278 |
279 | }
280 |
281 | result = chevron.render(**args)
282 | expected = '1234 not implemented'
283 | template_content = " {{& city }} || {{& town }} || {{& village }} "\
284 | "|| {{& state }} "
285 |
286 | self.assertEqual(result, expected)
287 | self.assertEqual(args_passed['content'], template_content)
288 |
289 | def test_callable_2(self):
290 |
291 | def first(content, render):
292 | result = render(content)
293 | result = [x.strip() for x in result.split(" || ") if x.strip()]
294 | return result[0]
295 |
296 | args = {
297 | 'template': '{{{postcode}}} {{#first}} {{{city}}} || {{{town}}} '
298 | '|| {{{village}}} || {{{state}}} {{/first}}',
299 | 'data': {
300 | "postcode": "1234",
301 | "town": "Mustache Town",
302 | "state": "Nowhere",
303 | "first": first,
304 | }
305 | }
306 |
307 | result = chevron.render(**args)
308 | expected = '1234 Mustache Town'
309 |
310 | self.assertEqual(result, expected)
311 |
312 | def test_callable_3(self):
313 | '''Test generating some data within the function
314 | '''
315 |
316 | def first(content, render):
317 | result = render(content, {'city': "Injected City"})
318 | result = [x.strip() for x in result.split(" || ") if x.strip()]
319 | return result[0]
320 |
321 | args = {
322 | 'template': '{{{postcode}}} {{#first}} {{{city}}} || {{{town}}} '
323 | '|| {{{village}}} || {{{state}}} {{/first}}',
324 | 'data': {
325 | "postcode": "1234",
326 | "town": "Mustache Town",
327 | "state": "Nowhere",
328 | "first": first,
329 | }
330 | }
331 |
332 | result = chevron.render(**args)
333 | expected = '1234 Injected City'
334 |
335 | self.assertEqual(result, expected)
336 |
337 | def test_callable_4(self):
338 | '''Test render of partial inside lambda
339 | '''
340 |
341 | def function(content, render):
342 | return render(content)
343 |
344 | args = {
345 | 'template': '{{#function}}{{>partial}}{{!comment}}{{/function}}',
346 | 'partials_dict': {
347 | 'partial': 'partial content',
348 | },
349 | 'data': {
350 | 'function': function,
351 | }
352 | }
353 |
354 | result = chevron.render(**args)
355 | expected = 'partial content'
356 |
357 | self.assertEqual(result, expected)
358 |
359 | # https://github.com/noahmorrison/chevron/issues/35
360 | def test_custom_falsy(self):
361 | class CustomData(dict):
362 | class LowercaseBool:
363 | _CHEVRON_return_scope_when_falsy = True
364 |
365 | def __init__(self, value):
366 | self.value = value
367 |
368 | def __bool__(self):
369 | return self.value
370 | __nonzero__ = __bool__
371 |
372 | def __str__(self):
373 | if self.value:
374 | return 'true'
375 | return 'false'
376 |
377 | def __getitem__(self, key):
378 | item = dict.__getitem__(self, key)
379 | if isinstance(item, dict):
380 | return CustomData(item)
381 | if isinstance(item, bool):
382 | return self.LowercaseBool(item)
383 | return item
384 |
385 | args = {
386 | 'data': CustomData({
387 | 'truthy': True,
388 | 'falsy': False,
389 | }),
390 | 'template': '{{ truthy }} {{ falsy }}',
391 | }
392 |
393 | result = chevron.render(**args)
394 | expected = 'true false'
395 |
396 | self.assertEqual(result, expected)
397 |
398 | # https://github.com/noahmorrison/chevron/issues/39
399 | def test_nest_loops_with_same_key(self):
400 | args = {
401 | 'template': 'A{{#x}}B{{#x}}{{.}}{{/x}}C{{/x}}D',
402 | 'data': {'x': ['z', 'x']}
403 | }
404 |
405 | result = chevron.render(**args)
406 | expected = 'ABzxCBzxCD'
407 |
408 | self.assertEqual(result, expected)
409 |
410 | # https://github.com/noahmorrison/chevron/issues/49
411 | def test_partial_indentation(self):
412 | args = {
413 | 'template': '\t{{> count }}',
414 | 'partials_dict': {
415 | 'count': '\tone\n\ttwo'
416 | }
417 | }
418 |
419 | result = chevron.render(**args)
420 | expected = '\t\tone\n\t\ttwo'
421 |
422 | self.assertEqual(result, expected)
423 |
424 | # https://github.com/noahmorrison/chevron/issues/52
425 | def test_indexed(self):
426 | args = {
427 | 'template': 'count {{count.0}}, {{count.1}}, '
428 | '{{count.100}}, {{nope.0}}',
429 | 'data': {
430 | "count": [5, 4, 3, 2, 1],
431 | }
432 | }
433 |
434 | result = chevron.render(**args)
435 | expected = 'count 5, 4, , '
436 |
437 | self.assertEqual(result, expected)
438 |
439 | def test_iterator_scope_indentation(self):
440 | args = {
441 | 'data': {
442 | 'thing': ['foo', 'bar', 'baz'],
443 | },
444 | 'template': '{{> count }}',
445 | 'partials_dict': {
446 | 'count': ' {{> iter_scope }}',
447 | 'iter_scope': 'foobar\n{{#thing}}\n {{.}}\n{{/thing}}'
448 | }
449 | }
450 |
451 | result = chevron.render(**args)
452 | expected = ' foobar\n foo\n bar\n baz\n'
453 |
454 | self.assertEqual(result, expected)
455 |
456 | # https://github.com/noahmorrison/chevron/pull/73
457 | def test_namedtuple_data(self):
458 | NT = collections.namedtuple('NT', ['foo', 'bar'])
459 | args = {
460 | 'template': '{{foo}} {{bar}}',
461 | 'data': NT('hello', 'world')
462 | }
463 |
464 | result = chevron.render(**args)
465 | expected = 'hello world'
466 |
467 | self.assertEqual(result, expected)
468 |
469 | def test_get_key_not_in_dunder_dict_returns_attribute(self):
470 | class C:
471 | foo = "bar"
472 |
473 | instance = C()
474 | self.assertTrue("foo" not in instance.__dict__)
475 |
476 | args = {
477 | 'template': '{{foo}}',
478 | 'data': instance
479 | }
480 | result = chevron.render(**args)
481 | expected = 'bar'
482 |
483 | self.assertEqual(result, expected)
484 |
485 | def test_disabled_partials(self):
486 | os.chdir('tests')
487 | resultNone = chevron.main('test.mustache', 'data.json',
488 | partials_path=None)
489 |
490 | resultEmpty = chevron.main('test.mustache', 'data.json',
491 | partials_path='')
492 |
493 | with io.open('test-partials-disabled.rendered', 'r', encoding='utf-8') as f:
494 | expected = f.read()
495 | if not python3:
496 | expected = expected.encode('utf-8')
497 |
498 | self.assertEqual(resultNone, expected)
499 | self.assertEqual(resultEmpty, expected)
500 | os.chdir('..')
501 |
502 | # https://github.com/noahmorrison/chevron/pull/94
503 | def test_keep(self):
504 | args = {
505 | 'template': '{{ first }} {{ second }} {{ third }}',
506 | 'data': {
507 | "first": "1st",
508 | "third": "3rd",
509 | },
510 | }
511 |
512 | result = chevron.render(**args)
513 | expected = '1st 3rd'
514 | self.assertEqual(result, expected)
515 |
516 | args['keep'] = True
517 |
518 | result = chevron.render(**args)
519 | expected = '1st {{ second }} 3rd'
520 | self.assertEqual(result, expected)
521 |
522 | args['template'] = '{{first}} {{second}} {{third}}'
523 | result = chevron.render(**args)
524 | expected = '1st {{ second }} 3rd'
525 | self.assertEqual(result, expected)
526 |
527 | args['template'] = '{{ first }} {{ second }} {{ third }}'
528 | result = chevron.render(**args)
529 | expected = '1st {{ second }} 3rd'
530 | self.assertEqual(result, expected)
531 |
532 | # https://github.com/noahmorrison/chevron/pull/94
533 | def test_keep_from_partials(self):
534 | args = {
535 | 'template': '{{ first }} {{> with_missing_key }} {{ third }}',
536 | 'data': {
537 | "first": "1st",
538 | "third": "3rd",
539 | },
540 | 'partials_dict': {
541 | 'with_missing_key': '{{missing_key}}',
542 | },
543 | }
544 |
545 | result = chevron.render(**args)
546 | expected = '1st 3rd'
547 | self.assertEqual(result, expected)
548 |
549 | args['keep'] = True
550 |
551 | result = chevron.render(**args)
552 | expected = '1st {{ missing_key }} 3rd'
553 | self.assertEqual(result, expected)
554 |
555 |
556 | # Run unit tests from command line
557 | if __name__ == "__main__":
558 | unittest.main()
559 |
--------------------------------------------------------------------------------