From 20405cd4d399649eda0d15751a4ab33d86ae0156 Mon Sep 17 00:00:00 2001 From: Khem Raj Date: Tue, 12 Oct 2021 22:29:51 -0700 Subject: python3-pystache: Remove need for python2x This helps compiling with latest setuptools 0.58+ Signed-off-by: Khem Raj Signed-off-by: Trevor Gamblin --- ...ython-versions-remove-py2x-and-fix-tests-.patch | 4150 ++++++++++++++++++++ 1 file changed, 4150 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-pystache/0001-Modernize-python-versions-remove-py2x-and-fix-tests-.patch (limited to 'meta-python/recipes-devtools/python/python3-pystache/0001-Modernize-python-versions-remove-py2x-and-fix-tests-.patch') diff --git a/meta-python/recipes-devtools/python/python3-pystache/0001-Modernize-python-versions-remove-py2x-and-fix-tests-.patch b/meta-python/recipes-devtools/python/python3-pystache/0001-Modernize-python-versions-remove-py2x-and-fix-tests-.patch new file mode 100644 index 0000000000..d268cc5cd9 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-pystache/0001-Modernize-python-versions-remove-py2x-and-fix-tests-.patch @@ -0,0 +1,4150 @@ +From 8bb1ac2d81f697598a766714f2c439d78c85d71e Mon Sep 17 00:00:00 2001 +From: Stephen L Arnold +Date: Sat, 7 Nov 2020 12:38:33 -0800 +Subject: [PATCH] Modernize python versions (remove py2x) and fix tests, update + spec + +* migrate to github actions for CI, add conda recipe/workflow +* fix document processing, update pandoc args and history +* convert doctests and modules to py3 +* convert packaging/setup.py to pep517, keep doc processing +* cleanup tox cfg, add coverage, readme status +* add pep8speaks cfg, cleanup warnings, use correct env +* update setup_description.rst for packaging +* set version for test release => 0.6.0 and deploy + +Upstream-Status: Backport [https://github.com/defunkt/pystache/pull/214] +Signed-off-by: Stephen L Arnold +--- + .coveragerc | 38 +++ + .gitchangelog.rc | 295 +++++++++++++++++++++ + .github/workflows/ci.yml | 73 ++++++ + .github/workflows/conda.yml | 55 ++++ + .github/workflows/release.yml | 94 +++++++ + .github/workflows/wheels.yml | 82 ++++++ + .pep8speaks.yml | 15 ++ + HISTORY.md | 37 ++- + MANIFEST.in | 8 +- + README.md | 141 +++++----- + TODO.md | 5 +- + conda/meta.yaml | 50 ++++ + pyproject.toml | 3 + + pystache/__init__.py | 2 +- + pystache/commands/render.py | 4 +- + pystache/common.py | 13 +- + pystache/defaults.py | 2 +- + pystache/loader.py | 14 +- + pystache/parsed.py | 6 +- + pystache/parser.py | 20 +- + pystache/renderengine.py | 2 +- + pystache/renderer.py | 22 +- + pystache/specloader.py | 2 +- + pystache/tests/benchmark.py | 15 +- + pystache/tests/common.py | 10 +- + pystache/tests/examples/unicode_output.py | 2 +- + pystache/tests/main.py | 28 +- + pystache/tests/spectesting.py | 16 +- + pystache/tests/test___init__.py | 4 +- + pystache/tests/test_commands.py | 2 +- + pystache/tests/test_defaults.py | 18 +- + pystache/tests/test_examples.py | 40 +-- + pystache/tests/test_loader.py | 46 ++-- + pystache/tests/test_pystache.py | 6 +- + pystache/tests/test_renderengine.py | 148 +++++------ + pystache/tests/test_renderer.py | 86 +++---- + pystache/tests/test_simple.py | 20 +- + pystache/tests/test_specloader.py | 60 ++--- + setup.cfg | 74 +++++- + setup.py | 134 +--------- + setup_description.rst | 297 +++++++++++++--------- + tox.ini | 118 +++++++-- + travis.yml_disabled | 52 ++++ + 43 files changed, 1487 insertions(+), 672 deletions(-) + create mode 100644 .coveragerc + create mode 100644 .gitchangelog.rc + create mode 100644 .github/workflows/ci.yml + create mode 100644 .github/workflows/conda.yml + create mode 100644 .github/workflows/release.yml + create mode 100644 .github/workflows/wheels.yml + create mode 100644 .pep8speaks.yml + create mode 100644 conda/meta.yaml + create mode 100644 pyproject.toml + create mode 100644 travis.yml_disabled + +diff --git a/.coveragerc b/.coveragerc +new file mode 100644 +index 0000000..9a336dd +--- /dev/null ++++ b/.coveragerc +@@ -0,0 +1,38 @@ ++# .coveragerc to control coverage.py ++[run] ++branch = True ++ ++source = pystache ++ ++omit = ++ .tox/* ++ setup.py ++ pystache/tests/* ++ ++#plugins = ++# coverage_python_version ++ ++[report] ++# must set this to True to see missing ++#show_missing = True ++ ++# Regexes for lines to exclude from consideration ++exclude_lines = ++ # Have to re-enable the standard pragma ++ pragma: no cover ++ ++ # Don't complain about missing debug-only code: ++ def __repr__ ++ if self\.debug ++ ++ # Don't complain if tests don't hit defensive assertion code: ++ raise AssertionError ++ raise NotImplementedError ++ ++ # Don't complain if non-runnable code isn't run: ++ if 0: ++ ++ignore_errors = True ++ ++[html] ++directory = cover +diff --git a/.gitchangelog.rc b/.gitchangelog.rc +new file mode 100644 +index 0000000..5cf63a0 +--- /dev/null ++++ b/.gitchangelog.rc +@@ -0,0 +1,295 @@ ++# -*- coding: utf-8; mode: python -*- ++## ++## Format ++## ++## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] ++## ++## Description ++## ++## ACTION is one of 'chg', 'fix', 'new' ++## ++## Is WHAT the change is about. ++## ++## 'chg' is for refactor, small improvement, cosmetic changes... ++## 'fix' is for bug fixes ++## 'new' is for new features, big improvement ++## ++## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' ++## ++## Is WHO is concerned by the change. ++## ++## 'dev' is for developpers (API changes, refactors...) ++## 'usr' is for final users (UI changes) ++## 'pkg' is for packagers (packaging changes) ++## 'test' is for testers (test only related changes) ++## 'doc' is for doc guys (doc only changes) ++## ++## COMMIT_MSG is ... well ... the commit message itself. ++## ++## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' ++## ++## They are preceded with a '!' or a '@' (prefer the former, as the ++## latter is wrongly interpreted in github.) Commonly used tags are: ++## ++## 'refactor' is obviously for refactoring code only ++## 'minor' is for a very meaningless change (a typo, adding a comment) ++## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) ++## 'wip' is for partial functionality but complete subfunctionality. ++## ++## Example: ++## ++## new: usr: support of bazaar implemented ++## chg: re-indentend some lines !cosmetic ++## new: dev: updated code to be compatible with last version of killer lib. ++## fix: pkg: updated year of licence coverage. ++## new: test: added a bunch of test around user usability of feature X. ++## fix: typo in spelling my name in comment. !minor ++## ++## Please note that multi-line commit message are supported, and only the ++## first line will be considered as the "summary" of the commit message. So ++## tags, and other rules only applies to the summary. The body of the commit ++## message will be displayed in the changelog without reformatting. ++ ++ ++## ++## ``ignore_regexps`` is a line of regexps ++## ++## Any commit having its full commit message matching any regexp listed here ++## will be ignored and won't be reported in the changelog. ++## ++ignore_regexps = [ ++ r'@minor', r'!minor', ++ r'@cosmetic', r'!cosmetic', ++ r'@refactor', r'!refactor', ++ r'@wip', r'!wip', ++ r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', ++ r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', ++ r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', ++ r'^$', ## ignore commits with empty messages ++] ++ ++ ++## ``section_regexps`` is a list of 2-tuples associating a string label and a ++## list of regexp ++## ++## Commit messages will be classified in sections thanks to this. Section ++## titles are the label, and a commit is classified under this section if any ++## of the regexps associated is matching. ++## ++## Please note that ``section_regexps`` will only classify commits and won't ++## make any changes to the contents. So you'll probably want to go check ++## ``subject_process`` (or ``body_process``) to do some changes to the subject, ++## whenever you are tweaking this variable. ++## ++section_regexps = [ ++ ('New', [ ++ r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ++ ]), ++ ('Features', [ ++ r'^([nN]ew|[fF]eat)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ++ ]), ++ ('Changes', [ ++ r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ++ ]), ++ ('Fixes', [ ++ r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ++ ]), ++ ++ ('Other', None ## Match all lines ++ ), ++] ++ ++ ++## ``body_process`` is a callable ++## ++## This callable will be given the original body and result will ++## be used in the changelog. ++## ++## Available constructs are: ++## ++## - any python callable that take one txt argument and return txt argument. ++## ++## - ReSub(pattern, replacement): will apply regexp substitution. ++## ++## - Indent(chars=" "): will indent the text with the prefix ++## Please remember that template engines gets also to modify the text and ++## will usually indent themselves the text if needed. ++## ++## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns ++## ++## - noop: do nothing ++## ++## - ucfirst: ensure the first letter is uppercase. ++## (usually used in the ``subject_process`` pipeline) ++## ++## - final_dot: ensure text finishes with a dot ++## (usually used in the ``subject_process`` pipeline) ++## ++## - strip: remove any spaces before or after the content of the string ++## ++## - SetIfEmpty(msg="No commit message."): will set the text to ++## whatever given ``msg`` if the current text is empty. ++## ++## Additionally, you can `pipe` the provided filters, for instance: ++#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") ++#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') ++#body_process = noop ++body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip ++ ++ ++## ``subject_process`` is a callable ++## ++## This callable will be given the original subject and result will ++## be used in the changelog. ++## ++## Available constructs are those listed in ``body_process`` doc. ++subject_process = (strip | ++ ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | ++ SetIfEmpty("No commit message.") | ucfirst | final_dot) ++ ++ ++## ``tag_filter_regexp`` is a regexp ++## ++## Tags that will be used for the changelog must match this regexp. ++## ++#tag_filter_regexp = r'^v?[0-9]+\.[0-9]+(\.[0-9]+)?$' ++tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' ++ ++ ++## ``unreleased_version_label`` is a string or a callable that outputs a string ++## ++## This label will be used as the changelog Title of the last set of changes ++## between last valid tag and HEAD if any. ++unreleased_version_label = "(unreleased)" ++#unreleased_version_label = lambda: swrap( ++# ["git", "describe", "--tags"], ++#shell=False) ++ ++ ++## ``output_engine`` is a callable ++## ++## This will change the output format of the generated changelog file ++## ++## Available choices are: ++## ++## - rest_py ++## ++## Legacy pure python engine, outputs ReSTructured text. ++## This is the default. ++## ++## - mustache() ++## ++## Template name could be any of the available templates in ++## ``templates/mustache/*.tpl``. ++## Requires python package ``pystache``. ++## Examples: ++## - mustache("markdown") ++## - mustache("restructuredtext") ++## ++## - makotemplate() ++## ++## Template name could be any of the available templates in ++## ``templates/mako/*.tpl``. ++## Requires python package ``mako``. ++## Examples: ++## - makotemplate("restructuredtext") ++## ++#output_engine = rest_py ++#output_engine = mustache("restructuredtext") ++output_engine = mustache("markdown") ++#output_engine = makotemplate("restructuredtext") ++ ++ ++## ``include_merge`` is a boolean ++## ++## This option tells git-log whether to include merge commits in the log. ++## The default is to include them. ++include_merge = True ++ ++ ++## ``log_encoding`` is a string identifier ++## ++## This option tells gitchangelog what encoding is outputed by ``git log``. ++## The default is to be clever about it: it checks ``git config`` for ++## ``i18n.logOutputEncoding``, and if not found will default to git's own ++## default: ``utf-8``. ++#log_encoding = 'utf-8' ++ ++ ++## ``publish`` is a callable ++## ++## Sets what ``gitchangelog`` should do with the output generated by ++## the output engine. ``publish`` is a callable taking one argument ++## that is an interator on lines from the output engine. ++## ++## Some helper callable are provided: ++## ++## Available choices are: ++## ++## - stdout ++## ++## Outputs directly to standard output ++## (This is the default) ++## ++## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start(), flags) ++## ++## Creates a callable that will parse given file for the given ++## regex pattern and will insert the output in the file. ++## ``idx`` is a callable that receive the matching object and ++## must return a integer index point where to insert the ++## the output in the file. Default is to return the position of ++## the start of the matched string. ++## ++## - FileRegexSubst(file, pattern, replace, flags) ++## ++## Apply a replace inplace in the given file. Your regex pattern must ++## take care of everything and might be more complex. Check the README ++## for a complete copy-pastable example. ++## ++# publish = FileInsertIntoFirstRegexMatch( ++# "CHANGELOG.rst", ++# r'/(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/', ++# idx=lambda m: m.start(1) ++# ) ++#publish = stdout ++ ++ ++## ``revs`` is a list of callable or a list of string ++## ++## callable will be called to resolve as strings and allow dynamical ++## computation of these. The result will be used as revisions for ++## gitchangelog (as if directly stated on the command line). This allows ++## to filter exaclty which commits will be read by gitchangelog. ++## ++## To get a full documentation on the format of these strings, please ++## refer to the ``git rev-list`` arguments. There are many examples. ++## ++## Using callables is especially useful, for instance, if you ++## are using gitchangelog to generate incrementally your changelog. ++## ++## Some helpers are provided, you can use them:: ++## ++## - FileFirstRegexMatch(file, pattern): will return a callable that will ++## return the first string match for the given pattern in the given file. ++## If you use named sub-patterns in your regex pattern, it'll output only ++## the string matching the regex pattern named "rev". ++## ++## - Caret(rev): will return the rev prefixed by a "^", which is a ++## way to remove the given revision and all its ancestor. ++## ++## Please note that if you provide a rev-list on the command line, it'll ++## replace this value (which will then be ignored). ++## ++## If empty, then ``gitchangelog`` will act as it had to generate a full ++## changelog. ++## ++## The default is to use all commits to make the changelog. ++#revs = ["^1.0.3", ] ++#revs = [ ++# Caret( ++# FileFirstRegexMatch( ++# "CHANGELOG.rst", ++# r"(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")), ++# "HEAD" ++#] ++revs = [] +diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml +new file mode 100644 +index 0000000..356dd2c +--- /dev/null ++++ b/.github/workflows/ci.yml +@@ -0,0 +1,73 @@ ++# This workflow will install Python dependencies, run tests and lint with a variety of Python versions ++# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions ++ ++name: ci ++ ++on: ++ push: ++ branches: [ master ] ++ pull_request: ++ branches: [ master ] ++ ++jobs: ++ build: ++ ++ runs-on: ${{ matrix.os }} ++ defaults: ++ run: ++ shell: bash ++ env: ++ OS: ${{ matrix.os }} ++ PYTHON: ${{ matrix.python-version }} ++ PYTHONIOENCODING: utf-8 ++ PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache ++ strategy: ++ fail-fast: false ++ matrix: ++ os: [ubuntu-20.04, macos-latest, windows-latest] ++ python-version: [3.6, 3.7, 3.8, 3.9] ++ steps: ++ - name: Set git crlf/eol ++ run: | ++ git config --global core.autocrlf false ++ git config --global core.eol lf ++ ++ - uses: actions/checkout@v2 ++ with: ++ submodules: True ++ ++ - name: Set up Python ${{ matrix.python-version }} ++ uses: actions/setup-python@v2 ++ with: ++ python-version: ${{ matrix.python-version }} ++ ++ - name: Install dependencies ++ run: | ++ python -m pip install --upgrade pip ++ pip install tox tox-gh-actions ++ ++ - name: Run tests with coverage ++ run: | ++ tox ++ env: ++ PLATFORM: ${{ matrix.os }} ++ ++ - name: Upload coverage to Codecov ++ uses: codecov/codecov-action@v1 ++ with: ++ env_vars: OS,PYTHON ++ ++ - name: Test with specs and pystache-test ++ run: | ++ tox -e setup . ext/spec/specs ++ ++ - name: Check pkg builds ++ run: | ++ tox -e deploy ++ ++ - name: Check docs ++ if: runner.os == 'Linux' ++ run: | ++ sudo apt-get -qq update ++ sudo apt-get install -y pandoc ++ tox -e docs +diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml +new file mode 100644 +index 0000000..261f9ad +--- /dev/null ++++ b/.github/workflows/conda.yml +@@ -0,0 +1,55 @@ ++name: Conda ++ ++on: ++ workflow_dispatch: ++ pull_request: ++ push: ++ branches: ++ - master ++ ++jobs: ++ build: ++ strategy: ++ fail-fast: false ++ matrix: ++ platform: [ubuntu-18.04, windows-latest, macos-latest] ++ python-version: [3.6, 3.7, 3.8, 3.9] ++ ++ runs-on: ${{ matrix.platform }} ++ ++ # The setup-miniconda action needs this to activate miniconda ++ defaults: ++ run: ++ shell: "bash -l {0}" ++ ++ steps: ++ - uses: actions/checkout@v2 ++ with: ++ fetch-depth: 0 ++ ++ - name: Cache conda ++ uses: actions/cache@v1 ++ with: ++ path: ~/conda_pkgs_dir ++ key: ${{matrix.os}}-conda-pkgs-${{hashFiles('**/conda/meta.yaml')}} ++ ++ - name: Get conda ++ uses: conda-incubator/setup-miniconda@v2 ++ with: ++ python-version: ${{ matrix.python-version }} ++ channels: conda-forge ++ channel-priority: strict ++ use-only-tar-bz2: true ++ auto-activate-base: true ++ ++ - name: Prepare ++ run: conda install conda-build conda-verify ++ ++ - name: Build ++ run: conda build conda ++ ++ - name: Install ++ run: conda install -c ${CONDA_PREFIX}/conda-bld/ pystache ++ ++ - name: Test ++ run: python test_pystache.py +diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml +new file mode 100644 +index 0000000..f33c4b5 +--- /dev/null ++++ b/.github/workflows/release.yml +@@ -0,0 +1,94 @@ ++name: Release ++ ++on: ++ push: ++ # release on tag push ++ tags: ++ - '*' ++ ++jobs: ++ wheels: ++ ++ runs-on: ${{ matrix.os }} ++ defaults: ++ run: ++ shell: bash ++ env: ++ PYTHONIOENCODING: utf-8 ++ strategy: ++ fail-fast: false ++ matrix: ++ os: [ubuntu-18.04, macos-latest, windows-latest] ++ python-version: [3.6, 3.7, 3.8, 3.9] ++ exclude: ++ - os: windows-latest ++ python-version: 2.7 ++ ++ steps: ++ - name: Set git crlf/eol ++ run: | ++ git config --global core.autocrlf false ++ git config --global core.eol lf ++ ++ - uses: actions/checkout@v2 ++ with: ++ fetch-depth: 0 ++ ++ - name: Set up Python ${{ matrix.python-version }} ++ uses: actions/setup-python@v2 ++ with: ++ python-version: ${{ matrix.python-version }} ++ ++ - name: Install dependencies ++ run: | ++ python -m pip install --upgrade pip wheel ++ pip install tox tox-gh-actions ++ ++ - name: Build dist pkgs ++ run: | ++ tox -e deploy ++ ++ - name: Upload artifacts ++ if: matrix.python-version == 3.7 && runner.os == 'Linux' ++ uses: actions/upload-artifact@v2 ++ with: ++ name: wheels ++ path: ./dist/*.whl ++ ++ create_release: ++ name: Create Release ++ needs: [wheels] ++ runs-on: ubuntu-18.04 ++ ++ steps: ++ - name: Get version ++ id: get_version ++ run: | ++ echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV ++ echo ${{ env.VERSION }} ++ ++ - uses: actions/checkout@v2 ++ with: ++ fetch-depth: 0 ++ ++ # download all artifacts to project dir ++ - uses: actions/download-artifact@v2 ++ ++ - name: Generate changes file ++ uses: sarnold/gitchangelog-action@master ++ with: ++ github_token: ${{ secrets.GITHUB_TOKEN}} ++ ++ - name: Create release ++ id: create_release ++ uses: softprops/action-gh-release@v1 ++ env: ++ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ++ with: ++ tag_name: ${{ env.VERSION }} ++ name: Release v${{ env.VERSION }} ++ body_path: CHANGES.md ++ draft: false ++ prerelease: false ++ files: | ++ wheels/pystache*.whl +diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml +new file mode 100644 +index 0000000..58f0c5e +--- /dev/null ++++ b/.github/workflows/wheels.yml +@@ -0,0 +1,82 @@ ++name: Wheels ++ ++on: ++ workflow_dispatch: ++ pull_request: ++ #push: ++ #branches: [ master ] ++ ++jobs: ++ build: ++ ++ runs-on: ${{ matrix.os }} ++ defaults: ++ run: ++ shell: bash ++ env: ++ PYTHONIOENCODING: utf-8 ++ strategy: ++ fail-fast: false ++ matrix: ++ os: [ubuntu-18.04, macos-latest, windows-latest] ++ python-version: [3.6, 3.7, 3.8, 3.9] ++ ++ steps: ++ - name: Set git crlf/eol ++ run: | ++ git config --global core.autocrlf false ++ git config --global core.eol lf ++ ++ - uses: actions/checkout@v2 ++ with: ++ fetch-depth: 0 ++ ++ - name: Set up Python ${{ matrix.python-version }} ++ uses: actions/setup-python@v2 ++ with: ++ python-version: ${{ matrix.python-version }} ++ ++ - name: Install dependencies ++ run: | ++ python -m pip install --upgrade pip wheel ++ pip install tox tox-gh-actions ++ ++ - name: Build dist pkgs ++ run: | ++ tox -e deploy ++ ++ - name: Upload artifacts ++ if: matrix.python-version == 3.7 && runner.os == 'Linux' ++ uses: actions/upload-artifact@v2 ++ with: ++ name: wheels ++ path: ./dist/*.whl ++ ++ check_artifact: ++ name: Check wheel artifact ++ needs: [build] ++ runs-on: ${{ matrix.os }} ++ defaults: ++ run: ++ shell: bash ++ env: ++ PYTHONIOENCODING: utf-8 ++ strategy: ++ fail-fast: false ++ matrix: ++ os: [ubuntu-18.04, macos-latest, windows-latest] ++ python-version: [3.6, 3.8, 3.9] ++ ++ steps: ++ - name: Set up Python ${{ matrix.python-version }} ++ uses: actions/setup-python@v2 ++ with: ++ python-version: ${{ matrix.python-version }} ++ ++ # download all artifacts to project dir ++ - uses: actions/download-artifact@v2 ++ ++ - name: Check wheel install ++ run: | ++ bash -c 'export WHL=$(ls wheels/*.whl); python -m pip install $WHL' ++ pystache-test +diff --git a/.pep8speaks.yml b/.pep8speaks.yml +new file mode 100644 +index 0000000..e841b66 +--- /dev/null ++++ b/.pep8speaks.yml +@@ -0,0 +1,15 @@ ++scanner: ++ linter: flake8 # Other option is pycodestyle ++ ++no_blank_comment: False # If True, no comment is made on PR without any errors. ++descending_issues_order: True # If True, PEP 8 issues in message will be displayed in descending order of line numbers in the file ++ ++[flake8] ++exclude = ++ .git, ++ .github, ++ __pycache__, ++ build, ++ dist ++ ++max-line-length = 110 +diff --git a/HISTORY.md b/HISTORY.md +index e5b7638..60b6308 100644 +--- a/HISTORY.md ++++ b/HISTORY.md +@@ -1,7 +1,42 @@ + History + ======= + +-**Note:** Official support for Python 2.4 will end with Pystache version 0.6.0. ++**Note:** Official support for Python 2.7 will end with Pystache version 0.6.0. ++ ++0.6.0 (2021-03-04) ++------------------ ++ ++- Bump spec versions to latest => v1.1.3 ++- Modernize python and CI tools, update docs/doctests ++- Update unicode conversion test for py3-only ++- Add pep8speaks cfg, cleanup warnings ++- Remove superfluous setup test/unused imports ++- Add conda recipe/CI build ++ ++0.5.6 (2021-02-28) ++------------------ ++ ++- Use correct wheel name in release workflow, limit wheels ++- Add install check/test of downloaded wheel ++- Update/add ci workflows and tox cfg, bump to next dev0 version ++ ++0.5.5 (2020-12-16) ++------------------ ++ ++- fix document processing, update pandoc args and history ++- add release.yml to CI, test env settings ++- fix bogus commit message, update versions and tox cf ++- add post-test steps for building pkgs with/without doc updates ++- add CI build check, fix MANIFEST.in pruning ++ ++0.5.4-2 (2020-11-09) ++-------------------- ++ ++- Merge pull request #1 from sarnold/rebase-up ++- Bugfix: test_specloader.py: fix test_find__with_directory on other OSs ++- Bugfix: pystache/loader.py: remove stray windows line-endings ++- fix crufty (and insecure) http urls ++- Bugfix: modernize python versions (keep py27) and fix spec_test load cmd + + 0.5.4 (2014-07-11) + ------------------ +diff --git a/MANIFEST.in b/MANIFEST.in +index bdc64bf..1593143 100644 +--- a/MANIFEST.in ++++ b/MANIFEST.in +@@ -1,7 +1,4 @@ +-include README.md +-include HISTORY.md +-include LICENSE +-include TODO.md ++include README.md HISTORY.md TODO.md LICENSE + include setup_description.rst + include tox.ini + include test_pystache.py +@@ -11,3 +8,6 @@ recursive-include pystache/tests *.mustache *.txt + # We deliberately exclude the gh/ directory because it contains copies + # of resources needed only for the web page hosted on GitHub (via the + # gh-pages branch). ++exclude *.ini *travis* ++prune gh ++prune .git* +diff --git a/README.md b/README.md +index 54a9608..1203b7a 100644 +--- a/README.md ++++ b/README.md +@@ -10,11 +10,25 @@ Pystache + + + +-![](http://defunkt.github.com/pystache/images/logo_phillips.png "mustachioed, monocled snake by David Phillips") ++[![ci](https://github.com/sarnold/pystache/actions/workflows/ci.yml/badge.svg)](https://github.com/sarnold/pystache/actions/workflows/ci.yml) ++[![Conda](https://github.com/sarnold/pystache/actions/workflows/conda.yml/badge.svg)](https://github.com/sarnold/pystache/actions/workflows/conda.yml) ++[![Wheels](https://github.com/sarnold/pystache/actions/workflows/wheels.yml/badge.svg)](https://github.com/sarnold/pystache/actions/workflows/wheels.yml) ++[![Release](https://github.com/sarnold/pystache/actions/workflows/release.yml/badge.svg)](https://github.com/sarnold/pystache/actions/workflows/release.yml) ++[![Python](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) + +-![](https://secure.travis-ci.org/defunkt/pystache.png "Travis CI current build status") ++[![Latest release](https://img.shields.io/github/v/release/sarnold/pystache?include_prereleases)](https://github.com/sarnold/pystache/releases/latest) ++[![License](https://img.shields.io/github/license/sarnold/pystache)](https://github.com/sarnold/pystache/blob/master/LICENSE) ++[![Maintainability](https://api.codeclimate.com/v1/badges/a8fa1bf4638bfc6581b6/maintainability)](https://codeclimate.com/github/sarnold/pystache/maintainability) ++[![codecov](https://codecov.io/gh/sarnold/pystache/branch/master/graph/badge.svg?token=5PZNMZBI6K)](https://codecov.io/gh/sarnold/pystache) + +-[Pystache](http://defunkt.github.com/pystache) is a Python ++ ++ ++This updated fork of Pystache is currently tested on Python 3.6+ and in ++Conda, on Linux, Macos, and Windows (Python 2.7 support has been removed). ++ ++![](gh/images/logo_phillips_small.png "mustachioed, monocled snake by David Phillips") ++ ++[Pystache](http://sarnold.github.com/pystache) is a Python + implementation of [Mustache](http://mustache.github.com/). Mustache is a + framework-agnostic, logic-free templating system inspired by + [ctemplate](http://code.google.com/p/google-ctemplate/) and +@@ -27,10 +41,10 @@ provides a good introduction to Mustache's syntax. For a more complete + (and more current) description of Mustache's behavior, see the official + [Mustache spec](https://github.com/mustache/spec). + +-Pystache is [semantically versioned](http://semver.org) and can be found +-on [PyPI](http://pypi.python.org/pypi/pystache). This version of +-Pystache passes all tests in [version +-1.1.2](https://github.com/mustache/spec/tree/v1.1.2) of the spec. ++Pystache is [semantically versioned](http://semver.org) and older versions ++can still be found on [PyPI](http://pypi.python.org/pypi/pystache). This ++version of Pystache now passes all tests in [version ++1.1.3](https://github.com/mustache/spec/tree/v1.1.3) of the spec. + + + Requirements +@@ -38,41 +52,25 @@ Requirements + + Pystache is tested with-- + +-- Python 2.4 (requires simplejson [version +- 2.0.9](http://pypi.python.org/pypi/simplejson/2.0.9) or earlier) +-- Python 2.5 (requires +- [simplejson](http://pypi.python.org/pypi/simplejson/)) +-- Python 2.6 +-- Python 2.7 +-- Python 3.1 +-- Python 3.2 +-- Python 3.3 +-- [PyPy](http://pypy.org/) ++- Python 3.6 ++- Python 3.7 ++- Python 3.8 ++- Python 3.9 ++- Conda (py36-py39) + + [Distribute](http://packages.python.org/distribute/) (the setuptools fork) +-is recommended over [setuptools](http://pypi.python.org/pypi/setuptools), +-and is required in some cases (e.g. for Python 3 support). +-If you use [pip](http://www.pip-installer.org/), you probably already satisfy +-this requirement. ++is no longer required over [setuptools](http://pypi.python.org/pypi/setuptools), ++as the current packaging is now PEP517-compliant. + + JSON support is needed only for the command-line interface and to run +-the spec tests. We require simplejson for earlier versions of Python +-since Python's [json](http://docs.python.org/library/json.html) module +-was added in Python 2.6. +- +-For Python 2.4 we require an earlier version of simplejson since +-simplejson stopped officially supporting Python 2.4 in simplejson +-version 2.1.0. Earlier versions of simplejson can be installed manually, +-as follows: ++the spec tests; PyYAML can still be used (see the Develop section). + +- pip install 'simplejson<2.1.0' +- +-Official support for Python 2.4 will end with Pystache version 0.6.0. ++Official support for Python 2 will end with Pystache version 0.6.0. + + Install It + ---------- + +- pip install pystache ++ pip install -U pystache -f https://github.com/sarnold/pystache/releases/ + + And test it-- + +@@ -85,12 +83,12 @@ Use It + ------ + + >>> import pystache +- >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'}) ++ >>> print(pystache.render('Hi {{person}}!', {'person': 'Mom'})) + Hi Mom! + + You can also create dedicated view classes to hold your view logic. + +-Here's your view class (in .../examples/readme.py): ++Here's your view class (in ../pystache/tests/examples/readme.py): + + class SayHello(object): + def to(self): +@@ -109,7 +107,7 @@ directory as your class definition): + Pull it together: + + >>> renderer = pystache.Renderer() +- >>> print renderer.render(hello) ++ >>> print(renderer.render(hello)) + Hello, Pizza! + + For greater control over rendering (e.g. to specify a custom template +@@ -117,22 +115,22 @@ directory), use the `Renderer` class like above. One can pass attributes + to the Renderer class constructor or set them on a Renderer instance. To + customize template loading on a per-view basis, subclass `TemplateSpec`. + See the docstrings of the +-[Renderer](https://github.com/defunkt/pystache/blob/master/pystache/renderer.py) ++[Renderer](https://github.com/sarnold/pystache/blob/master/pystache/renderer.py) + class and +-[TemplateSpec](https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py) ++[TemplateSpec](https://github.com/sarnold/pystache/blob/master/pystache/template_spec.py) + class for more information. + + You can also pre-parse a template: + + >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") +- >>> print parsed +- [u'Hey ', _SectionNode(key=u'who', index_begin=12, index_end=18, parsed=[_EscapeNode(key=u'.'), u'!'])] ++ >>> print(parsed) ++ ['Hey ', _SectionNode(key='who', index_begin=12, index_end=18, parsed=[_EscapeNode(key='.'), '!'])] + + And then: + +- >>> print renderer.render(parsed, {'who': 'Pops'}) ++ >>> print(renderer.render(parsed, {'who': 'Pops'})) + Hey Pops! +- >>> print renderer.render(parsed, {'who': 'you'}) ++ >>> print(renderer.render(parsed, {'who': 'you'})) + Hey you! + + Python 3 +@@ -194,15 +192,16 @@ To test from a source distribution (without installing)-- + python test_pystache.py + + To test Pystache with multiple versions of Python (with a single +-command!), you can use [tox](http://pypi.python.org/pypi/tox): ++command!) and different platforms, you can use [tox](http://pypi.python.org/pypi/tox): ++ ++ pip install tox ++ tox -e setup + +- pip install 'virtualenv<1.8' # Version 1.8 dropped support for Python 2.4. +- pip install 'tox<1.4' # Version 1.4 dropped support for Python 2.4. +- tox ++To run tests on multiple versions with coverage, run: + +-If you do not have all Python versions listed in `tox.ini`-- ++ tox -e py38-linux,py39-linux # for example + +- tox -e py26,py32 # for example ++(substitute your platform above, eg, macos or windows) + + The source distribution tests also include doctests and tests from the + Mustache spec. To include tests from the Mustache spec in your test +@@ -217,57 +216,33 @@ parses the json files. To install PyYAML-- + + pip install pyyaml + ++Once the submodule is available, you can run the full test set with: ++ ++ tox -e setup . ext/spec/specs ++ + To run a subset of the tests, you can use + [nose](http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html): + + pip install nose + nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present + +-### Using Python 3 with Pystache from source +- +-Pystache is written in Python 2 and must be converted to Python 3 prior to +-using it with Python 3. The installation process (and tox) do this +-automatically. + +-To convert the code to Python 3 manually (while using Python 3)-- ++Mailing List (old) ++------------------ + +- python setup.py build +- +-This writes the converted code to a subdirectory called `build`. +-By design, Python 3 builds +-[cannot](https://bitbucket.org/tarek/distribute/issue/292/allow-use_2to3-with-python-2) +-be created from Python 2. +- +-To convert the code without using setup.py, you can use +-[2to3](http://docs.python.org/library/2to3.html) as follows (two steps)-- +- +- 2to3 --write --nobackups --no-diffs --doctests_only pystache +- 2to3 --write --nobackups --no-diffs pystache +- +-This converts the code (and doctests) in place. +- +-To `import pystache` from a source distribution while using Python 3, be +-sure that you are importing from a directory containing a converted +-version of the code (e.g. from the `build` directory after converting), +-and not from the original (unconverted) source directory. Otherwise, you will +-get a syntax error. You can help prevent this by not running the Python +-IDE from the project directory when importing Pystache while using Python 3. +- +- +-Mailing List +------------- +- +-There is a [mailing list](http://librelist.com/browser/pystache/). Note ++There is(was) a [mailing list](http://librelist.com/browser/pystache/). Note + that there is a bit of a delay between posting a message and seeing it + appear in the mailing list archive. + + Credits + ------- + +- >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' } +- >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context) ++ >>> import pystache ++ >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek','refurbisher': 'Steve Arnold' } ++ >>> print(pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}\nRefurbisher: {{refurbisher}}", context)) + Author: Chris Wanstrath + Maintainer: Chris Jerdonek ++ Refurbisher: Steve Arnold + + Pystache logo by [David Phillips](http://davidphillips.us/) is licensed + under a [Creative Commons Attribution-ShareAlike 3.0 Unported +diff --git a/TODO.md b/TODO.md +index cd82417..76853a4 100644 +--- a/TODO.md ++++ b/TODO.md +@@ -6,11 +6,10 @@ In development branch: + * Figure out a way to suppress center alignment of images in reST output. + * Add a unit test for the change made in 7ea8e7180c41. This is with regard + to not requiring spec tests when running tests from a downloaded sdist. +-* End support for Python 2.4. +-* Add Python 3.3 to tox file (after deprecating 2.4). ++* End support for Python 2.7 (done as of 03/03/21 - SA) ++* Release 0.6.0 on github, make a pypi account (SA) + * Turn the benchmarking script at pystache/tests/benchmark.py into a command + in pystache/commands, or make it a subcommand of one of the existing + commands (i.e. using a command argument). + * Provide support for logging in at least one of the commands. +-* Make sure command parsing to pystache-test doesn't break with Python 2.4 and earlier. + * Combine pystache-test with the main command. +diff --git a/conda/meta.yaml b/conda/meta.yaml +new file mode 100644 +index 0000000..e7f4fd9 +--- /dev/null ++++ b/conda/meta.yaml +@@ -0,0 +1,50 @@ ++{% set name = "pystache" %} ++{% set version = "0.6.0.dev0" %} ++ ++package: ++ name: {{ name|lower }} ++ version: {{ version }} ++ ++source: ++ path: .. ++ ++build: ++ number: 0 ++ script: {{ PYTHON }} -m pip install . --no-deps --ignore-installed -vvv ++ noarch: python ++ entry_points: ++ - pystache = pystache.commands.render:main ++ - pystache-test = pystache.commands.test:main ++ ++requirements: ++ build: ++ - python ++ - setuptools ++ ++ run: ++ - python ++ ++test: ++ imports: ++ - pystache ++ - pystache.commands ++ - pystache.tests ++ - pystache.tests.data ++ - pystache.tests.data.locator ++ - pystache.tests.examples ++ ++ commands: ++ - pystache --help ++ - pystache-test ++ ++ ++about: ++ home: https://github.com/sarnold/pystache ++ license: MIT ++ license_family: MIT ++ license_file: LICENSE ++ summary: Mustache for Python ++ ++extra: ++ recipe-maintainers: ++ - sarnold +diff --git a/pyproject.toml b/pyproject.toml +new file mode 100644 +index 0000000..2f21011 +--- /dev/null ++++ b/pyproject.toml +@@ -0,0 +1,3 @@ ++[build-system] ++requires = ["setuptools>=40.8.0", "wheel"] ++build-backend = "setuptools.build_meta" +diff --git a/pystache/__init__.py b/pystache/__init__.py +index 4cf2434..5edc1c5 100644 +--- a/pystache/__init__.py ++++ b/pystache/__init__.py +@@ -10,4 +10,4 @@ from pystache.init import parse, render, Renderer, TemplateSpec + + __all__ = ['parse', 'render', 'Renderer', 'TemplateSpec'] + +-__version__ = '0.5.4' # Also change in setup.py. ++__version__ = '0.6.0' +diff --git a/pystache/commands/render.py b/pystache/commands/render.py +index 1a9c309..9c913e7 100644 +--- a/pystache/commands/render.py ++++ b/pystache/commands/render.py +@@ -22,7 +22,7 @@ except: + from sys import exc_info + ex_type, ex_value, tb = exc_info() + new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) +- raise new_ex.__class__, new_ex, tb ++ raise new_ex.__class__(new_ex).with_traceback(tb) + + # The optparse module is deprecated in Python 2.7 in favor of argparse. + # However, argparse is not available in Python 2.6 and earlier. +@@ -88,7 +88,7 @@ def main(sys_argv=sys.argv): + context = json.loads(context) + + rendered = renderer.render(template, context) +- print rendered ++ print(rendered) + + + if __name__=='__main__': +diff --git a/pystache/common.py b/pystache/common.py +index fb266dd..0e9b091 100644 +--- a/pystache/common.py ++++ b/pystache/common.py +@@ -5,17 +5,12 @@ Exposes functionality needed throughout the project. + + """ + +-from sys import version_info + + def _get_string_types(): +- # TODO: come up with a better solution for this. One of the issues here +- # is that in Python 3 there is no common base class for unicode strings +- # and byte strings, and 2to3 seems to convert all of "str", "unicode", +- # and "basestring" to Python 3's "str". +- if version_info < (3, ): +- return basestring +- # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3. +- return (unicode, type(u"a".encode('utf-8'))) ++ """ ++ Return the Python3 string type (no more python2) ++ """ ++ return (str, type("a".encode('utf-8'))) + + + _STRING_TYPES = _get_string_types() +diff --git a/pystache/defaults.py b/pystache/defaults.py +index bcfdf4c..2fab0e0 100644 +--- a/pystache/defaults.py ++++ b/pystache/defaults.py +@@ -39,7 +39,7 @@ STRING_ENCODING = sys.getdefaultencoding() + FILE_ENCODING = sys.getdefaultencoding() + + # The delimiters to start with when parsing. +-DELIMITERS = (u'{{', u'}}') ++DELIMITERS = ('{{', '}}') + + # How to handle missing tags when rendering a template. + MISSING_TAGS = MissingTags.ignore +diff --git a/pystache/loader.py b/pystache/loader.py +index d4a7e53..ea01d17 100644 +--- a/pystache/loader.py ++++ b/pystache/loader.py +@@ -6,6 +6,7 @@ This module provides a Loader class for locating and reading templates. + """ + + import os ++import platform + import sys + + from pystache import common +@@ -24,7 +25,7 @@ def _make_to_unicode(): + """ + if encoding is None: + encoding = defaults.STRING_ENCODING +- return unicode(s, encoding, defaults.DECODE_ERRORS) ++ return str(s, encoding, defaults.DECODE_ERRORS) + return to_unicode + + +@@ -86,7 +87,7 @@ class Loader(object): + def _make_locator(self): + return Locator(extension=self.extension) + +- def unicode(self, s, encoding=None): ++ def str(self, s, encoding=None): + """ + Convert a string to unicode using the given encoding, and return it. + +@@ -104,8 +105,8 @@ class Loader(object): + Defaults to None. + + """ +- if isinstance(s, unicode): +- return unicode(s) ++ if isinstance(s, str): ++ return str(s) + + return self.to_unicode(s, encoding) + +@@ -118,8 +119,9 @@ class Loader(object): + + if encoding is None: + encoding = self.file_encoding +- +- return self.unicode(b, encoding) ++ if platform.system() == "Windows": ++ return self.str(b, encoding).replace('\r', '') ++ return self.str(b, encoding) + + def load_file(self, file_name): + """ +diff --git a/pystache/parsed.py b/pystache/parsed.py +index 372d96c..75d417d 100644 +--- a/pystache/parsed.py ++++ b/pystache/parsed.py +@@ -41,10 +41,10 @@ class ParsedTemplate(object): + """ + # We avoid use of the ternary operator for Python 2.4 support. + def get_unicode(node): +- if type(node) is unicode: ++ if type(node) is str: + return node + return node.render(engine, context) +- parts = map(get_unicode, self._parse_tree) ++ parts = list(map(get_unicode, self._parse_tree)) + s = ''.join(parts) + +- return unicode(s) ++ return str(s) +diff --git a/pystache/parser.py b/pystache/parser.py +index c6a171f..1afd50a 100644 +--- a/pystache/parser.py ++++ b/pystache/parser.py +@@ -11,8 +11,8 @@ from pystache import defaults + from pystache.parsed import ParsedTemplate + + +-END_OF_LINE_CHARACTERS = [u'\r', u'\n'] +-NON_BLANK_RE = re.compile(ur'^(.)', re.M) ++END_OF_LINE_CHARACTERS = ['\r', '\n'] ++NON_BLANK_RE = re.compile(r'^(.)', re.M) + + + # TODO: add some unit tests for this. +@@ -30,12 +30,12 @@ def parse(template, delimiters=None): + + Examples: + +- >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}") +- >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3. ++ >>> parsed = parse("Hey {{#who}}{{name}}!{{/who}}") ++ >>> print(str(parsed).replace('u', '')) # This is an old hack. + ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])] + + """ +- if type(template) is not unicode: ++ if type(template) is not str: + raise Exception("Template is not unicode: %s" % type(template)) + parser = _Parser(delimiters) + return parser.parse(template) +@@ -94,7 +94,7 @@ class _CommentNode(object): + return _format(self) + + def render(self, engine, context): +- return u'' ++ return '' + + + class _ChangeNode(object): +@@ -106,7 +106,7 @@ class _ChangeNode(object): + return _format(self) + + def render(self, engine, context): +- return u'' ++ return '' + + + class _EscapeNode(object): +@@ -147,7 +147,7 @@ class _PartialNode(object): + def render(self, engine, context): + template = engine.resolve_partial(self.key) + # Indent before rendering. +- template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template) ++ template = re.sub(NON_BLANK_RE, self.indent + r'\1', template) + + return engine.render(template, context) + +@@ -168,7 +168,7 @@ class _InvertedNode(object): + # Note that lambdas are considered truthy for inverted sections + # per the spec. + if data: +- return u'' ++ return '' + return self.parsed_section.render(engine, context) + + +@@ -218,7 +218,7 @@ class _SectionNode(object): + parts.append(self.parsed.render(engine, context)) + context.pop() + +- return unicode(''.join(parts)) ++ return str(''.join(parts)) + + + class _Parser(object): +diff --git a/pystache/renderengine.py b/pystache/renderengine.py +index c797b17..2f1e341 100644 +--- a/pystache/renderengine.py ++++ b/pystache/renderengine.py +@@ -160,7 +160,7 @@ class RenderEngine(object): + if not is_string(val): + # In case the template is an integer, for example. + val = self.to_str(val) +- if type(val) is not unicode: ++ if type(val) is not str: + val = self.literal(val) + return self.render(val, context, delimiters) + +diff --git a/pystache/renderer.py b/pystache/renderer.py +index ff6a90c..064f040 100644 +--- a/pystache/renderer.py ++++ b/pystache/renderer.py +@@ -32,7 +32,7 @@ class Renderer(object): + >>> partials = {'partial': 'Hello, {{thing}}!'} + >>> renderer = Renderer(partials=partials) + >>> # We apply print to make the test work in Python 3 after 2to3. +- >>> print renderer.render('{{>partial}}', {'thing': 'world'}) ++ >>> print(renderer.render('{{>partial}}', {'thing': 'world'})) + Hello, world! + + To customize string coercion (e.g. to render False values as ''), one can +@@ -130,7 +130,7 @@ class Renderer(object): + if string_encoding is None: + string_encoding = defaults.STRING_ENCODING + +- if isinstance(search_dirs, basestring): ++ if isinstance(search_dirs, str): + search_dirs = [search_dirs] + + self._context = None +@@ -177,16 +177,16 @@ class Renderer(object): + """ + # We type-check to avoid "TypeError: decoding Unicode is not supported". + # We avoid the Python ternary operator for Python 2.4 support. +- if isinstance(s, unicode): ++ if isinstance(s, str): + return s +- return self.unicode(s) ++ return self.str(s) + + def _to_unicode_hard(self, s): + """ + Convert a basestring to a string with type unicode (not subclass). + + """ +- return unicode(self._to_unicode_soft(s)) ++ return str(self._to_unicode_soft(s)) + + def _escape_to_unicode(self, s): + """ +@@ -195,9 +195,9 @@ class Renderer(object): + Returns a unicode string (not subclass). + + """ +- return unicode(self.escape(self._to_unicode_soft(s))) ++ return str(self.escape(self._to_unicode_soft(s))) + +- def unicode(self, b, encoding=None): ++ def str(self, b, encoding=None): + """ + Convert a byte string to unicode, using string_encoding and decode_errors. + +@@ -222,7 +222,7 @@ class Renderer(object): + + # TODO: Wrap UnicodeDecodeErrors with a message about setting + # the string_encoding and decode_errors attributes. +- return unicode(b, encoding, self.decode_errors) ++ return str(b, encoding, self.decode_errors) + + def _make_loader(self): + """ +@@ -230,7 +230,7 @@ class Renderer(object): + + """ + return Loader(file_encoding=self.file_encoding, extension=self.file_extension, +- to_unicode=self.unicode, search_dirs=self.search_dirs) ++ to_unicode=self.str, search_dirs=self.search_dirs) + + def _make_load_template(self): + """ +@@ -299,7 +299,7 @@ class Renderer(object): + try: + return load_partial(name) + except TemplateNotFoundError: +- return u'' ++ return '' + + return resolve_partial + +@@ -316,7 +316,7 @@ class Renderer(object): + try: + return context_get(stack, name) + except KeyNotFoundError: +- return u'' ++ return '' + + return resolve_context + +diff --git a/pystache/specloader.py b/pystache/specloader.py +index 3a77d4c..a82d52a 100644 +--- a/pystache/specloader.py ++++ b/pystache/specloader.py +@@ -83,7 +83,7 @@ class SpecLoader(object): + + """ + if spec.template is not None: +- return self.loader.unicode(spec.template, spec.template_encoding) ++ return self.loader.str(spec.template, spec.template_encoding) + + path = self._find(spec) + +diff --git a/pystache/tests/benchmark.py b/pystache/tests/benchmark.py +index d46e973..6cb54f8 100755 +--- a/pystache/tests/benchmark.py ++++ b/pystache/tests/benchmark.py +@@ -13,6 +13,13 @@ tests/benchmark.py 10000 + import sys + from timeit import Timer + ++try: ++ import chevron as pystache ++ print('Using module: chevron') ++except (ImportError): ++ import pystache ++ print('Using module: pystache') ++ + import pystache + + # TODO: make the example realistic. +@@ -76,17 +83,17 @@ def main(sys_argv): + args = sys_argv[1:] + count = int(args[0]) + +- print "Benchmarking: %sx" % count +- print ++ print("Benchmarking: %sx" % count) ++ print() + + for example in examples: + + test = make_test_function(example) + + t = Timer(test,) +- print min(t.repeat(repeat=3, number=count)) ++ print(min(t.repeat(repeat=3, number=count))) + +- print "Done" ++ print("Done") + + + if __name__ == '__main__': +diff --git a/pystache/tests/common.py b/pystache/tests/common.py +index 222e14f..12b76b5 100644 +--- a/pystache/tests/common.py ++++ b/pystache/tests/common.py +@@ -72,8 +72,8 @@ def _find_files(root_dir, should_include): + # http://docs.python.org/library/os.html#os.walk + for dir_path, dir_names, file_names in os.walk(root_dir): + new_paths = [os.path.join(dir_path, file_name) for file_name in file_names] +- new_paths = filter(is_module, new_paths) +- new_paths = filter(should_include, new_paths) ++ new_paths = list(filter(is_module, new_paths)) ++ new_paths = list(filter(should_include, new_paths)) + paths.extend(new_paths) + + return paths +@@ -183,7 +183,7 @@ class AssertExceptionMixin: + try: + callable(*args, **kwds) + raise Exception("Expected exception: %s: %s" % (exception_type, repr(msg))) +- except exception_type, err: ++ except exception_type as err: + self.assertEqual(str(err), msg) + + +@@ -228,10 +228,10 @@ class Attachable(object): + """ + def __init__(self, **kwargs): + self.__args__ = kwargs +- for arg, value in kwargs.iteritems(): ++ for arg, value in kwargs.items(): + 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())) ++ for k, v in self.__args__.items())) +diff --git a/pystache/tests/examples/unicode_output.py b/pystache/tests/examples/unicode_output.py +index da0e1d2..7bdea36 100644 +--- a/pystache/tests/examples/unicode_output.py ++++ b/pystache/tests/examples/unicode_output.py +@@ -8,4 +8,4 @@ TODO: add a docstring. + class UnicodeOutput(object): + + def name(self): +- return u'Henri Poincaré' ++ return 'Henri Poincaré' +diff --git a/pystache/tests/main.py b/pystache/tests/main.py +index 8af6b2e..17f2fb2 100644 +--- a/pystache/tests/main.py ++++ b/pystache/tests/main.py +@@ -88,7 +88,7 @@ def main(sys_argv): + + """ + # TODO: use logging module +- print "pystache: running tests: argv: %s" % repr(sys_argv) ++ print("pystache: running tests: argv: %s" % repr(sys_argv)) + + should_source_exist = False + spec_test_dir = None +@@ -131,11 +131,9 @@ def main(sys_argv): + module_names = _discover_test_modules(PACKAGE_DIR) + sys_argv.extend(module_names) + if project_dir is not None: +- # Add the current module for unit tests contained here (e.g. +- # to include SetupTests). ++ # Add the current module for unit tests contained here + sys_argv.append(__name__) + +- SetupTests.project_dir = project_dir + + extra_tests = make_extra_tests(project_dir, spec_test_dir) + test_program_class = make_test_program_class(extra_tests) +@@ -166,25 +164,3 @@ def _discover_test_modules(package_dir): + raise Exception("No unit-test modules found--\n in %s" % package_dir) + + return names +- +- +-class SetupTests(TestCase): +- +- """Tests about setup.py.""" +- +- project_dir = None +- +- def test_version(self): +- """ +- Test that setup.py's version matches the package's version. +- +- """ +- original_path = list(sys.path) +- +- sys.path.insert(0, self.project_dir) +- +- try: +- from setup import VERSION +- self.assertEqual(VERSION, pystache.__version__) +- finally: +- sys.path = original_path +diff --git a/pystache/tests/spectesting.py b/pystache/tests/spectesting.py +index ec8a08d..2dd57e8 100644 +--- a/pystache/tests/spectesting.py ++++ b/pystache/tests/spectesting.py +@@ -37,7 +37,7 @@ except ImportError: + from sys import exc_info + ex_type, ex_value, tb = exc_info() + new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) +- raise new_ex.__class__, new_ex, tb ++ raise new_ex.__class__(new_ex).with_traceback(tb) + file_extension = 'json' + parser = json + else: +@@ -62,7 +62,7 @@ def get_spec_tests(spec_test_dir): + + """ + # TODO: use logging module instead. +- print "pystache: spec tests: using %s" % _get_parser_info() ++ print("pystache: spec tests: using %s" % _get_parser_info()) + + cases = [] + +@@ -103,7 +103,7 @@ def _read_spec_tests(path): + + """ + b = common.read(path) +- u = unicode(b, encoding=FILE_ENCODING) ++ u = str(b, encoding=FILE_ENCODING) + spec_data = parse(u) + tests = spec_data['tests'] + +@@ -133,7 +133,7 @@ def _convert_children(node): + return + # Otherwise, node is a dict, so attempt the conversion. + +- for key in node.keys(): ++ for key in list(node.keys()): + val = node[key] + + if not isinstance(val, dict) or val.get('__tag__') != 'code': +@@ -158,9 +158,9 @@ def _deserialize_spec_test(data, file_path): + context = data['data'] + description = data['desc'] + # PyYAML seems to leave ASCII strings as byte strings. +- expected = unicode(data['expected']) ++ expected = str(data['expected']) + # TODO: switch to using dict.get(). +- partials = data.has_key('partials') and data['partials'] or {} ++ partials = 'partials' in data and data['partials'] or {} + template = data['template'] + test_name = data['name'] + +@@ -237,8 +237,8 @@ def parse(u): + value = loader.construct_mapping(node) + return eval(value['python'], {}) + +- yaml.add_constructor(u'!code', code_constructor) +- return yaml.load(u) ++ yaml.add_constructor('!code', code_constructor) ++ return yaml.full_load(u) + + + class SpecTestBase(unittest.TestCase, AssertStringMixin): +diff --git a/pystache/tests/test___init__.py b/pystache/tests/test___init__.py +index eae42c1..63d2c3b 100644 +--- a/pystache/tests/test___init__.py ++++ b/pystache/tests/test___init__.py +@@ -6,9 +6,9 @@ Tests of __init__.py. + """ + + # Calling "import *" is allowed only at the module level. +-GLOBALS_INITIAL = globals().keys() ++GLOBALS_INITIAL = list(globals().keys()) + from pystache import * +-GLOBALS_PYSTACHE_IMPORTED = globals().keys() ++GLOBALS_PYSTACHE_IMPORTED = list(globals().keys()) + + import unittest + +diff --git a/pystache/tests/test_commands.py b/pystache/tests/test_commands.py +index 2529d25..34fe8ba 100644 +--- a/pystache/tests/test_commands.py ++++ b/pystache/tests/test_commands.py +@@ -39,7 +39,7 @@ class CommandsTestCase(unittest.TestCase): + + """ + actual = self.callScript("Hi {{thing}}", '{"thing": "world"}') +- self.assertEqual(actual, u"Hi world\n") ++ self.assertEqual(actual, "Hi world\n") + + def tearDown(self): + sys.stdout = ORIGINAL_STDOUT +diff --git a/pystache/tests/test_defaults.py b/pystache/tests/test_defaults.py +index c78ea7c..5399bb0 100644 +--- a/pystache/tests/test_defaults.py ++++ b/pystache/tests/test_defaults.py +@@ -31,37 +31,37 @@ class DefaultsConfigurableTestCase(unittest.TestCase, AssertStringMixin): + self.saved[e] = getattr(pystache.defaults, e) + + def tearDown(self): +- for key, value in self.saved.items(): ++ for key, value in list(self.saved.items()): + setattr(pystache.defaults, key, value) + + def test_tag_escape(self): + """Test that changes to defaults.TAG_ESCAPE take effect.""" +- template = u"{{foo}}" ++ template = "{{foo}}" + context = {'foo': '<'} + actual = pystache.render(template, context) +- self.assertString(actual, u"<") ++ self.assertString(actual, "<") + + pystache.defaults.TAG_ESCAPE = lambda u: u + actual = pystache.render(template, context) +- self.assertString(actual, u"<") ++ self.assertString(actual, "<") + + def test_delimiters(self): + """Test that changes to defaults.DELIMITERS take effect.""" +- template = u"[[foo]]{{foo}}" ++ template = "[[foo]]{{foo}}" + context = {'foo': 'FOO'} + actual = pystache.render(template, context) +- self.assertString(actual, u"[[foo]]FOO") ++ self.assertString(actual, "[[foo]]FOO") + + pystache.defaults.DELIMITERS = ('[[', ']]') + actual = pystache.render(template, context) +- self.assertString(actual, u"FOO{{foo}}") ++ self.assertString(actual, "FOO{{foo}}") + + def test_missing_tags(self): + """Test that changes to defaults.MISSING_TAGS take effect.""" +- template = u"{{foo}}" ++ template = "{{foo}}" + context = {} + actual = pystache.render(template, context) +- self.assertString(actual, u"") ++ self.assertString(actual, "") + + pystache.defaults.MISSING_TAGS = 'strict' + self.assertRaises(pystache.context.KeyNotFoundError, +diff --git a/pystache/tests/test_examples.py b/pystache/tests/test_examples.py +index 5c9f74d..9f93de3 100644 +--- a/pystache/tests/test_examples.py ++++ b/pystache/tests/test_examples.py +@@ -7,15 +7,15 @@ TODO: add a docstring. + + import unittest + +-from examples.comments import Comments +-from examples.double_section import DoubleSection +-from examples.escaped import Escaped +-from examples.unescaped import Unescaped +-from examples.template_partial import TemplatePartial +-from examples.delimiters import Delimiters +-from examples.unicode_output import UnicodeOutput +-from examples.unicode_input import UnicodeInput +-from examples.nested_context import NestedContext ++from .examples.comments import Comments ++from .examples.double_section import DoubleSection ++from .examples.escaped import Escaped ++from .examples.unescaped import Unescaped ++from .examples.template_partial import TemplatePartial ++from .examples.delimiters import Delimiters ++from .examples.unicode_output import UnicodeOutput ++from .examples.unicode_input import UnicodeInput ++from .examples.nested_context import NestedContext + from pystache import Renderer + from pystache.tests.common import EXAMPLES_DIR + from pystache.tests.common import AssertStringMixin +@@ -29,34 +29,34 @@ class TestView(unittest.TestCase, AssertStringMixin): + self.assertString(actual, expected) + + def test_comments(self): +- self._assert(Comments(), u"

A Comedy of Errors

") ++ self._assert(Comments(), "

A Comedy of Errors

") + + def test_double_section(self): +- self._assert(DoubleSection(), u"* first\n* second\n* third") ++ self._assert(DoubleSection(), "* first\n* second\n* third") + + def test_unicode_output(self): + renderer = Renderer() + actual = renderer.render(UnicodeOutput()) +- self.assertString(actual, u'

Name: Henri Poincaré

') ++ self.assertString(actual, '

Name: Henri Poincaré

') + + def test_unicode_input(self): + renderer = Renderer() + actual = renderer.render(UnicodeInput()) +- self.assertString(actual, u'abcdé') ++ self.assertString(actual, 'abcdé') + + def test_escaping(self): +- self._assert(Escaped(), u"

Bear > Shark

") ++ self._assert(Escaped(), "

Bear > Shark

") + + def test_literal(self): + renderer = Renderer() + actual = renderer.render(Unescaped()) +- self.assertString(actual, u"

Bear > Shark

") ++ self.assertString(actual, "

Bear > Shark

") + + def test_template_partial(self): + renderer = Renderer(search_dirs=EXAMPLES_DIR) + actual = renderer.render(TemplatePartial(renderer=renderer)) + +- self.assertString(actual, u"""

Welcome

++ self.assertString(actual, """

Welcome

+ Again, Welcome!""") + + def test_template_partial_extension(self): +@@ -65,7 +65,7 @@ Again, Welcome!""") + view = TemplatePartial(renderer=renderer) + + actual = renderer.render(view) +- self.assertString(actual, u"""Welcome ++ self.assertString(actual, """Welcome + ------- + + ## Again, Welcome! ##""") +@@ -73,7 +73,7 @@ Again, Welcome!""") + def test_delimiters(self): + renderer = Renderer() + actual = renderer.render(Delimiters()) +- self.assertString(actual, u"""\ ++ self.assertString(actual, """\ + * It worked the first time. + * And it worked the second time. + * Then, surprisingly, it worked the third time. +@@ -82,7 +82,7 @@ Again, Welcome!""") + def test_nested_context(self): + renderer = Renderer() + actual = renderer.render(NestedContext(renderer)) +- self.assertString(actual, u"one and foo and two") ++ self.assertString(actual, "one and foo and two") + + def test_nested_context_is_available_in_view(self): + renderer = Renderer() +@@ -91,7 +91,7 @@ Again, Welcome!""") + view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}' + + actual = renderer.render(view) +- self.assertString(actual, u'it works!') ++ self.assertString(actual, 'it works!') + + def test_partial_in_partial_has_access_to_grand_parent_context(self): + renderer = Renderer(search_dirs=EXAMPLES_DIR) +diff --git a/pystache/tests/test_loader.py b/pystache/tests/test_loader.py +index f2c2187..315daff 100644 +--- a/pystache/tests/test_loader.py ++++ b/pystache/tests/test_loader.py +@@ -55,23 +55,23 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + + def test_init__to_unicode__default(self): + loader = Loader() +- self.assertRaises(TypeError, loader.to_unicode, u"abc") ++ self.assertRaises(TypeError, loader.to_unicode, "abc") + + decode_errors = defaults.DECODE_ERRORS + string_encoding = defaults.STRING_ENCODING + +- nonascii = u'abcdé'.encode('utf-8') ++ nonascii = 'abcdé'.encode('utf-8') + + loader = Loader() + self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii) + + defaults.DECODE_ERRORS = 'ignore' + loader = Loader() +- self.assertString(loader.to_unicode(nonascii), u'abcd') ++ self.assertString(loader.to_unicode(nonascii), 'abcd') + + defaults.STRING_ENCODING = 'utf-8' + loader = Loader() +- self.assertString(loader.to_unicode(nonascii), u'abcdé') ++ self.assertString(loader.to_unicode(nonascii), 'abcdé') + + + def _get_path(self, filename): +@@ -83,9 +83,9 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + + """ + loader = Loader() +- actual = loader.unicode("foo") ++ actual = loader.str("foo") + +- self.assertString(actual, u"foo") ++ self.assertString(actual, "foo") + + def test_unicode__basic__input_unicode(self): + """ +@@ -93,24 +93,24 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + + """ + loader = Loader() +- actual = loader.unicode(u"foo") ++ actual = loader.str("foo") + +- self.assertString(actual, u"foo") ++ self.assertString(actual, "foo") + + def test_unicode__basic__input_unicode_subclass(self): + """ + Test unicode(): default arguments with unicode-subclass input. + + """ +- class UnicodeSubclass(unicode): ++ class UnicodeSubclass(str): + pass + +- s = UnicodeSubclass(u"foo") ++ s = UnicodeSubclass("foo") + + loader = Loader() +- actual = loader.unicode(s) ++ actual = loader.str(s) + +- self.assertString(actual, u"foo") ++ self.assertString(actual, "foo") + + def test_unicode__to_unicode__attribute(self): + """ +@@ -119,16 +119,16 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + """ + loader = Loader() + +- non_ascii = u'abcdé'.encode('utf-8') +- self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii) ++ non_ascii = 'abcdé'.encode('utf-8') ++ self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) + + def to_unicode(s, encoding=None): + if encoding is None: + encoding = 'utf-8' +- return unicode(s, encoding) ++ return str(s, encoding) + + loader.to_unicode = to_unicode +- self.assertString(loader.unicode(non_ascii), u"abcdé") ++ self.assertString(loader.str(non_ascii), "abcdé") + + def test_unicode__encoding_argument(self): + """ +@@ -137,12 +137,12 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + """ + loader = Loader() + +- non_ascii = u'abcdé'.encode('utf-8') ++ non_ascii = 'abcdé'.encode('utf-8') + +- self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii) ++ self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) + +- actual = loader.unicode(non_ascii, encoding='utf-8') +- self.assertString(actual, u'abcdé') ++ actual = loader.str(non_ascii, encoding='utf-8') ++ self.assertString(actual, 'abcdé') + + # TODO: check the read() unit tests. + def test_read(self): +@@ -153,7 +153,7 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + loader = Loader() + path = self._get_path('ascii.mustache') + actual = loader.read(path) +- self.assertString(actual, u'ascii: abc') ++ self.assertString(actual, 'ascii: abc') + + def test_read__file_encoding__attribute(self): + """ +@@ -167,7 +167,7 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + + loader.file_encoding = 'utf-8' + actual = loader.read(path) +- self.assertString(actual, u'non-ascii: é') ++ self.assertString(actual, 'non-ascii: é') + + def test_read__encoding__argument(self): + """ +@@ -180,7 +180,7 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): + self.assertRaises(UnicodeDecodeError, loader.read, path) + + actual = loader.read(path, encoding='utf-8') +- self.assertString(actual, u'non-ascii: é') ++ self.assertString(actual, 'non-ascii: é') + + def test_read__to_unicode__attribute(self): + """ +diff --git a/pystache/tests/test_pystache.py b/pystache/tests/test_pystache.py +index 5447f8d..cf5d6af 100644 +--- a/pystache/tests/test_pystache.py ++++ b/pystache/tests/test_pystache.py +@@ -71,14 +71,14 @@ class PystacheTests(unittest.TestCase): + template = "{{#stats}}({{key}} & {{value}}){{/stats}}" + stats = [] + stats.append({'key': 123, 'value': ['something']}) +- stats.append({'key': u"chris", 'value': 0.900}) ++ stats.append({'key': "chris", 'value': 0.900}) + context = { 'stats': stats } + self._assert_rendered(self.non_strings_expected, template, context) + + def test_unicode(self): + template = 'Name: {{name}}; Age: {{age}}' +- context = {'name': u'Henri Poincaré', 'age': 156 } +- self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context) ++ context = {'name': 'Henri Poincaré', 'age': 156} ++ self._assert_rendered('Name: Henri Poincaré; Age: 156', template, context) + + def test_sections(self): + template = """
    {{#users}}
  • {{name}}
  • {{/users}}
""" +diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py +index db916f7..ed604c5 100644 +--- a/pystache/tests/test_renderengine.py ++++ b/pystache/tests/test_renderengine.py +@@ -33,11 +33,11 @@ def mock_literal(s): + s: a byte string or unicode string. + + """ +- if isinstance(s, unicode): ++ if isinstance(s, str): + # Strip off unicode super classes, if present. +- u = unicode(s) ++ u = str(s) + else: +- u = unicode(s, encoding='ascii') ++ u = str(s, encoding='ascii') + + # We apply upper() to make sure we are actually using our custom + # function in the tests +@@ -94,17 +94,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + engine = kwargs.get('engine', self._engine()) + + if partials is not None: +- engine.resolve_partial = lambda key: unicode(partials[key]) ++ engine.resolve_partial = lambda key: str(partials[key]) + + context = ContextStack(*context) + + # RenderEngine.render() only accepts unicode template strings. +- actual = engine.render(unicode(template), context) ++ actual = engine.render(str(template), context) + + self.assertString(actual=actual, expected=expected) + + def test_render(self): +- self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) ++ self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) + + def test__resolve_partial(self): + """ +@@ -112,10 +112,10 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + """ + engine = self._engine() +- partials = {'partial': u"{{person}}"} ++ partials = {'partial': "{{person}}"} + engine.resolve_partial = lambda key: partials[key] + +- self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) ++ self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) + + def test__literal(self): + """ +@@ -125,13 +125,13 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + engine = self._engine() + engine.literal = lambda s: s.upper() + +- self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) ++ self._assert_render('BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) + + def test_literal__sigil(self): + template = "

{{& thing}}

" + context = {'thing': 'Bear > Giraffe'} + +- expected = u"

Bear > Giraffe

" ++ expected = "

Bear > Giraffe

" + + self._assert_render(expected, template, context) + +@@ -143,7 +143,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + engine = self._engine() + engine.escape = lambda s: "**" + s + +- self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) ++ self._assert_render('**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) + + def test__escape_does_not_call_literal(self): + """ +@@ -157,7 +157,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = 'literal: {{{foo}}} escaped: {{foo}}' + context = {'foo': 'bar'} + +- self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine) ++ self._assert_render('literal: BAR escaped: **bar', template, context, engine=engine) + + def test__escape_preserves_unicode_subclasses(self): + """ +@@ -167,7 +167,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + variable value is markupsafe.Markup when escaping. + + """ +- class MyUnicode(unicode): ++ class MyUnicode(str): + pass + + def escape(s): +@@ -182,7 +182,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{foo1}} {{foo2}}' + context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'} + +- self._assert_render(u'**bar bar**', template, context, engine=engine) ++ self._assert_render('**bar bar**', template, context, engine=engine) + + # Custom to_str for testing purposes. + def _to_str(self, val): +@@ -197,9 +197,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{value}}' + context = {'value': None} + +- self._assert_render(u'None', template, context, engine=engine) ++ self._assert_render('None', template, context, engine=engine) + engine.to_str = self._to_str +- self._assert_render(u'', template, context, engine=engine) ++ self._assert_render('', template, context, engine=engine) + + def test_to_str__lambda(self): + """Test the to_str attribute for a lambda.""" +@@ -207,9 +207,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{value}}' + context = {'value': lambda: None} + +- self._assert_render(u'None', template, context, engine=engine) ++ self._assert_render('None', template, context, engine=engine) + engine.to_str = self._to_str +- self._assert_render(u'', template, context, engine=engine) ++ self._assert_render('', template, context, engine=engine) + + def test_to_str__section_list(self): + """Test the to_str attribute for a section list.""" +@@ -217,9 +217,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#list}}{{.}}{{/list}}' + context = {'list': [None, None]} + +- self._assert_render(u'NoneNone', template, context, engine=engine) ++ self._assert_render('NoneNone', template, context, engine=engine) + engine.to_str = self._to_str +- self._assert_render(u'', template, context, engine=engine) ++ self._assert_render('', template, context, engine=engine) + + def test_to_str__section_lambda(self): + # TODO: add a test for a "method with an arity of 1". +@@ -239,7 +239,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{text}} {{int}} {{{int}}}' + context = {'int': 100, 'text': 'foo'} + +- self._assert_render(u'FOO 100 100', template, context, engine=engine) ++ self._assert_render('FOO 100 100', template, context, engine=engine) + + def test_tag__output_not_interpolated(self): + """ +@@ -248,7 +248,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{template}}: {{planet}}' + context = {'template': '{{planet}}', 'planet': 'Earth'} +- self._assert_render(u'{{planet}}: Earth', template, context) ++ self._assert_render('{{planet}}: Earth', template, context) + + def test_tag__output_not_interpolated__section(self): + """ +@@ -257,7 +257,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{test}}' + context = {'test': '{{#hello}}'} +- self._assert_render(u'{{#hello}}', template, context) ++ self._assert_render('{{#hello}}', template, context) + + ## Test interpolation with "falsey" values + # +@@ -268,17 +268,17 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + def test_interpolation__falsey__zero(self): + template = '{{.}}' + context = 0 +- self._assert_render(u'0', template, context) ++ self._assert_render('0', template, context) + + def test_interpolation__falsey__none(self): + template = '{{.}}' + context = None +- self._assert_render(u'None', template, context) ++ self._assert_render('None', template, context) + + def test_interpolation__falsey__zero(self): + template = '{{.}}' + context = False +- self._assert_render(u'False', template, context) ++ self._assert_render('False', template, context) + + # Built-in types: + # +@@ -310,7 +310,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + Check tag interpolation with a built-in type: string. + + """ +- self._assert_builtin_type('abc', 'upper', 'ABC', u'xyz') ++ self._assert_builtin_type('abc', 'upper', 'ABC', 'xyz') + + def test_interpolation__built_in_type__integer(self): + """ +@@ -324,7 +324,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + # + # we need to resort to built-in attributes (double-underscored) on + # the integer type. +- self._assert_builtin_type(15, '__neg__', -15, u'999') ++ self._assert_builtin_type(15, '__neg__', -15, '999') + + def test_interpolation__built_in_type__list(self): + """ +@@ -338,7 +338,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + template = '{{#section}}{{%s}}{{/section}}' % attr_name + context = {'section': item, attr_name: 7} +- self._assert_render(u'7', template, context) ++ self._assert_render('7', template, context) + + # This test is also important for testing 2to3. + def test_interpolation__nonascii_nonunicode(self): +@@ -347,8 +347,8 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + """ + template = '{{nonascii}}' +- context = {'nonascii': u'abcdé'.encode('utf-8')} +- self._assert_render(u'abcdé', template, context) ++ context = {'nonascii': 'abcdé'.encode('utf-8')} ++ self._assert_render('abcdé', template, context) + + def test_implicit_iterator__literal(self): + """ +@@ -358,7 +358,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = """{{#test}}{{{.}}}{{/test}}""" + context = {'test': ['<', '>']} + +- self._assert_render(u'<>', template, context) ++ self._assert_render('<>', template, context) + + def test_implicit_iterator__escaped(self): + """ +@@ -368,7 +368,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = """{{#test}}{{.}}{{/test}}""" + context = {'test': ['<', '>']} + +- self._assert_render(u'<>', template, context) ++ self._assert_render('<>', template, context) + + def test_literal__in_section(self): + """ +@@ -378,7 +378,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#test}}1 {{{less_than}}} 2{{/test}}' + context = {'test': {'less_than': '<'}} + +- self._assert_render(u'1 < 2', template, context) ++ self._assert_render('1 < 2', template, context) + + def test_literal__in_partial(self): + """ +@@ -389,11 +389,11 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + partials = {'partial': '1 {{{less_than}}} 2'} + context = {'less_than': '<'} + +- self._assert_render(u'1 < 2', template, context, partials=partials) ++ self._assert_render('1 < 2', template, context, partials=partials) + + def test_partial(self): + partials = {'partial': "{{person}}"} +- self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) ++ self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) + + def test_partial__context_values(self): + """ +@@ -406,7 +406,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} + context = {'foo': '<'} + +- self._assert_render(u'unescaped: < escaped: <', template, context, engine=engine, partials=partials) ++ self._assert_render( ++ 'unescaped: < escaped: <', ++ template, context, engine=engine, partials=partials) + + ## Test cases related specifically to lambdas. + +@@ -417,8 +419,8 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + """ + template = '{{#nonascii}}{{.}}{{/nonascii}}' +- context = {'nonascii': u'abcdé'.encode('utf-8')} +- self._assert_render(u'abcdé', template, context) ++ context = {'nonascii': 'abcdé'.encode('utf-8')} ++ self._assert_render('abcdé', template, context) + + # This test is also important for testing 2to3. + def test_lambda__returning_nonascii_nonunicode(self): +@@ -427,8 +429,8 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + """ + template = '{{lambda}}' +- context = {'lambda': lambda: u'abcdé'.encode('utf-8')} +- self._assert_render(u'abcdé', template, context) ++ context = {'lambda': lambda: 'abcdé'.encode('utf-8')} ++ self._assert_render('abcdé', template, context) + + ## Test cases related specifically to sections. + +@@ -440,7 +442,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{/section}}' + try: + self._assert_render(None, template) +- except ParsingError, err: ++ except ParsingError as err: + self.assertEqual(str(err), "Section end tag mismatch: section != None") + + def test_section__end_tag_mismatch(self): +@@ -451,7 +453,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#section_start}}{{/section_end}}' + try: + self._assert_render(None, template) +- except ParsingError, err: ++ except ParsingError as err: + self.assertEqual(str(err), "Section end tag mismatch: section_end != section_start") + + def test_section__context_values(self): +@@ -464,7 +466,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}' + context = {'test': {'foo': '<'}} + +- self._assert_render(u'unescaped: < escaped: <', template, context, engine=engine) ++ self._assert_render('unescaped: < escaped: <', template, context, engine=engine) + + def test_section__context_precedence(self): + """ +@@ -473,7 +475,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}' + context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}} +- self._assert_render(u'chicken : beans and rice', template, context) ++ self._assert_render('chicken : beans and rice', template, context) + + def test_section__list_referencing_outer_context(self): + """ +@@ -491,7 +493,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + template = "{{#list}}{{greeting}} {{name}}, {{/list}}" + +- self._assert_render(u"Hi Al, Hi Bob, ", template, context) ++ self._assert_render("Hi Al, Hi Bob, ", template, context) + + def test_section__output_not_interpolated(self): + """ +@@ -500,7 +502,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{#section}}{{template}}{{/section}}: {{planet}}' + context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} +- self._assert_render(u'{{planet}}: Earth', template, context) ++ self._assert_render('{{planet}}: Earth', template, context) + + # TODO: have this test case added to the spec. + def test_section__string_values_not_lists(self): +@@ -511,7 +513,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#section}}foo{{/section}}' + context = {'section': '123'} + # If strings were interpreted as lists, this would give "foofoofoo". +- self._assert_render(u'foo', template, context) ++ self._assert_render('foo', template, context) + + def test_section__nested_truthy(self): + """ +@@ -525,7 +527,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |' + context = {'bool': True} +- self._assert_render(u'| A B C D E |', template, context) ++ self._assert_render('| A B C D E |', template, context) + + def test_section__nested_with_same_keys(self): + """ +@@ -537,16 +539,16 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + # Start with an easier, working case. + template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}' + context = {'x': {'z': {'y': 1}}} +- self._assert_render(u'1', template, context) ++ self._assert_render('1', template, context) + + template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}' + context = {'x': {'x': {'y': 1}}} +- self._assert_render(u'1', template, context) ++ self._assert_render('1', template, context) + + def test_section__lambda(self): + template = '{{#test}}Mom{{/test}}' + context = {'test': (lambda text: 'Hi %s' % text)} +- self._assert_render(u'Hi Mom', template, context) ++ self._assert_render('Hi Mom', template, context) + + # This test is also important for testing 2to3. + def test_section__lambda__returning_nonascii_nonunicode(self): +@@ -555,8 +557,8 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + + """ + template = '{{#lambda}}{{/lambda}}' +- context = {'lambda': lambda text: u'abcdé'.encode('utf-8')} +- self._assert_render(u'abcdé', template, context) ++ context = {'lambda': lambda text: 'abcdé'.encode('utf-8')} ++ self._assert_render('abcdé', template, context) + + def test_section__lambda__returning_nonstring(self): + """ +@@ -565,7 +567,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{#lambda}}foo{{/lambda}}' + context = {'lambda': lambda text: len(text)} +- self._assert_render(u'3', template, context) ++ self._assert_render('3', template, context) + + def test_section__iterable(self): + """ +@@ -575,10 +577,10 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + template = '{{#iterable}}{{.}}{{/iterable}}' + + context = {'iterable': (i for i in range(3))} # type 'generator' +- self._assert_render(u'012', template, context) ++ self._assert_render('012', template, context) + +- context = {'iterable': xrange(4)} # type 'xrange' +- self._assert_render(u'0123', template, context) ++ context = {'iterable': range(4)} # type 'xrange' ++ self._assert_render('0123', template, context) + + d = {'foo': 0, 'bar': 0} + # We don't know what order of keys we'll be given, but from the +@@ -586,8 +588,8 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + # "If items(), keys(), values(), iteritems(), iterkeys(), and + # itervalues() are called with no intervening modifications to + # the dictionary, the lists will directly correspond." +- expected = u''.join(d.keys()) +- context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator' ++ expected = ''.join(list(d.keys())) ++ context = {'iterable': iter(d.keys())} # type 'dictionary-keyiterator' + self._assert_render(expected, template, context) + + def test_section__lambda__tag_in_output(self): +@@ -605,7 +607,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{#test}}Hi {{person}}{{/test}}' + context = {'person': 'Mom', 'test': (lambda text: text + " :)")} +- self._assert_render(u'Hi Mom :)', template, context) ++ self._assert_render('Hi Mom :)', template, context) + + def test_section__lambda__list(self): + """ +@@ -621,7 +623,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + 'lambdas': [lambda text: "~{{%s}}~" % text, + lambda text: "#{{%s}}#" % text]} + +- self._assert_render(u'<~bar~#bar#>', template, context) ++ self._assert_render('<~bar~#bar#>', template, context) + + def test_section__lambda__mixed_list(self): + """ +@@ -636,7 +638,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + context = {'foo': 'bar', + 'lambdas': [lambda text: "~{{%s}}~" % text, 1]} + +- self._assert_render(u'<~bar~foo>', template, context) ++ self._assert_render('<~bar~foo>', template, context) + + def test_section__lambda__not_on_context_stack(self): + """ +@@ -653,7 +655,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + context = {'foo': 'bar', 'lambda': (lambda text: "{{.}}")} + template = '{{#foo}}{{#lambda}}blah{{/lambda}}{{/foo}}' +- self._assert_render(u'bar', template, context) ++ self._assert_render('bar', template, context) + + def test_section__lambda__no_reinterpolation(self): + """ +@@ -670,15 +672,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{#planet}}{{#lambda}}dot{{/lambda}}{{/planet}}' + context = {'planet': 'Earth', 'dot': '~{{.}}~', 'lambda': (lambda text: "#{{%s}}#" % text)} +- self._assert_render(u'#~{{.}}~#', template, context) ++ self._assert_render('#~{{.}}~#', template, context) + + def test_comment__multiline(self): + """ + Check that multiline comments are permitted. + + """ +- self._assert_render(u'foobar', 'foo{{! baz }}bar') +- self._assert_render(u'foobar', 'foo{{! \nbaz }}bar') ++ self._assert_render('foobar', 'foo{{! baz }}bar') ++ self._assert_render('foobar', 'foo{{! \nbaz }}bar') + + def test_custom_delimiters__sections(self): + """ +@@ -689,7 +691,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]' + context = {'foo': True} +- self._assert_render(u'bar', template, context) ++ self._assert_render('bar', template, context) + + def test_custom_delimiters__not_retroactive(self): + """ +@@ -698,7 +700,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + Test case for issue #35: https://github.com/defunkt/pystache/issues/35 + + """ +- expected = u' {{foo}} ' ++ expected = ' {{foo}} ' + self._assert_render(expected, '{{=$ $=}} {{foo}} ') + self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. + +@@ -713,7 +715,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + 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) ++ self._assert_render('Hello, Biggles. I see you are 42.', template, context) + + def test_dot_notation__multiple_levels(self): + """ +@@ -722,7 +724,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + """ + template = """Hello, Mr. {{person.name.lastname}}. + I see you're back from {{person.travels.last.country.city}}.""" +- expected = u"""Hello, Mr. Pither. ++ expected = """Hello, Mr. Pither. + I see you're back from Cornwall.""" + context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'}, + 'travels': {'last': {'country': {'city': 'Cornwall'}}}, +@@ -758,10 +760,10 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): + context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} } + + template = '{{a.b}}' +- self._assert_render(u'A.B', template, context) ++ self._assert_render('A.B', template, context) + + template = '{{#c}}{{a}}{{/c}}' +- self._assert_render(u'A', template, context) ++ self._assert_render('A', template, context) + + template = '{{#c}}{{a.b}}{{/c}}' + self.assertException(KeyNotFoundError, "Key %(unicode)s'a.b' not found: missing %(unicode)s'b'" % +diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py +index 0dbe0d9..e0d2448 100644 +--- a/pystache/tests/test_renderer.py ++++ b/pystache/tests/test_renderer.py +@@ -10,7 +10,7 @@ import os + import sys + import unittest + +-from examples.simple import Simple ++from .examples.simple import Simple + from pystache import Renderer + from pystache import TemplateSpec + from pystache.common import TemplateNotFoundError +@@ -33,7 +33,7 @@ def _make_renderer(): + def mock_unicode(b, encoding=None): + if encoding is None: + encoding = 'ascii' +- u = unicode(b, encoding=encoding) ++ u = str(b, encoding=encoding) + return u.upper() + + +@@ -197,13 +197,13 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + + """ + renderer = self._renderer() +- b = u"é".encode('utf-8') ++ b = "é".encode('utf-8') + + renderer.string_encoding = "ascii" +- self.assertRaises(UnicodeDecodeError, renderer.unicode, b) ++ self.assertRaises(UnicodeDecodeError, renderer.str, b) + + renderer.string_encoding = "utf-8" +- self.assertEqual(renderer.unicode(b), u"é") ++ self.assertEqual(renderer.str(b), "é") + + def test_unicode__decode_errors(self): + """ +@@ -212,14 +212,14 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + """ + renderer = self._renderer() + renderer.string_encoding = "ascii" +- b = u"déf".encode('utf-8') ++ b = "déf".encode('utf-8') + + renderer.decode_errors = "ignore" +- self.assertEqual(renderer.unicode(b), "df") ++ self.assertEqual(renderer.str(b), "df") + + renderer.decode_errors = "replace" + # U+FFFD is the official Unicode replacement character. +- self.assertEqual(renderer.unicode(b), u'd\ufffd\ufffdf') ++ self.assertEqual(renderer.str(b), u'd\ufffd\ufffdf') + + ## Test the _make_loader() method. + +@@ -243,7 +243,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + renderer = self._renderer() + renderer.file_encoding = 'enc' + renderer.file_extension = 'ext' +- renderer.unicode = unicode_ ++ renderer.str = unicode_ + + loader = renderer._make_loader() + +@@ -260,12 +260,12 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + """ + renderer = self._renderer() + rendered = renderer.render('foo') +- self.assertEqual(type(rendered), unicode) ++ self.assertEqual(type(rendered), str) + + def test_render__unicode(self): + renderer = self._renderer() +- actual = renderer.render(u'foo') +- self.assertEqual(actual, u'foo') ++ actual = renderer.render('foo') ++ self.assertEqual(actual, 'foo') + + def test_render__str(self): + renderer = self._renderer() +@@ -274,8 +274,8 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + + def test_render__non_ascii_character(self): + renderer = self._renderer() +- actual = renderer.render(u'Poincaré') +- self.assertEqual(actual, u'Poincaré') ++ actual = renderer.render('Poincaré') ++ self.assertEqual(actual, 'Poincaré') + + def test_render__context(self): + """ +@@ -326,7 +326,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + + """ + renderer = _make_renderer() +- template = u"déf".encode("utf-8") ++ template = "déf".encode("utf-8") + + # Check that decode_errors and string_encoding are both respected. + renderer.decode_errors = 'ignore' +@@ -334,7 +334,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + self.assertEqual(renderer.render(template), "df") + + renderer.string_encoding = 'utf_8' +- self.assertEqual(renderer.render(template), u"déf") ++ self.assertEqual(renderer.render(template), "déf") + + def test_make_resolve_partial(self): + """ +@@ -347,7 +347,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + + actual = resolve_partial('foo') + self.assertEqual(actual, 'bar') +- self.assertEqual(type(actual), unicode, "RenderEngine requires that " ++ self.assertEqual(type(actual), str, "RenderEngine requires that " + "resolve_partial return unicode strings.") + + def test_make_resolve_partial__unicode(self): +@@ -362,7 +362,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + self.assertEqual(resolve_partial("partial"), "foo") + + # Now with a value that is already unicode. +- renderer.partials = {'partial': u'foo'} ++ renderer.partials = {'partial': 'foo'} + resolve_partial = renderer._make_resolve_partial() + # If the next line failed, we would get the following error: + # TypeError: decoding Unicode is not supported +@@ -373,7 +373,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + data_dir = get_data_path() + renderer = Renderer(search_dirs=data_dir) + actual = renderer.render_name("say_hello", to='foo') +- self.assertString(actual, u"Hello, foo") ++ self.assertString(actual, "Hello, foo") + + def test_render_path(self): + """ +@@ -412,7 +412,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): + + spec = Spec() + actual = renderer.render(spec) +- self.assertString(actual, u'hello, world') ++ self.assertString(actual, 'hello, world') + + def test_render__view(self): + """ +@@ -484,7 +484,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + Check that resolve_partial returns unicode (and not a subclass). + + """ +- class MyUnicode(unicode): ++ class MyUnicode(str): + pass + + renderer = Renderer() +@@ -495,12 +495,12 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + + actual = engine.resolve_partial('str') + self.assertEqual(actual, "foo") +- self.assertEqual(type(actual), unicode) ++ self.assertEqual(type(actual), str) + + # Check that unicode subclasses are not preserved. + actual = engine.resolve_partial('subclass') + self.assertEqual(actual, "abc") +- self.assertEqual(type(actual), unicode) ++ self.assertEqual(type(actual), str) + + def test__resolve_partial__not_found(self): + """ +@@ -512,7 +512,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + engine = renderer._make_render_engine() + resolve_partial = engine.resolve_partial + +- self.assertString(resolve_partial('foo'), u'') ++ self.assertString(resolve_partial('foo'), '') + + def test__resolve_partial__not_found__missing_tags_strict(self): + """ +@@ -539,7 +539,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + engine = renderer._make_render_engine() + resolve_partial = engine.resolve_partial + +- self.assertString(resolve_partial('foo'), u'') ++ self.assertString(resolve_partial('foo'), '') + + def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self): + """ +@@ -566,12 +566,12 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + + """ + renderer = self._make_renderer() +- renderer.unicode = mock_unicode ++ renderer.str = mock_unicode + + engine = renderer._make_render_engine() + literal = engine.literal + +- b = u"foo".encode("ascii") ++ b = "foo".encode("ascii") + self.assertEqual(literal(b), "FOO") + + def test__literal__handles_unicode(self): +@@ -585,7 +585,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + engine = renderer._make_render_engine() + literal = engine.literal + +- self.assertEqual(literal(u"foo"), "foo") ++ self.assertEqual(literal("foo"), "foo") + + def test__literal__returns_unicode(self): + """ +@@ -598,16 +598,16 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + engine = renderer._make_render_engine() + literal = engine.literal + +- self.assertEqual(type(literal("foo")), unicode) ++ self.assertEqual(type(literal("foo")), str) + +- class MyUnicode(unicode): ++ class MyUnicode(str): + pass + + s = MyUnicode("abc") + + self.assertEqual(type(s), MyUnicode) +- self.assertTrue(isinstance(s, unicode)) +- self.assertEqual(type(literal(s)), unicode) ++ self.assertTrue(isinstance(s, str)) ++ self.assertEqual(type(literal(s)), str) + + ## Test the engine's escape attribute. + +@@ -630,12 +630,12 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + + """ + renderer = Renderer() +- renderer.unicode = mock_unicode ++ renderer.str = mock_unicode + + engine = renderer._make_render_engine() + escape = engine.escape + +- b = u"foo".encode('ascii') ++ b = "foo".encode('ascii') + self.assertEqual(escape(b), "FOO") + + def test__escape__has_access_to_original_unicode_subclass(self): +@@ -644,16 +644,16 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + + """ + renderer = Renderer() +- renderer.escape = lambda s: unicode(type(s).__name__) ++ renderer.escape = lambda s: str(type(s).__name__) + + engine = renderer._make_render_engine() + escape = engine.escape + +- class MyUnicode(unicode): ++ class MyUnicode(str): + pass + +- self.assertEqual(escape(u"foo".encode('ascii')), unicode.__name__) +- self.assertEqual(escape(u"foo"), unicode.__name__) ++ self.assertEqual(escape("foo".encode('ascii')), str.__name__) ++ self.assertEqual(escape("foo"), str.__name__) + self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__) + + def test__escape__returns_unicode(self): +@@ -667,17 +667,17 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + engine = renderer._make_render_engine() + escape = engine.escape + +- self.assertEqual(type(escape("foo")), unicode) ++ self.assertEqual(type(escape("foo")), str) + + # Check that literal doesn't preserve unicode subclasses. +- class MyUnicode(unicode): ++ class MyUnicode(str): + pass + + s = MyUnicode("abc") + + self.assertEqual(type(s), MyUnicode) +- self.assertTrue(isinstance(s, unicode)) +- self.assertEqual(type(escape(s)), unicode) ++ self.assertTrue(isinstance(s, str)) ++ self.assertEqual(type(escape(s)), str) + + ## Test the missing_tags attribute. + +@@ -706,7 +706,7 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, Asser + stack = ContextStack({'foo': 'bar'}) + + self.assertEqual('bar', engine.resolve_context(stack, 'foo')) +- self.assertString(u'', engine.resolve_context(stack, 'missing')) ++ self.assertString('', engine.resolve_context(stack, 'missing')) + + def test__resolve_context__missing_tags_strict(self): + """ +diff --git a/pystache/tests/test_simple.py b/pystache/tests/test_simple.py +index 07b059f..b88bf35 100644 +--- a/pystache/tests/test_simple.py ++++ b/pystache/tests/test_simple.py +@@ -2,11 +2,11 @@ import unittest + + import pystache + from pystache import Renderer +-from examples.nested_context import NestedContext +-from examples.complex import Complex +-from examples.lambdas import Lambdas +-from examples.template_partial import TemplatePartial +-from examples.simple import Simple ++from .examples.nested_context import NestedContext ++from .examples.complex import Complex ++from .examples.lambdas import Lambdas ++from .examples.template_partial import TemplatePartial ++from .examples.simple import Simple + + from pystache.tests.common import EXAMPLES_DIR + from pystache.tests.common import AssertStringMixin +@@ -20,7 +20,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin): + view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}' + + actual = renderer.render(view) +- self.assertString(actual, u"one and foo and two") ++ self.assertString(actual, "one and foo and two") + + def test_looping_and_negation_context(self): + template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' +@@ -40,7 +40,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin): + + renderer = Renderer() + actual = renderer.render(view) +- self.assertString(actual, u'bar != bar. oh, it does!') ++ self.assertString(actual, 'bar != bar. oh, it does!') + + def test_rendering_partial(self): + renderer = Renderer(search_dirs=EXAMPLES_DIR) +@@ -49,11 +49,11 @@ class TestSimple(unittest.TestCase, AssertStringMixin): + view.template = '{{>inner_partial}}' + + actual = renderer.render(view) +- self.assertString(actual, u'Again, Welcome!') ++ self.assertString(actual, 'Again, Welcome!') + + view.template = '{{#looping}}{{>inner_partial}} {{/looping}}' + actual = renderer.render(view) +- self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ") ++ self.assertString(actual, "Again, Welcome! Again, Welcome! Again, Welcome! ") + + def test_non_existent_value_renders_blank(self): + view = Simple() +@@ -77,7 +77,7 @@ class TestSimple(unittest.TestCase, AssertStringMixin): + view = TemplatePartial(renderer=renderer) + + actual = renderer.render(view) +- self.assertString(actual, u"""Welcome ++ self.assertString(actual, """Welcome + ------- + + ## Again, Welcome! ##""") +diff --git a/pystache/tests/test_specloader.py b/pystache/tests/test_specloader.py +index cacc0fc..dcdc55f 100644 +--- a/pystache/tests/test_specloader.py ++++ b/pystache/tests/test_specloader.py +@@ -9,11 +9,11 @@ import os.path + import sys + import unittest + +-import examples +-from examples.simple import Simple +-from examples.complex import Complex +-from examples.lambdas import Lambdas +-from examples.inverted import Inverted, InvertedLists ++from . import examples ++from .examples.simple import Simple ++from .examples.complex import Complex ++from .examples.lambdas import Lambdas ++from .examples.inverted import Inverted, InvertedLists + from pystache import Renderer + from pystache import TemplateSpec + from pystache.common import TemplateNotFoundError +@@ -70,7 +70,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + renderer2 = Renderer(search_dirs=EXAMPLES_DIR) + + actual = renderer1.render(spec) +- self.assertString(actual, u"Partial: ") ++ self.assertString(actual, "Partial: ") + + actual = renderer2.render(spec) + self.assertEqual(actual, "Partial: No tags...") +@@ -79,7 +79,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + renderer = Renderer() + actual = renderer.render(Simple()) + +- self.assertString(actual, u"Hi pizza!") ++ self.assertString(actual, "Hi pizza!") + + def test_non_callable_attributes(self): + view = Simple() +@@ -92,7 +92,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + def test_complex(self): + renderer = Renderer() + actual = renderer.render(Complex()) +- self.assertString(actual, u"""\ ++ self.assertString(actual, """\ +

Colors

+
    +
  • red
  • +@@ -111,7 +111,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + + renderer = Renderer() + actual = renderer.render(view) +- self.assertString(actual, u'nopqrstuvwxyz') ++ self.assertString(actual, 'nopqrstuvwxyz') + + def test_higher_order_lambda(self): + view = Lambdas() +@@ -119,7 +119,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + + renderer = Renderer() + actual = renderer.render(view) +- self.assertString(actual, u'abcdefghijklmnopqrstuvwxyz') ++ self.assertString(actual, 'abcdefghijklmnopqrstuvwxyz') + + def test_partials_with_lambda(self): + view = Lambdas() +@@ -127,7 +127,7 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + + renderer = Renderer(search_dirs=EXAMPLES_DIR) + actual = renderer.render(view) +- self.assertEqual(actual, u'nopqrstuvwxyz') ++ self.assertEqual(actual, 'nopqrstuvwxyz') + + def test_hierarchical_partials_with_lambdas(self): + view = Lambdas() +@@ -135,12 +135,12 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + + renderer = Renderer(search_dirs=EXAMPLES_DIR) + actual = renderer.render(view) +- self.assertString(actual, u'nopqrstuvwxyznopqrstuvwxyz') ++ self.assertString(actual, 'nopqrstuvwxyznopqrstuvwxyz') + + def test_inverted(self): + renderer = Renderer() + actual = renderer.render(Inverted()) +- self.assertString(actual, u"""one, two, three, empty list""") ++ self.assertString(actual, """one, two, three, empty list""") + + def test_accessing_properties_on_parent_object_from_child_objects(self): + parent = Thing() +@@ -152,12 +152,12 @@ class ViewTestCase(unittest.TestCase, AssertStringMixin): + renderer = Renderer() + actual = renderer.render(view, {'parent': parent}) + +- self.assertString(actual, u'derp') ++ self.assertString(actual, 'derp') + + def test_inverted_lists(self): + renderer = Renderer() + actual = renderer.render(InvertedLists()) +- self.assertString(actual, u"""one, two, three, empty list""") ++ self.assertString(actual, """one, two, three, empty list""") + + + def _make_specloader(): +@@ -176,7 +176,7 @@ def _make_specloader(): + """ + if encoding is None: + encoding = 'ascii' +- return unicode(s, encoding, 'strict') ++ return str(s, encoding, 'strict') + + loader = Loader(file_encoding='ascii', to_unicode=to_unicode) + return SpecLoader(loader=loader) +@@ -222,7 +222,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + custom.template = "abc" + + spec_loader = self._make_specloader() +- self._assert_template(spec_loader, custom, u"abc") ++ self._assert_template(spec_loader, custom, "abc") + + def test_load__template__type_unicode(self): + """ +@@ -230,10 +230,10 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + + """ + custom = TemplateSpec() +- custom.template = u"abc" ++ custom.template = "abc" + + spec_loader = self._make_specloader() +- self._assert_template(spec_loader, custom, u"abc") ++ self._assert_template(spec_loader, custom, "abc") + + def test_load__template__unicode_non_ascii(self): + """ +@@ -241,10 +241,10 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + + """ + custom = TemplateSpec() +- custom.template = u"é" ++ custom.template = "é" + + spec_loader = self._make_specloader() +- self._assert_template(spec_loader, custom, u"é") ++ self._assert_template(spec_loader, custom, "é") + + def test_load__template__with_template_encoding(self): + """ +@@ -252,14 +252,14 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + + """ + custom = TemplateSpec() +- custom.template = u'é'.encode('utf-8') ++ custom.template = 'é'.encode('utf-8') + + spec_loader = self._make_specloader() + +- self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, u'é') ++ self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, 'é') + + custom.template_encoding = 'utf-8' +- self._assert_template(spec_loader, custom, u'é') ++ self._assert_template(spec_loader, custom, 'é') + + # TODO: make this test complete. + def test_load__template__correct_loader(self): +@@ -279,10 +279,10 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + self.encoding = None + + # Overrides the existing method. +- def unicode(self, s, encoding=None): ++ def str(self, s, encoding=None): + self.s = s + self.encoding = encoding +- return u"foo" ++ return "foo" + + loader = MockLoader() + custom_loader = SpecLoader() +@@ -293,7 +293,7 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, + view.template_encoding = "encoding-foo" + + # Check that our unicode() above was called. +- self._assert_template(custom_loader, view, u'foo') ++ self._assert_template(custom_loader, view, 'foo') + self.assertEqual(loader.s, "template-foo") + self.assertEqual(loader.encoding, "encoding-foo") + +@@ -410,7 +410,7 @@ class TemplateSpecTests(unittest.TestCase, AssertPathsMixin): + loader = self._make_loader() + actual = loader.load(custom) + +- self.assertEqual(type(actual), unicode) ++ self.assertEqual(type(actual), str) + self.assertEqual(actual, expected) + + def test_get_template(self): +@@ -420,7 +420,7 @@ class TemplateSpecTests(unittest.TestCase, AssertPathsMixin): + """ + view = SampleView() + +- self._assert_get_template(view, u"ascii: abc") ++ self._assert_get_template(view, "ascii: abc") + + def test_get_template__template_encoding(self): + """ +@@ -432,4 +432,4 @@ class TemplateSpecTests(unittest.TestCase, AssertPathsMixin): + self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') + + view.template_encoding = 'utf-8' +- self._assert_get_template(view, u"non-ascii: é") ++ self._assert_get_template(view, "non-ascii: é") +diff --git a/setup.cfg b/setup.cfg +index 861a9f5..f6f1279 100644 +--- a/setup.cfg ++++ b/setup.cfg +@@ -1,5 +1,71 @@ +-[egg_info] +-tag_build = +-tag_date = 0 +-tag_svn_revision = 0 ++[metadata] ++name = pystache ++version = attr: pystache.__version__ ++author = Chris Wanstrath ++author_email = chris@ozmm.org ++maintainer = Steve Arnold ++maintainer_email = nerdboy@gentoo.org ++description = Mustache for Python ++url = https://github.com/sarnold/pystache ++license = MIT ++license_files = LICENSE ++classifiers = ++ Development Status :: 4 - Beta ++ Intended Audience :: Developers ++ License :: OSI Approved :: MIT License ++ Programming Language :: Python :: 3 ++ Programming Language :: Python :: 3.6 ++ Programming Language :: Python :: 3.7 ++ Programming Language :: Python :: 3.8 ++ Programming Language :: Python :: 3.9 + ++[options] ++python_requires = >=3.6 ++zip_safe = True ++include_package_data = True ++packages = find: ++ ++[options.package_data] ++* = *.mustache, *.txt ++ ++[options.entry_points] ++console_scripts = ++ pystache=pystache.commands.render:main ++ pystache-test=pystache.commands.test:main ++ ++[options.extras_require] ++test = ++ nose ++ ++cov = ++ coverage ++ ++[bdist_wheel] ++universal = 0 ++ ++[check-manifest] ++ignore = ++ .codeclimate.yml ++ .gitattributes ++ .coveragerc ++ .gitignore ++ .pep8speaks.yml ++ codecov.yml ++ ++[flake8] ++exclude = ++ .git, ++ __pycache__, ++ build, ++ dist ++ ++max-line-length = 110 ++ ++[nosetests] ++traverse-namespace = 1 ++verbosity = 3 ++with-coverage = 1 ++with-doctest = 1 ++doctest-extension = rst ++cover-package = pystache ++cover-xml = 1 +diff --git a/setup.py b/setup.py +index 0d99aae..f0b7d7f 100644 +--- a/setup.py ++++ b/setup.py +@@ -28,7 +28,7 @@ it on the PyPI project page. If PyPI finds any issues, it will render it + instead as plain-text, which we do not want. + + To check in advance that PyPI will accept and parse the reST file as HTML, +-you can use the rst2html program installed by the docutils package ++you can use the rst2html.py program installed by the docutils package + (http://docutils.sourceforge.net/). To install docutils: + + $ pip install docutils +@@ -89,30 +89,7 @@ import os + import shutil + import sys + +- +-py_version = sys.version_info +- +-# distutils does not seem to support the following setup() arguments. +-# It displays a UserWarning when setup() is passed those options: +-# +-# * entry_points +-# * install_requires +-# +-# distribute works with Python 2.3.5 and above: +-# +-# http://packages.python.org/distribute/setuptools.html#building-and-distributing-packages-with-distribute +-# +-if py_version < (2, 3, 5): +- # TODO: this might not work yet. +- import distutils as dist +- from distutils import core +- setup = core.setup +-else: +- import setuptools as dist +- setup = dist.setup +- +- +-VERSION = '0.5.4' # Also change in pystache/__init__.py. ++from setuptools import setup + + FILE_ENCODING = 'utf-8' + +@@ -126,22 +103,6 @@ TEMP_EXTENSION = '.temp' + + PREP_COMMAND = 'prep' + +-CLASSIFIERS = ( +- 'Development Status :: 4 - Beta', +- 'License :: OSI Approved :: MIT License', +- 'Programming Language :: Python', +- 'Programming Language :: Python :: 2', +- 'Programming Language :: Python :: 2.4', +- 'Programming Language :: Python :: 2.5', +- 'Programming Language :: Python :: 2.6', +- 'Programming Language :: Python :: 2.7', +- 'Programming Language :: Python :: 3', +- 'Programming Language :: Python :: 3.1', +- 'Programming Language :: Python :: 3.2', +- 'Programming Language :: Python :: 3.3', +- 'Programming Language :: Python :: Implementation :: PyPy', +-) +- + # Comments in reST begin with two dots. + RST_LONG_DESCRIPTION_INTRO = """\ + .. Do not edit this file. This file is auto-generated for PyPI by setup.py +@@ -221,7 +182,7 @@ def convert_md_to_rst(md_path, rst_temp_path): + + """ + # Pandoc uses the UTF-8 character encoding for both input and output. +- command = "pandoc --write=rst --output=%s %s" % (rst_temp_path, md_path) ++ command = "pandoc -f markdown-smart --write=rst --output=%s %s" % (rst_temp_path, md_path) + print("converting with pandoc: %s to %s\n-->%s" % (md_path, rst_temp_path, + command)) + +@@ -308,65 +269,9 @@ Run the following command and commit the changes-- + os.system('python setup.py sdist upload') + + +-# We use the package simplejson for older Python versions since Python +-# does not contain the module json before 2.6: +-# +-# http://docs.python.org/library/json.html +-# +-# Moreover, simplejson stopped officially support for Python 2.4 in version 2.1.0: +-# +-# https://github.com/simplejson/simplejson/blob/master/CHANGES.txt +-# +-requires = [] +-if py_version < (2, 5): +- requires.append('simplejson<2.1') +-elif py_version < (2, 6): +- requires.append('simplejson') +- +-INSTALL_REQUIRES = requires +- +-# TODO: decide whether to use find_packages() instead. I'm not sure that +-# find_packages() is available with distutils, for example. +-PACKAGES = [ +- 'pystache', +- 'pystache.commands', +- # The following packages are only for testing. +- 'pystache.tests', +- 'pystache.tests.data', +- 'pystache.tests.data.locator', +- 'pystache.tests.examples', +-] +- +- +-# The purpose of this function is to follow the guidance suggested here: +-# +-# http://packages.python.org/distribute/python3.html#note-on-compatibility-with-setuptools +-# +-# The guidance is for better compatibility when using setuptools (e.g. with +-# earlier versions of Python 2) instead of Distribute, because of new +-# keyword arguments to setup() that setuptools may not recognize. +-def get_extra_args(): +- """ +- Return a dictionary of extra args to pass to setup(). +- +- """ +- extra = {} +- # TODO: it might be more correct to check whether we are using +- # Distribute instead of setuptools, since use_2to3 doesn't take +- # effect when using Python 2, even when using Distribute. +- if py_version >= (3, ): +- # Causes 2to3 to be run during the build step. +- extra['use_2to3'] = True +- +- return extra +- +- + def main(sys_argv): + + # TODO: use the logging module instead of printing. +- # TODO: include the following in a verbose mode. +- sys.stderr.write("pystache: using: version %s of %s\n" % (repr(dist.__version__), repr(dist))) +- + command = sys_argv[-1] + + if command == 'publish': +@@ -377,35 +282,10 @@ def main(sys_argv): + sys.exit() + + long_description = read(RST_DESCRIPTION_PATH) +- template_files = ['*.mustache', '*.txt'] +- extra_args = get_extra_args() +- +- setup(name='pystache', +- version=VERSION, +- license='MIT', +- description='Mustache for Python', +- long_description=long_description, +- author='Chris Wanstrath', +- author_email='chris@ozmm.org', +- maintainer='Chris Jerdonek', +- maintainer_email='chris.jerdonek@gmail.com', +- url='http://github.com/defunkt/pystache', +- install_requires=INSTALL_REQUIRES, +- packages=PACKAGES, +- package_data = { +- # Include template files so tests can be run. +- 'pystache.tests.data': template_files, +- 'pystache.tests.data.locator': template_files, +- 'pystache.tests.examples': template_files, +- }, +- entry_points = { +- 'console_scripts': [ +- 'pystache=pystache.commands.render:main', +- 'pystache-test=pystache.commands.test:main', +- ], +- }, +- classifiers = CLASSIFIERS, +- **extra_args ++ ++ setup( ++ long_description=long_description, ++ long_description_content_type='text/x-rst', + ) + + +diff --git a/setup_description.rst b/setup_description.rst +index 724c457..d7f1bc0 100644 +--- a/setup_description.rst ++++ b/setup_description.rst +@@ -4,13 +4,17 @@ + Pystache + ======== + +-.. figure:: http://defunkt.github.com/pystache/images/logo_phillips.png +- :alt: mustachioed, monocled snake by David Phillips ++|ci| |Conda| |Wheels| |Release| |Python| + +-.. figure:: https://secure.travis-ci.org/defunkt/pystache.png +- :alt: Travis CI current build status ++|Latest release| |License| |Maintainability| |codecov| + +-`Pystache `__ is a Python ++This updated fork of Pystache is currently tested on Python 3.6+ and in ++Conda, on Linux, Macos, and Windows (Python 2.7 support has been ++removed). ++ ++|image9| ++ ++`Pystache `__ is a Python + implementation of `Mustache `__. Mustache + is a framework-agnostic, logic-free templating system inspired by + `ctemplate `__ and +@@ -23,62 +27,45 @@ page provides a good introduction to Mustache's syntax. For a more + complete (and more current) description of Mustache's behavior, see the + official `Mustache spec `__. + +-Pystache is `semantically versioned `__ and can be +-found on `PyPI `__. This version +-of Pystache passes all tests in `version +-1.1.2 `__ of the spec. ++Pystache is `semantically versioned `__ and older ++versions can still be found on ++`PyPI `__. This version of ++Pystache now passes all tests in `version ++1.1.3 `__ of the spec. + + Requirements + ------------ + + Pystache is tested with-- + +-- Python 2.4 (requires simplejson `version +- 2.0.9 `__ or earlier) +-- Python 2.5 (requires +- `simplejson `__) +-- Python 2.6 +-- Python 2.7 +-- Python 3.1 +-- Python 3.2 +-- Python 3.3 +-- `PyPy `__ ++- Python 3.6 ++- Python 3.7 ++- Python 3.8 ++- Python 3.9 ++- Conda (py36-py39) + + `Distribute `__ (the setuptools +-fork) is recommended over +-`setuptools `__, and is required +-in some cases (e.g. for Python 3 support). If you use +-`pip `__, you probably already satisfy +-this requirement. ++fork) is no longer required over ++`setuptools `__, as the current ++packaging is now PEP517-compliant. + + JSON support is needed only for the command-line interface and to run +-the spec tests. We require simplejson for earlier versions of Python +-since Python's `json `__ +-module was added in Python 2.6. +- +-For Python 2.4 we require an earlier version of simplejson since +-simplejson stopped officially supporting Python 2.4 in simplejson +-version 2.1.0. Earlier versions of simplejson can be installed manually, +-as follows: +- +-:: +- +- pip install 'simplejson<2.1.0' ++the spec tests; PyYAML can still be used (see the Develop section). + +-Official support for Python 2.4 will end with Pystache version 0.6.0. ++Official support for Python 2 will end with Pystache version 0.6.0. + + Install It + ---------- + + :: + +- pip install pystache ++ pip install -U pystache -f https://github.com/sarnold/pystache/releases/ + + And test it-- + + :: + +- pystache-test ++ pystache-test + + To install and test from source (e.g. from GitHub), see the Develop + section. +@@ -88,68 +75,68 @@ Use It + + :: + +- >>> import pystache +- >>> print pystache.render('Hi {{person}}!', {'person': 'Mom'}) +- Hi Mom! ++ >>> import pystache ++ >>> print(pystache.render('Hi {{person}}!', {'person': 'Mom'})) ++ Hi Mom! + + You can also create dedicated view classes to hold your view logic. + +-Here's your view class (in .../examples/readme.py): ++Here's your view class (in ../pystache/tests/examples/readme.py): + + :: + +- class SayHello(object): +- def to(self): +- return "Pizza" ++ class SayHello(object): ++ def to(self): ++ return "Pizza" + + Instantiating like so: + + :: + +- >>> from pystache.tests.examples.readme import SayHello +- >>> hello = SayHello() ++ >>> from pystache.tests.examples.readme import SayHello ++ >>> hello = SayHello() + +-Then your template, say\_hello.mustache (by default in the same +-directory as your class definition): ++Then your template, say_hello.mustache (by default in the same directory ++as your class definition): + + :: + +- Hello, {{to}}! ++ Hello, {{to}}! + + Pull it together: + + :: + +- >>> renderer = pystache.Renderer() +- >>> print renderer.render(hello) +- Hello, Pizza! ++ >>> renderer = pystache.Renderer() ++ >>> print(renderer.render(hello)) ++ Hello, Pizza! + + For greater control over rendering (e.g. to specify a custom template + directory), use the ``Renderer`` class like above. One can pass + attributes to the Renderer class constructor or set them on a Renderer + instance. To customize template loading on a per-view basis, subclass + ``TemplateSpec``. See the docstrings of the +-`Renderer `__ ++`Renderer `__ + class and +-`TemplateSpec `__ ++`TemplateSpec `__ + class for more information. + + You can also pre-parse a template: + + :: + +- >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") +- >>> print parsed +- [u'Hey ', _SectionNode(key=u'who', index_begin=12, index_end=18, parsed=[_EscapeNode(key=u'.'), u'!'])] ++ >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") ++ >>> print(parsed) ++ ['Hey ', _SectionNode(key='who', index_begin=12, index_end=18, parsed=[_EscapeNode(key='.'), '!'])] + + And then: + + :: + +- >>> print renderer.render(parsed, {'who': 'Pops'}) +- Hey Pops! +- >>> print renderer.render(parsed, {'who': 'you'}) +- Hey you! ++ >>> print(renderer.render(parsed, {'who': 'Pops'})) ++ Hey Pops! ++ >>> print(renderer.render(parsed, {'who': 'you'})) ++ Hey you! + + Python 3 + -------- +@@ -211,22 +198,24 @@ To test from a source distribution (without installing)-- + + :: + +- python test_pystache.py ++ python test_pystache.py + + To test Pystache with multiple versions of Python (with a single +-command!), you can use `tox `__: ++command!) and different platforms, you can use ++`tox `__: + + :: + +- pip install 'virtualenv<1.8' # Version 1.8 dropped support for Python 2.4. +- pip install 'tox<1.4' # Version 1.4 dropped support for Python 2.4. +- tox ++ pip install tox ++ tox -e setup + +-If you do not have all Python versions listed in ``tox.ini``-- ++To run tests on multiple versions with coverage, run: + + :: + +- tox -e py26,py32 # for example ++ tox -e py38-linux,py39-linux # for example ++ ++(substitute your platform above, eg, macos or windows) + + The source distribution tests also include doctests and tests from the + Mustache spec. To include tests from the Mustache spec in your test +@@ -234,8 +223,8 @@ runs: + + :: + +- git submodule init +- git submodule update ++ git submodule init ++ git submodule update + + The test harness parses the spec's (more human-readable) yaml files if + `PyYAML `__ is present. Otherwise, +@@ -243,94 +232,113 @@ it parses the json files. To install PyYAML-- + + :: + +- pip install pyyaml ++ pip install pyyaml ++ ++Once the submodule is available, you can run the full test set with: ++ ++:: ++ ++ tox -e setup . ext/spec/specs + + To run a subset of the tests, you can use + `nose `__: + + :: + +- pip install nose +- nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present ++ pip install nose ++ nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present + +-Using Python 3 with Pystache from source +-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++Mailing List (old) ++------------------ + +-Pystache is written in Python 2 and must be converted to Python 3 prior +-to using it with Python 3. The installation process (and tox) do this +-automatically. ++There is(was) a `mailing ++list `__. Note that there is a ++bit of a delay between posting a message and seeing it appear in the ++mailing list archive. + +-To convert the code to Python 3 manually (while using Python 3)-- ++Credits ++------- + + :: + +- python setup.py build ++ >>> import pystache ++ >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek','refurbisher': 'Steve Arnold' } ++ >>> print(pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}\nRefurbisher: {{refurbisher}}", context)) ++ Author: Chris Wanstrath ++ Maintainer: Chris Jerdonek ++ Refurbisher: Steve Arnold + +-This writes the converted code to a subdirectory called ``build``. By +-design, Python 3 builds +-`cannot `__ +-be created from Python 2. ++Pystache logo by `David Phillips `__ is ++licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported ++License `__. ++|image10| + +-To convert the code without using setup.py, you can use +-`2to3 `__ as follows (two +-steps)-- ++History ++======= + +-:: ++**Note:** Official support for Python 2.7 will end with Pystache version ++0.6.0. + +- 2to3 --write --nobackups --no-diffs --doctests_only pystache +- 2to3 --write --nobackups --no-diffs pystache ++0.6.0 (2021-03-04) ++------------------ + +-This converts the code (and doctests) in place. ++- Bump spec versions to latest => v1.1.3 ++- Modernize python and CI tools, update docs/doctests ++- Update unicode conversion test for py3-only ++- Add pep8speaks cfg, cleanup warnings ++- Remove superfluous setup test/unused imports ++- Add conda recipe/CI build + +-To ``import pystache`` from a source distribution while using Python 3, +-be sure that you are importing from a directory containing a converted +-version of the code (e.g. from the ``build`` directory after +-converting), and not from the original (unconverted) source directory. +-Otherwise, you will get a syntax error. You can help prevent this by not +-running the Python IDE from the project directory when importing +-Pystache while using Python 3. ++.. _section-1: + +-Mailing List +------------- ++0.5.6 (2021-02-28) ++------------------ + +-There is a `mailing list `__. +-Note that there is a bit of a delay between posting a message and seeing +-it appear in the mailing list archive. ++- Use correct wheel name in release workflow, limit wheels ++- Add install check/test of downloaded wheel ++- Update/add ci workflows and tox cfg, bump to next dev0 version + +-Credits +-------- ++.. _section-2: + +-:: ++0.5.5 (2020-12-16) ++------------------ + +- >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek' } +- >>> print pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}", context) +- Author: Chris Wanstrath +- Maintainer: Chris Jerdonek ++- fix document processing, update pandoc args and history ++- add release.yml to CI, test env settings ++- fix bogus commit message, update versions and tox cf ++- add post-test steps for building pkgs with/without doc updates ++- add CI build check, fix MANIFEST.in pruning + +-Pystache logo by `David Phillips `__ is +-licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported +-License `__. +-|image0| ++.. _section-3: + +-History +-======= ++0.5.4-2 (2020-11-09) ++-------------------- + +-**Note:** Official support for Python 2.4 will end with Pystache version +-0.6.0. ++- Merge pull request #1 from sarnold/rebase-up ++- Bugfix: test_specloader.py: fix test_find__with_directory on other ++ OSs ++- Bugfix: pystache/loader.py: remove stray windows line-endings ++- fix crufty (and insecure) http urls ++- Bugfix: modernize python versions (keep py27) and fix spec_test load ++ cmd ++ ++.. _section-4: + + 0.5.4 (2014-07-11) + ------------------ + + - Bugfix: made test with filenames OS agnostic (issue #162). + ++.. _section-5: ++ + 0.5.3 (2012-11-03) + ------------------ + + - Added ability to customize string coercion (e.g. to have None render + as ``''``) (issue #130). +-- Added Renderer.render\_name() to render a template by name (issue ++- Added Renderer.render_name() to render a template by name (issue + #122). +-- Added TemplateSpec.template\_path to specify an absolute path to a ++- Added TemplateSpec.template_path to specify an absolute path to a + template (issue #41). + - Added option of raising errors on missing tags/partials: + ``Renderer(missing_tags='strict')`` (issue #110). +@@ -355,6 +363,8 @@ History + - More robust handling of byte strings in Python 3. + - Added Creative Commons license for David Phillips's logo. + ++.. _section-6: ++ + 0.5.2 (2012-05-03) + ------------------ + +@@ -367,16 +377,20 @@ History + context stack (issue #113). + - Bugfix: lists of lambdas for sections were not rendered (issue #114). + ++.. _section-7: ++ + 0.5.1 (2012-04-24) + ------------------ + + - Added support for Python 3.1 and 3.2. + - Added tox support to test multiple Python versions. + - Added test script entry point: pystache-test. +-- Added \_\_version\_\_ package attribute. ++- Added \__version_\_ package attribute. + - Test harness now supports both YAML and JSON forms of Mustache spec. + - Test harness no longer requires nose. + ++.. _section-8: ++ + 0.5.0 (2012-04-03) + ------------------ + +@@ -435,11 +449,15 @@ Bug fixes: + - Passing ``**kwargs`` to ``Template()`` with no context no longer + raises an exception. + ++.. _section-9: ++ + 0.4.1 (2012-03-25) + ------------------ + + - Added support for Python 2.4. [wangtz, jvantuyl] + ++.. _section-10: ++ + 0.4.0 (2011-01-12) + ------------------ + +@@ -447,19 +465,25 @@ Bug fixes: + - Add support for inverted lists + - Decoupled template loading + ++.. _section-11: ++ + 0.3.1 (2010-05-07) + ------------------ + + - Fix package + ++.. _section-12: ++ + 0.3.0 (2010-05-03) + ------------------ + +-- View.template\_path can now hold a list of path ++- View.template_path can now hold a list of path + - Add {{& blah}} as an alias for {{{ blah }}} + - Higher Order Sections + - Inverted sections + ++.. _section-13: ++ + 0.2.0 (2010-02-15) + ------------------ + +@@ -473,12 +497,16 @@ Bug fixes: + [enaeseth] + - Template file encoding awareness. [enaeseth] + ++.. _section-14: ++ + 0.1.1 (2009-11-13) + ------------------ + + - Ensure we're dealing with strings, always + - Tests can be run by executing the test file directly + ++.. _section-15: ++ + 0.1.0 (2009-11-12) + ------------------ + +@@ -510,4 +538,23 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-.. |image0| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png ++.. |ci| image:: https://github.com/sarnold/pystache/actions/workflows/ci.yml/badge.svg ++ :target: https://github.com/sarnold/pystache/actions/workflows/ci.yml ++.. |Conda| image:: https://github.com/sarnold/pystache/actions/workflows/conda.yml/badge.svg ++ :target: https://github.com/sarnold/pystache/actions/workflows/conda.yml ++.. |Wheels| image:: https://github.com/sarnold/pystache/actions/workflows/wheels.yml/badge.svg ++ :target: https://github.com/sarnold/pystache/actions/workflows/wheels.yml ++.. |Release| image:: https://github.com/sarnold/pystache/actions/workflows/release.yml/badge.svg ++ :target: https://github.com/sarnold/pystache/actions/workflows/release.yml ++.. |Python| image:: https://img.shields.io/badge/python-3.6+-blue.svg ++ :target: https://www.python.org/downloads/ ++.. |Latest release| image:: https://img.shields.io/github/v/release/sarnold/pystache?include_prereleases ++ :target: https://github.com/sarnold/pystache/releases/latest ++.. |License| image:: https://img.shields.io/github/license/sarnold/pystache ++ :target: https://github.com/sarnold/pystache/blob/master/LICENSE ++.. |Maintainability| image:: https://api.codeclimate.com/v1/badges/a8fa1bf4638bfc6581b6/maintainability ++ :target: https://codeclimate.com/github/sarnold/pystache/maintainability ++.. |codecov| image:: https://codecov.io/gh/sarnold/pystache/branch/master/graph/badge.svg?token=5PZNMZBI6K ++ :target: https://codecov.io/gh/sarnold/pystache ++.. |image9| image:: gh/images/logo_phillips_small.png ++.. |image10| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png +diff --git a/tox.ini b/tox.ini +index d1eaebf..66c4515 100644 +--- a/tox.ini ++++ b/tox.ini +@@ -1,36 +1,110 @@ +-# A tox configuration file to test across multiple Python versions. +-# +-# http://pypi.python.org/pypi/tox +-# + [tox] +-# Tox 1.4 drops py24 and adds py33. In the current version, we want to +-# support 2.4, so we can't simultaneously support 3.3. +-envlist = py24,py25,py26,py27,py27-yaml,py27-noargs,py31,py32,pypy ++envlist = py{36,37,38,39}-{linux,macos,windows} ++skip_missing_interpreters = true ++isolated_build = true ++#skipsdist = true ++ ++[gh-actions] ++python = ++ 3.6: py36 ++ 3.7: py37 ++ 3.8: py38 ++ 3.9: py39 ++ ++[gh-actions:env] ++PLATFORM = ++ ubuntu-18.04: linux ++ macos-latest: macos ++ windows-latest: windows + + [testenv] ++passenv = CI PYTHON PYTHONIOENCODING ++ ++deps = ++ pip>=20.0.1 ++ nose ++ coverage ++ ++commands = ++ nosetests -sx . {posargs} ++ ++[testenv:bare] + # Change the working directory so that we don't import the pystache located + # in the original location. ++deps = ++ pip>=20.0.1 ++ -e . ++ + changedir = + {envbindir} ++ + commands = +- pystache-test {toxinidir} ++ pystache-test ++ ++[testenv:bench] ++passenv = CI PYTHON PYTHONIOENCODING + +-# Check that the spec tests work with PyYAML. +-[testenv:py27-yaml] +-basepython = +- python2.7 + deps = +- PyYAML +-changedir = +- {envbindir} ++ pip>=20.0.1 ++ # uncomment for comparison, posargs expects a number, eg, 10000 ++ #chevron ++ ++commands_pre = ++ pip install . ++ + commands = +- pystache-test {toxinidir} ++ python pystache/tests/benchmark.py {posargs} ++ ++[testenv:setup] ++passenv = CI PYTHON PYTHONIOENCODING ++ ++deps = ++ pyyaml ++ twine ++ ++commands = ++ python setup.py install ++ twine check dist/* ++ pystache-test {posargs} ++ ++[testenv:deploy] ++passenv = CI PYTHON PYTHONIOENCODING ++allowlist_externals = bash ++ ++deps = ++ pip>=19.0.1 ++ wheel ++ pep517 ++ twine ++ ++commands = ++ python -m pep517.build . ++ twine check dist/* ++ ++[testenv:check] ++passenv = CI PYTHON PYTHONIOENCODING ++skip_install = true ++ ++allowlist_externals = bash ++ ++deps = ++ pip>=20.0.1 + +-# Check that pystache-test works from an install with no arguments. +-[testenv:py27-noargs] +-basepython = +- python2.7 +-changedir = +- {envbindir} + commands = ++ bash -c 'export WHL_FILE=$(ls dist/*.whl); \ ++ python -m pip install $WHL_FILE' + pystache-test ++ ++[testenv:docs] ++passenv = CI PYTHON PYTHONIOENCODING ++allowlist_externals = bash ++ ++deps = ++ pip>=19.0.1 ++ wheel ++ docutils ++ # apt/emerge pandoc first ++ ++commands = ++ python setup.py prep ++ bash -c 'python setup.py --long-description | rst2html.py -v --no-raw > out.html' +diff --git a/travis.yml_disabled b/travis.yml_disabled +new file mode 100644 +index 0000000..f0b4042 +--- /dev/null ++++ b/travis.yml_disabled +@@ -0,0 +1,52 @@ ++dist: xenial ++language: python ++ ++# Travis CI has no plans to support Jython and no longer supports Python 2.5. ++python: ++ - "2.7" ++ - "3.5" ++ - "3.6" ++ - "3.7" ++ - "3.8" ++ - "3.9-dev" ++ - "nightly" ++ ++matrix: ++ fast_finish: true ++ include: ++ - os: osx ++ # osx is goofy, ``python`` is always py2, images mutate fast ++ language: shell ++ before_install: ++ - pip3 install --upgrade pip wheel ++ install: ++ - python3 setup.py install ++ script: ++ - pystache-test . ext/spec/specs ++ - os: windows ++ # windows is even goofier, install path is different for python/python3 ++ # but either way you get python3 and the cmd is always ``python`` o.O ++ # (also versions mutuate like bacteria) ++ language: shell ++ before_install: ++ - choco install python3 --params "/InstallDir:C:\\Python" ++ - python -m pip install --upgrade pip wheel ++ env: PATH="/c/Python:/c/Python/Scripts:$PATH" ++ install: ++ - python setup.py install ++ script: ++ - pystache-test . ext/spec/specs ++ allow_failures: ++ - python: "nightly" ++ ++# command to install dependencies ++install: ++ - pip install --upgrade pip ++ - pip install codecov ++ ++script: ++ - python setup.py install ++ # Include the spec tests directory for Mustache spec tests and the ++ # project directory for doctests. ++ - pystache-test . ext/spec/specs ++ #- tox +-- +2.33.0 + -- cgit v1.2.3-54-g00ecf