├── .flake8 ├── .git_archival.txt ├── .gitattributes ├── .gitchangelog.rc ├── .github └── workflows │ ├── bandit.yml │ ├── ci.yml │ ├── conda.yml │ ├── coverage.yml │ ├── pylint.yml │ ├── release.yml │ └── sphinx.yml ├── .gitignore ├── .gitmodules ├── .pep8speaks.yml ├── .pre-commit-config.yaml ├── .pylintrc ├── CHANGELOG.rst ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO.rst ├── conda └── meta.yaml ├── docs ├── Makefile ├── make.bat └── source │ ├── changelog.rst │ ├── conf.py │ ├── dev │ ├── generate-changelog.rst │ ├── pre-commit-config.rst │ └── pre-commit-usage.rst │ ├── gh │ └── images │ │ └── logo_phillips_small.png │ ├── index.rst │ └── readme_include.rst ├── gh ├── fix_pkg_name.sh └── images │ ├── logo_phillips.png │ └── logo_phillips_small.png ├── pyproject.toml ├── pystache ├── __init__.py ├── commands │ ├── __init__.py │ ├── render.py │ └── test.py ├── common.py ├── context.py ├── defaults.py ├── init.py ├── loader.py ├── locator.py ├── parsed.py ├── parser.py ├── renderengine.py ├── renderer.py ├── specloader.py ├── template_spec.py └── tests │ ├── __init__.py │ ├── benchmark.py │ ├── common.py │ ├── data │ ├── __init__.py │ ├── ascii.mustache │ ├── duplicate.mustache │ ├── locator │ │ ├── __init__.py │ │ ├── duplicate.mustache │ │ └── template.txt │ ├── non_ascii.mustache │ ├── sample_view.mustache │ ├── say_hello.mustache │ └── views.py │ ├── doctesting.py │ ├── examples │ ├── __init__.py │ ├── comments.mustache │ ├── comments.py │ ├── complex.mustache │ ├── complex.py │ ├── delimiters.mustache │ ├── delimiters.py │ ├── double_section.mustache │ ├── double_section.py │ ├── escaped.mustache │ ├── escaped.py │ ├── extensionless │ ├── inner_partial.mustache │ ├── inner_partial.txt │ ├── inverted.mustache │ ├── inverted.py │ ├── lambdas.mustache │ ├── lambdas.py │ ├── looping_partial.mustache │ ├── nested_context.mustache │ ├── nested_context.py │ ├── partial_in_partial.mustache │ ├── partial_with_lambda.mustache │ ├── partial_with_partial_and_lambda.mustache │ ├── partials_with_lambdas.py │ ├── readme.py │ ├── say_hello.mustache │ ├── simple.mustache │ ├── simple.py │ ├── tagless.mustache │ ├── template_partial.mustache │ ├── template_partial.py │ ├── template_partial.txt │ ├── unescaped.mustache │ ├── unescaped.py │ ├── unicode_input.mustache │ ├── unicode_input.py │ ├── unicode_output.mustache │ └── unicode_output.py │ ├── main.py │ ├── spectesting.py │ ├── test___init__.py │ ├── test_commands.py │ ├── test_context.py │ ├── test_defaults.py │ ├── test_examples.py │ ├── test_loader.py │ ├── test_locator.py │ ├── test_parser.py │ ├── test_pystache.py │ ├── test_renderengine.py │ ├── test_renderer.py │ ├── test_simple.py │ └── test_specloader.py ├── setup.py ├── test_pystache.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | __pycache__, 5 | pystache/tests, 6 | test_pystache.py, 7 | build, 8 | dist 9 | 10 | max-line-length = 110 11 | max-complexity = 25 12 | addons = file,open,basestring,xrange,unicode,long,cmp 13 | ignore = 14 | E266, 15 | E731, 16 | E203, 17 | E221, 18 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: 531a0ecea00a696423175ef46c319b29408dc3e2 2 | node-date: 2025-03-18T04:49:05-07:00 3 | describe-name: v0.6.8 4 | ref-names: HEAD -> master, tag: v0.6.8 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .git_archival.txt export-subst 2 | -------------------------------------------------------------------------------- /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python -*- 2 | ## 3 | ## Format 4 | ## 5 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 6 | ## 7 | ## Description 8 | ## 9 | ## ACTION is one of 'chg', 'fix', 'new' 10 | ## 11 | ## Is WHAT the change is about. 12 | ## 13 | ## 'chg' is for refactor, small improvement, cosmetic changes... 14 | ## 'fix' is for bug fixes 15 | ## 'new' is for new features, big improvement 16 | ## 17 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 18 | ## 19 | ## Is WHO is concerned by the change. 20 | ## 21 | ## 'dev' is for developpers (API changes, refactors...) 22 | ## 'usr' is for final users (UI changes) 23 | ## 'pkg' is for packagers (packaging changes) 24 | ## 'test' is for testers (test only related changes) 25 | ## 'doc' is for doc guys (doc only changes) 26 | ## 27 | ## COMMIT_MSG is ... well ... the commit message itself. 28 | ## 29 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' 30 | ## 31 | ## They are preceded with a '!' or a '@' (prefer the former, as the 32 | ## latter is wrongly interpreted in github.) Commonly used tags are: 33 | ## 34 | ## 'refactor' is obviously for refactoring code only 35 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 36 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 37 | ## 'wip' is for partial functionality but complete subfunctionality. 38 | ## 39 | ## Example: 40 | ## 41 | ## new: usr: support of bazaar implemented 42 | ## chg: re-indentend some lines !cosmetic 43 | ## new: dev: updated code to be compatible with last version of killer lib. 44 | ## fix: pkg: updated year of licence coverage. 45 | ## new: test: added a bunch of test around user usability of feature X. 46 | ## fix: typo in spelling my name in comment. !minor 47 | ## 48 | ## Please note that multi-line commit message are supported, and only the 49 | ## first line will be considered as the "summary" of the commit message. So 50 | ## tags, and other rules only applies to the summary. The body of the commit 51 | ## message will be displayed in the changelog without reformatting. 52 | 53 | 54 | ## 55 | ## ``ignore_regexps`` is a line of regexps 56 | ## 57 | ## Any commit having its full commit message matching any regexp listed here 58 | ## will be ignored and won't be reported in the changelog. 59 | ## 60 | ignore_regexps = [ 61 | r'@minor', r'!minor', 62 | r'@cosmetic', r'!cosmetic', 63 | r'@refactor', r'!refactor', 64 | r'@wip', r'!wip', 65 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 66 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 67 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 68 | r'^$', ## ignore commits with empty messages 69 | ] 70 | 71 | 72 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 73 | ## list of regexp 74 | ## 75 | ## Commit messages will be classified in sections thanks to this. Section 76 | ## titles are the label, and a commit is classified under this section if any 77 | ## of the regexps associated is matching. 78 | ## 79 | ## Please note that ``section_regexps`` will only classify commits and won't 80 | ## make any changes to the contents. So you'll probably want to go check 81 | ## ``subject_process`` (or ``body_process``) to do some changes to the subject, 82 | ## whenever you are tweaking this variable. 83 | ## 84 | section_regexps = [ 85 | ('New', [ 86 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 87 | ]), 88 | ('Features', [ 89 | r'^([nN]ew|[fF]eat)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 90 | ]), 91 | ('Changes', [ 92 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 93 | ]), 94 | ('Fixes', [ 95 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 96 | ]), 97 | 98 | ('Other', None ## Match all lines 99 | ), 100 | ] 101 | 102 | 103 | ## ``body_process`` is a callable 104 | ## 105 | ## This callable will be given the original body and result will 106 | ## be used in the changelog. 107 | ## 108 | ## Available constructs are: 109 | ## 110 | ## - any python callable that take one txt argument and return txt argument. 111 | ## 112 | ## - ReSub(pattern, replacement): will apply regexp substitution. 113 | ## 114 | ## - Indent(chars=" "): will indent the text with the prefix 115 | ## Please remember that template engines gets also to modify the text and 116 | ## will usually indent themselves the text if needed. 117 | ## 118 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns 119 | ## 120 | ## - noop: do nothing 121 | ## 122 | ## - ucfirst: ensure the first letter is uppercase. 123 | ## (usually used in the ``subject_process`` pipeline) 124 | ## 125 | ## - final_dot: ensure text finishes with a dot 126 | ## (usually used in the ``subject_process`` pipeline) 127 | ## 128 | ## - strip: remove any spaces before or after the content of the string 129 | ## 130 | ## - SetIfEmpty(msg="No commit message."): will set the text to 131 | ## whatever given ``msg`` if the current text is empty. 132 | ## 133 | ## Additionally, you can `pipe` the provided filters, for instance: 134 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") 135 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') 136 | #body_process = noop 137 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip 138 | 139 | 140 | ## ``subject_process`` is a callable 141 | ## 142 | ## This callable will be given the original subject and result will 143 | ## be used in the changelog. 144 | ## 145 | ## Available constructs are those listed in ``body_process`` doc. 146 | subject_process = (strip | 147 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | 148 | SetIfEmpty("No commit message.") | ucfirst | final_dot) 149 | 150 | 151 | ## ``tag_filter_regexp`` is a regexp 152 | ## 153 | ## Tags that will be used for the changelog must match this regexp. 154 | ## 155 | tag_filter_regexp = r'^v?[0-9]+\.[0-9]+(\.[0-9]+)?$' 156 | #tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' 157 | 158 | 159 | ## ``unreleased_version_label`` is a string or a callable that outputs a string 160 | ## 161 | ## This label will be used as the changelog Title of the last set of changes 162 | ## between last valid tag and HEAD if any. 163 | #unreleased_version_label = "(unreleased)" 164 | unreleased_version_label = lambda: swrap( 165 | ["git", "describe", "--tags"], 166 | shell=False) 167 | 168 | 169 | ## ``output_engine`` is a callable 170 | ## 171 | ## This will change the output format of the generated changelog file 172 | ## 173 | ## Available choices are: 174 | ## 175 | ## - rest_py 176 | ## 177 | ## Legacy pure python engine, outputs ReSTructured text. 178 | ## This is the default. 179 | ## 180 | ## - mustache() 181 | ## 182 | ## Template name could be any of the available templates in 183 | ## ``templates/mustache/*.tpl``. 184 | ## Requires python package ``pystache``. 185 | ## Examples: 186 | ## - mustache("markdown") 187 | ## - mustache("restructuredtext") 188 | ## 189 | ## - makotemplate() 190 | ## 191 | ## Template name could be any of the available templates in 192 | ## ``templates/mako/*.tpl``. 193 | ## Requires python package ``mako``. 194 | ## Examples: 195 | ## - makotemplate("restructuredtext") 196 | ## 197 | output_engine = rest_py 198 | #output_engine = mustache("restructuredtext") 199 | #output_engine = mustache("markdown") 200 | #output_engine = makotemplate("restructuredtext") 201 | 202 | 203 | ## ``include_merge`` is a boolean 204 | ## 205 | ## This option tells git-log whether to include merge commits in the log. 206 | ## The default is to include them. 207 | include_merge = False 208 | 209 | 210 | ## ``log_encoding`` is a string identifier 211 | ## 212 | ## This option tells gitchangelog what encoding is outputed by ``git log``. 213 | ## The default is to be clever about it: it checks ``git config`` for 214 | ## ``i18n.logOutputEncoding``, and if not found will default to git's own 215 | ## default: ``utf-8``. 216 | #log_encoding = 'utf-8' 217 | 218 | 219 | ## ``publish`` is a callable 220 | ## 221 | ## Sets what ``gitchangelog`` should do with the output generated by 222 | ## the output engine. ``publish`` is a callable taking one argument 223 | ## that is an interator on lines from the output engine. 224 | ## 225 | ## Some helper callable are provided: 226 | ## 227 | ## Available choices are: 228 | ## 229 | ## - stdout 230 | ## 231 | ## Outputs directly to standard output 232 | ## (This is the default) 233 | ## 234 | ## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start(), flags) 235 | ## 236 | ## Creates a callable that will parse given file for the given 237 | ## regex pattern and will insert the output in the file. 238 | ## ``idx`` is a callable that receive the matching object and 239 | ## must return a integer index point where to insert the 240 | ## the output in the file. Default is to return the position of 241 | ## the start of the matched string. 242 | ## 243 | ## - FileRegexSubst(file, pattern, replace, flags) 244 | ## 245 | ## Apply a replace inplace in the given file. Your regex pattern must 246 | ## take care of everything and might be more complex. Check the README 247 | ## for a complete copy-pastable example. 248 | ## 249 | # publish = FileInsertIntoFirstRegexMatch( 250 | # "CHANGELOG.rst", 251 | # r'/(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/', 252 | # idx=lambda m: m.start(1) 253 | # ) 254 | #publish = stdout 255 | 256 | 257 | ## ``revs`` is a list of callable or a list of string 258 | ## 259 | ## callable will be called to resolve as strings and allow dynamical 260 | ## computation of these. The result will be used as revisions for 261 | ## gitchangelog (as if directly stated on the command line). This allows 262 | ## to filter exaclty which commits will be read by gitchangelog. 263 | ## 264 | ## To get a full documentation on the format of these strings, please 265 | ## refer to the ``git rev-list`` arguments. There are many examples. 266 | ## 267 | ## Using callables is especially useful, for instance, if you 268 | ## are using gitchangelog to generate incrementally your changelog. 269 | ## 270 | ## Some helpers are provided, you can use them:: 271 | ## 272 | ## - FileFirstRegexMatch(file, pattern): will return a callable that will 273 | ## return the first string match for the given pattern in the given file. 274 | ## If you use named sub-patterns in your regex pattern, it'll output only 275 | ## the string matching the regex pattern named "rev". 276 | ## 277 | ## - Caret(rev): will return the rev prefixed by a "^", which is a 278 | ## way to remove the given revision and all its ancestor. 279 | ## 280 | ## Please note that if you provide a rev-list on the command line, it'll 281 | ## replace this value (which will then be ignored). 282 | ## 283 | ## If empty, then ``gitchangelog`` will act as it had to generate a full 284 | ## changelog. 285 | ## 286 | ## The default is to use all commits to make the changelog. 287 | #revs = ["^1.0.3", ] 288 | #revs = [ 289 | # Caret( 290 | # FileFirstRegexMatch( 291 | # "CHANGELOG.rst", 292 | # r"(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")), 293 | # "HEAD" 294 | #] 295 | revs = [] 296 | -------------------------------------------------------------------------------- /.github/workflows/bandit.yml: -------------------------------------------------------------------------------- 1 | name: Security check - Bandit 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-20.04 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Run bandit 15 | uses: VCTLabs/bandit-report-artifacts@master 16 | with: 17 | project_path: pystache 18 | ignore_failure: true 19 | exclude_paths: 'pystache/tests' 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ${{ matrix.os }} 14 | defaults: 15 | run: 16 | shell: bash 17 | env: 18 | OS: ${{ matrix.os }} 19 | PYTHON: ${{ matrix.python-version }} 20 | PYTHONIOENCODING: utf-8 21 | PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [ubuntu-20.04, macos-latest, windows-latest] 26 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 27 | steps: 28 | - name: Set git crlf/eol 29 | run: | 30 | git config --global core.autocrlf false 31 | git config --global core.eol lf 32 | 33 | - uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v4 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install tox tox-gh-actions 46 | 47 | - name: Run full (internal) test suite 48 | run: | 49 | tox 50 | env: 51 | PLATFORM: ${{ matrix.os }} 52 | -------------------------------------------------------------------------------- /.github/workflows/conda.yml: -------------------------------------------------------------------------------- 1 | name: Conda 2 | 3 | on: 4 | workflow_dispatch: 5 | #pull_request: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [ubuntu-20.04, windows-latest, macos-latest] 16 | python-version: [3.8, '3.10'] 17 | 18 | runs-on: ${{ matrix.platform }} 19 | 20 | # The setup-miniconda action needs this to activate miniconda 21 | defaults: 22 | run: 23 | shell: "bash -l {0}" 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Cache conda 31 | uses: actions/cache@v3 32 | with: 33 | path: ~/conda_pkgs_dir 34 | key: ${{matrix.os}}-conda-pkgs-${{hashFiles('**/conda/meta.yaml')}} 35 | 36 | - name: Get conda 37 | uses: conda-incubator/setup-miniconda@v2 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | channels: conda-forge 41 | channel-priority: strict 42 | use-only-tar-bz2: true 43 | auto-activate-base: true 44 | 45 | - name: Prepare 46 | run: conda install conda-build conda-verify 47 | 48 | - name: Build 49 | run: conda build conda 50 | 51 | - name: Install 52 | run: conda install -c ${CONDA_PREFIX}/conda-bld/ pystache 53 | 54 | - name: Test 55 | run: pystache-test 56 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | # internal coverage with PR comment and badge v0.0.5 2 | # Note this works for public orgs but only for "internal" pull 3 | # requests. In the case of fork PRs, there needs to be org-level 4 | # github app with private key => ACCESS_TOKEN, with more job isolation 5 | # and output passing in this workflow. 6 | # 7 | # This version has updated actions and coverage value regex, no fork isolation 8 | # yet. This version checks for both repo owner and author_association = MEMBER 9 | # so PR comments *should* be more available. 10 | name: Coverage 11 | 12 | on: 13 | workflow_dispatch: 14 | pull_request: 15 | push: 16 | branches: 17 | - master 18 | 19 | jobs: 20 | pre_ci: 21 | name: Prepare CI environment 22 | runs-on: ubuntu-20.04 23 | outputs: 24 | #commit_message: ${{ steps.get_commit_message.outputs.commit_message }} 25 | branch: ${{ steps.extract_branch.outputs.branch }} 26 | 27 | steps: 28 | - name: Checkout Project 29 | uses: actions/checkout@v3 30 | with: 31 | # We need to fetch with a depth of 2 for pull_request so we can do HEAD^2 32 | fetch-depth: 2 33 | 34 | - name: Environment 35 | run: | 36 | bash -c set 37 | 38 | #- name: "Get commit message" 39 | #id: get_commit_message 40 | #env: 41 | #COMMIT_PUSH: ${{ github.event.head_commit.message }} 42 | #run: | 43 | #COMMIT_MESSAGE="${COMMIT_PUSH:-$(git log --format=%B -n 1 HEAD^2)}" 44 | #echo "commit_message=${COMMIT_MESSAGE}" >> $GITHUB_OUTPUT 45 | 46 | - name: Extract branch name 47 | id: extract_branch 48 | shell: bash 49 | run: | 50 | TMP_PULL_HEAD_REF="${{ github.head_ref }}" 51 | TMP_GITHUB_REF="${GITHUB_REF#refs/heads/}" 52 | EXPORT_VALUE="" 53 | if [ "${TMP_PULL_HEAD_REF}" != "" ] 54 | then 55 | EXPORT_VALUE="${TMP_PULL_HEAD_REF}" 56 | else 57 | EXPORT_VALUE="${TMP_GITHUB_REF}" 58 | fi 59 | echo "branch=${EXPORT_VALUE}" >> $GITHUB_OUTPUT 60 | 61 | base: 62 | name: Base coverage 63 | runs-on: ubuntu-20.04 64 | outputs: 65 | base_branch: ${{ steps.get_base.outputs.base_branch }} 66 | base_cov: ${{ steps.get_base.outputs.base_cov }} 67 | 68 | steps: 69 | - uses: actions/checkout@v3 70 | with: 71 | ref: badges 72 | path: badges 73 | 74 | - name: Get base ref and coverage score 75 | id: get_base 76 | env: 77 | FILE: 'test-coverage.txt' 78 | working-directory: ./badges 79 | shell: bash 80 | run: | 81 | TMP_PULL_BASE_REF="${{ github.base_ref }}" 82 | TMP_GITHUB_REF="${GITHUB_REF#refs/heads/}" 83 | EXPORT_VALUE="" 84 | if [ "${TMP_PULL_BASE_REF}" != "" ] 85 | then 86 | EXPORT_VALUE="${TMP_PULL_BASE_REF}" 87 | else 88 | EXPORT_VALUE="${TMP_GITHUB_REF}" 89 | fi 90 | echo "base_branch=${EXPORT_VALUE}" >> $GITHUB_OUTPUT 91 | if [ -f "${EXPORT_VALUE}/${FILE}" ] 92 | then 93 | echo "Base coverage found on ${EXPORT_VALUE}" 94 | BASE_COV=$(cat "${EXPORT_VALUE}/${FILE}") 95 | echo "Base coverage is: ${BASE_COV}" 96 | echo "base_cov=${BASE_COV}" >> $GITHUB_OUTPUT 97 | else 98 | echo "Base coverage NOT found on ${EXPORT_VALUE}!!" 99 | fi 100 | 101 | check: 102 | name: Pre CI check 103 | runs-on: ubuntu-20.04 104 | needs: [pre_ci, base] 105 | 106 | steps: 107 | - name: Check github variables 108 | # NOTE base coverage env var may be empty here 109 | env: 110 | #COMMIT_MESSAGE: ${{ needs.pre_ci.outputs.commit_message }} 111 | EXPORT_VALUE: ${{ needs.pre_ci.outputs.branch }} 112 | BASE_BRANCH: ${{ needs.base.outputs.base_branch }} 113 | BASE_COVERAGE: ${{ needs.base.outputs.base_cov }} 114 | run: | 115 | #echo "Commit message: ${COMMIT_MESSAGE}" 116 | echo "Export value (head_ref): ${EXPORT_VALUE}" 117 | echo "Base value (base_ref): ${BASE_BRANCH}" 118 | echo "Base coverage (percent): ${{ env.BASE_COVERAGE }}" 119 | 120 | cov_data: 121 | name: Generate test coverage data 122 | runs-on: ubuntu-20.04 123 | needs: [check] 124 | defaults: 125 | run: 126 | shell: bash 127 | outputs: 128 | coverage: ${{ steps.coverage.outputs.coverage }} 129 | coverage-rounded-display: ${{ steps.coverage.outputs.coverage-rounded-display }} 130 | env: 131 | PLATFORM: ubuntu-20.04 132 | PYTHON: '3.11' 133 | PYTHONIOENCODING: utf-8 134 | PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache 135 | 136 | steps: 137 | - uses: actions/checkout@v3 138 | with: 139 | fetch-depth: 0 140 | 141 | - uses: actions/setup-python@v4 142 | with: 143 | python-version: ${{ env.PYTHON }} 144 | 145 | - name: Add python requirements 146 | run: | 147 | python -m pip install --upgrade pip 148 | pip install tox 149 | 150 | - name: Generate coverage and fix pkg name 151 | run: | 152 | tox -e py 153 | 154 | - name: Code Coverage Summary Report (data) 155 | uses: irongut/CodeCoverageSummary@v1.3.0 156 | with: 157 | filename: coverage.xml 158 | output: 'both' 159 | 160 | - uses: actions/upload-artifact@v4 161 | with: 162 | name: src_coverage_rpts 163 | path: | 164 | coverage.xml 165 | code-coverage-results.txt 166 | retention-days: 1 167 | 168 | - name: Check code coverage 169 | id: coverage 170 | env: 171 | VALUE: "Branch Rate" 172 | run: | 173 | COVERAGE=$( cat code-coverage-results.txt | grep -e ^Summary | grep -o -E "${VALUE} = .{3}" | egrep -o '([0-9]+)' ) 174 | echo "coverage=${COVERAGE}" >> $GITHUB_OUTPUT 175 | echo "coverage-rounded-display=${COVERAGE}%" >> $GITHUB_OUTPUT 176 | echo "Current coverage is: ${COVERAGE}%" 177 | 178 | - name: Code Coverage Summary Report 179 | uses: irongut/CodeCoverageSummary@v1.3.0 180 | if: ${{ github.event_name == 'pull_request' }} 181 | with: 182 | filename: coverage.xml 183 | format: 'markdown' 184 | output: 'both' 185 | 186 | - name: Add Coverage PR Comment 187 | uses: marocchino/sticky-pull-request-comment@v2 188 | if: github.event_name == 'pull_request' && (github.event.pull_request.author_association == 'MEMBER' || github.actor == github.repository_owner) 189 | with: 190 | header: coverage 191 | recreate: true 192 | path: code-coverage-results.md 193 | 194 | test: 195 | name: Coverage check 196 | runs-on: ubuntu-20.04 197 | needs: [cov_data, base] 198 | outputs: 199 | coverage: ${{ needs.cov_data.outputs.coverage }} 200 | coverage-base: ${{ needs.base.outputs.base_cov }} 201 | coverage-rounded-display: ${{ needs.cov_data.outputs.coverage-rounded-display }} 202 | 203 | steps: 204 | - name: Check test coverage 205 | env: 206 | COVERAGE: ${{ needs.cov_data.outputs.coverage }} 207 | COVERAGE_ROUNDED: ${{ needs.cov_data.outputs.coverage-rounded-display }} 208 | BASE_COVERAGE: ${{ needs.base.outputs.base_cov }} 209 | MEMBER: ${{ github.event.pull_request.author_association }} 210 | run: | 211 | echo "Coverage: ${COVERAGE}" 212 | echo "Coverage Rounded: ${COVERAGE_ROUNDED}" 213 | echo "Coverage on Base Branch: ${BASE_COVERAGE}" 214 | echo "Author assoc: ${MEMBER}" 215 | 216 | comment_cov_change: 217 | name: Comment on PR with coverage delta 218 | runs-on: ubuntu-20.04 219 | needs: [test, base] 220 | 221 | steps: 222 | - name: Environment 223 | run: | 224 | bash -c set 225 | 226 | - name: Set whether base coverage was found 227 | shell: bash 228 | env: 229 | BASE: ${{ needs.test.outputs.coverage-base }} 230 | run: | 231 | if [ -n "${BASE}" ] 232 | then 233 | BASE_RESULT="true" 234 | else 235 | BASE_RESULT="false" 236 | fi 237 | echo "HAVE_BASE_COVERAGE is ${BASE_RESULT}" 238 | echo "HAVE_BASE_COVERAGE=${BASE_RESULT}" >> $GITHUB_ENV 239 | echo "BASE_COVERAGE=${BASE}" >> $GITHUB_ENV 240 | 241 | - name: Collect variables and construct comment for delta message 242 | if: env.HAVE_BASE_COVERAGE == 'true' 243 | shell: bash 244 | env: 245 | BASE_BRANCH: ${{ needs.base.outputs.base_branch }} 246 | COVERAGE: ${{ needs.test.outputs.coverage }} 247 | BASE_COVERAGE: ${{ needs.test.outputs.coverage-base }} 248 | DELTA_WORD: "not change" 249 | RATE: "Branch Rate" 250 | 251 | run: | 252 | if [ "${COVERAGE}" -gt "${BASE_COVERAGE}" ] 253 | then 254 | DELTA_WORD="increase" 255 | elif [ "${COVERAGE}" -lt "${BASE_COVERAGE}" ] 256 | then 257 | DELTA_WORD="decrease" 258 | fi 259 | CHG=$(( COVERAGE - BASE_COVERAGE )) 260 | CHG="${CHG/-/}" 261 | echo "" > coverage-delta.md 262 | echo "Hello @${{ github.actor }}! Thanks for opening this PR. We found the following information based on analysis of the coverage report:" >> coverage-delta.md 263 | echo "" >> coverage-delta.md 264 | echo "__Base__ ${RATE} coverage is __${BASE_COVERAGE}%__" >> coverage-delta.md 265 | if [ "${CHG}" = "0" ] 266 | then 267 | echo "Merging ${{ github.sha }} into ${BASE_BRANCH} will __${DELTA_WORD}__ coverage" >> coverage-delta.md 268 | else 269 | echo "Merging ${{ github.sha }} into ${BASE_BRANCH} will __${DELTA_WORD}__ coverage by __${CHG}%__" >> coverage-delta.md 270 | fi 271 | if ! [ "${DELTA_WORD}" = "decrease" ] 272 | then 273 | echo "" >> coverage-delta.md 274 | echo "Nice work, @${{ github.actor }}. Cheers! :beers:" >> coverage-delta.md 275 | fi 276 | 277 | - name: Comment PR with test coverage delta 278 | uses: marocchino/sticky-pull-request-comment@v2 279 | if: env.HAVE_BASE_COVERAGE == 'true' && (github.event.pull_request.author_association == 'MEMBER' || github.actor == github.repository_owner) 280 | with: 281 | header: delta 282 | recreate: true 283 | path: coverage-delta.md 284 | 285 | badge: 286 | # Only generate and publish if these conditions are met: 287 | # - The test step ended successfully 288 | # - One of these is met: 289 | # - This is a push event and the push event is on branch 'master' or 'develop' 290 | # Note: if this repo is personal (ie, not an org repo) then you can 291 | # use the following to change the scope of the next 2 jobs 292 | # instead of running on branch push as shown below: 293 | # - This is a pull request event and the pull actor is the same as the repo owner 294 | # if: ${{ ( github.event_name == 'pull_request' && github.actor == github.repository_owner ) || github.ref == 'refs/heads/master' }} 295 | name: Generate badge image with test coverage value 296 | runs-on: ubuntu-20.04 297 | needs: [test, pre_ci] 298 | if: github.event_name == 'push' 299 | outputs: 300 | url: ${{ steps.url.outputs.url }} 301 | markdown: ${{ steps.url.outputs.markdown }} 302 | 303 | steps: 304 | - uses: actions/checkout@v3 305 | with: 306 | ref: badges 307 | path: badges 308 | 309 | # Use the output from the `coverage` step 310 | - name: Generate the badge SVG image 311 | uses: emibcn/badge-action@v2.0.2 312 | id: badge 313 | with: 314 | label: 'Branch Coverage' 315 | status: ${{ needs.test.outputs.coverage-rounded-display }} 316 | color: ${{ 317 | needs.test.outputs.coverage > 90 && 'green' || 318 | needs.test.outputs.coverage > 80 && 'yellow,green' || 319 | needs.test.outputs.coverage > 70 && 'yellow' || 320 | needs.test.outputs.coverage > 60 && 'orange,yellow' || 321 | needs.test.outputs.coverage > 50 && 'orange' || 322 | needs.test.outputs.coverage > 40 && 'red,orange' || 323 | needs.test.outputs.coverage > 30 && 'red,red,orange' || 324 | needs.test.outputs.coverage > 20 && 'red,red,red,orange' || 325 | 'red' }} 326 | path: badges/test-coverage.svg 327 | 328 | - name: Commit badge and data 329 | env: 330 | BRANCH: ${{ needs.pre_ci.outputs.branch }} 331 | COVERAGE: ${{ needs.test.outputs.coverage }} 332 | FILE: 'test-coverage.svg' 333 | DATA: 'test-coverage.txt' 334 | working-directory: ./badges 335 | run: | 336 | git config --local user.email "action@github.com" 337 | git config --local user.name "GitHub Action" 338 | mkdir -p "${BRANCH}" 339 | mv "${FILE}" "${BRANCH}" 340 | echo "${COVERAGE}" > "${BRANCH}/${DATA}" 341 | git add "${BRANCH}/${FILE}" "${BRANCH}/${DATA}" 342 | # Will give error if badge has not changed 343 | git commit -m "Add/Update badge" || true 344 | 345 | - name: Push badge commit 346 | uses: ad-m/github-push-action@master 347 | with: 348 | github_token: ${{ secrets.GITHUB_TOKEN }} 349 | branch: badges 350 | directory: badges 351 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ master ] 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | pylint: 12 | 13 | runs-on: ubuntu-20.04 14 | defaults: 15 | run: 16 | shell: bash 17 | outputs: 18 | branch: ${{ steps.extract_branch.outputs.branch }} 19 | rating: ${{ steps.analyze.outputs.rating }} 20 | path: ${{ steps.analyze.outputs.path }} 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Extract base branch name 28 | id: extract_branch 29 | shell: bash 30 | run: | 31 | TMP_PULL_BASE_REF="${{ github.base_ref }}" 32 | TMP_GITHUB_REF="${GITHUB_REF#refs/heads/}" 33 | EXPORT_VALUE="" 34 | if [ "${TMP_PULL_BASE_REF}" != "" ] 35 | then 36 | EXPORT_VALUE="${TMP_PULL_BASE_REF}" 37 | else 38 | EXPORT_VALUE="${TMP_GITHUB_REF}" 39 | fi 40 | echo "branch=${EXPORT_VALUE}" >> $GITHUB_OUTPUT 41 | 42 | - name: Set up Python 3.9 43 | uses: actions/setup-python@v4 44 | with: 45 | python-version: 3.9 46 | 47 | - name: Install tox 48 | run: | 49 | python -m pip install --upgrade pip wheel 50 | pip install tox 51 | 52 | - name: Run pylint 53 | id: analyze 54 | env: 55 | BADGE_PATH: badges/pylint-score.svg 56 | run: | 57 | rating=$(bash -c 'tox -e lint' | grep 'Your code has been rated at' | cut -f7 -d " ") 58 | echo "Pylint score: ${rating}" 59 | echo "rating=${rating}" >> $GITHUB_OUTPUT 60 | echo "path=${BADGE_PATH}" >> $GITHUB_OUTPUT 61 | 62 | badge: 63 | # Only generate and publish if these conditions are met: 64 | # - The previous job/analyze step ended successfully 65 | # - At least one of these is true: 66 | # - This is a push event and the push event is on branch 'main' or 'develop' 67 | # Note: if this repo is personal (ie, not an org repo) then you can 68 | # use the following to change the scope of the next 2 jobs 69 | # instead of running on branch push as shown below: 70 | # - This is a pull request event and the pull actor is the same as the repo owner 71 | # if: ${{ ( github.event_name == 'pull_request' && github.actor == github.repository_owner ) || github.ref == 'refs/heads/main' }} 72 | name: Generate badge image with pylint score 73 | runs-on: ubuntu-20.04 74 | needs: [pylint] 75 | if: ${{ github.event_name == 'push' }} 76 | 77 | steps: 78 | - uses: actions/checkout@v3 79 | with: 80 | ref: badges 81 | path: badges 82 | 83 | # Use the output from the `analyze` step 84 | - name: Create pylint badge 85 | uses: emibcn/badge-action@v2.0.2 86 | id: badge 87 | with: 88 | label: 'Pylint score' 89 | status: ${{ needs.pylint.outputs.rating }} 90 | color: 'green' 91 | path: ${{ needs.pylint.outputs.path }} 92 | 93 | - name: Commit badge 94 | env: 95 | BRANCH: ${{ needs.pylint.outputs.branch }} 96 | FILE: 'pylint-score.svg' 97 | working-directory: ./badges 98 | run: | 99 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 100 | git config --local user.name "github-actions[bot]" 101 | mkdir -p "${BRANCH}" 102 | mv "${FILE}" "${BRANCH}" 103 | git add "${BRANCH}/${FILE}" 104 | # Will give error if badge has not changed 105 | git commit -m "Add/Update badge" || true 106 | 107 | - name: Push badge commit 108 | uses: ad-m/github-push-action@master 109 | with: 110 | github_token: ${{ secrets.GITHUB_TOKEN }} 111 | branch: badges 112 | directory: badges 113 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | # release on tag push 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | packaging: 11 | 12 | runs-on: ${{ matrix.os }} 13 | defaults: 14 | run: 15 | shell: bash 16 | env: 17 | PYTHONIOENCODING: utf-8 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-20.04, macos-latest, windows-latest] 22 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 23 | 24 | steps: 25 | - name: Set git crlf/eol 26 | run: | 27 | git config --global core.autocrlf false 28 | git config --global core.eol lf 29 | 30 | - uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip wheel 42 | pip install tox tox-gh-actions 43 | 44 | - name: Build dist pkgs 45 | run: | 46 | tox -e build 47 | 48 | - name: Upload artifacts 49 | if: matrix.python-version == 3.7 && runner.os == 'Linux' 50 | uses: actions/upload-artifact@v2 51 | with: 52 | name: packages 53 | path: ./dist/pystache-* 54 | 55 | create_release: 56 | name: Create Release 57 | needs: [packaging] 58 | runs-on: ubuntu-20.04 59 | 60 | steps: 61 | - name: Get version 62 | id: get_version 63 | run: | 64 | echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 65 | echo ${{ env.VERSION }} 66 | 67 | - uses: actions/checkout@v3 68 | with: 69 | fetch-depth: 0 70 | 71 | # download all artifacts to project dir 72 | - uses: actions/download-artifact@v2 73 | 74 | - name: Generate changes file 75 | uses: sarnold/gitchangelog-action@master 76 | with: 77 | github_token: ${{ secrets.GITHUB_TOKEN}} 78 | 79 | - name: Create release 80 | id: create_release 81 | uses: softprops/action-gh-release@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | tag_name: ${{ env.VERSION }} 86 | name: Release ${{ env.VERSION }} 87 | body_path: CHANGES.md 88 | draft: false 89 | prerelease: false 90 | files: | 91 | packages/pystache-* 92 | 93 | # When a GitHub release is made, upload the artifacts to PyPI 94 | upload: 95 | name: Upload to PyPI 96 | runs-on: ubuntu-20.04 97 | needs: [packaging] 98 | 99 | steps: 100 | - uses: actions/setup-python@v2 101 | 102 | - uses: actions/download-artifact@v2 103 | 104 | - name: check artifacts 105 | run: find . -maxdepth 2 -name pystache-\* -type f 106 | 107 | # - name: Publish bdist and sdist packages 108 | # uses: pypa/gh-action-pypi-publish@release/v1 109 | # with: 110 | # password: ${{ secrets.PYPI_API_TOKEN }} 111 | # packages_dir: packages/ 112 | # print_hash: true 113 | -------------------------------------------------------------------------------- /.github/workflows/sphinx.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Add python requirements 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install tox 26 | 27 | - name: Build docs 28 | run: | 29 | #tox -e docs-lint 30 | tox -e docs 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: ApiDocsHTML 35 | path: "docs/_build/html/" 36 | 37 | - name: set nojekyll for github 38 | run: | 39 | sudo touch docs/_build/html/.nojekyll 40 | 41 | - name: Deploy docs to gh-pages 42 | if: ${{ github.event_name == 'push' }} 43 | uses: JamesIves/github-pages-deploy-action@3.7.1 44 | with: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | BRANCH: gh-pages 47 | FOLDER: docs/_build/html/ 48 | SINGLE_COMMIT: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | # Tox support. See: http://pypi.python.org/pypi/tox 4 | .tox 5 | # Our tox runs convert the doctests in *.rst files to Python 3 prior to 6 | # running tests. Ignore these temporary files. 7 | *.temp2to3.rst 8 | # The setup.py "prep" command converts *.md to *.temp.rst (via *.temp.md). 9 | *.temp.md 10 | *.temp.rst 11 | # TextMate project file 12 | *.tmproj 13 | # Distribution-related folders and files. 14 | pystache/_version.py 15 | pystache.egg-info 16 | docs/_build 17 | docs/source/api 18 | build 19 | dist 20 | MANIFEST 21 | coverage.xml 22 | .coverage 23 | .idea 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/spec"] 2 | path = ext/spec 3 | url = https://github.com/mustache/spec.git 4 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | scanner: 2 | diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned. 3 | linter: flake8 # Other option is pycodestyle 4 | 5 | no_blank_comment: False # If True, no comment is made on PR without any errors. 6 | descending_issues_order: True # If True, PEP 8 issues in message will be displayed in descending order of line numbers in the file 7 | 8 | pycodestyle: # Same as scanner.linter value. Other option is flake8 9 | max-line-length: 110 # Default is 79 in PEP 8 10 | 11 | flake8: 12 | max-line-length: 110 # Default is 79 in PEP 8 13 | ignore: # Errors and warnings to ignore 14 | - E266 15 | - E731 16 | - E203 17 | - E221 18 | exclude: 19 | - pystache/tests 20 | - test_pystache.py 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # To install the git pre-commit hook run: 2 | # pre-commit install 3 | # To update the pre-commit hooks run: 4 | # pre-commit install-hooks 5 | exclude: '^(.tox/|.*\.mustache$|.*conf.py$)' 6 | repos: 7 | - repo: meta 8 | hooks: 9 | - id: check-useless-excludes 10 | - id: check-hooks-apply 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v5.0.0 13 | hooks: 14 | - id: trailing-whitespace 15 | - id: end-of-file-fixer 16 | exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' 17 | - id: mixed-line-ending 18 | args: [--fix=lf] 19 | - id: check-toml 20 | - id: check-yaml 21 | exclude: '(conda/meta.yaml|.pep8speaks.yml)' 22 | 23 | - repo: https://github.com/ambv/black 24 | rev: 24.10.0 25 | hooks: 26 | - id: black 27 | name: "Format code" 28 | exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' 29 | language_version: python3 30 | 31 | - repo: "https://github.com/asottile/blacken-docs" 32 | rev: "1.19.1" 33 | hooks: 34 | - id: "blacken-docs" 35 | name: "Format docs (blacken-docs)" 36 | args: ["-l", "64"] 37 | additional_dependencies: 38 | - "black==23.1.0" 39 | 40 | - repo: https://github.com/PyCQA/doc8 41 | rev: v1.1.2 42 | hooks: 43 | - id: doc8 44 | args: 45 | - '--max-line-length=90' 46 | - '--ignore=D001' 47 | 48 | - repo: https://github.com/pre-commit/pygrep-hooks 49 | rev: v1.10.0 50 | hooks: 51 | - id: rst-backticks 52 | # exclude: ChangeLog\.rst$ 53 | - id: rst-directive-colons 54 | - id: rst-inline-touching-normal 55 | 56 | - repo: https://github.com/myint/autoflake 57 | rev: v2.3.1 58 | hooks: 59 | - id: autoflake 60 | exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' 61 | args: 62 | - --in-place 63 | - --remove-all-unused-imports 64 | - --remove-duplicate-keys 65 | - --remove-unused-variables 66 | 67 | - repo: https://github.com/PyCQA/flake8 68 | rev: 7.1.1 69 | hooks: 70 | - id: flake8 71 | exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' 72 | additional_dependencies: ["flake8-bugbear"] 73 | 74 | - repo: https://github.com/PyCQA/bandit 75 | rev: 1.8.0 76 | hooks: 77 | - id: bandit 78 | args: ["-ll", "-x", "pystache/tests"] 79 | 80 | - repo: https://github.com/lovesegfault/beautysh 81 | rev: v6.2.1 82 | hooks: 83 | - id: beautysh 84 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # https://github.com/cmheisel/pylintrcs/blob/master/pylintrc 2 | [MASTER] 3 | ignore=_version.py 4 | 5 | ignore-paths= 6 | .*tests/, 7 | .*test.py$, 8 | 9 | jobs=1 10 | 11 | suggestion-mode=yes 12 | 13 | [MESSAGES CONTROL] 14 | disable= 15 | too-few-public-methods, 16 | 17 | [REPORTS] 18 | output-format=colorized 19 | 20 | [FORMAT] 21 | max-line-length=110 22 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | v0.6.6 (2024-12-12) 2 | ------------------- 3 | 4 | - Update README to match supported versions. [Thomas David Baker] 5 | - Update pre-commit. [Thomas David Baker] 6 | - Update pyproject.toml with 3.12 and 3.13 Python versions. [Alvaro Crespo] 7 | - Update tox config to use Python 3.12 and 3.13 versions. [Alvaro Crespo] 8 | - Update yml files with Python 3.12 and 3.13. [Alvaro Crespo] 9 | - Update changelog for v0.6.5 a bit belatedly. [Thomas David Baker] 10 | 11 | v0.6.5 (2023-08-26) 12 | ------------------- 13 | 14 | - Bump the version bits called out in the readme. [Stephen L Arnold] 15 | - Keep changelog up to date manually as I don't know how to 16 | autogenerate. [Thomas David Baker] 17 | 18 | 19 | v0.6.4 (2023-08-13) 20 | ------------------- 21 | 22 | Other 23 | ~~~~~ 24 | 25 | - Merge pull request #23 from PennyDreadfulMTG/more-fixes. [Thomas David Baker] 26 | - Use the content-type for RST that pypi now wants 27 | - Use the content-type for RST that pypi now wants. [Thomas David Baker] 28 | 29 | v0.6.3 (2023-08-13) 30 | ------------------- 31 | 32 | New 33 | ~~~ 34 | 35 | - Add full sphinx apidoc build, include readme/extras. [Stephen L Arnold] 36 | * add new tox commands for 'docs' and 'docs-lint' 37 | * cleanup link errors found by docs-lint 38 | * add sphinx doc build workflow, update ci workflow 39 | * remove new version var from init.py globals 40 | 41 | - Display repo state in docs build, include CHANGELOG. [Stephen L Arnold] 42 | * add sphinx_git extension to docs conf and setup deps 43 | * display branch/commit/state docs were built from 44 | * include CHANGELOG (but not HISTORY) in docs build/toc 45 | * Convert readme.md to readme.rst, move extra docs. [Stephen L Arnold] 46 | 47 | Fixes 48 | ~~~~~ 49 | 50 | - Fix included filename and link cleanup. [Stephen L Arnold] 51 | - Remove more py2 cruft from doctesting (py3.10 warnings) [Stephen L Arnold] 52 | - Update maintainer info and spec test cmd. [Stephen L Arnold] 53 | * update coverage value for delta/base, allow digits only 54 | - Use updated bandit action and workflow excludes (exclude test) [Stephen L Arnold] 55 | * also fix missing PR event check in coverage workflow 56 | - Use current org-level coverage workflow. [Stephen L Arnold] 57 | * increase fetch depth and update regex 58 | * updated action deps, relaxed run criteria 59 | * go back to "normal" tokens, remove permission hacks 60 | * still needs more job isolation => refactor for another day 61 | 62 | Other 63 | ~~~~~ 64 | 65 | - Merge pull request #21 from PennyDreadfulMTG/update-pypi. [Thomas David Baker] 66 | - Update a few small things before making a release for pypi 67 | - Update location of flake8 for pre-commit, official location has moved. [Thomas David Baker] 68 | - Correct small issue in README. [Thomas David Baker] 69 | - Specify passenv in a way that tox is happy with. [Thomas David Baker] 70 | - Ignore PyCharm dir. [Thomas David Baker] 71 | - Update TODO to remove some things that have been TODOne. [Thomas David Baker] 72 | - Merge pull request #20 from VCTLabs/new-docs-cleanup. [Katelyn Gigante] 73 | - New docs cleanup 74 | - Merge pull request #19 from VCTLabs/auto-docs. [Thomas David Baker] 75 | - New docs and automation, more modernization 76 | - Do pre-release (manual) updates for changes and conda recipe. [Stephen L Arnold] 77 | * create changes file: gitchangelog v0.6.0.. > CHANGELOG.rst 78 | * edit top line in CHANGELOG.rst using current date/new tag 79 | * edit conda/meta.yaml using new tag, then tag this commit 80 | - Merge pull request #18 from VCTLabs/mst-upstream. [Thomas David Baker] 81 | - Workflow and test driver fixes 82 | - Use buildbot account. [Katelyn Gigante] 83 | - Merge pull request #16 from PennyDreadfulMTG/fix-coverage. [Katelyn Gigante] 84 | - Use ACCESS_TOKEN secret rather than provided GITHUB_TOKEN 85 | - Use ACCESS_TOKEN secret rather than provided GITHUB_TOKEN. [Katelyn Gigante] 86 | - Should fix the coverage badge 87 | 88 | v0.6.2 (2022-09-14) 89 | -------------------- 90 | 91 | New 92 | ~~~ 93 | - Add full sphinx apidoc build, include readme/extras. [Stephen L 94 | Arnold] 95 | 96 | * add new tox commands for 'docs' and 'docs-lint' 97 | * cleanup link errors found by docs-lint 98 | * add sphinx doc build workflow, update ci workflow 99 | * remove new version var from __init__.py globals 100 | 101 | Changes 102 | ~~~~~~~ 103 | - Convert readme.md to readme.rst, move extra docs. [Stephen L Arnold] 104 | 105 | Fixes 106 | ~~~~~ 107 | - Fix included filename and link cleanup. [Stephen L Arnold] 108 | - Remove more py2 cruft from doctesting (py3.10 warnings) [Stephen L Arnold] 109 | - Update maintainer info and spec test cmd. [Stephen L Arnold] 110 | 111 | * update coverage value for delta/base, allow digits only 112 | - Use updated bandit action and workflow excludes (exclude test) 113 | [Stephen L Arnold] 114 | 115 | * also fix missing PR event check in coverage workflow 116 | - Use current org-level coverage workflow. [Stephen L Arnold] 117 | 118 | * increase fetch depth and update regex 119 | * updated action deps, relaxed run criteria 120 | * go back to "normal" tokens, remove permission hacks 121 | * still needs more job isolation => refactor for another day 122 | 123 | Other 124 | ~~~~~ 125 | - Use buildbot account. [Katelyn Gigante] 126 | - Use ACCESS_TOKEN secret rather than provided GITHUB_TOKEN. [Katelyn 127 | Gigante] 128 | 129 | Should fix the coverage badge 130 | 131 | 132 | v0.6.1 (2021-11-24) 133 | ------------------- 134 | 135 | Changes 136 | ~~~~~~~ 137 | - Add shallow checkout for testing. [Stephen L Arnold] 138 | - Bump comment action to latest release, verify checkout depth. [Stephen 139 | L Arnold] 140 | 141 | * see: https://github.com/marocchino/sticky-pull-request-comment/issues/298 142 | in upstream action repo 143 | 144 | Fixes 145 | ~~~~~ 146 | - Use workflow PR target and checkout params. [Stephen L Arnold] 147 | - Split coverage (checkout) job from PR comment job. [Stephen L Arnold] 148 | - Use correct tox env cmd for single platform/version. [Stephen L 149 | Arnold] 150 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | **Note:** Official support for Python 2.7 will end with Pystache version 0.6.0. 5 | 6 | 0.6.0 (2021-03-04) 7 | ------------------ 8 | 9 | - Bump spec versions to latest => v1.1.3 10 | - Modernize python and CI tools, update docs/doctests 11 | - Update unicode conversion test for py3-only 12 | - Add pep8speaks cfg, cleanup warnings 13 | - Remove superfluous setup test/unused imports 14 | - Add conda recipe/CI build 15 | 16 | 0.5.6 (2021-02-28) 17 | ------------------ 18 | 19 | - Use correct wheel name in release workflow, limit wheels 20 | - Add install check/test of downloaded wheel 21 | - Update/add ci workflows and tox cfg, bump to next dev0 version 22 | 23 | 0.5.5 (2020-12-16) 24 | ------------------ 25 | 26 | - fix document processing, update pandoc args and history 27 | - add release.yml to CI, test env settings 28 | - fix bogus commit message, update versions and tox cf 29 | - add post-test steps for building pkgs with/without doc updates 30 | - add CI build check, fix MANIFEST.in pruning 31 | 32 | 0.5.4-2 (2020-11-09) 33 | -------------------- 34 | 35 | - Merge pull request #1 from sarnold/rebase-up 36 | - Bugfix: test_specloader.py: fix test_find__with_directory on other OSs 37 | - Bugfix: pystache/loader.py: remove stray windows line-endings 38 | - fix crufty (and insecure) http urls 39 | - Bugfix: modernize python versions (keep py27) and fix spec_test load cmd 40 | 41 | 0.5.4 (2014-07-11) 42 | ------------------ 43 | 44 | - Bugfix: made test with filenames OS agnostic (issue \#162). 45 | 46 | 0.5.3 (2012-11-03) 47 | ------------------ 48 | 49 | - Added ability to customize string coercion (e.g. to have None render as 50 | `''`) (issue \#130). 51 | - Added Renderer.render_name() to render a template by name (issue \#122). 52 | - Added TemplateSpec.template_path to specify an absolute path to a 53 | template (issue \#41). 54 | - Added option of raising errors on missing tags/partials: 55 | `Renderer(missing_tags='strict')` (issue \#110). 56 | - Added support for finding and loading templates by file name in 57 | addition to by template name (issue \#127). [xgecko] 58 | - Added a `parse()` function that yields a printable, pre-compiled 59 | parse tree. 60 | - Added support for rendering pre-compiled templates. 61 | - Added Python 3.3 to the list of supported versions. 62 | - Added support for [PyPy](http://pypy.org/) (issue \#125). 63 | - Added support for [Travis CI](http://travis-ci.org) (issue \#124). 64 | [msabramo] 65 | - Bugfix: `defaults.DELIMITERS` can now be changed at runtime (issue \#135). 66 | [bennoleslie] 67 | - Bugfix: exceptions raised from a property are no longer swallowed 68 | when getting a key from a context stack (issue \#110). 69 | - Bugfix: lambda section values can now return non-ascii, non-unicode 70 | strings (issue \#118). 71 | - Bugfix: allow `test_pystache.py` and `tox` to pass when run from a 72 | downloaded sdist (i.e. without the spec test directory). 73 | - Convert HISTORY and README files from reST to Markdown. 74 | - More robust handling of byte strings in Python 3. 75 | - Added Creative Commons license for David Phillips's logo. 76 | 77 | 0.5.2 (2012-05-03) 78 | ------------------ 79 | 80 | - Added support for dot notation and version 1.1.2 of the spec (issue 81 | \#99). [rbp] 82 | - Missing partials now render as empty string per latest version of 83 | spec (issue \#115). 84 | - Bugfix: falsey values now coerced to strings using str(). 85 | - Bugfix: lambda return values for sections no longer pushed onto 86 | context stack (issue \#113). 87 | - Bugfix: lists of lambdas for sections were not rendered (issue 88 | \#114). 89 | 90 | 0.5.1 (2012-04-24) 91 | ------------------ 92 | 93 | - Added support for Python 3.1 and 3.2. 94 | - Added tox support to test multiple Python versions. 95 | - Added test script entry point: pystache-test. 96 | - Added \_\_version\_\_ package attribute. 97 | - Test harness now supports both YAML and JSON forms of Mustache spec. 98 | - Test harness no longer requires nose. 99 | 100 | 0.5.0 (2012-04-03) 101 | ------------------ 102 | 103 | This version represents a major rewrite and refactoring of the code base 104 | that also adds features and fixes many bugs. All functionality and 105 | nearly all unit tests have been preserved. However, some backwards 106 | incompatible changes to the API have been made. 107 | 108 | Below is a selection of some of the changes (not exhaustive). 109 | 110 | Highlights: 111 | 112 | - Pystache now passes all tests in version 1.0.3 of the [Mustache 113 | spec](https://github.com/mustache/spec). [pvande] 114 | - Removed View class: it is no longer necessary to subclass from View 115 | or from any other class to create a view. 116 | - Replaced Template with Renderer class: template rendering behavior 117 | can be modified via the Renderer constructor or by setting 118 | attributes on a Renderer instance. 119 | - Added TemplateSpec class: template rendering can be specified on a 120 | per-view basis by subclassing from TemplateSpec. 121 | - Introduced separation of concerns and removed circular dependencies 122 | (e.g. between Template and View classes, cf. [issue 123 | \#13](https://github.com/defunkt/pystache/issues/13)). 124 | - Unicode now used consistently throughout the rendering process. 125 | - Expanded test coverage: nosetests now runs doctests and \~105 test 126 | cases from the Mustache spec (increasing the number of tests from 56 127 | to \~315). 128 | - Added a rudimentary benchmarking script to gauge performance while 129 | refactoring. 130 | - Extensive documentation added (e.g. docstrings). 131 | 132 | Other changes: 133 | 134 | - Added a command-line interface. [vrde] 135 | - The main rendering class now accepts a custom partial loader (e.g. a 136 | dictionary) and a custom escape function. 137 | - Non-ascii characters in str strings are now supported while 138 | rendering. 139 | - Added string encoding, file encoding, and errors options for 140 | decoding to unicode. 141 | - Removed the output encoding option. 142 | - Removed the use of markupsafe. 143 | 144 | Bug fixes: 145 | 146 | - Context values no longer processed as template strings. 147 | [jakearchibald] 148 | - Whitespace surrounding sections is no longer altered, per the spec. 149 | [heliodor] 150 | - Zeroes now render correctly when using PyPy. [alex] 151 | - Multline comments now permitted. [fczuardi] 152 | - Extensionless template files are now supported. 153 | - Passing `**kwargs` to `Template()` no longer modifies the context. 154 | - Passing `**kwargs` to `Template()` with no context no longer raises 155 | an exception. 156 | 157 | 0.4.1 (2012-03-25) 158 | ------------------ 159 | 160 | - Added support for Python 2.4. [wangtz, jvantuyl] 161 | 162 | 0.4.0 (2011-01-12) 163 | ------------------ 164 | 165 | - Add support for nested contexts (within template and view) 166 | - Add support for inverted lists 167 | - Decoupled template loading 168 | 169 | 0.3.1 (2010-05-07) 170 | ------------------ 171 | 172 | - Fix package 173 | 174 | 0.3.0 (2010-05-03) 175 | ------------------ 176 | 177 | - View.template\_path can now hold a list of path 178 | - Add {{& blah}} as an alias for {{{ blah }}} 179 | - Higher Order Sections 180 | - Inverted sections 181 | 182 | 0.2.0 (2010-02-15) 183 | ------------------ 184 | 185 | - Bugfix: Methods returning False or None are not rendered 186 | - Bugfix: Don't render an empty string when a tag's value is 0. 187 | [enaeseth] 188 | - Add support for using non-callables as View attributes. 189 | [joshthecoder] 190 | - Allow using View instances as attributes. [joshthecoder] 191 | - Support for Unicode and non-ASCII-encoded bytestring output. 192 | [enaeseth] 193 | - Template file encoding awareness. [enaeseth] 194 | 195 | 0.1.1 (2009-11-13) 196 | ------------------ 197 | 198 | - Ensure we're dealing with strings, always 199 | - Tests can be run by executing the test file directly 200 | 201 | 0.1.0 (2009-11-12) 202 | ------------------ 203 | 204 | - First release 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Chris Jerdonek. All rights reserved. 2 | 3 | Copyright (c) 2009 Chris Wanstrath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude .github * 2 | recursive-exclude conda * 3 | recursive-exclude gh * 4 | 5 | include *.py 6 | include tox.ini 7 | include *.rst 8 | include LICENSE 9 | include *.toml 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pystache 2 | ======== 3 | 4 | |ci| |conda| |coverage| |bandit| |release| 5 | 6 | |pre| |cov| |pylint| 7 | 8 | |tag| |license| |python| 9 | 10 | 11 | This updated fork of Pystache is currently tested on Python 3.8+ and in 12 | Conda, on Linux, Macos, and Windows. 13 | 14 | |logo| 15 | 16 | `Pystache `__ is a Python 17 | implementation of `Mustache `__. 18 | Mustache is a framework-agnostic, logic-free templating system inspired 19 | by `ctemplate `__ and 20 | et. Like ctemplate, Mustache "emphasizes separating logic from presentation: 21 | it is impossible to embed application logic in this template language." 22 | 23 | The `mustache(5) `__ man 24 | page provides a good introduction to Mustache's syntax. For a more 25 | complete (and more current) description of Mustache's behavior, see the 26 | official `Mustache spec `__. 27 | 28 | Pystache is `semantically versioned `__ and older 29 | versions can still be found on `PyPI `__. 30 | This version of Pystache now passes all tests in `version 1.1.3 31 | `__ of the spec. 32 | 33 | 34 | Requirements 35 | ============ 36 | 37 | Pystache is tested with: 38 | 39 | - Python 3.8 40 | - Python 3.9 41 | - Python 3.10 42 | - Python 3.11 43 | - Python 3.12 44 | - Python 3.13 45 | - Conda (py38 and py310) 46 | 47 | JSON support is needed only for the command-line interface and to run 48 | the spec tests; PyYAML can still be used (see the Develop section). 49 | 50 | Official support for Python 2 has ended with Pystache version 0.6.0. 51 | 52 | 53 | .. note:: This project uses setuptools_scm_ to generate and maintain the 54 | version file, which only gets included in the sdist/wheel 55 | packages. In a fresh clone, running any of the tox_ commands 56 | should generate the current version file. 57 | 58 | .. _setuptools_scm: https://github.com/pypa/setuptools_scm 59 | .. _tox: https://github.com/tox-dev/tox 60 | 61 | 62 | Quick Start 63 | =========== 64 | 65 | Be sure to get the latest release from either Pypi or Github. 66 | 67 | Install It 68 | ---------- 69 | 70 | From Pypi:: 71 | 72 | $ pip install pystache 73 | 74 | Or Github:: 75 | 76 | $ pip install -U pystache -f https://github.com/PennyDreadfulMTG/pystache/releases/ 77 | 78 | 79 | And test it:: 80 | 81 | $ pystache-test 82 | 83 | To install and test from source (e.g. from GitHub), see the Develop 84 | section. 85 | 86 | Use It 87 | ------ 88 | 89 | Open a python console:: 90 | 91 | >>> import pystache 92 | >>> print(pystache.render('Hi {{person}}!', {'person': 'Mom'})) 93 | Hi Mom! 94 | 95 | You can also create dedicated view classes to hold your view logic. 96 | 97 | Here's your view class (in ../pystache/tests/examples/readme.py): 98 | 99 | :: 100 | 101 | class SayHello(object): 102 | def to(self): 103 | return "Pizza" 104 | 105 | Instantiating like so: 106 | 107 | :: 108 | 109 | >>> from pystache.tests.examples.readme import SayHello 110 | >>> hello = SayHello() 111 | 112 | Then your template, say_hello.mustache (by default in the same directory 113 | as your class definition): 114 | 115 | :: 116 | 117 | Hello, {{to}}! 118 | 119 | Pull it together: 120 | 121 | :: 122 | 123 | >>> renderer = pystache.Renderer() 124 | >>> print(renderer.render(hello)) 125 | Hello, Pizza! 126 | 127 | For greater control over rendering (e.g. to specify a custom template 128 | directory), use the ``Renderer`` class like above. One can pass 129 | attributes to the Renderer class constructor or set them on a Renderer 130 | instance. To customize template loading on a per-view basis, subclass 131 | ``TemplateSpec``. See the docstrings of the 132 | `Renderer `__ 133 | class and 134 | `TemplateSpec `__ 135 | class for more information. 136 | 137 | You can also pre-parse a template: 138 | 139 | :: 140 | 141 | >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") 142 | >>> print(parsed) 143 | ['Hey ', _SectionNode(key='who', index_begin=12, index_end=18, parsed=[_EscapeNode(key='.'), '!'])] 144 | 145 | And then: 146 | 147 | :: 148 | 149 | >>> print(renderer.render(parsed, {'who': 'Pops'})) 150 | Hey Pops! 151 | >>> print(renderer.render(parsed, {'who': 'you'})) 152 | Hey you! 153 | 154 | 155 | Unicode 156 | ------- 157 | 158 | This section describes how Pystache handles unicode, strings, and 159 | encodings. 160 | 161 | Internally, Pystache uses `only unicode strings`_ (``str`` in Python 3). 162 | For input, Pystache accepts byte strings (``bytes`` in Python 3). 163 | For output, Pystache's template rendering methods return only unicode. 164 | 165 | .. _only unicode strings: https://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs 166 | 167 | Pystache's ``Renderer`` class supports a number of attributes to control 168 | how Pystache converts byte strings to unicode on input. These include 169 | the ``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes. 170 | 171 | The ``file_encoding`` attribute is the encoding the renderer uses to 172 | convert to unicode any files read from the file system. Similarly, 173 | ``string_encoding`` is the encoding the renderer uses to convert any other 174 | byte strings encountered during the rendering process into unicode (e.g. 175 | context values that are encoded byte strings). 176 | 177 | The ``decode_errors`` attribute is what the renderer passes as the 178 | ``errors`` argument to Python's built-in unicode-decoding function 179 | (``str()`` in Python 3). The valid values for this argument are 180 | ``strict``, ``ignore``, and ``replace``. 181 | 182 | Each of these attributes can be set via the ``Renderer`` class's 183 | constructor using a keyword argument of the same name. See the Renderer 184 | class's docstrings for further details. In addition, the ``file_encoding`` 185 | attribute can be controlled on a per-view basis by subclassing the 186 | ``TemplateSpec`` class. When not specified explicitly, these attributes 187 | default to values set in Pystache's ``defaults`` module. 188 | 189 | 190 | Develop 191 | ======= 192 | 193 | To test from a source distribution (without installing):: 194 | 195 | $ python test_pystache.py 196 | 197 | To test Pystache with multiple versions of Python (with a single 198 | command!) and different platforms, you can use [tox](https://pypi.python.org/pypi/tox):: 199 | 200 | $ pip install tox 201 | $ tox -e py 202 | 203 | To run tests on multiple versions with coverage, run:: 204 | 205 | $ tox -e py38-linux,py39-linux # for example 206 | 207 | (substitute your platform above, eg, macos or windows) 208 | 209 | The source distribution tests also include doctests and tests from the 210 | Mustache spec. To include tests from the Mustache spec in your test 211 | runs:: 212 | 213 | $ git submodule update --init 214 | 215 | The test harness parses the spec's (more human-readable) yaml files if 216 | `PyYAML `__ is present. Otherwise, 217 | it parses the json files. To install PyYAML:: 218 | 219 | $ pip install pyyaml # note this is installed automatically by tox 220 | 221 | Once the submodule is available, you can run the full test set with:: 222 | 223 | $ tox -e setup -- ext/spec/specs 224 | 225 | 226 | Making Changes & Contributing 227 | ----------------------------- 228 | 229 | We use the gitchangelog_ action to generate our github Release page, as 230 | well as the gitchangelog message format to help it categorize/filter 231 | commits for a tidier release page. Please use the appropriate ACTION 232 | modifiers in any Pull Requests. 233 | 234 | This repo is also pre-commit_ enabled for various linting and format 235 | checks. The checks run automatically on commit and will fail the 236 | commit (if not clean) with some checks performing simple file corrections. 237 | 238 | If other checks fail on commit, the failure display should explain the error 239 | types and line numbers. Note you must fix any fatal errors for the 240 | commit to succeed; some errors should be fixed automatically (use 241 | ``git status`` and ``git diff`` to review any changes). 242 | 243 | Note ``pylint`` is the primary check that requires your own input, as well 244 | as a decision as to the appropriate fix action. You must fix any ``pylint`` 245 | warnings (relative to the baseline config score) for the commit to succeed. 246 | 247 | See the following pages for more information on gitchangelog and pre-commit. 248 | 249 | .. inclusion-marker-1 250 | 251 | * generate-changelog_ 252 | * pre-commit-config_ 253 | * pre-commit-usage_ 254 | 255 | .. _generate-changelog: docs/source/dev/generate-changelog.rst 256 | .. _pre-commit-config: docs/source/dev/pre-commit-config.rst 257 | .. _pre-commit-usage: docs/source/dev/pre-commit-usage.rst 258 | .. inclusion-marker-2 259 | 260 | You will need to install pre-commit before contributing any changes; 261 | installing it using your system's package manager is recommended, 262 | otherwise install with pip into your usual virtual environment using 263 | something like:: 264 | 265 | $ sudo emerge pre-commit --or-- 266 | $ pip install pre-commit 267 | 268 | then install it into the repo you just cloned:: 269 | 270 | $ git clone https://github.com/PennyDreadfulMTG/pystache 271 | $ cd pystache/ 272 | $ pre-commit install 273 | 274 | It's usually a good idea to update the hooks to the latest version:: 275 | 276 | pre-commit autoupdate 277 | 278 | .. _gitchangelog: https://github.com/sarnold/gitchangelog-action 279 | .. _pre-commit: https://pre-commit.com/ 280 | 281 | 282 | Credits 283 | ======= 284 | 285 | >>> import pystache 286 | >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek','refurbisher': 'Steve Arnold', 'new_maintainer': 'Thomas David Baker' } 287 | >>> print(pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}\nRefurbisher: {{refurbisher}}\nNew maintainer: {{new_maintainer}}", context)) 288 | Author: Chris Wanstrath 289 | Maintainer: Chris Jerdonek 290 | Refurbisher: Steve Arnold 291 | New maintainer: Thomas David Baker 292 | 293 | 294 | Pystache logo by `David Phillips `__ is 295 | licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported 296 | License `__. 297 | 298 | |ccbysa| 299 | 300 | 301 | .. |ci| image:: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/ci.yml/badge.svg 302 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/ci.yml 303 | :alt: CI Status 304 | 305 | .. |conda| image:: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/conda.yml/badge.svg 306 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/conda.yml 307 | :alt: Conda Status 308 | 309 | .. |coverage| image:: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/coverage.yml/badge.svg 310 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/coverage.yml 311 | :alt: Coverage workflow 312 | 313 | .. |bandit| image:: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/bandit.yml/badge.svg 314 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/bandit.yml 315 | :alt: Security check - Bandit 316 | 317 | .. |release| image:: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/release.yml/badge.svg 318 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/release.yml 319 | :alt: Release Status 320 | 321 | .. |cov| image:: https://raw.githubusercontent.com/PennyDreadfulMTG/pystache/badges/master/test-coverage.svg 322 | :target: https://github.com/PennyDreadfulMTG/pystache/ 323 | :alt: Test coverage 324 | 325 | .. |pylint| image:: https://raw.githubusercontent.com/PennyDreadfulMTG/pystache/badges/master/pylint-score.svg 326 | :target: https://github.com/PennyDreadfulMTG/pystache/actions/workflows/pylint.yml 327 | :alt: Pylint Score 328 | 329 | .. |license| image:: https://img.shields.io/github/license/PennyDreadfulMTG/pystache 330 | :target: https://github.com/PennyDreadfulMTG/pystache/blob/master/LICENSE 331 | :alt: License 332 | 333 | .. |tag| image:: https://img.shields.io/github/v/tag/PennyDreadfulMTG/pystache?color=green&include_prereleases&label=latest%20release 334 | :target: https://github.com/PennyDreadfulMTG/pystache/releases 335 | :alt: GitHub tag 336 | 337 | .. |python| image:: https://img.shields.io/badge/python-3.6+-blue.svg 338 | :target: https://www.python.org/downloads/ 339 | :alt: Python 340 | 341 | .. |pre| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white 342 | :target: https://github.com/pre-commit/pre-commit 343 | :alt: pre-commit 344 | 345 | .. |logo| image:: gh/images/logo_phillips_small.png 346 | 347 | .. |ccbysa| image:: https://i.creativecommons.org/l/by-sa/3.0/88x31.png 348 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | In development branch: 5 | 6 | * Figure out a way to suppress center alignment of images in reST output. 7 | * Add a unit test for the change made in 7ea8e7180c41. This is with regard 8 | to not requiring spec tests when running tests from a downloaded sdist. 9 | * Turn the benchmarking script at pystache/tests/benchmark.py into a command 10 | in pystache/commands, or make it a subcommand of one of the existing 11 | commands (i.e. using a command argument). 12 | * Provide support for logging in at least one of the commands. 13 | * Combine pystache-test with the main command. 14 | -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "pystache" %} 2 | {% set version = "0.6.4" %} 3 | 4 | package: 5 | name: {{ name|lower }} 6 | version: {{ version }} 7 | 8 | source: 9 | path: .. 10 | 11 | build: 12 | number: 0 13 | script: {{ PYTHON }} -m pip install . --no-deps --ignore-installed -vvv 14 | noarch: python 15 | entry_points: 16 | - pystache = pystache.commands.render:main 17 | - pystache-test = pystache.commands.test:main 18 | 19 | requirements: 20 | host: 21 | - pip 22 | - python 23 | - setuptools 24 | - setuptools-scm 25 | run: 26 | - python >=3.8 27 | 28 | test: 29 | imports: 30 | - pystache 31 | - pystache.commands 32 | - pystache.tests 33 | - pystache.tests.data 34 | - pystache.tests.data.locator 35 | - pystache.tests.examples 36 | 37 | commands: 38 | - pystache --help 39 | - pystache-test 40 | 41 | about: 42 | home: https://github.com/PennyDreadfulMTG/pystache 43 | license: MIT 44 | license_family: MIT 45 | license_file: LICENSE 46 | summary: Mustache for Python 47 | 48 | extra: 49 | recipe-maintainers: 50 | - sarnold 51 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS ?= 6 | SPHINXBUILD ?= sphinx-build 7 | SPHINXPROJ = MAVConn 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=MAVConn 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Change history (recent) 2 | ======================= 3 | 4 | .. include:: ../../CHANGELOG.rst 5 | 6 | For older changes through v0.6.0 see the HISTORY.md file in the repository. 7 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # MAVConn documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Oct 31 14:20:31 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | 23 | from importlib.metadata import version 24 | 25 | __version__ = version('pystache') 26 | 27 | #sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) 28 | #__version__ = pkg_resources.get_distribution('pystache').version 29 | 30 | # -- General configuration ------------------------------------------------ 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # 34 | # needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx_git', 41 | 'sphinxcontrib.apidoc', 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.viewcode', 47 | 'recommonmark', 48 | ] 49 | 50 | apidoc_module_dir = '../../pystache/' 51 | apidoc_output_dir = 'api' 52 | apidoc_excluded_paths = ['ext'] 53 | apidoc_separate_modules = True 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = ['_templates'] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # General information about the project. 68 | project = 'pystache' 69 | copyright = '2022, PennyDreadfulMTG' 70 | author = 'Thomas David Baker' 71 | 72 | # The version info for the project you're documenting, acts as replacement for 73 | # |version| and |release|, also used in various other places throughout the 74 | # built documents. 75 | # 76 | description = 'A Python implementation of Mustache (spec v1.1.3).' 77 | version = __version__ 78 | release = version 79 | 80 | # The language for content autogenerated by Sphinx. Refer to documentation 81 | # for a list of supported languages. 82 | # 83 | # This is also used if you do content translation via gettext catalogs. 84 | # Usually you set "language" from the command line for these cases. 85 | language = 'en' 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | # This patterns also effect to html_static_path and html_extra_path 90 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # If true, `todo` and `todoList` produce output, else they produce nothing. 96 | todo_include_todos = True 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | # 104 | html_theme = 'sphinx_rtd_theme' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | # 110 | # html_theme_options = {} 111 | 112 | # Add any paths that contain custom static files (such as style sheets) here, 113 | # relative to this directory. They are copied after the builtin static files, 114 | # so a file named "default.css" will overwrite the builtin "default.css". 115 | #html_static_path = ['_static'] 116 | 117 | # Custom sidebar templates, must be a dictionary that maps document names 118 | # to template names. 119 | # 120 | # This is required for the alabaster theme 121 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 122 | html_sidebars = { 123 | '**': [ 124 | 'relations.html', # needs 'show_related': True theme option to display 125 | 'searchbox.html', 126 | ] 127 | } 128 | 129 | 130 | # -- Options for HTMLHelp output ------------------------------------------ 131 | 132 | # Output file base name for HTML help builder. 133 | htmlhelp_basename = 'pystachedoc' 134 | 135 | 136 | # -- Options for LaTeX output --------------------------------------------- 137 | 138 | latex_elements = { 139 | # The paper size ('letterpaper' or 'a4paper'). 140 | # 141 | # 'papersize': 'letterpaper', 142 | 143 | # The font size ('10pt', '11pt' or '12pt'). 144 | # 145 | # 'pointsize': '10pt', 146 | 147 | # Additional stuff for the LaTeX preamble. 148 | # 149 | # 'preamble': '', 150 | 151 | # Latex figure (float) alignment 152 | # 153 | # 'figure_align': 'htbp', 154 | } 155 | 156 | # Grouping the document tree into LaTeX files. List of tuples 157 | # (source start file, target name, title, 158 | # author, documentclass [howto, manual, or own class]). 159 | latex_documents = [ 160 | (master_doc, 'pystache.tex', 'pystache Documentation', 161 | [author], 'manual'), 162 | ] 163 | 164 | 165 | # -- Options for manual page output --------------------------------------- 166 | 167 | # One entry per manual page. List of tuples 168 | # (source start file, name, description, authors, manual section). 169 | man_pages = [ 170 | (master_doc, 'pystache', 'pystache Documentation', 171 | [author], 1) 172 | ] 173 | 174 | 175 | # -- Options for Texinfo output ------------------------------------------- 176 | 177 | # Grouping the document tree into Texinfo files. List of tuples 178 | # (source start file, target name, title, author, 179 | # dir menu entry, description, category) 180 | texinfo_documents = [ 181 | (master_doc, 'pystache', 'pystache Documentation', 182 | [author], 'pystache', description, 183 | 'Miscellaneous'), 184 | ] 185 | -------------------------------------------------------------------------------- /docs/source/dev/generate-changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog Generation 2 | ==================== 3 | 4 | Changelogs help document important changes. We use an updated version of 5 | gitchangelog_ to produce a nice Github Release page (or just generate a 6 | shell SVD-style document) using the gitchangelog-action_ in the Release 7 | workflow. 8 | 9 | .. _gitchangelog: https://github.com/sarnold/gitchangelog 10 | .. _gitchangelog-action: https://github.com/marketplace/actions/gitchangelog-action 11 | 12 | 13 | To generate a (full) changelog from the repository root, run: 14 | 15 | .. code-block:: bash 16 | 17 | (venv) $ gitchangelog > CHANGELOG.rst 18 | 19 | You can use ``gitchangelog`` to create the changelog automatically. It 20 | examines git commit history and uses custom "filters" to produce its 21 | output. The configuration for this is in the file ``.gitchangelog.rc``. 22 | 23 | To make your changelog even more useful/readable, you should use good 24 | commit messages and consider using the gitchangelog message modifiers. 25 | Since the ``.gitchangelog.rc`` is actually written in Python, it becomes 26 | quite dynamic, thus the configured modifiers and associated documentation 27 | are usually documented in the file itself (unless someone strips out all 28 | the comments). For this config, the message format uses 3 types of 29 | modifier:: 30 | 31 | Message Format 32 | ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 33 | 34 | Description 35 | ACTION is one of 'chg', 'fix', 'new' 36 | 37 | Is WHAT the change is about. 38 | 39 | 'chg' is for refactor, small improvement, cosmetic changes... 40 | 'fix' is for bug fixes 41 | 'new' is for new features, big improvement 42 | 43 | AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 44 | 45 | Is WHO is concerned by the change. 46 | 47 | 'dev' is for developers (API changes, refactors...) 48 | 'usr' is for final users (UI changes) 49 | 'pkg' is for packagers (packaging changes) 50 | 'test' is for testers (test only related changes) 51 | 'doc' is for doc guys (doc only changes) 52 | 53 | COMMIT_MSG is ... well ... the commit message itself. 54 | 55 | TAGs are additional adjective as 'refactor' 'minor' 'cosmetic' 56 | 57 | They are preceded with a '!' or a '@' (prefer the former, as the 58 | latter is wrongly interpreted in github.) Commonly used tags are: 59 | 60 | 'refactor' is obviously for refactoring code only 61 | 'minor' is for a very meaningless change (a typo, adding a comment) 62 | 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 63 | 'wip' is for partial functionality but complete sub-functionality. 64 | 65 | Example: 66 | 67 | new: usr: support of bazaar implemented 68 | chg: re-indented some lines !cosmetic 69 | new: dev: updated code to be compatible with last version of killer lib. 70 | fix: pkg: updated year of license coverage. 71 | new: test: added a bunch of test around user usability of feature X. 72 | fix: typo in spelling my name in comment. !minor 73 | 74 | 75 | See the current `.gitchangelog.rc`_ in the repo for more details. 76 | 77 | Read more about gitchangelog_. 78 | 79 | .. _.gitchangelog.rc: https://github.com/VCTLabs/redis-ipc/blob/develop/.gitchangelog.rc 80 | .. _gitchangelog: https://github.com/sarnold/gitchangelog 81 | 82 | 83 | Git Tags 84 | -------- 85 | 86 | Git tags are a way to bookmark commits, and come in two varieties: 87 | lightweight and signed/annotated. Both signed and annotated tags 88 | contain author information and when used they will help organize the 89 | changelog. 90 | 91 | To create an annotated tag for a version ``0.1.1`` release: 92 | 93 | .. code-block:: bash 94 | 95 | $ git tag -a v0.1.1 -m "v0.1.1" 96 | 97 | Using tags like this will break the changelog into sections based on 98 | versions. If you forgot to make a tag you can checkout an old commit 99 | and make the tag (don't forget to adjust the date - you may want to 100 | google this...) 101 | 102 | 103 | Sections 104 | -------- 105 | 106 | The sections in the changelog are created from the git log commit 107 | messages, and are parsed using the regex defined in the 108 | ``.gitchangelog.rc`` configuration file. 109 | -------------------------------------------------------------------------------- /docs/source/dev/pre-commit-config.rst: -------------------------------------------------------------------------------- 1 | ================================================== 2 | Contents of the ``.pre-commit-config.yaml`` file 3 | ================================================== 4 | 5 | The file ``.pre-commit-config.yaml`` is used to configure the program 6 | ``pre-commit``, which controls the setup and execution of `Git hooks`_. 7 | 8 | The ``.pre-commit-config.yaml`` file has a list of git repos, each repo may 9 | define one or more hooks. 10 | 11 | In this document we will review the various hooks. Some of the hooks will 12 | modify files, some will not. 13 | 14 | .. _pre-commit: https://pre-commit.com 15 | .. _Git hooks: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks 16 | 17 | 18 | Hook Descriptions 19 | ================= 20 | 21 | Basic warning checks include: 22 | 23 | * ``check-added-large-files`` 24 | * ``check-case-conflict`` 25 | * ``check-executables-have-shebangs`` 26 | * ``check-shebang-scripts-are-executable`` 27 | * ``check-merge-conflict`` 28 | * ``detect-private-key`` 29 | 30 | 31 | ``end-of-file-fixer`` 32 | --------------------- 33 | 34 | This will modify files by making sure that each file ends in a blank line. 35 | 36 | If a commit fails due to this hook, just commit again. 37 | 38 | 39 | ``trailing-whitespace`` 40 | ----------------------- 41 | 42 | This will modify files by ensuring there is no trailing whitespace on any line. 43 | 44 | If a commit fails due to this hook, just commit again. 45 | 46 | ``mixed-line-ending`` 47 | --------------------- 48 | 49 | This will modify files by ensuring there are no mixed line endings in any file. 50 | 51 | If a commit fails due to this hook, just commit again. 52 | 53 | ``check-yaml`` 54 | -------------- 55 | 56 | This will NOT modify files. It will examine YAML files and report any 57 | issues. The rules for its configuration are defined in 58 | ``.pre-commit-config.yaml`` in the ``exclude`` section. 59 | 60 | If a commit fails due to this hook, all reported issues must be manually 61 | fixed before committing again. 62 | 63 | ``ffffff`` 64 | ---------- 65 | 66 | (fork of ``black`` with single-quote normalization) 67 | 68 | This will modify Python files by re-formatting the code. The rules 69 | for the formatting are defined in ``.pre-commit-config.yaml`` in the 70 | ``args`` section, and should match the rules in ``pyproject.toml`` (for 71 | example the line-length must be the same). 72 | 73 | If a commit fails due to this hook, just commit again. 74 | 75 | ``flake8`` 76 | ---------- 77 | 78 | This will NOT modify files. It will examine Python files for adherence to 79 | PEP8 and report any issues. Typically ``black`` will correct any issues that 80 | ``flake8`` may find. The rules for this are defined in ``.flake8``, and must 81 | be carefully selected to be compatible with ``black``. 82 | 83 | If a commit fails due to this hook, all reported issues must be manually 84 | fixed before committing again (if not corrected by black/ffffff). 85 | 86 | ``autoflake`` 87 | ------------- 88 | 89 | This will modify Python files by re-formatting the code. The rules 90 | for the formatting are defined in ``.pre-commit-config.yaml`` in the 91 | ``args`` section. 92 | 93 | ``pylint`` 94 | ---------- 95 | 96 | This will NOT modify files. It will examine Python files for errors and code 97 | smells, and offer suggestions for refactoring. The rules for the formatting 98 | and minimum score are defined in ``.pre-commit-config.yaml`` in the ``args`` 99 | section. If the score falls below the minimum, the commit will fail and you 100 | must correct it manually before committing again. 101 | 102 | ``bandit`` 103 | ---------- 104 | 105 | This will NOT modify files. It will examine Python files for security issues 106 | and report any potential problems. There is currently one allowed issue (in 107 | the baseline.json file) in the spec testing code. Any issues found in 108 | non-test code must be resolved manually before committing again. 109 | 110 | ``beautysh`` 111 | ------------ 112 | 113 | This will modify files. It will examine shell files and fix some 114 | formatting issues. The rules for its configuration are defined in 115 | ``.pre-commit-config.yaml`` in the ``args`` section. 116 | 117 | If a commit fails due to this hook, review the proposed changes in the 118 | console, and check the files using ``git diff ...`` 119 | 120 | Doc formatting (.rst files) 121 | --------------------------- 122 | 123 | * blacken-docs 124 | * doc8 125 | * pygrep 126 | 127 | - rst-backticks 128 | - rst-directive-colons 129 | - rst-inline-touching-normal 130 | 131 | 132 | The blacken-docs tool will check for (and correct) any issues with python code 133 | blocks in documentation files; the latter checks will NOT modify files. They 134 | will examine all RST files (except ChangeLog.rst) and report any issues. 135 | 136 | If a commit fails due to the (non)blacken-docs hooks, all reported issues must be 137 | manually fixed before committing again. 138 | -------------------------------------------------------------------------------- /docs/source/dev/pre-commit-usage.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Using Pre-Commit 3 | ================== 4 | 5 | `pre-commit`_ is a program used to configure and run Git hooks. These 6 | hooks can be triggered in different Git stages, though typically we use 7 | them in only commit and push stages. 8 | 9 | See the `pre-commit config contents`_ document for descriptions of the 10 | current hooks. 11 | 12 | Each of the hooks will run in its own small virtual environment. 13 | 14 | .. _pre-commit: https://pre-commit.com 15 | .. _pre-commit config contents: pre-commit-config.rst 16 | 17 | 18 | Setup 19 | ----- 20 | 21 | The program must be installed and the hooks must be configured. The 22 | program should be installed in your usual virtual environment, for 23 | example, "venv" (this could also be a conda environment). 24 | 25 | After activating your environment, run the following commands: 26 | 27 | .. code-block:: bash 28 | 29 | (venv) $ pip install pre-commit 30 | (venv) $ pre-commit install 31 | (venv) $ pre-commit install-hooks 32 | (venv) $ pre-commit autoupdate 33 | 34 | 35 | Automatic Usage 36 | --------------- 37 | 38 | In normal usage, ``pre-commit`` will trigger with every ``git commit`` 39 | and every ``git push``. The hooks that trigger in each stage can be 40 | configured by editing the ``.pre-commit-config.yaml`` file. The files 41 | that have changed will be passed to the various hooks before the git 42 | operation completes. If one of the hooks exits with a non-zero 43 | exit-code, then the commit (or push) will fail. 44 | 45 | Manual Usage 46 | ------------ 47 | 48 | To manually trigger ``pre-commit`` to run all hooks on CHANGED files: 49 | 50 | .. code-block:: bash 51 | 52 | (venv) $ pre-commit run 53 | 54 | To manually trigger ``pre-commit`` to run all hooks on ALL files, 55 | regardless if they are changed or not: 56 | 57 | .. code-block:: bash 58 | 59 | (venv) $ pre-commit run --all-files 60 | 61 | To manually trigger ``pre-commit`` to run a single hook on changed files: 62 | 63 | .. code-block:: bash 64 | 65 | (venv) $ pre-commit run 66 | 67 | To manually trigger ``pre-commit`` to run a single hook on all files: 68 | 69 | .. code-block:: bash 70 | 71 | (venv) $ pre-commit run --all-files 72 | 73 | For example, to run ``pylint`` on all files: 74 | 75 | .. code-block:: bash 76 | 77 | (venv) $ pre-commit run pylint --all-files 78 | -------------------------------------------------------------------------------- /docs/source/gh/images/logo_phillips_small.png: -------------------------------------------------------------------------------- 1 | ../../../../gh/images/logo_phillips_small.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to the pystache documentation! 2 | ====================================== 3 | 4 | .. git_commit_detail:: 5 | :branch: 6 | :commit: 7 | :sha_length: 10 8 | :uncommitted: 9 | :untracked: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | :caption: Contents: 14 | 15 | readme_include 16 | dev/generate-changelog 17 | dev/pre-commit-config 18 | dev/pre-commit-usage 19 | api/modules 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | -------------------------------------------------------------------------------- /docs/source/readme_include.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | :end-before: inclusion-marker-1 3 | 4 | 5 | .. include:: ../../README.rst 6 | :start-after: inclusion-marker-2 7 | 8 | 9 | .. include:: changelog.rst 10 | -------------------------------------------------------------------------------- /gh/fix_pkg_name.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # This fixes package name="." in coverage.xml or another coverage filename 4 | # as the only optional argument: ./fix_pkg_name.sh other-name.xml 5 | # We default to grepping pkg name from (python) setup.cfg 6 | # otherwise you should set the REAL_NAME environment override, eg: 7 | # 8 | # REAL_NAME="re2" ./fix_pkg_name.sh 9 | # 10 | # or export it first in your shell env. 11 | 12 | set -euo pipefail 13 | 14 | failures=0 15 | trap 'failures=$((failures+1))' ERR 16 | 17 | COV_FILE=${1:-coverage.xml} 18 | REAL_NAME=${REAL_NAME:-""} 19 | VERBOSE="false" # set to "true" for extra output 20 | 21 | NAME_CHECK=$(grep -o 'name="."' "${COV_FILE}" || true) 22 | 23 | [[ -z "$NAME_CHECK" ]] && echo "Nothing to fix ..." && exit 0 24 | [[ -n $REAL_NAME ]] || REAL_NAME=$(grep ^name pyproject.toml | cut -d'"' -f2) 25 | [[ -n $REAL_NAME ]] && sed -i -e "s|name=\".\"|name=\"${REAL_NAME}\"|" $COV_FILE 26 | [[ -n $REAL_NAME ]] && echo "Replaced \".\" with ${REAL_NAME} in ${COV_FILE} ..." 27 | 28 | if ((failures != 0)); then 29 | echo "Something went wrong !!!" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /gh/images/logo_phillips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PennyDreadfulMTG/pystache/531a0ecea00a696423175ef46c319b29408dc3e2/gh/images/logo_phillips.png -------------------------------------------------------------------------------- /gh/images/logo_phillips_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PennyDreadfulMTG/pystache/531a0ecea00a696423175ef46c319b29408dc3e2/gh/images/logo_phillips_small.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61", 4 | "setuptools_scm[toml]>=6.2", 5 | ] 6 | 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "pystache" 11 | description = "Mustache for Python" 12 | readme = "README.rst" 13 | requires-python = ">=3.8" 14 | license = {file = "LICENSE"} 15 | authors = [ 16 | {name = "Chris Wanstrath", email = "chris@ozmm.org"}, 17 | ] 18 | maintainers = [ 19 | {name = "Thomas David Baker", email = "bakert@gmail.com"}, 20 | ] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3 :: Only", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Programming Language :: Python :: 3.13", 33 | "Topic :: Software Development :: Libraries", 34 | ] 35 | 36 | dynamic = ["version"] 37 | 38 | dependencies = [ 39 | 'importlib-metadata>=4.6; python_version < "3.10"', 40 | ] 41 | 42 | [project.optional-dependencies] 43 | cov = [ 44 | "coverage", 45 | "coverage_python_version", 46 | ] 47 | doc = [ 48 | "sphinx", 49 | "sphinx_git", 50 | "recommonmark", 51 | "sphinx_rtd_theme", 52 | "sphinxcontrib-apidoc", 53 | ] 54 | test = [ 55 | "pytest", 56 | "pytest-cov", 57 | ] 58 | [project.urls] 59 | Homepage = "https://github.com/PennyDreadfulMTG/pystache" 60 | Documentation = "http://mustache.github.io/" 61 | Repository = "https://github.com/PennyDreadfulMTG/pystache.git" 62 | Changelog = "https://github.com/PennyDreadfulMTG/pystache/blob/master/CHANGELOG.rst" 63 | 64 | [project.scripts] 65 | pystache = "pystache.commands.render:main" 66 | pystache-test = "pystache.commands.test:main" 67 | 68 | [tool.setuptools.packages] 69 | find = {namespaces = false} # Disable implicit namespaces 70 | 71 | [tool.setuptools_scm] 72 | write_to = "pystache/_version.py" 73 | 74 | [tool.pytest.ini_options] 75 | minversion = "6.0" 76 | testpaths = ["pystache/tests",] 77 | log_cli = false 78 | doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE",] 79 | addopts = "--strict-markers" 80 | markers = "subscript" 81 | 82 | [tool.coverage.run] 83 | branch = true 84 | source = ["pystache"] 85 | plugins = ["coverage_python_version"] 86 | omit = [ 87 | "pystache/tests/*", 88 | "setup.py", 89 | ".tox", 90 | ] 91 | [tool.coverage.paths] 92 | source = ["pystache"] 93 | 94 | [tool.coverage.report] 95 | fail_under = 95 96 | show_missing = true 97 | ignore_errors = true 98 | exclude_lines = [ 99 | "pragma: no cover", 100 | "raise NotImplementedError", 101 | "raise AssertionError", 102 | "if typing.TYPE_CHECKING:", 103 | "if TYPE_CHECKING:", 104 | ] 105 | 106 | [tool.black] 107 | line-length = 110 108 | skip-string-normalization = true 109 | include = '\.py$' 110 | exclude = ''' 111 | /( 112 | \.git 113 | | \.hg 114 | | \.mypy_cache 115 | | \.tox 116 | | \.venv 117 | | _build 118 | | buck-out 119 | | build 120 | | dist 121 | | pystache/tests 122 | )/ 123 | ''' 124 | 125 | [tool.isort] 126 | line_length = 72 127 | multi_line_output = 3 128 | include_trailing_comma = true 129 | force_grid_wrap = 0 130 | use_parentheses = true 131 | ensure_newline_before_comments = true 132 | -------------------------------------------------------------------------------- /pystache/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | 6 | # We keep all initialization code in a separate module. 7 | 8 | from pystache.init import Renderer, TemplateSpec, parse, render 9 | 10 | from ._version import __version__ 11 | 12 | version = __version__ 13 | 14 | __all__ = ['parse', 'render', 'Renderer', 'TemplateSpec'] 15 | -------------------------------------------------------------------------------- /pystache/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /pystache/commands/render.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module provides command-line access to pystache. 5 | 6 | Run this script using the -h option for command-line help. 7 | 8 | """ 9 | 10 | import json 11 | import sys 12 | 13 | # TODO: switch to argparse already, sheesh... 14 | # The optparse module is deprecated in Python 2.7 in favor of argparse. 15 | # However, argparse is not available in Python 2.6 and earlier. 16 | from optparse import OptionParser 17 | 18 | # We use absolute imports here to allow use of this script from its 19 | # location in source control (e.g. for development purposes). 20 | # Otherwise, the following error occurs: 21 | # 22 | # ValueError: Attempted relative import in non-package 23 | # 24 | from pystache.common import TemplateNotFoundError 25 | from pystache.renderer import Renderer 26 | 27 | USAGE = """\ 28 | %prog [-h] template context 29 | 30 | Render a mustache template with the given context. 31 | 32 | positional arguments: 33 | template A filename or template string. 34 | context A filename or JSON string.""" 35 | 36 | 37 | def parse_args(sys_argv, usage): 38 | """ 39 | Return an OptionParser for the script. 40 | 41 | """ 42 | args = sys_argv[1:] 43 | 44 | parser = OptionParser(usage=usage) 45 | options, args = parser.parse_args(args) 46 | 47 | template, context = args 48 | 49 | return template, context 50 | 51 | 52 | # TODO: verify whether the setup() method's entry_points argument 53 | # supports passing arguments to main: 54 | # 55 | # http://packages.python.org/distribute/setuptools.html#automatic-script-creation 56 | # 57 | def main(sys_argv=sys.argv): 58 | template, context = parse_args(sys_argv, USAGE) 59 | 60 | if template.endswith('.mustache'): 61 | template = template[:-9] 62 | 63 | renderer = Renderer() 64 | 65 | try: 66 | template = renderer.load_template(template) 67 | except TemplateNotFoundError: 68 | pass 69 | 70 | try: 71 | context = json.load(open(context)) 72 | except IOError: 73 | context = json.loads(context) 74 | 75 | rendered = renderer.render(template, context) 76 | print(rendered) 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /pystache/commands/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module provides a command to test pystache (unit tests, doctests, etc). 5 | 6 | """ 7 | 8 | import sys 9 | 10 | from pystache.tests.main import main as run_tests 11 | 12 | 13 | def main(sys_argv=sys.argv): 14 | run_tests(sys_argv=sys_argv) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /pystache/common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes functionality needed throughout the project. 5 | 6 | """ 7 | 8 | 9 | def _get_string_types(): 10 | """ 11 | Return the Python3 string type (no more python2) 12 | """ 13 | return (str, type('a'.encode('utf-8'))) 14 | 15 | 16 | _STRING_TYPES = _get_string_types() 17 | 18 | 19 | def is_string(obj): 20 | """ 21 | Return whether the given object is a byte string or unicode string. 22 | 23 | This function is provided for compatibility with both Python 2 and 3 24 | when using 2to3. 25 | 26 | """ 27 | return isinstance(obj, _STRING_TYPES) 28 | 29 | 30 | # This function was designed to be portable across Python versions -- both 31 | # with older versions and with Python 3 after applying 2to3. 32 | def read(path): 33 | """ 34 | Return the contents of a text file as a byte string. 35 | 36 | """ 37 | # Opening in binary mode is necessary for compatibility across Python 38 | # 2 and 3. In both Python 2 and 3, open() defaults to opening files in 39 | # text mode. However, in Python 2, open() returns file objects whose 40 | # read() method returns byte strings (strings of type `str` in Python 2), 41 | # whereas in Python 3, the file object returns unicode strings (strings 42 | # of type `str` in Python 3). 43 | f = open(path, 'rb') 44 | # We avoid use of the with keyword for Python 2.4 support. 45 | try: 46 | return f.read() 47 | finally: 48 | f.close() 49 | 50 | 51 | class MissingTags(object): 52 | 53 | """Contains the valid values for Renderer.missing_tags.""" 54 | 55 | ignore = 'ignore' 56 | strict = 'strict' 57 | 58 | 59 | class PystacheError(Exception): 60 | """Base class for Pystache exceptions.""" 61 | 62 | 63 | class TemplateNotFoundError(PystacheError): 64 | """An exception raised when a template is not found.""" 65 | -------------------------------------------------------------------------------- /pystache/context.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes a ContextStack class. 5 | 6 | The Mustache spec makes a special distinction between two types of context 7 | stack elements: hashes and objects. For the purposes of interpreting the 8 | spec, we define these categories mutually exclusively as follows: 9 | 10 | (1) Hash: an item whose type is a subclass of dict. 11 | 12 | (2) Object: an item that is neither a hash nor an instance of a 13 | built-in type. 14 | 15 | """ 16 | 17 | from pystache.common import PystacheError 18 | 19 | # This equals '__builtin__' in Python 2 and 'builtins' in Python 3. 20 | _BUILTIN_MODULE = type(0).__module__ 21 | 22 | 23 | # We use this private global variable as a return value to represent a key 24 | # not being found on lookup. This lets us distinguish between the case 25 | # of a key's value being None with the case of a key not being found -- 26 | # without having to rely on exceptions (e.g. KeyError) for flow control. 27 | # 28 | # TODO: eliminate the need for a private global variable, e.g. by using the 29 | # preferred Python approach of "easier to ask for forgiveness than permission": 30 | # http://docs.python.org/glossary.html#term-eafp 31 | class NotFound(object): 32 | pass 33 | 34 | 35 | _NOT_FOUND = NotFound() 36 | 37 | 38 | def _get_value(context, key): 39 | """ 40 | Retrieve a key's value from a context item. 41 | 42 | Returns _NOT_FOUND if the key does not exist. 43 | 44 | The ContextStack.get() docstring documents this function's intended behavior. 45 | 46 | """ 47 | if isinstance(context, dict): 48 | # Then we consider the argument a "hash" for the purposes of the spec. 49 | # 50 | # We do a membership test to avoid using exceptions for flow control 51 | # (e.g. catching KeyError). 52 | if key in context: 53 | return context[key] 54 | elif type(context).__module__ != _BUILTIN_MODULE: 55 | # Then we consider the argument an "object" for the purposes of 56 | # the spec. 57 | # 58 | # The elif test above lets us avoid treating instances of built-in 59 | # types like integers and strings as objects (cf. issue #81). 60 | # Instances of user-defined classes on the other hand, for example, 61 | # are considered objects by the test above. 62 | try: 63 | attr = getattr(context, key) 64 | except AttributeError: 65 | # TODO: distinguish the case of the attribute not existing from 66 | # an AttributeError being raised by the call to the attribute. 67 | # See the following issue for implementation ideas: 68 | # http://bugs.python.org/issue7559 69 | pass 70 | else: 71 | # TODO: consider using EAFP here instead. 72 | # http://docs.python.org/glossary.html#term-eafp 73 | if callable(attr): 74 | return attr() 75 | return attr 76 | 77 | return _NOT_FOUND 78 | 79 | 80 | class KeyNotFoundError(PystacheError): 81 | 82 | """ 83 | An exception raised when a key is not found in a context stack. 84 | 85 | """ 86 | 87 | def __init__(self, key, details): 88 | self.key = key 89 | self.details = details 90 | 91 | def __str__(self): 92 | return 'Key %s not found: %s' % (repr(self.key), self.details) 93 | 94 | 95 | class ContextStack(object): 96 | 97 | """ 98 | Provides dictionary-like access to a stack of zero or more items. 99 | 100 | Instances of this class are meant to act as the rendering context 101 | when rendering Mustache templates in accordance with mustache(5) 102 | and the Mustache spec. 103 | 104 | Instances encapsulate a private stack of hashes, objects, and built-in 105 | type instances. Querying the stack for the value of a key queries 106 | the items in the stack in order from last-added objects to first 107 | (last in, first out). 108 | 109 | Caution: this class does not currently support recursive nesting in 110 | that items in the stack cannot themselves be ContextStack instances. 111 | 112 | See the docstrings of the methods of this class for more details. 113 | 114 | """ 115 | 116 | # We reserve keyword arguments for future options (e.g. a "strict=True" 117 | # option for enabling a strict mode). 118 | def __init__(self, *items): 119 | """ 120 | Construct an instance, and initialize the private stack. 121 | 122 | The *items arguments are the items with which to populate the 123 | initial stack. Items in the argument list are added to the 124 | stack in order so that, in particular, items at the end of 125 | the argument list are queried first when querying the stack. 126 | 127 | Caution: items should not themselves be ContextStack instances, as 128 | recursive nesting does not behave as one might expect. 129 | 130 | """ 131 | self._stack = list(items) 132 | 133 | def __repr__(self): 134 | """ 135 | Return a string representation of the instance. 136 | 137 | For example-- 138 | 139 | >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123}) 140 | >>> repr(context) 141 | "ContextStack({'alpha': 'abc'}, {'numeric': 123})" 142 | 143 | """ 144 | return '%s%s' % (self.__class__.__name__, tuple(self._stack)) 145 | 146 | @staticmethod 147 | def create(*context, **kwargs): 148 | """ 149 | Build a ContextStack instance from a sequence of context-like items. 150 | 151 | This factory-style method is more general than the ContextStack class's 152 | constructor in that, unlike the constructor, the argument list 153 | can itself contain ContextStack instances. 154 | 155 | Here is an example illustrating various aspects of this method: 156 | 157 | >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'} 158 | >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'}) 159 | >>> 160 | >>> context = ContextStack.create(obj1, None, obj2, mineral='gold') 161 | >>> 162 | >>> context.get('animal') 163 | 'cat' 164 | >>> context.get('vegetable') 165 | 'spinach' 166 | >>> context.get('mineral') 167 | 'gold' 168 | 169 | Arguments: 170 | 171 | :context: zero or more dictionaries, ContextStack instances, or objects 172 | with which to populate the initial context stack. None 173 | arguments will be skipped. Items in the context list are 174 | added to the stack in order so that later items in the argument 175 | list take precedence over earlier items. This behavior is the 176 | same as the constructor. 177 | 178 | :kwargs: additional key-value data to add to the context stack. 179 | As these arguments appear after all items in the context list, 180 | in the case of key conflicts these values take precedence over 181 | all items in the context list. This behavior is the same as 182 | the constructor's. 183 | 184 | """ 185 | items = context 186 | 187 | context = ContextStack() 188 | 189 | for item in items: 190 | if item is None: 191 | continue 192 | if isinstance(item, ContextStack): 193 | context._stack.extend(item._stack) 194 | else: 195 | context.push(item) 196 | 197 | if kwargs: 198 | context.push(kwargs) 199 | 200 | return context 201 | 202 | # TODO: add more unit tests for this. 203 | # TODO: update the docstring for dotted names. 204 | def get(self, name): 205 | """ 206 | Resolve a dotted name against the current context stack. 207 | 208 | This function follows the rules outlined in the section of the 209 | spec regarding tag interpolation. This function returns the value 210 | as is and does not coerce the return value to a string. 211 | 212 | Arguments: 213 | 214 | :name: a dotted or non-dotted name. 215 | 216 | :default: the value to return if name resolution fails at any point. 217 | Defaults to the empty string per the Mustache spec. 218 | 219 | This method queries items in the stack in order from last-added 220 | objects to first (last in, first out). The value returned is 221 | the value of the key in the first item that contains the key. 222 | If the key is not found in any item in the stack, then the default 223 | value is returned. The default value defaults to None. 224 | 225 | In accordance with the spec, this method queries items in the 226 | stack for a key differently depending on whether the item is a 227 | hash, object, or neither (as defined in the module docstring): 228 | 229 | (1) Hash: if the item is a hash, then the key's value is the 230 | dictionary value of the key. If the dictionary doesn't contain 231 | the key, then the key is considered not found. 232 | 233 | (2) Object: if the item is an an object, then the method looks for 234 | an attribute with the same name as the key. If an attribute 235 | with that name exists, the value of the attribute is returned. 236 | If the attribute is callable, however (i.e. if the attribute 237 | is a method), then the attribute is called with no arguments 238 | and that value is returned. If there is no attribute with 239 | the same name as the key, then the key is considered not found. 240 | 241 | (3) Neither: if the item is neither a hash nor an object, then 242 | the key is considered not found. 243 | 244 | *Caution*: 245 | 246 | Callables are handled differently depending on whether they are 247 | dictionary values, as in (1) above, or attributes, as in (2). 248 | The former are returned as-is, while the latter are first 249 | called and that value returned. 250 | 251 | Here is an example to illustrate: 252 | 253 | >>> def greet(): 254 | ... return "Hi Bob!" 255 | >>> 256 | >>> class Greeter(object): 257 | ... greet = None 258 | >>> 259 | >>> dct = {'greet': greet} 260 | >>> obj = Greeter() 261 | >>> obj.greet = greet 262 | >>> 263 | >>> dct['greet'] is obj.greet 264 | True 265 | >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS 266 | 267 | >>> ContextStack(obj).get('greet') 268 | 'Hi Bob!' 269 | 270 | TODO: explain the rationale for this difference in treatment. 271 | 272 | """ 273 | if name == '.': 274 | try: 275 | return self.top() 276 | except IndexError: 277 | raise KeyNotFoundError('.', 'empty context stack') 278 | 279 | parts = name.split('.') 280 | 281 | try: 282 | result = self._get_simple(parts[0]) 283 | except KeyNotFoundError: 284 | raise KeyNotFoundError(name, 'first part') 285 | 286 | for part in parts[1:]: 287 | # The full context stack is not used to resolve the remaining parts. 288 | # From the spec-- 289 | # 290 | # 5) If any name parts were retained in step 1, each should be 291 | # resolved against a context stack containing only the result 292 | # from the former resolution. If any part fails resolution, the 293 | # result should be considered falsey, and should interpolate as 294 | # the empty string. 295 | # 296 | # TODO: make sure we have a test case for the above point. 297 | result = _get_value(result, part) 298 | # TODO: consider using EAFP here instead. 299 | # http://docs.python.org/glossary.html#term-eafp 300 | if result is _NOT_FOUND: 301 | raise KeyNotFoundError(name, 'missing %s' % repr(part)) 302 | 303 | return result 304 | 305 | def _get_simple(self, name): 306 | """ 307 | Query the stack for a non-dotted name. 308 | 309 | """ 310 | for item in reversed(self._stack): 311 | result = _get_value(item, name) 312 | if result is not _NOT_FOUND: 313 | return result 314 | 315 | raise KeyNotFoundError(name, 'part missing') 316 | 317 | def push(self, item): 318 | """ 319 | Push an item onto the stack. 320 | 321 | """ 322 | self._stack.append(item) 323 | 324 | def pop(self): 325 | """ 326 | Pop an item off of the stack, and return it. 327 | 328 | """ 329 | return self._stack.pop() 330 | 331 | def top(self): 332 | """ 333 | Return the item last added to the stack. 334 | 335 | """ 336 | return self._stack[-1] 337 | 338 | def copy(self): 339 | """ 340 | Return a copy of this instance. 341 | 342 | """ 343 | return ContextStack(*self._stack) 344 | -------------------------------------------------------------------------------- /pystache/defaults.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module provides a central location for defining default behavior. 5 | 6 | Throughout the package, these defaults take effect only when the user 7 | does not otherwise specify a value. 8 | 9 | """ 10 | 11 | try: 12 | # Python 3.2 adds html.escape() and deprecates cgi.escape(). 13 | from html import escape 14 | except ImportError: 15 | from cgi import escape 16 | 17 | import os 18 | import sys 19 | 20 | from pystache.common import MissingTags 21 | 22 | # How to handle encoding errors when decoding strings from str to unicode. 23 | # 24 | # This value is passed as the "errors" argument to Python's built-in 25 | # unicode() function: 26 | # 27 | # http://docs.python.org/library/functions.html#unicode 28 | # 29 | DECODE_ERRORS = 'strict' 30 | 31 | # The name of the encoding to use when converting to unicode any strings of 32 | # type str encountered during the rendering process. 33 | STRING_ENCODING = sys.getdefaultencoding() 34 | 35 | # The name of the encoding to use when converting file contents to unicode. 36 | # This default takes precedence over the STRING_ENCODING default for 37 | # strings that arise from files. 38 | FILE_ENCODING = sys.getdefaultencoding() 39 | 40 | # The delimiters to start with when parsing. 41 | DELIMITERS = ('{{', '}}') 42 | 43 | # How to handle missing tags when rendering a template. 44 | MISSING_TAGS = MissingTags.ignore 45 | 46 | # The starting list of directories in which to search for templates when 47 | # loading a template by file name. 48 | SEARCH_DIRS = [os.curdir] # i.e. ['.'] 49 | 50 | # The escape function to apply to strings that require escaping when 51 | # rendering templates (e.g. for tags enclosed in double braces). 52 | # Only unicode strings will be passed to this function. 53 | # 54 | # The quote=True argument causes double but not single quotes to be escaped 55 | # in Python 3.1 and earlier, and both double and single quotes to be 56 | # escaped in Python 3.2 and later: 57 | # 58 | # http://docs.python.org/library/cgi.html#cgi.escape 59 | # http://docs.python.org/dev/library/html.html#html.escape 60 | # 61 | TAG_ESCAPE = lambda u: escape(u, quote=True) # noqa 62 | 63 | # The default template extension, without the leading dot. 64 | TEMPLATE_EXTENSION = 'mustache' 65 | -------------------------------------------------------------------------------- /pystache/init.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | This module contains the initialization logic called by __init__.py. 5 | 6 | """ 7 | 8 | # do not let linter tools remove any imports !! 9 | from pystache.parser import parse # noqa 10 | from pystache.renderer import Renderer 11 | from pystache.template_spec import TemplateSpec # noqa 12 | 13 | 14 | def render(template, context=None, **kwargs): 15 | """ 16 | Return the given template string rendered using the given context. 17 | 18 | """ 19 | renderer = Renderer() 20 | return renderer.render(template, context, **kwargs) 21 | -------------------------------------------------------------------------------- /pystache/loader.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module provides a Loader class for locating and reading templates. 5 | 6 | """ 7 | 8 | import platform 9 | 10 | from pystache import common, defaults 11 | from pystache.locator import Locator 12 | 13 | # We make a function so that the current defaults take effect. 14 | # TODO: revisit whether this is necessary. 15 | 16 | 17 | def _make_to_unicode(): 18 | def to_unicode(s, encoding=None): 19 | """ 20 | Raises a TypeError exception if the given string is already unicode. 21 | 22 | """ 23 | if encoding is None: 24 | encoding = defaults.STRING_ENCODING 25 | return str(s, encoding, defaults.DECODE_ERRORS) 26 | 27 | return to_unicode 28 | 29 | 30 | class Loader(object): 31 | 32 | """ 33 | Loads the template associated to a name or user-defined object. 34 | 35 | All load_*() methods return the template as a unicode string. 36 | 37 | """ 38 | 39 | def __init__( 40 | self, 41 | file_encoding=None, 42 | extension=None, 43 | to_unicode=None, 44 | search_dirs=None, 45 | ): 46 | """ 47 | Construct a template loader instance. 48 | 49 | Arguments: 50 | 51 | extension: the template file extension, without the leading dot. 52 | Pass False for no extension (e.g. to use extensionless template 53 | files). Defaults to the package default. 54 | 55 | file_encoding: the name of the encoding to use when converting file 56 | contents to unicode. Defaults to the package default. 57 | 58 | search_dirs: the list of directories in which to search when loading 59 | a template by name or file name. Defaults to the package default. 60 | 61 | to_unicode: the function to use when converting strings of type 62 | str to unicode. The function should have the signature: 63 | 64 | to_unicode(s, encoding=None) 65 | 66 | It should accept a string of type str and an optional encoding 67 | name and return a string of type unicode. Defaults to calling 68 | Python's built-in function unicode() using the package string 69 | encoding and decode errors defaults. 70 | 71 | """ 72 | if extension is None: 73 | extension = defaults.TEMPLATE_EXTENSION 74 | 75 | if file_encoding is None: 76 | file_encoding = defaults.FILE_ENCODING 77 | 78 | if search_dirs is None: 79 | search_dirs = defaults.SEARCH_DIRS 80 | 81 | if to_unicode is None: 82 | to_unicode = _make_to_unicode() 83 | 84 | self.extension = extension 85 | self.file_encoding = file_encoding 86 | # TODO: unit test setting this attribute. 87 | self.search_dirs = search_dirs 88 | self.to_unicode = to_unicode 89 | 90 | def _make_locator(self): 91 | return Locator(extension=self.extension) 92 | 93 | def str(self, s, encoding=None): 94 | """ 95 | Convert a string to unicode using the given encoding, and return it. 96 | 97 | This function uses the underlying to_unicode attribute. 98 | 99 | Arguments: 100 | 101 | s: a basestring instance to convert to unicode. Unlike Python's 102 | built-in unicode() function, it is okay to pass unicode strings 103 | to this function. (Passing a unicode string to Python's unicode() 104 | with the encoding argument throws the error, "TypeError: decoding 105 | Unicode is not supported.") 106 | 107 | encoding: the encoding to pass to the to_unicode attribute. 108 | Defaults to None. 109 | 110 | """ 111 | if isinstance(s, str): 112 | return str(s) 113 | 114 | return self.to_unicode(s, encoding) 115 | 116 | def read(self, path, encoding=None): 117 | """ 118 | Read the template at the given path, and return it as a unicode string. 119 | 120 | """ 121 | b = common.read(path) 122 | 123 | if encoding is None: 124 | encoding = self.file_encoding 125 | if platform.system() == 'Windows': 126 | return self.str(b, encoding).replace('\r', '') 127 | return self.str(b, encoding) 128 | 129 | def load_file(self, file_name): 130 | """ 131 | Find and return the template with the given file name. 132 | 133 | Arguments: 134 | 135 | file_name: the file name of the template. 136 | 137 | """ 138 | locator = self._make_locator() 139 | 140 | path = locator.find_file(file_name, self.search_dirs) 141 | 142 | return self.read(path) 143 | 144 | def load_name(self, name): 145 | """ 146 | Find and return the template with the given template name. 147 | 148 | Arguments: 149 | 150 | name: the name of the template. 151 | 152 | """ 153 | locator = self._make_locator() 154 | 155 | path = locator.find_name(name, self.search_dirs) 156 | 157 | return self.read(path) 158 | 159 | # TODO: unit-test this method. 160 | def load_object(self, obj): 161 | """ 162 | Find and return the template associated to the given object. 163 | 164 | Arguments: 165 | 166 | obj: an instance of a user-defined class. 167 | 168 | search_dirs: the list of directories in which to search. 169 | 170 | """ 171 | locator = self._make_locator() 172 | 173 | path = locator.find_object(obj, self.search_dirs) 174 | 175 | return self.read(path) 176 | -------------------------------------------------------------------------------- /pystache/locator.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module provides a Locator class for finding template files. 5 | 6 | """ 7 | 8 | import os 9 | import re 10 | import sys 11 | 12 | from pystache import defaults 13 | from pystache.common import TemplateNotFoundError 14 | 15 | 16 | class Locator(object): 17 | def __init__(self, extension=None): 18 | """ 19 | Construct a template locator. 20 | 21 | Arguments: 22 | 23 | extension: the template file extension, without the leading dot. 24 | Pass False for no extension (e.g. to use extensionless template 25 | files). Defaults to the package default. 26 | 27 | """ 28 | if extension is None: 29 | extension = defaults.TEMPLATE_EXTENSION 30 | 31 | self.template_extension = extension 32 | 33 | def get_object_directory(self, obj): 34 | """ 35 | Return the directory containing an object's defining class. 36 | 37 | Returns None if there is no such directory, for example if the 38 | class was defined in an interactive Python session, or in a 39 | doctest that appears in a text file (rather than a Python file). 40 | 41 | """ 42 | if not hasattr(obj, '__module__'): 43 | return None 44 | 45 | module = sys.modules[obj.__module__] 46 | 47 | if not hasattr(module, '__file__'): 48 | # TODO: add a unit test for this case. 49 | return None 50 | 51 | path = module.__file__ 52 | 53 | return os.path.dirname(path) 54 | 55 | def make_template_name(self, obj): 56 | """ 57 | Return the canonical template name for an object instance. 58 | 59 | This method converts Python-style class names (PEP 8's recommended 60 | CamelCase, aka CapWords) to lower_case_with_underscords. Here 61 | is an example with code: 62 | 63 | >>> class HelloWorld(object): 64 | ... pass 65 | >>> hi = HelloWorld() 66 | >>> 67 | >>> locator = Locator() 68 | >>> locator.make_template_name(hi) 69 | 'hello_world' 70 | 71 | """ 72 | template_name = obj.__class__.__name__ 73 | 74 | def repl(match): 75 | return '_' + match.group(0).lower() 76 | 77 | return re.sub('[A-Z]', repl, template_name)[1:] 78 | 79 | def make_file_name(self, template_name, template_extension=None): 80 | """ 81 | Generate and return the file name for the given template name. 82 | 83 | Arguments: 84 | 85 | template_extension: defaults to the instance's extension. 86 | 87 | """ 88 | file_name = template_name 89 | 90 | if template_extension is None: 91 | template_extension = self.template_extension 92 | 93 | if template_extension is not False: 94 | file_name += os.path.extsep + template_extension 95 | 96 | return file_name 97 | 98 | def _find_path(self, search_dirs, file_name): 99 | """ 100 | Search for the given file, and return the path. 101 | 102 | Returns None if the file is not found. 103 | 104 | """ 105 | for dir_path in search_dirs: 106 | file_path = os.path.join(dir_path, file_name) 107 | if os.path.exists(file_path): 108 | return file_path 109 | 110 | return None 111 | 112 | def _find_path_required(self, search_dirs, file_name): 113 | """ 114 | Return the path to a template with the given file name. 115 | 116 | """ 117 | path = self._find_path(search_dirs, file_name) 118 | 119 | if path is None: 120 | raise TemplateNotFoundError( 121 | 'File %s not found in dirs: %s' % (repr(file_name), repr(search_dirs)) 122 | ) 123 | 124 | return path 125 | 126 | def find_file(self, file_name, search_dirs): 127 | """ 128 | Return the path to a template with the given file name. 129 | 130 | Arguments: 131 | 132 | file_name: the file name of the template. 133 | 134 | search_dirs: the list of directories in which to search. 135 | 136 | """ 137 | return self._find_path_required(search_dirs, file_name) 138 | 139 | def find_name(self, template_name, search_dirs): 140 | """ 141 | Return the path to a template with the given name. 142 | 143 | Arguments: 144 | 145 | template_name: the name of the template. 146 | 147 | search_dirs: the list of directories in which to search. 148 | 149 | """ 150 | file_name = self.make_file_name(template_name) 151 | 152 | return self._find_path_required(search_dirs, file_name) 153 | 154 | def find_object(self, obj, search_dirs, file_name=None): 155 | """ 156 | Return the path to a template associated with the given object. 157 | 158 | """ 159 | if file_name is None: 160 | # TODO: should we define a make_file_name() method? 161 | template_name = self.make_template_name(obj) 162 | file_name = self.make_file_name(template_name) 163 | 164 | dir_path = self.get_object_directory(obj) 165 | 166 | if dir_path is not None: 167 | search_dirs = [dir_path] + search_dirs 168 | 169 | path = self._find_path_required(search_dirs, file_name) 170 | 171 | return path 172 | -------------------------------------------------------------------------------- /pystache/parsed.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes a class that represents a parsed (or compiled) template. 5 | 6 | """ 7 | 8 | 9 | class ParsedTemplate(object): 10 | 11 | """ 12 | Represents a parsed or compiled template. 13 | 14 | An instance wraps a list of unicode strings and node objects. A node 15 | object must have a `render(engine, stack)` method that accepts a 16 | RenderEngine instance and a ContextStack instance and returns a unicode 17 | string. 18 | 19 | """ 20 | 21 | def __init__(self): 22 | self._parse_tree = [] 23 | 24 | def __repr__(self): 25 | return repr(self._parse_tree) 26 | 27 | def add(self, node): 28 | """ 29 | Arguments: 30 | 31 | node: a unicode string or node object instance. See the class 32 | docstring for information. 33 | 34 | """ 35 | self._parse_tree.append(node) 36 | 37 | def render(self, engine, context): 38 | """ 39 | Returns: a string of type unicode. 40 | 41 | """ 42 | 43 | # We avoid use of the ternary operator for Python 2.4 support. 44 | def get_unicode(node): 45 | if type(node) is str: 46 | return node 47 | return node.render(engine, context) 48 | 49 | parts = list(map(get_unicode, self._parse_tree)) 50 | s = ''.join(parts) 51 | 52 | return str(s) 53 | -------------------------------------------------------------------------------- /pystache/renderengine.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Defines a class responsible for rendering logic. 5 | 6 | """ 7 | 8 | from pystache.common import is_string 9 | from pystache.parser import parse 10 | 11 | 12 | def context_get(stack, name): 13 | """ 14 | Find and return a name from a ContextStack instance. 15 | 16 | """ 17 | return stack.get(name) 18 | 19 | 20 | class RenderEngine(object): 21 | 22 | """ 23 | Provides a render() method. 24 | 25 | This class is meant only for internal use. 26 | 27 | As a rule, the code in this class operates on unicode strings where 28 | possible rather than, say, strings of type str or markupsafe.Markup. 29 | This means that strings obtained from "external" sources like partials 30 | and variable tag values are immediately converted to unicode (or 31 | escaped and converted to unicode) before being operated on further. 32 | This makes maintaining, reasoning about, and testing the correctness 33 | of the code much simpler. In particular, it keeps the implementation 34 | of this class independent of the API details of one (or possibly more) 35 | unicode subclasses (e.g. markupsafe.Markup). 36 | 37 | """ 38 | 39 | # TODO: it would probably be better for the constructor to accept 40 | # and set as an attribute a single RenderResolver instance 41 | # that encapsulates the customizable aspects of converting 42 | # strings and resolving partials and names from context. 43 | def __init__( 44 | self, 45 | literal=None, 46 | escape=None, 47 | resolve_context=None, 48 | resolve_partial=None, 49 | to_str=None, 50 | ): 51 | """ 52 | Arguments: 53 | 54 | literal: the function used to convert unescaped variable tag 55 | values to unicode, e.g. the value corresponding to a tag 56 | "{{{name}}}". The function should accept a string of type 57 | str or unicode (or a subclass) and return a string of type 58 | unicode (but not a proper subclass of unicode). 59 | This class will only pass basestring instances to this 60 | function. For example, it will call str() on integer variable 61 | values prior to passing them to this function. 62 | 63 | escape: the function used to escape and convert variable tag 64 | values to unicode, e.g. the value corresponding to a tag 65 | "{{name}}". The function should obey the same properties 66 | described above for the "literal" function argument. 67 | This function should take care to convert any str 68 | arguments to unicode just as the literal function should, as 69 | this class will not pass tag values to literal prior to passing 70 | them to this function. This allows for more flexibility, 71 | for example using a custom escape function that handles 72 | incoming strings of type markupsafe.Markup differently 73 | from plain unicode strings. 74 | 75 | resolve_context: the function to call to resolve a name against 76 | a context stack. The function should accept two positional 77 | arguments: a ContextStack instance and a name to resolve. 78 | 79 | resolve_partial: the function to call when loading a partial. 80 | The function should accept a template name string and return a 81 | template string of type unicode (not a subclass). 82 | 83 | to_str: a function that accepts an object and returns a string (e.g. 84 | the built-in function str). This function is used for string 85 | coercion whenever a string is required (e.g. for converting None 86 | or 0 to a string). 87 | 88 | """ 89 | self.escape = escape 90 | self.literal = literal 91 | self.resolve_context = resolve_context 92 | self.resolve_partial = resolve_partial 93 | self.to_str = to_str 94 | 95 | # TODO: Rename context to stack throughout this module. 96 | 97 | # From the spec: 98 | # 99 | # When used as the data value for an Interpolation tag, the lambda 100 | # MUST be treatable as an arity 0 function, and invoked as such. 101 | # The returned value MUST be rendered against the default delimiters, 102 | # then interpolated in place of the lambda. 103 | # 104 | def fetch_string(self, context, name): 105 | """ 106 | Get a value from the given context as a basestring instance. 107 | 108 | """ 109 | val = self.resolve_context(context, name) 110 | 111 | if callable(val): 112 | # Return because _render_value() is already a string. 113 | return self._render_value(val(), context) 114 | 115 | if not is_string(val): 116 | return self.to_str(val) 117 | 118 | return val 119 | 120 | def fetch_section_data(self, context, name): 121 | """ 122 | Fetch the value of a section as a list. 123 | 124 | """ 125 | data = self.resolve_context(context, name) 126 | 127 | # From the spec: 128 | # 129 | # If the data is not of a list type, it is coerced into a list 130 | # as follows: if the data is truthy (e.g. `!!data == true`), 131 | # use a single-element list containing the data, otherwise use 132 | # an empty list. 133 | # 134 | if not data: 135 | data = [] 136 | else: 137 | # The least brittle way to determine whether something 138 | # supports iteration is by trying to call iter() on it: 139 | # 140 | # http://docs.python.org/library/functions.html#iter 141 | # 142 | # It is not sufficient, for example, to check whether the item 143 | # implements __iter__ () (the iteration protocol). There is 144 | # also __getitem__() (the sequence protocol). In Python 2, 145 | # strings do not implement __iter__(), but in Python 3 they do. 146 | try: 147 | iter(data) 148 | except TypeError: 149 | # Then the value does not support iteration. 150 | data = [data] 151 | else: 152 | if is_string(data) or isinstance(data, dict): 153 | # Do not treat strings and dicts (which are iterable) as lists. 154 | data = [data] 155 | # Otherwise, treat the value as a list. 156 | 157 | return data 158 | 159 | def _render_value(self, val, context, delimiters=None): 160 | """ 161 | Render an arbitrary value. 162 | 163 | """ 164 | if not is_string(val): 165 | # In case the template is an integer, for example. 166 | val = self.to_str(val) 167 | if type(val) is not str: 168 | val = self.literal(val) 169 | return self.render(val, context, delimiters) 170 | 171 | def render(self, template, context_stack, delimiters=None): 172 | """ 173 | Render a unicode template string, and return as unicode. 174 | 175 | Arguments: 176 | 177 | template: a template string of type unicode (but not a proper 178 | subclass of unicode). 179 | 180 | context_stack: a ContextStack instance. 181 | 182 | """ 183 | parsed_template = parse(template, delimiters) 184 | 185 | return parsed_template.render(self, context_stack) 186 | -------------------------------------------------------------------------------- /pystache/specloader.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | This module supports customized (aka special or specified) template loading. 5 | 6 | """ 7 | 8 | import os.path 9 | 10 | from pystache.loader import Loader 11 | 12 | 13 | # TODO: add test cases for this class. 14 | class SpecLoader(object): 15 | 16 | """ 17 | Supports loading custom-specified templates (from TemplateSpec instances). 18 | 19 | """ 20 | 21 | def __init__(self, loader=None): 22 | if loader is None: 23 | loader = Loader() 24 | 25 | self.loader = loader 26 | 27 | def _find_relative(self, spec): 28 | """ 29 | Return the path to the template as a relative (dir, file_name) pair. 30 | 31 | The directory returned is relative to the directory containing the 32 | class definition of the given object. The method returns None for 33 | this directory if the directory is unknown without first searching 34 | the search directories. 35 | 36 | """ 37 | if spec.template_rel_path is not None: 38 | return os.path.split(spec.template_rel_path) 39 | # Otherwise, determine the file name separately. 40 | 41 | locator = self.loader._make_locator() 42 | 43 | # We do not use the ternary operator for Python 2.4 support. 44 | if spec.template_name is not None: 45 | template_name = spec.template_name 46 | else: 47 | template_name = locator.make_template_name(spec) 48 | 49 | file_name = locator.make_file_name(template_name, spec.template_extension) 50 | 51 | return (spec.template_rel_directory, file_name) 52 | 53 | def _find(self, spec): 54 | """ 55 | Find and return the path to the template associated to the instance. 56 | 57 | """ 58 | if spec.template_path is not None: 59 | return spec.template_path 60 | 61 | dir_path, file_name = self._find_relative(spec) 62 | 63 | locator = self.loader._make_locator() 64 | 65 | if dir_path is None: 66 | # Then we need to search for the path. 67 | path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) 68 | else: 69 | obj_dir = locator.get_object_directory(spec) 70 | path = os.path.join(obj_dir, dir_path, file_name) 71 | 72 | return path 73 | 74 | def load(self, spec): 75 | """ 76 | Find and return the template associated to a TemplateSpec instance. 77 | 78 | Returns the template as a unicode string. 79 | 80 | Arguments: 81 | 82 | spec: a TemplateSpec instance. 83 | 84 | """ 85 | if spec.template is not None: 86 | return self.loader.str(spec.template, spec.template_encoding) 87 | 88 | path = self._find(spec) 89 | 90 | return self.loader.read(path, spec.template_encoding) 91 | -------------------------------------------------------------------------------- /pystache/template_spec.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Provides a class to customize template information on a per-view basis. 5 | 6 | To customize template properties for a particular view, create that view 7 | from a class that subclasses TemplateSpec. The "spec" in TemplateSpec 8 | stands for "special" or "specified" template information. 9 | 10 | """ 11 | 12 | 13 | class TemplateSpec(object): 14 | 15 | """ 16 | A mixin or interface for specifying custom template information. 17 | 18 | The "spec" in TemplateSpec can be taken to mean that the template 19 | information is either "specified" or "special." 20 | 21 | A view should subclass this class only if customized template loading 22 | is needed. The following attributes allow one to customize/override 23 | template information on a per view basis. A None value means to use 24 | default behavior for that value and perform no customization. All 25 | attributes are initialized to None. 26 | 27 | Attributes: 28 | 29 | template: the template as a string. 30 | 31 | template_encoding: the encoding used by the template. 32 | 33 | template_extension: the template file extension. Defaults to "mustache". 34 | Pass False for no extension (i.e. extensionless template files). 35 | 36 | template_name: the name of the template. 37 | 38 | template_path: absolute path to the template. 39 | 40 | template_rel_directory: the directory containing the template file, 41 | relative to the directory containing the module defining the class. 42 | 43 | template_rel_path: the path to the template file, relative to the 44 | directory containing the module defining the class. 45 | 46 | """ 47 | 48 | template = None 49 | template_encoding = None 50 | template_extension = None 51 | template_name = None 52 | template_path = None 53 | template_rel_directory = None 54 | template_rel_path = None 55 | -------------------------------------------------------------------------------- /pystache/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /pystache/tests/benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | A rudimentary backward- and forward-compatible script to benchmark pystache. 6 | 7 | Usage: 8 | 9 | tests/benchmark.py 10000 10 | 11 | """ 12 | 13 | import sys 14 | from timeit import Timer 15 | 16 | try: 17 | import chevron as pystache 18 | print('Using module: chevron') 19 | except (ImportError): 20 | import pystache 21 | print('Using module: pystache') 22 | 23 | import pystache 24 | 25 | # TODO: make the example realistic. 26 | 27 | examples = [ 28 | # Test case: 1 29 | ("""{{#person}}Hi {{name}}{{/person}}""", 30 | {"person": {"name": "Jon"}}, 31 | "Hi Jon"), 32 | 33 | # Test case: 2 34 | ("""\ 35 |
36 |

{{header}}

37 |
    38 | {{#comments}}
  • 39 |
    {{name}}

    {{body}}

    40 |
  • {{/comments}} 41 |
42 |
""", 43 | {'header': "My Post Comments", 44 | 'comments': [ 45 | {'name': "Joe", 'body': "Thanks for this post!"}, 46 | {'name': "Sam", 'body': "Thanks for this post!"}, 47 | {'name': "Heather", 'body': "Thanks for this post!"}, 48 | {'name': "Kathy", 'body': "Thanks for this post!"}, 49 | {'name': "George", 'body': "Thanks for this post!"}]}, 50 | """\ 51 |
52 |

My Post Comments

53 |
    54 |
  • 55 |
    Joe

    Thanks for this post!

    56 |
  • 57 |
    Sam

    Thanks for this post!

    58 |
  • 59 |
    Heather

    Thanks for this post!

    60 |
  • 61 |
    Kathy

    Thanks for this post!

    62 |
  • 63 |
    George

    Thanks for this post!

    64 |
  • 65 |
66 |
"""), 67 | ] 68 | 69 | 70 | def make_test_function(example): 71 | 72 | template, context, expected = example 73 | 74 | def test(): 75 | actual = pystache.render(template, context) 76 | if actual != expected: 77 | raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual)) 78 | 79 | return test 80 | 81 | 82 | def main(sys_argv): 83 | args = sys_argv[1:] 84 | count = int(args[0]) 85 | 86 | print("Benchmarking: %sx" % count) 87 | print() 88 | 89 | for example in examples: 90 | 91 | test = make_test_function(example) 92 | 93 | t = Timer(test,) 94 | print(min(t.repeat(repeat=3, number=count))) 95 | 96 | print("Done") 97 | 98 | 99 | if __name__ == '__main__': 100 | main(sys.argv) 101 | -------------------------------------------------------------------------------- /pystache/tests/common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Provides test-related code that can be used by all tests. 5 | 6 | """ 7 | 8 | import os 9 | 10 | import pystache 11 | from pystache import defaults 12 | from pystache.tests import examples 13 | 14 | # Save a reference to the original function to avoid recursion. 15 | _DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE 16 | _TESTS_DIR = os.path.dirname(pystache.tests.__file__) 17 | 18 | DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'. 19 | EXAMPLES_DIR = os.path.dirname(examples.__file__) 20 | PACKAGE_DIR = os.path.dirname(pystache.__file__) 21 | PROJECT_DIR = os.path.join(PACKAGE_DIR, '..') 22 | # TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files) 23 | # containing doctests. The paths should be relative to the project directory. 24 | TEXT_DOCTEST_PATHS = ['README.md'] 25 | 26 | UNITTEST_FILE_PREFIX = "test_" 27 | 28 | 29 | def get_spec_test_dir(project_dir): 30 | return os.path.join(project_dir, 'ext', 'spec', 'specs') 31 | 32 | 33 | def html_escape(u): 34 | """ 35 | An html escape function that behaves the same in both Python 2 and 3. 36 | 37 | This function is needed because single quotes are escaped in Python 3 38 | (to '''), but not in Python 2. 39 | 40 | The global defaults.TAG_ESCAPE can be set to this function in the 41 | setUp() and tearDown() of unittest test cases, for example, for 42 | consistent test results. 43 | 44 | """ 45 | u = _DEFAULT_TAG_ESCAPE(u) 46 | return u.replace("'", ''') 47 | 48 | 49 | def get_data_path(file_name=None): 50 | """Return the path to a file in the test data directory.""" 51 | if file_name is None: 52 | file_name = "" 53 | return os.path.join(DATA_DIR, file_name) 54 | 55 | 56 | # Functions related to get_module_names(). 57 | 58 | def _find_files(root_dir, should_include): 59 | """ 60 | Return a list of paths to all modules below the given directory. 61 | 62 | Arguments: 63 | 64 | should_include: a function that accepts a file path and returns True or False. 65 | 66 | """ 67 | paths = [] # Return value. 68 | 69 | is_module = lambda path: path.endswith(".py") 70 | 71 | # os.walk() is new in Python 2.3 72 | # http://docs.python.org/library/os.html#os.walk 73 | for dir_path, dir_names, file_names in os.walk(root_dir): 74 | new_paths = [os.path.join(dir_path, file_name) for file_name in file_names] 75 | new_paths = list(filter(is_module, new_paths)) 76 | new_paths = list(filter(should_include, new_paths)) 77 | paths.extend(new_paths) 78 | 79 | return paths 80 | 81 | 82 | def _make_module_names(package_dir, paths): 83 | """ 84 | Return a list of fully-qualified module names given a list of module paths. 85 | 86 | """ 87 | package_dir = os.path.abspath(package_dir) 88 | package_name = os.path.split(package_dir)[1] 89 | 90 | prefix_length = len(package_dir) 91 | 92 | module_names = [] 93 | for path in paths: 94 | path = os.path.abspath(path) # for example /subpackage/module.py 95 | rel_path = path[prefix_length:] # for example /subpackage/module.py 96 | rel_path = os.path.splitext(rel_path)[0] # for example /subpackage/module 97 | 98 | parts = [] 99 | while True: 100 | (rel_path, tail) = os.path.split(rel_path) 101 | if not tail: 102 | break 103 | parts.insert(0, tail) 104 | # We now have, for example, ['subpackage', 'module']. 105 | parts.insert(0, package_name) 106 | module = ".".join(parts) 107 | module_names.append(module) 108 | 109 | return module_names 110 | 111 | 112 | def get_module_names(package_dir=None, should_include=None): 113 | """ 114 | Return a list of fully-qualified module names in the given package. 115 | 116 | """ 117 | if package_dir is None: 118 | package_dir = PACKAGE_DIR 119 | 120 | if should_include is None: 121 | should_include = lambda path: True 122 | 123 | paths = _find_files(package_dir, should_include) 124 | names = _make_module_names(package_dir, paths) 125 | names.sort() 126 | 127 | return names 128 | 129 | 130 | class AssertStringMixin: 131 | 132 | """A unittest.TestCase mixin to check string equality.""" 133 | 134 | def assertString(self, actual, expected, format=None): 135 | """ 136 | Assert that the given strings are equal and have the same type. 137 | 138 | Arguments: 139 | 140 | format: a format string containing a single conversion specifier %s. 141 | Defaults to "%s". 142 | 143 | """ 144 | if format is None: 145 | format = "%s" 146 | 147 | # Show both friendly and literal versions. 148 | details = """String mismatch: %%s 149 | 150 | Expected: \"""%s\""" 151 | Actual: \"""%s\""" 152 | 153 | Expected: %s 154 | Actual: %s""" % (expected, actual, repr(expected), repr(actual)) 155 | 156 | def make_message(reason): 157 | description = details % reason 158 | return format % description 159 | 160 | self.assertEqual(actual, expected, make_message("different characters")) 161 | 162 | reason = "types different: %s != %s (actual)" % (repr(type(expected)), repr(type(actual))) 163 | self.assertEqual(type(expected), type(actual), make_message(reason)) 164 | 165 | 166 | class AssertIsMixin: 167 | 168 | """A unittest.TestCase mixin adding assertIs().""" 169 | 170 | # unittest.assertIs() is not available until Python 2.7: 171 | # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone 172 | def assertIs(self, first, second): 173 | self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) 174 | 175 | 176 | class AssertExceptionMixin: 177 | 178 | """A unittest.TestCase mixin adding assertException().""" 179 | 180 | # unittest.assertRaisesRegexp() is not available until Python 2.7: 181 | # http://docs.python.org/library/unittest.html#unittest.TestCase.assertRaisesRegexp 182 | def assertException(self, exception_type, msg, callable, *args, **kwds): 183 | try: 184 | callable(*args, **kwds) 185 | raise Exception("Expected exception: %s: %s" % (exception_type, repr(msg))) 186 | except exception_type as err: 187 | self.assertEqual(str(err), msg) 188 | 189 | 190 | class SetupDefaults(object): 191 | 192 | """ 193 | Mix this class in to a unittest.TestCase for standard defaults. 194 | 195 | This class allows for consistent test results across Python 2/3. 196 | 197 | """ 198 | 199 | def setup_defaults(self): 200 | self.original_decode_errors = defaults.DECODE_ERRORS 201 | self.original_file_encoding = defaults.FILE_ENCODING 202 | self.original_string_encoding = defaults.STRING_ENCODING 203 | 204 | defaults.DECODE_ERRORS = 'strict' 205 | defaults.FILE_ENCODING = 'ascii' 206 | defaults.STRING_ENCODING = 'ascii' 207 | 208 | def teardown_defaults(self): 209 | defaults.DECODE_ERRORS = self.original_decode_errors 210 | defaults.FILE_ENCODING = self.original_file_encoding 211 | defaults.STRING_ENCODING = self.original_string_encoding 212 | 213 | 214 | class Attachable(object): 215 | """ 216 | A class that attaches all constructor named parameters as attributes. 217 | 218 | For example-- 219 | 220 | >>> obj = Attachable(foo=42, size="of the universe") 221 | >>> repr(obj) 222 | "Attachable(foo=42, size='of the universe')" 223 | >>> obj.foo 224 | 42 225 | >>> obj.size 226 | 'of the universe' 227 | 228 | """ 229 | def __init__(self, **kwargs): 230 | self.__args__ = kwargs 231 | for arg, value in kwargs.items(): 232 | setattr(self, arg, value) 233 | 234 | def __repr__(self): 235 | return "%s(%s)" % (self.__class__.__name__, 236 | ", ".join("%s=%s" % (k, repr(v)) 237 | for k, v in self.__args__.items())) 238 | -------------------------------------------------------------------------------- /pystache/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /pystache/tests/data/ascii.mustache: -------------------------------------------------------------------------------- 1 | ascii: abc -------------------------------------------------------------------------------- /pystache/tests/data/duplicate.mustache: -------------------------------------------------------------------------------- 1 | This file is used to test locate_path()'s search order. -------------------------------------------------------------------------------- /pystache/tests/data/locator/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /pystache/tests/data/locator/duplicate.mustache: -------------------------------------------------------------------------------- 1 | This file is used to test locate_path()'s search order. -------------------------------------------------------------------------------- /pystache/tests/data/locator/template.txt: -------------------------------------------------------------------------------- 1 | Test template file 2 | -------------------------------------------------------------------------------- /pystache/tests/data/non_ascii.mustache: -------------------------------------------------------------------------------- 1 | non-ascii: é -------------------------------------------------------------------------------- /pystache/tests/data/sample_view.mustache: -------------------------------------------------------------------------------- 1 | ascii: abc -------------------------------------------------------------------------------- /pystache/tests/data/say_hello.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{to}} -------------------------------------------------------------------------------- /pystache/tests/data/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | TODO: add a docstring. 5 | 6 | """ 7 | 8 | from pystache import TemplateSpec 9 | 10 | 11 | class SayHello(object): 12 | 13 | def to(self): 14 | return "World" 15 | 16 | 17 | class SampleView(TemplateSpec): 18 | pass 19 | 20 | 21 | class NonAscii(TemplateSpec): 22 | pass 23 | -------------------------------------------------------------------------------- /pystache/tests/doctesting.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes a get_doctests() function for the project's test harness. 5 | 6 | """ 7 | 8 | import doctest 9 | import os 10 | import pkgutil 11 | import sys 12 | import traceback 13 | 14 | from pystache.tests.common import TEXT_DOCTEST_PATHS, get_module_names 15 | 16 | # This module is now a stub due to py 3.10 warnings - 18 Aug 2022 17 | # 18 | # This module follows the guidance documented here: 19 | # 20 | # http://docs.python.org/library/doctest.html#unittest-api 21 | # 22 | 23 | def get_doctests(text_file_dir): 24 | """ 25 | Return a list of TestSuite instances for all doctests in the project. 26 | 27 | Arguments: 28 | 29 | text_file_dir: the directory in which to search for all text files 30 | (i.e. non-module files) containing doctests. 31 | 32 | """ 33 | # Since module_relative is False in our calls to DocFileSuite below, 34 | # paths should be OS-specific. See the following for more info-- 35 | # 36 | # http://docs.python.org/library/doctest.html#doctest.DocFileSuite 37 | # 38 | # paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS] 39 | 40 | paths = [] 41 | suites = [] 42 | 43 | for path in paths: 44 | suite = doctest.DocFileSuite(path, module_relative=False) 45 | suites.append(suite) 46 | 47 | modules = get_module_names() 48 | for module in modules: 49 | suite = doctest.DocTestSuite(module) 50 | suites.append(suite) 51 | 52 | return suites 53 | -------------------------------------------------------------------------------- /pystache/tests/examples/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: add a docstring. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /pystache/tests/examples/comments.mustache: -------------------------------------------------------------------------------- 1 |

{{title}}{{! just something interesting... #or not... }}

-------------------------------------------------------------------------------- /pystache/tests/examples/comments.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class Comments(object): 8 | 9 | def title(self): 10 | return "A Comedy of Errors" 11 | -------------------------------------------------------------------------------- /pystache/tests/examples/complex.mustache: -------------------------------------------------------------------------------- 1 |

{{ header }}

2 | {{#list}} 3 |
    4 | {{#item}}{{# current }}
  • {{name}}
  • 5 | {{/ current }}{{#link}}
  • {{name}}
  • 6 | {{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}} -------------------------------------------------------------------------------- /pystache/tests/examples/complex.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class Complex(object): 8 | 9 | def header(self): 10 | return "Colors" 11 | 12 | def item(self): 13 | items = [] 14 | items.append({ 'name': 'red', 'current': True, 'url': '#Red' }) 15 | items.append({ 'name': 'green', 'link': True, 'url': '#Green' }) 16 | items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' }) 17 | return items 18 | 19 | def list(self): 20 | return not self.empty() 21 | 22 | def empty(self): 23 | return len(self.item()) == 0 24 | 25 | def empty_list(self): 26 | return []; 27 | -------------------------------------------------------------------------------- /pystache/tests/examples/delimiters.mustache: -------------------------------------------------------------------------------- 1 | {{=<% %>=}} 2 | * <% first %> 3 | <%=| |=%> 4 | * | second | 5 | |={{ }}=| 6 | * {{ third }} 7 | -------------------------------------------------------------------------------- /pystache/tests/examples/delimiters.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class Delimiters(object): 8 | 9 | def first(self): 10 | return "It worked the first time." 11 | 12 | def second(self): 13 | return "And it worked the second time." 14 | 15 | def third(self): 16 | return "Then, surprisingly, it worked the third time." 17 | -------------------------------------------------------------------------------- /pystache/tests/examples/double_section.mustache: -------------------------------------------------------------------------------- 1 | {{#t}}* first{{/t}} 2 | * {{two}} 3 | {{#t}}* third{{/t}} -------------------------------------------------------------------------------- /pystache/tests/examples/double_section.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class DoubleSection(object): 8 | 9 | def t(self): 10 | return True 11 | 12 | def two(self): 13 | return "second" 14 | -------------------------------------------------------------------------------- /pystache/tests/examples/escaped.mustache: -------------------------------------------------------------------------------- 1 |

{{title}}

-------------------------------------------------------------------------------- /pystache/tests/examples/escaped.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class Escaped(object): 8 | 9 | def title(self): 10 | return "Bear > Shark" 11 | -------------------------------------------------------------------------------- /pystache/tests/examples/extensionless: -------------------------------------------------------------------------------- 1 | No file extension: {{foo}} -------------------------------------------------------------------------------- /pystache/tests/examples/inner_partial.mustache: -------------------------------------------------------------------------------- 1 | Again, {{title}}! -------------------------------------------------------------------------------- /pystache/tests/examples/inner_partial.txt: -------------------------------------------------------------------------------- 1 | ## Again, {{title}}! ## -------------------------------------------------------------------------------- /pystache/tests/examples/inverted.mustache: -------------------------------------------------------------------------------- 1 | {{^f}}one{{/f}}, {{ two }}, {{^f}}three{{/f}}{{^t}}, four!{{/t}}{{^empty_list}}, empty list{{/empty_list}}{{^populated_list}}, shouldn't see me{{/populated_list}} -------------------------------------------------------------------------------- /pystache/tests/examples/inverted.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | class Inverted(object): 11 | 12 | def t(self): 13 | return True 14 | 15 | def f(self): 16 | return False 17 | 18 | def two(self): 19 | return 'two' 20 | 21 | def empty_list(self): 22 | return [] 23 | 24 | def populated_list(self): 25 | return ['some_value'] 26 | 27 | class InvertedLists(Inverted, TemplateSpec): 28 | template_name = 'inverted' 29 | 30 | def t(self): 31 | return [0, 1, 2] 32 | 33 | def f(self): 34 | return [] 35 | -------------------------------------------------------------------------------- /pystache/tests/examples/lambdas.mustache: -------------------------------------------------------------------------------- 1 | {{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}} -------------------------------------------------------------------------------- /pystache/tests/examples/lambdas.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | def rot(s, n=13): 11 | r = "" 12 | for c in s: 13 | cc = c 14 | if cc.isalpha(): 15 | cc = cc.lower() 16 | o = ord(cc) 17 | ro = (o+n) % 122 18 | if ro == 0: ro = 122 19 | if ro < 97: ro += 96 20 | cc = chr(ro) 21 | r = ''.join((r,cc)) 22 | return r 23 | 24 | def replace(subject, this='foo', with_this='bar'): 25 | return subject.replace(this, with_this) 26 | 27 | 28 | # This class subclasses TemplateSpec because at least one unit test 29 | # sets the template attribute. 30 | class Lambdas(TemplateSpec): 31 | 32 | def replace_foo_with_bar(self, text=None): 33 | return replace 34 | 35 | def rot13(self, text=None): 36 | return rot 37 | 38 | def sort(self, text=None): 39 | return lambda text: ''.join(sorted(text)) 40 | -------------------------------------------------------------------------------- /pystache/tests/examples/looping_partial.mustache: -------------------------------------------------------------------------------- 1 | Looping partial {{item}}! -------------------------------------------------------------------------------- /pystache/tests/examples/nested_context.mustache: -------------------------------------------------------------------------------- 1 | {{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}} -------------------------------------------------------------------------------- /pystache/tests/examples/nested_context.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | class NestedContext(TemplateSpec): 11 | 12 | def __init__(self, renderer): 13 | self.renderer = renderer 14 | 15 | def _context_get(self, key): 16 | return self.renderer.context.get(key) 17 | 18 | def outer_thing(self): 19 | return "two" 20 | 21 | def foo(self): 22 | return {'thing1': 'one', 'thing2': 'foo'} 23 | 24 | def derp(self): 25 | return [{'inner': 'car'}] 26 | 27 | def herp(self): 28 | return [{'outer': 'car'}] 29 | 30 | def nested_context_in_view(self): 31 | if self._context_get('outer') == self._context_get('inner'): 32 | return 'it works!' 33 | return '' 34 | -------------------------------------------------------------------------------- /pystache/tests/examples/partial_in_partial.mustache: -------------------------------------------------------------------------------- 1 | {{>simple}} -------------------------------------------------------------------------------- /pystache/tests/examples/partial_with_lambda.mustache: -------------------------------------------------------------------------------- 1 | {{#rot13}}abcdefghijklm{{/rot13}} -------------------------------------------------------------------------------- /pystache/tests/examples/partial_with_partial_and_lambda.mustache: -------------------------------------------------------------------------------- 1 | {{>partial_with_lambda}}{{#rot13}}abcdefghijklm{{/rot13}} -------------------------------------------------------------------------------- /pystache/tests/examples/partials_with_lambdas.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache.tests.examples.lambdas import rot 8 | 9 | 10 | class PartialsWithLambdas(object): 11 | 12 | def rot(self): 13 | return rot 14 | -------------------------------------------------------------------------------- /pystache/tests/examples/readme.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class SayHello(object): 8 | def to(self): 9 | return "Pizza" 10 | -------------------------------------------------------------------------------- /pystache/tests/examples/say_hello.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{to}}! -------------------------------------------------------------------------------- /pystache/tests/examples/simple.mustache: -------------------------------------------------------------------------------- 1 | Hi {{thing}}!{{blank}} -------------------------------------------------------------------------------- /pystache/tests/examples/simple.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | class Simple(TemplateSpec): 11 | 12 | def thing(self): 13 | return "pizza" 14 | 15 | def blank(self): 16 | return '' 17 | -------------------------------------------------------------------------------- /pystache/tests/examples/tagless.mustache: -------------------------------------------------------------------------------- 1 | No tags... -------------------------------------------------------------------------------- /pystache/tests/examples/template_partial.mustache: -------------------------------------------------------------------------------- 1 |

{{title}}

2 | {{>inner_partial}} -------------------------------------------------------------------------------- /pystache/tests/examples/template_partial.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | class TemplatePartial(TemplateSpec): 11 | 12 | def __init__(self, renderer): 13 | self.renderer = renderer 14 | 15 | def _context_get(self, key): 16 | return self.renderer.context.get(key) 17 | 18 | def title(self): 19 | return "Welcome" 20 | 21 | def title_bars(self): 22 | return '-' * len(self.title()) 23 | 24 | def looping(self): 25 | return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] 26 | 27 | def thing(self): 28 | return self._context_get('prop') 29 | -------------------------------------------------------------------------------- /pystache/tests/examples/template_partial.txt: -------------------------------------------------------------------------------- 1 | {{title}} 2 | {{title_bars}} 3 | 4 | {{>inner_partial}} 5 | -------------------------------------------------------------------------------- /pystache/tests/examples/unescaped.mustache: -------------------------------------------------------------------------------- 1 |

{{{title}}}

-------------------------------------------------------------------------------- /pystache/tests/examples/unescaped.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | class Unescaped(object): 8 | 9 | def title(self): 10 | return "Bear > Shark" 11 | -------------------------------------------------------------------------------- /pystache/tests/examples/unicode_input.mustache: -------------------------------------------------------------------------------- 1 | abcdé -------------------------------------------------------------------------------- /pystache/tests/examples/unicode_input.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | TODO: add a docstring. 4 | 5 | """ 6 | 7 | from pystache import TemplateSpec 8 | 9 | 10 | class UnicodeInput(TemplateSpec): 11 | 12 | template_encoding = 'utf8' 13 | 14 | def age(self): 15 | return 156 16 | -------------------------------------------------------------------------------- /pystache/tests/examples/unicode_output.mustache: -------------------------------------------------------------------------------- 1 |

Name: {{name}}

-------------------------------------------------------------------------------- /pystache/tests/examples/unicode_output.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | TODO: add a docstring. 5 | 6 | """ 7 | 8 | class UnicodeOutput(object): 9 | 10 | def name(self): 11 | return 'Henri Poincaré' 12 | -------------------------------------------------------------------------------- /pystache/tests/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes a main() function that runs all tests in the project. 5 | 6 | This module is for our test console script. 7 | 8 | """ 9 | 10 | import os 11 | import sys 12 | import unittest 13 | from unittest import TestCase, TestProgram 14 | 15 | import pystache 16 | from pystache.tests.common import ( 17 | PACKAGE_DIR, 18 | PROJECT_DIR, 19 | UNITTEST_FILE_PREFIX, 20 | get_module_names, 21 | get_spec_test_dir, 22 | ) 23 | from pystache.tests.doctesting import get_doctests 24 | from pystache.tests.spectesting import get_spec_tests 25 | 26 | # If this command option is present, then the spec test and doctest directories 27 | # will be inserted if not provided. 28 | FROM_SOURCE_OPTION = "--from-source" 29 | 30 | 31 | def make_extra_tests(text_doctest_dir, spec_test_dir): 32 | tests = [] 33 | 34 | if text_doctest_dir is not None: 35 | doctest_suites = get_doctests(text_doctest_dir) 36 | tests.extend(doctest_suites) 37 | 38 | if spec_test_dir is not None: 39 | spec_testcases = get_spec_tests(spec_test_dir) 40 | tests.extend(spec_testcases) 41 | 42 | return unittest.TestSuite(tests) 43 | 44 | 45 | def make_test_program_class(extra_tests): 46 | """ 47 | Return a subclass of unittest.TestProgram. 48 | 49 | """ 50 | # The function unittest.main() is an alias for unittest.TestProgram's 51 | # constructor. TestProgram's constructor does the following: 52 | # 53 | # 1. calls self.parseArgs(argv), 54 | # 2. which in turn calls self.createTests(). 55 | # 3. then the constructor calls self.runTests(). 56 | # 57 | # The createTests() method sets the self.test attribute by calling one 58 | # of self.testLoader's "loadTests" methods. Each loadTest method returns 59 | # a unittest.TestSuite instance. Thus, self.test is set to a TestSuite 60 | # instance prior to calling runTests(). 61 | class PystacheTestProgram(TestProgram): 62 | 63 | """ 64 | Instantiating an instance of this class runs all tests. 65 | 66 | """ 67 | 68 | def createTests(self): 69 | """ 70 | Load tests and set self.test to a unittest.TestSuite instance 71 | 72 | Compare-- 73 | 74 | http://docs.python.org/library/unittest.html#unittest.TestSuite 75 | 76 | """ 77 | super(PystacheTestProgram, self).createTests() 78 | self.test.addTests(extra_tests) 79 | 80 | return PystacheTestProgram 81 | 82 | 83 | # Do not include "test" in this function's name to avoid it getting 84 | # picked up by nosetests. 85 | def main(sys_argv): 86 | """ 87 | Run all tests in the project. 88 | 89 | Arguments: 90 | 91 | sys_argv: a reference to sys.argv. 92 | 93 | """ 94 | # TODO: use logging module 95 | print("pystache: running tests: argv: %s" % repr(sys_argv)) 96 | 97 | should_source_exist = False 98 | spec_test_dir = None 99 | project_dir = None 100 | 101 | if len(sys_argv) > 1 and sys_argv[1] == FROM_SOURCE_OPTION: 102 | # This usually means the test_pystache.py convenience script 103 | # in the source directory was run. 104 | should_source_exist = True 105 | sys_argv.pop(1) 106 | 107 | try: 108 | # TODO: use optparse command options instead. 109 | project_dir = sys_argv[1] 110 | sys_argv.pop(1) 111 | except IndexError: 112 | if should_source_exist: 113 | project_dir = PROJECT_DIR 114 | 115 | try: 116 | # TODO: use optparse command options instead. 117 | spec_test_dir = sys_argv[1] 118 | sys_argv.pop(1) 119 | except IndexError: 120 | if project_dir is not None: 121 | # Then auto-detect the spec test directory. 122 | _spec_test_dir = get_spec_test_dir(project_dir) 123 | if not os.path.exists(_spec_test_dir): 124 | # Then the user is probably using a downloaded sdist rather 125 | # than a repository clone (since the sdist does not include 126 | # the spec test directory). 127 | print("pystache: skipping spec tests: spec test directory " 128 | "not found") 129 | else: 130 | spec_test_dir = _spec_test_dir 131 | 132 | if len(sys_argv) <= 1 or sys_argv[-1].startswith("-"): 133 | # Then no explicit module or test names were provided, so 134 | # auto-detect all unit tests. 135 | module_names = _discover_test_modules(PACKAGE_DIR) 136 | sys_argv.extend(module_names) 137 | if project_dir is not None: 138 | # Add the current module for unit tests contained here 139 | sys_argv.append(__name__) 140 | 141 | 142 | extra_tests = make_extra_tests(project_dir, spec_test_dir) 143 | test_program_class = make_test_program_class(extra_tests) 144 | 145 | # We pass None for the module because we do not want the unittest 146 | # module to resolve module names relative to a given module. 147 | # (This would require importing all of the unittest modules from 148 | # this module.) See the loadTestsFromName() method of the 149 | # unittest.TestLoader class for more details on this parameter. 150 | test_program_class(argv=sys_argv, module=None) 151 | # No need to return since unitttest.main() exits. 152 | 153 | 154 | def _discover_test_modules(package_dir): 155 | """ 156 | Discover and return a sorted list of the names of unit-test modules. 157 | 158 | """ 159 | def is_unittest_module(path): 160 | file_name = os.path.basename(path) 161 | return file_name.startswith(UNITTEST_FILE_PREFIX) 162 | 163 | names = get_module_names(package_dir=package_dir, should_include=is_unittest_module) 164 | 165 | # This is a sanity check to ensure that the unit-test discovery 166 | # methods are working. 167 | if len(names) < 1: 168 | raise Exception("No unit-test modules found--\n in %s" % package_dir) 169 | 170 | return names 171 | -------------------------------------------------------------------------------- /pystache/tests/spectesting.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Exposes a get_spec_tests() function for the project's test harness. 5 | 6 | Creates a unittest.TestCase for the tests defined in the mustache spec. 7 | 8 | """ 9 | 10 | # TODO: this module can be cleaned up somewhat. 11 | # TODO: move all of this code to pystache/tests/spectesting.py and 12 | # have it expose a get_spec_tests(spec_test_dir) function. 13 | 14 | FILE_ENCODING = 'utf-8' # the encoding of the spec test files. 15 | 16 | yaml = None 17 | 18 | try: 19 | # We try yaml first since it is more convenient when adding and modifying 20 | # test cases by hand (since the YAML is human-readable and is the master 21 | # from which the JSON format is generated). 22 | import yaml 23 | except ImportError: 24 | try: 25 | import json 26 | except: 27 | # The module json is not available prior to Python 2.6, whereas 28 | # simplejson is. The simplejson package dropped support for Python 2.4 29 | # in simplejson v2.1.0, so Python 2.4 requires a simplejson install 30 | # older than the most recent version. 31 | try: 32 | import simplejson as json 33 | except ImportError: 34 | # Raise an error with a type different from ImportError as a hack around 35 | # this issue: 36 | # http://bugs.python.org/issue7559 37 | from sys import exc_info 38 | ex_type, ex_value, tb = exc_info() 39 | new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) 40 | raise new_ex.__class__(new_ex).with_traceback(tb) 41 | file_extension = 'json' 42 | parser = json 43 | else: 44 | file_extension = 'yml' 45 | parser = yaml 46 | 47 | 48 | import ast 49 | import codecs 50 | import glob 51 | import os.path 52 | import unittest 53 | 54 | import pystache 55 | from pystache import common 56 | from pystache.renderer import Renderer 57 | from pystache.tests.common import AssertStringMixin 58 | 59 | 60 | def get_spec_tests(spec_test_dir): 61 | """ 62 | Return a list of unittest.TestCase instances. 63 | 64 | """ 65 | # TODO: use logging module instead. 66 | print("pystache: spec tests: using %s" % _get_parser_info()) 67 | 68 | cases = [] 69 | 70 | # Make this absolute for easier diagnosis in case of error. 71 | spec_test_dir = os.path.abspath(spec_test_dir) 72 | spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension)) 73 | 74 | for path in spec_paths: 75 | new_cases = _read_spec_tests(path) 76 | cases.extend(new_cases) 77 | 78 | # Store this as a value so that CheckSpecTestsFound is not checking 79 | # a reference to cases that contains itself. 80 | spec_test_count = len(cases) 81 | 82 | # This test case lets us alert the user that spec tests are missing. 83 | class CheckSpecTestsFound(unittest.TestCase): 84 | 85 | def runTest(self): 86 | if spec_test_count > 0: 87 | return 88 | raise Exception("Spec tests not found--\n in %s\n" 89 | " Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir)) 90 | 91 | case = CheckSpecTestsFound() 92 | cases.append(case) 93 | 94 | return cases 95 | 96 | 97 | def _get_parser_info(): 98 | return "%s (version %s)" % (parser.__name__, parser.__version__) 99 | 100 | 101 | def _read_spec_tests(path): 102 | """ 103 | Return a list of unittest.TestCase instances. 104 | 105 | """ 106 | b = common.read(path) 107 | u = str(b, encoding=FILE_ENCODING) 108 | spec_data = parse(u) 109 | tests = spec_data['tests'] 110 | 111 | cases = [] 112 | for data in tests: 113 | case = _deserialize_spec_test(data, path) 114 | cases.append(case) 115 | 116 | return cases 117 | 118 | 119 | # TODO: simplify the implementation of this function. 120 | def _convert_children(node): 121 | """ 122 | Recursively convert to functions all "code strings" below the node. 123 | 124 | This function is needed only for the json format. 125 | 126 | """ 127 | if not isinstance(node, (list, dict)): 128 | # Then there is nothing to iterate over and recurse. 129 | return 130 | 131 | if isinstance(node, list): 132 | for child in node: 133 | _convert_children(child) 134 | return 135 | # Otherwise, node is a dict, so attempt the conversion. 136 | 137 | for key in list(node.keys()): 138 | val = node[key] 139 | 140 | if not isinstance(val, dict) or val.get('__tag__') != 'code': 141 | _convert_children(val) 142 | continue 143 | # Otherwise, we are at a "leaf" node. 144 | 145 | val = ast.literal_eval(val['python']) 146 | node[key] = val 147 | continue 148 | 149 | 150 | def _deserialize_spec_test(data, file_path): 151 | """ 152 | Return a unittest.TestCase instance representing a spec test. 153 | 154 | Arguments: 155 | 156 | data: the dictionary of attributes for a single test. 157 | 158 | """ 159 | context = data['data'] 160 | description = data['desc'] 161 | # PyYAML seems to leave ASCII strings as byte strings. 162 | expected = str(data['expected']) 163 | # TODO: switch to using dict.get(). 164 | partials = 'partials' in data and data['partials'] or {} 165 | template = data['template'] 166 | test_name = data['name'] 167 | 168 | _convert_children(context) 169 | 170 | test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path) 171 | 172 | return test_case 173 | 174 | 175 | def _make_spec_test(expected, template, context, partials, description, test_name, file_path): 176 | """ 177 | Return a unittest.TestCase instance representing a spec test. 178 | 179 | """ 180 | file_name = os.path.basename(file_path) 181 | test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name)) 182 | 183 | # We subclass SpecTestBase in order to control the test method name (for 184 | # the purposes of improved reporting). 185 | class SpecTest(SpecTestBase): 186 | pass 187 | 188 | def run_test(self): 189 | self._runTest() 190 | 191 | # TODO: should we restore this logic somewhere? 192 | # If we don't convert unicode to str, we get the following error: 193 | # "TypeError: __name__ must be set to a string object" 194 | # test.__name__ = str(name) 195 | setattr(SpecTest, test_method_name, run_test) 196 | case = SpecTest(test_method_name) 197 | 198 | case._context = context 199 | case._description = description 200 | case._expected = expected 201 | case._file_path = file_path 202 | case._partials = partials 203 | case._template = template 204 | case._test_name = test_name 205 | 206 | return case 207 | 208 | 209 | def parse(u): 210 | """ 211 | Parse the contents of a spec test file, and return a dict. 212 | 213 | Arguments: 214 | 215 | u: a unicode string. 216 | 217 | """ 218 | # TODO: find a cleaner mechanism for choosing between the two. 219 | if yaml is None: 220 | # Then use json. 221 | 222 | # The only way to get the simplejson module to return unicode strings 223 | # is to pass it unicode. See, for example-- 224 | # 225 | # http://code.google.com/p/simplejson/issues/detail?id=40 226 | # 227 | # and the documentation of simplejson.loads(): 228 | # 229 | # "If s is a str then decoded JSON strings that contain only ASCII 230 | # characters may be parsed as str for performance and memory reasons. 231 | # If your code expects only unicode the appropriate solution is 232 | # decode s to unicode prior to calling loads." 233 | # 234 | return json.loads(u) 235 | # Otherwise, yaml. 236 | 237 | def code_constructor(loader, node): 238 | value = loader.construct_mapping(node) 239 | # ast.literal_eval will not work here => lambda expression 240 | # plus this a test, and spec tests are optional => #nosec ? 241 | return eval(value['python'], {}) 242 | 243 | yaml.add_constructor('!code', code_constructor) 244 | return yaml.full_load(u) 245 | 246 | 247 | class SpecTestBase(unittest.TestCase, AssertStringMixin): 248 | 249 | def _runTest(self): 250 | context = self._context 251 | description = self._description 252 | expected = self._expected 253 | file_path = self._file_path 254 | partials = self._partials 255 | template = self._template 256 | test_name = self._test_name 257 | 258 | renderer = Renderer(partials=partials) 259 | actual = renderer.render(template, context) 260 | 261 | # We need to escape the strings that occur in our format string because 262 | # they can contain % symbols, for example (in delimiters.yml)-- 263 | # 264 | # "template: '{{=<% %>=}}(<%text%>)'" 265 | # 266 | def escape(s): 267 | return s.replace("%", "%%") 268 | 269 | parser_info = _get_parser_info() 270 | subs = [repr(test_name), description, os.path.abspath(file_path), 271 | template, repr(context), parser_info] 272 | subs = tuple([escape(sub) for sub in subs]) 273 | # We include the parsing module version info to help with troubleshooting 274 | # yaml/json/simplejson issues. 275 | message = """%s: %s 276 | 277 | File: %s 278 | 279 | Template: \"""%s\""" 280 | 281 | Context: %s 282 | 283 | %%s 284 | 285 | [using %s] 286 | """ % subs 287 | 288 | self.assertString(actual, expected, format=message) 289 | -------------------------------------------------------------------------------- /pystache/tests/test___init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Tests of __init__.py. 5 | 6 | """ 7 | 8 | # Calling "import *" is allowed only at the module level. 9 | GLOBALS_INITIAL = list(globals().keys()) 10 | from pystache import * 11 | 12 | GLOBALS_PYSTACHE_IMPORTED = list(globals().keys()) 13 | 14 | import unittest 15 | 16 | import pystache 17 | 18 | 19 | class InitTests(unittest.TestCase): 20 | 21 | def test___all__(self): 22 | """ 23 | Test that "from pystache import ``*``" works as expected. 24 | 25 | """ 26 | actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL) 27 | expected = set(['parse', 'render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL']) 28 | 29 | self.assertEqual(actual, expected) 30 | 31 | def test_version_defined(self): 32 | """ 33 | Test that pystache.__version__ is set. 34 | 35 | """ 36 | actual_version = pystache.__version__ 37 | self.assertTrue(actual_version) 38 | -------------------------------------------------------------------------------- /pystache/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Unit tests of commands.py. 5 | 6 | """ 7 | 8 | import sys 9 | import unittest 10 | 11 | from pystache.commands.render import main 12 | 13 | ORIGINAL_STDOUT = sys.stdout 14 | 15 | 16 | class MockStdout(object): 17 | 18 | def __init__(self): 19 | self.output = "" 20 | 21 | def write(self, str): 22 | self.output += str 23 | 24 | 25 | class CommandsTestCase(unittest.TestCase): 26 | 27 | def setUp(self): 28 | sys.stdout = MockStdout() 29 | 30 | def callScript(self, template, context): 31 | argv = ['pystache', template, context] 32 | main(argv) 33 | return sys.stdout.output 34 | 35 | def testMainSimple(self): 36 | """ 37 | Test a simple command-line case. 38 | 39 | """ 40 | actual = self.callScript("Hi {{thing}}", '{"thing": "world"}') 41 | self.assertEqual(actual, "Hi world\n") 42 | 43 | def tearDown(self): 44 | sys.stdout = ORIGINAL_STDOUT 45 | -------------------------------------------------------------------------------- /pystache/tests/test_defaults.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Unit tests for defaults.py. 5 | 6 | """ 7 | 8 | import unittest 9 | 10 | import pystache 11 | from pystache.tests.common import AssertStringMixin 12 | 13 | 14 | # TODO: make sure each default has at least one test. 15 | class DefaultsConfigurableTestCase(unittest.TestCase, AssertStringMixin): 16 | 17 | """Tests that the user can change the defaults at runtime.""" 18 | 19 | # TODO: switch to using a context manager after 2.4 is deprecated. 20 | def setUp(self): 21 | """Save the defaults.""" 22 | defaults = [ 23 | 'DECODE_ERRORS', 'DELIMITERS', 24 | 'FILE_ENCODING', 'MISSING_TAGS', 25 | 'SEARCH_DIRS', 'STRING_ENCODING', 26 | 'TAG_ESCAPE', 'TEMPLATE_EXTENSION' 27 | ] 28 | self.saved = {} 29 | for e in defaults: 30 | self.saved[e] = getattr(pystache.defaults, e) 31 | 32 | def tearDown(self): 33 | for key, value in list(self.saved.items()): 34 | setattr(pystache.defaults, key, value) 35 | 36 | def test_tag_escape(self): 37 | """Test that changes to defaults.TAG_ESCAPE take effect.""" 38 | template = "{{foo}}" 39 | context = {'foo': '<'} 40 | actual = pystache.render(template, context) 41 | self.assertString(actual, "<") 42 | 43 | pystache.defaults.TAG_ESCAPE = lambda u: u 44 | actual = pystache.render(template, context) 45 | self.assertString(actual, "<") 46 | 47 | def test_delimiters(self): 48 | """Test that changes to defaults.DELIMITERS take effect.""" 49 | template = "[[foo]]{{foo}}" 50 | context = {'foo': 'FOO'} 51 | actual = pystache.render(template, context) 52 | self.assertString(actual, "[[foo]]FOO") 53 | 54 | pystache.defaults.DELIMITERS = ('[[', ']]') 55 | actual = pystache.render(template, context) 56 | self.assertString(actual, "FOO{{foo}}") 57 | 58 | def test_missing_tags(self): 59 | """Test that changes to defaults.MISSING_TAGS take effect.""" 60 | template = "{{foo}}" 61 | context = {} 62 | actual = pystache.render(template, context) 63 | self.assertString(actual, "") 64 | 65 | pystache.defaults.MISSING_TAGS = 'strict' 66 | self.assertRaises(pystache.context.KeyNotFoundError, 67 | pystache.render, template, context) 68 | -------------------------------------------------------------------------------- /pystache/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | TODO: add a docstring. 5 | 6 | """ 7 | 8 | import unittest 9 | 10 | from pystache import Renderer 11 | from pystache.tests.common import EXAMPLES_DIR, AssertStringMixin 12 | 13 | from .examples.comments import Comments 14 | from .examples.delimiters import Delimiters 15 | from .examples.double_section import DoubleSection 16 | from .examples.escaped import Escaped 17 | from .examples.nested_context import NestedContext 18 | from .examples.template_partial import TemplatePartial 19 | from .examples.unescaped import Unescaped 20 | from .examples.unicode_input import UnicodeInput 21 | from .examples.unicode_output import UnicodeOutput 22 | 23 | 24 | class TestView(unittest.TestCase, AssertStringMixin): 25 | 26 | def _assert(self, obj, expected): 27 | renderer = Renderer() 28 | actual = renderer.render(obj) 29 | self.assertString(actual, expected) 30 | 31 | def test_comments(self): 32 | self._assert(Comments(), "

A Comedy of Errors

") 33 | 34 | def test_double_section(self): 35 | self._assert(DoubleSection(), "* first\n* second\n* third") 36 | 37 | def test_unicode_output(self): 38 | renderer = Renderer() 39 | actual = renderer.render(UnicodeOutput()) 40 | self.assertString(actual, '

Name: Henri Poincaré

') 41 | 42 | def test_unicode_input(self): 43 | renderer = Renderer() 44 | actual = renderer.render(UnicodeInput()) 45 | self.assertString(actual, 'abcdé') 46 | 47 | def test_escaping(self): 48 | self._assert(Escaped(), "

Bear > Shark

") 49 | 50 | def test_literal(self): 51 | renderer = Renderer() 52 | actual = renderer.render(Unescaped()) 53 | self.assertString(actual, "

Bear > Shark

") 54 | 55 | def test_template_partial(self): 56 | renderer = Renderer(search_dirs=EXAMPLES_DIR) 57 | actual = renderer.render(TemplatePartial(renderer=renderer)) 58 | 59 | self.assertString(actual, """

Welcome

60 | Again, Welcome!""") 61 | 62 | def test_template_partial_extension(self): 63 | renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') 64 | 65 | view = TemplatePartial(renderer=renderer) 66 | 67 | actual = renderer.render(view) 68 | self.assertString(actual, """Welcome 69 | ------- 70 | 71 | ## Again, Welcome! ##""") 72 | 73 | def test_delimiters(self): 74 | renderer = Renderer() 75 | actual = renderer.render(Delimiters()) 76 | self.assertString(actual, """\ 77 | * It worked the first time. 78 | * And it worked the second time. 79 | * Then, surprisingly, it worked the third time. 80 | """) 81 | 82 | def test_nested_context(self): 83 | renderer = Renderer() 84 | actual = renderer.render(NestedContext(renderer)) 85 | self.assertString(actual, "one and foo and two") 86 | 87 | def test_nested_context_is_available_in_view(self): 88 | renderer = Renderer() 89 | 90 | view = NestedContext(renderer) 91 | view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}' 92 | 93 | actual = renderer.render(view) 94 | self.assertString(actual, 'it works!') 95 | 96 | def test_partial_in_partial_has_access_to_grand_parent_context(self): 97 | renderer = Renderer(search_dirs=EXAMPLES_DIR) 98 | 99 | view = TemplatePartial(renderer=renderer) 100 | view.template = '''{{>partial_in_partial}}''' 101 | 102 | actual = renderer.render(view, {'prop': 'derp'}) 103 | self.assertEqual(actual, 'Hi derp!') 104 | 105 | if __name__ == '__main__': 106 | unittest.main() 107 | -------------------------------------------------------------------------------- /pystache/tests/test_loader.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | Unit tests of loader.py. 5 | 6 | """ 7 | 8 | import os 9 | import sys 10 | import unittest 11 | 12 | from pystache import defaults 13 | from pystache.loader import Loader 14 | from pystache.tests.common import ( 15 | DATA_DIR, 16 | AssertStringMixin, 17 | SetupDefaults, 18 | ) 19 | 20 | # We use the same directory as the locator tests for now. 21 | LOADER_DATA_DIR = os.path.join(DATA_DIR, 'locator') 22 | 23 | 24 | class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): 25 | 26 | def setUp(self): 27 | self.setup_defaults() 28 | 29 | def tearDown(self): 30 | self.teardown_defaults() 31 | 32 | def test_init__extension(self): 33 | loader = Loader(extension='foo') 34 | self.assertEqual(loader.extension, 'foo') 35 | 36 | def test_init__extension__default(self): 37 | # Test the default value. 38 | loader = Loader() 39 | self.assertEqual(loader.extension, 'mustache') 40 | 41 | def test_init__file_encoding(self): 42 | loader = Loader(file_encoding='bar') 43 | self.assertEqual(loader.file_encoding, 'bar') 44 | 45 | def test_init__file_encoding__default(self): 46 | file_encoding = defaults.FILE_ENCODING 47 | try: 48 | defaults.FILE_ENCODING = 'foo' 49 | loader = Loader() 50 | self.assertEqual(loader.file_encoding, 'foo') 51 | finally: 52 | defaults.FILE_ENCODING = file_encoding 53 | 54 | def test_init__to_unicode(self): 55 | to_unicode = lambda x: x 56 | loader = Loader(to_unicode=to_unicode) 57 | self.assertEqual(loader.to_unicode, to_unicode) 58 | 59 | def test_init__to_unicode__default(self): 60 | loader = Loader() 61 | self.assertRaises(TypeError, loader.to_unicode, "abc") 62 | 63 | decode_errors = defaults.DECODE_ERRORS 64 | string_encoding = defaults.STRING_ENCODING 65 | 66 | nonascii = 'abcdé'.encode('utf-8') 67 | 68 | loader = Loader() 69 | self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii) 70 | 71 | defaults.DECODE_ERRORS = 'ignore' 72 | loader = Loader() 73 | self.assertString(loader.to_unicode(nonascii), 'abcd') 74 | 75 | defaults.STRING_ENCODING = 'utf-8' 76 | loader = Loader() 77 | self.assertString(loader.to_unicode(nonascii), 'abcdé') 78 | 79 | 80 | def _get_path(self, filename): 81 | return os.path.join(DATA_DIR, filename) 82 | 83 | def test_unicode__basic__input_str(self): 84 | """ 85 | Test unicode(): default arguments with str input. 86 | 87 | """ 88 | loader = Loader() 89 | actual = loader.str("foo") 90 | 91 | self.assertString(actual, "foo") 92 | 93 | def test_unicode__basic__input_unicode(self): 94 | """ 95 | Test unicode(): default arguments with unicode input. 96 | 97 | """ 98 | loader = Loader() 99 | actual = loader.str("foo") 100 | 101 | self.assertString(actual, "foo") 102 | 103 | def test_unicode__basic__input_unicode_subclass(self): 104 | """ 105 | Test unicode(): default arguments with unicode-subclass input. 106 | 107 | """ 108 | class UnicodeSubclass(str): 109 | pass 110 | 111 | s = UnicodeSubclass("foo") 112 | 113 | loader = Loader() 114 | actual = loader.str(s) 115 | 116 | self.assertString(actual, "foo") 117 | 118 | def test_unicode__to_unicode__attribute(self): 119 | """ 120 | Test unicode(): encoding attribute. 121 | 122 | """ 123 | loader = Loader() 124 | 125 | non_ascii = 'abcdé'.encode('utf-8') 126 | self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) 127 | 128 | def to_unicode(s, encoding=None): 129 | if encoding is None: 130 | encoding = 'utf-8' 131 | return str(s, encoding) 132 | 133 | loader.to_unicode = to_unicode 134 | self.assertString(loader.str(non_ascii), "abcdé") 135 | 136 | def test_unicode__encoding_argument(self): 137 | """ 138 | Test unicode(): encoding argument. 139 | 140 | """ 141 | loader = Loader() 142 | 143 | non_ascii = 'abcdé'.encode('utf-8') 144 | 145 | self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) 146 | 147 | actual = loader.str(non_ascii, encoding='utf-8') 148 | self.assertString(actual, 'abcdé') 149 | 150 | # TODO: check the read() unit tests. 151 | def test_read(self): 152 | """ 153 | Test read(). 154 | 155 | """ 156 | loader = Loader() 157 | path = self._get_path('ascii.mustache') 158 | actual = loader.read(path) 159 | self.assertString(actual, 'ascii: abc') 160 | 161 | def test_read__file_encoding__attribute(self): 162 | """ 163 | Test read(): file_encoding attribute respected. 164 | 165 | """ 166 | loader = Loader() 167 | path = self._get_path('non_ascii.mustache') 168 | 169 | self.assertRaises(UnicodeDecodeError, loader.read, path) 170 | 171 | loader.file_encoding = 'utf-8' 172 | actual = loader.read(path) 173 | self.assertString(actual, 'non-ascii: é') 174 | 175 | def test_read__encoding__argument(self): 176 | """ 177 | Test read(): encoding argument respected. 178 | 179 | """ 180 | loader = Loader() 181 | path = self._get_path('non_ascii.mustache') 182 | 183 | self.assertRaises(UnicodeDecodeError, loader.read, path) 184 | 185 | actual = loader.read(path, encoding='utf-8') 186 | self.assertString(actual, 'non-ascii: é') 187 | 188 | def test_read__to_unicode__attribute(self): 189 | """ 190 | Test read(): to_unicode attribute respected. 191 | 192 | """ 193 | loader = Loader() 194 | path = self._get_path('non_ascii.mustache') 195 | 196 | self.assertRaises(UnicodeDecodeError, loader.read, path) 197 | 198 | #loader.decode_errors = 'ignore' 199 | #actual = loader.read(path) 200 | #self.assertString(actual, u'non-ascii: ') 201 | 202 | def test_load_file(self): 203 | loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR]) 204 | template = loader.load_file('template.txt') 205 | self.assertEqual(template, 'Test template file\n') 206 | 207 | def test_load_name(self): 208 | loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR], 209 | extension='txt') 210 | template = loader.load_name('template') 211 | self.assertEqual(template, 'Test template file\n') 212 | -------------------------------------------------------------------------------- /pystache/tests/test_locator.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | Unit tests for locator.py. 5 | 6 | """ 7 | 8 | import os 9 | import sys 10 | import unittest 11 | from datetime import datetime 12 | 13 | # TODO: remove this alias. 14 | from pystache.common import TemplateNotFoundError 15 | from pystache.loader import Loader as Reader 16 | from pystache.locator import Locator 17 | from pystache.tests.common import ( 18 | DATA_DIR, 19 | EXAMPLES_DIR, 20 | AssertExceptionMixin, 21 | ) 22 | from pystache.tests.data.views import SayHello 23 | 24 | LOCATOR_DATA_DIR = os.path.join(DATA_DIR, 'locator') 25 | 26 | 27 | class LocatorTests(unittest.TestCase, AssertExceptionMixin): 28 | 29 | def _locator(self): 30 | return Locator(search_dirs=DATA_DIR) 31 | 32 | def test_init__extension(self): 33 | # Test the default value. 34 | locator = Locator() 35 | self.assertEqual(locator.template_extension, 'mustache') 36 | 37 | locator = Locator(extension='txt') 38 | self.assertEqual(locator.template_extension, 'txt') 39 | 40 | locator = Locator(extension=False) 41 | self.assertTrue(locator.template_extension is False) 42 | 43 | def _assert_paths(self, actual, expected): 44 | """ 45 | Assert that two paths are the same. 46 | 47 | """ 48 | self.assertEqual(actual, expected) 49 | 50 | def test_get_object_directory(self): 51 | locator = Locator() 52 | 53 | obj = SayHello() 54 | actual = locator.get_object_directory(obj) 55 | 56 | self._assert_paths(actual, DATA_DIR) 57 | 58 | def test_get_object_directory__not_hasattr_module(self): 59 | locator = Locator() 60 | 61 | # Previously, we used a genuine object -- a datetime instance -- 62 | # because datetime instances did not have the __module__ attribute 63 | # in CPython. See, for example-- 64 | # 65 | # http://bugs.python.org/issue15223 66 | # 67 | # However, since datetime instances do have the __module__ attribute 68 | # in PyPy, we needed to switch to something else once we added 69 | # support for PyPi. This was so that our test runs would pass 70 | # in all systems. 71 | obj = "abc" 72 | self.assertFalse(hasattr(obj, '__module__')) 73 | self.assertEqual(locator.get_object_directory(obj), None) 74 | 75 | self.assertFalse(hasattr(None, '__module__')) 76 | self.assertEqual(locator.get_object_directory(None), None) 77 | 78 | def test_make_file_name(self): 79 | locator = Locator() 80 | 81 | locator.template_extension = 'bar' 82 | self.assertEqual(locator.make_file_name('foo'), 'foo.bar') 83 | 84 | locator.template_extension = False 85 | self.assertEqual(locator.make_file_name('foo'), 'foo') 86 | 87 | locator.template_extension = '' 88 | self.assertEqual(locator.make_file_name('foo'), 'foo.') 89 | 90 | def test_make_file_name__template_extension_argument(self): 91 | locator = Locator() 92 | 93 | self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar') 94 | 95 | def test_find_file(self): 96 | locator = Locator() 97 | path = locator.find_file('template.txt', [LOCATOR_DATA_DIR]) 98 | 99 | expected_path = os.path.join(LOCATOR_DATA_DIR, 'template.txt') 100 | self.assertEqual(path, expected_path) 101 | 102 | def test_find_name(self): 103 | locator = Locator() 104 | path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple') 105 | 106 | self.assertEqual(os.path.basename(path), 'simple.mustache') 107 | 108 | def test_find_name__using_list_of_paths(self): 109 | locator = Locator() 110 | path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple') 111 | 112 | self.assertTrue(path) 113 | 114 | def test_find_name__precedence(self): 115 | """ 116 | Test the order in which find_name() searches directories. 117 | 118 | """ 119 | locator = Locator() 120 | 121 | dir1 = DATA_DIR 122 | dir2 = LOCATOR_DATA_DIR 123 | 124 | self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate')) 125 | self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate')) 126 | 127 | path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate') 128 | dirpath = os.path.dirname(path) 129 | dirname = os.path.split(dirpath)[-1] 130 | 131 | self.assertEqual(dirname, 'locator') 132 | 133 | def test_find_name__non_existent_template_fails(self): 134 | locator = Locator() 135 | 136 | self.assertException(TemplateNotFoundError, "File 'doesnt_exist.mustache' not found in dirs: []", 137 | locator.find_name, search_dirs=[], template_name='doesnt_exist') 138 | 139 | def test_find_object(self): 140 | locator = Locator() 141 | 142 | obj = SayHello() 143 | 144 | actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') 145 | expected = os.path.join(DATA_DIR, 'sample_view.mustache') 146 | 147 | self._assert_paths(actual, expected) 148 | 149 | def test_find_object__none_file_name(self): 150 | locator = Locator() 151 | 152 | obj = SayHello() 153 | 154 | actual = locator.find_object(search_dirs=[], obj=obj) 155 | expected = os.path.join(DATA_DIR, 'say_hello.mustache') 156 | 157 | self.assertEqual(actual, expected) 158 | 159 | def test_find_object__none_object_directory(self): 160 | locator = Locator() 161 | 162 | obj = None 163 | self.assertEqual(None, locator.get_object_directory(obj)) 164 | 165 | actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') 166 | expected = os.path.join(DATA_DIR, 'say_hello.mustache') 167 | 168 | self.assertEqual(actual, expected) 169 | 170 | def test_make_template_name(self): 171 | """ 172 | Test make_template_name(). 173 | 174 | """ 175 | locator = Locator() 176 | 177 | class FooBar(object): 178 | pass 179 | foo = FooBar() 180 | 181 | self.assertEqual(locator.make_template_name(foo), 'foo_bar') 182 | -------------------------------------------------------------------------------- /pystache/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Unit tests of parser.py. 5 | 6 | """ 7 | 8 | import unittest 9 | 10 | from pystache.defaults import DELIMITERS 11 | from pystache.parser import _compile_template_re as make_re, parse, ParsingError 12 | 13 | 14 | class RegularExpressionTestCase(unittest.TestCase): 15 | """Tests the regular expression returned by _compile_template_re().""" 16 | 17 | def test_re(self): 18 | """ 19 | Test getting a key from a dictionary. 20 | """ 21 | re = make_re(DELIMITERS) 22 | match = re.search("b {{test}}") 23 | 24 | self.assertEqual(match.start(), 1) 25 | 26 | 27 | class ParseTestCase(unittest.TestCase): 28 | """Tests the parse() function.""" 29 | 30 | def test_parse_okay(self): 31 | """ 32 | Test parsing templates in the cases there are no errors. 33 | """ 34 | ts = [ 35 | '
{{>A}}
', 36 | '{{#A}}
some text
', 37 | '{{^A}}
some text
{{/A}}', 38 | '{{#A}} {{^B}} {{/B}} {{/A}}', 39 | '{{#A}} {{^B}} {{/B}} {{/A}} {{#C}} {{/C}}', 40 | ] 41 | for t in ts: 42 | with self.subTest(template=t): 43 | parse(t) 44 | 45 | def test_parse_fail(self): 46 | """ 47 | Test parsing templates in the cases there are errors. 48 | """ 49 | ts = [ 50 | '{{#A}}
some text
', 51 | '{{#A}}
some text
{{/A}}
TEXT
{{/B}}', 52 | '{{#A}} {{#B}} {{/A}} {{/B}}', 53 | ] 54 | for t in ts: 55 | with self.subTest(template=t): 56 | with self.assertRaises(ParsingError) as e: 57 | parse(t, raise_on_mismatch=True) 58 | self.assertTrue('Did not find a matching tag', str(e)) 59 | -------------------------------------------------------------------------------- /pystache/tests/test_pystache.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import unittest 4 | 5 | import pystache 6 | from pystache import defaults, renderer 7 | from pystache.tests.common import html_escape 8 | 9 | 10 | class PystacheTests(unittest.TestCase): 11 | 12 | 13 | def setUp(self): 14 | self.original_escape = defaults.TAG_ESCAPE 15 | defaults.TAG_ESCAPE = html_escape 16 | 17 | def tearDown(self): 18 | defaults.TAG_ESCAPE = self.original_escape 19 | 20 | def _assert_rendered(self, expected, template, context): 21 | actual = pystache.render(template, context) 22 | self.assertEqual(actual, expected) 23 | 24 | def test_basic(self): 25 | ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) 26 | self.assertEqual(ret, "Hi world!") 27 | 28 | def test_kwargs(self): 29 | ret = pystache.render("Hi {{thing}}!", thing='world') 30 | self.assertEqual(ret, "Hi world!") 31 | 32 | def test_less_basic(self): 33 | template = "It's a nice day for {{beverage}}, right {{person}}?" 34 | context = { 'beverage': 'soda', 'person': 'Bob' } 35 | self._assert_rendered("It's a nice day for soda, right Bob?", template, context) 36 | 37 | def test_even_less_basic(self): 38 | template = "I think {{name}} wants a {{thing}}, right {{name}}?" 39 | context = { 'name': 'Jon', 'thing': 'racecar' } 40 | self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context) 41 | 42 | def test_ignores_misses(self): 43 | template = "I think {{name}} wants a {{thing}}, right {{name}}?" 44 | context = { 'name': 'Jon' } 45 | self._assert_rendered("I think Jon wants a , right Jon?", template, context) 46 | 47 | def test_render_zero(self): 48 | template = 'My value is {{value}}.' 49 | context = { 'value': 0 } 50 | self._assert_rendered('My value is 0.', template, context) 51 | 52 | def test_comments(self): 53 | template = "What {{! the }} what?" 54 | actual = pystache.render(template) 55 | self.assertEqual("What what?", actual) 56 | 57 | def test_false_sections_are_hidden(self): 58 | template = "Ready {{#set}}set {{/set}}go!" 59 | context = { 'set': False } 60 | self._assert_rendered("Ready go!", template, context) 61 | 62 | def test_true_sections_are_shown(self): 63 | template = "Ready {{#set}}set{{/set}} go!" 64 | context = { 'set': True } 65 | self._assert_rendered("Ready set go!", template, context) 66 | 67 | non_strings_expected = """(123 & ['something'])(chris & 0.9)""" 68 | 69 | def test_non_strings(self): 70 | template = "{{#stats}}({{key}} & {{value}}){{/stats}}" 71 | stats = [] 72 | stats.append({'key': 123, 'value': ['something']}) 73 | stats.append({'key': "chris", 'value': 0.900}) 74 | context = { 'stats': stats } 75 | self._assert_rendered(self.non_strings_expected, template, context) 76 | 77 | def test_unicode(self): 78 | template = 'Name: {{name}}; Age: {{age}}' 79 | context = {'name': 'Henri Poincaré', 'age': 156} 80 | self._assert_rendered('Name: Henri Poincaré; Age: 156', template, context) 81 | 82 | def test_sections(self): 83 | template = """
    {{#users}}
  • {{name}}
  • {{/users}}
""" 84 | 85 | context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } 86 | expected = """
  • Chris
  • Tom
  • PJ
""" 87 | self._assert_rendered(expected, template, context) 88 | 89 | def test_implicit_iterator(self): 90 | template = """
    {{#users}}
  • {{.}}
  • {{/users}}
""" 91 | context = { 'users': [ 'Chris', 'Tom','PJ' ] } 92 | expected = """
  • Chris
  • Tom
  • PJ
""" 93 | self._assert_rendered(expected, template, context) 94 | 95 | # The spec says that sections should not alter surrounding whitespace. 96 | def test_surrounding_whitepace_not_altered(self): 97 | template = "first{{#spacing}} second {{/spacing}}third" 98 | context = {"spacing": True} 99 | self._assert_rendered("first second third", template, context) 100 | 101 | def test__section__non_false_value(self): 102 | """ 103 | Test when a section value is a (non-list) "non-false value". 104 | 105 | From mustache(5): 106 | 107 | When the value [of a section key] is non-false but not a list, it 108 | will be used as the context for a single rendering of the block. 109 | 110 | """ 111 | template = """{{#person}}Hi {{name}}{{/person}}""" 112 | context = {"person": {"name": "Jon"}} 113 | self._assert_rendered("Hi Jon", template, context) 114 | 115 | def test_later_list_section_with_escapable_character(self): 116 | """ 117 | This is a simple test case intended to cover issue #53. 118 | 119 | The test case failed with markupsafe enabled, as follows: 120 | 121 | AssertionError: Markup(u'foo <') != 'foo <' 122 | 123 | """ 124 | template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}""" 125 | context = {'s1': True, 's2': [True]} 126 | self._assert_rendered("foo <", template, context) 127 | -------------------------------------------------------------------------------- /pystache/tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pystache 4 | from pystache import Renderer 5 | from pystache.tests.common import EXAMPLES_DIR, AssertStringMixin 6 | 7 | from .examples.complex import Complex 8 | from .examples.lambdas import Lambdas 9 | from .examples.nested_context import NestedContext 10 | from .examples.simple import Simple 11 | from .examples.template_partial import TemplatePartial 12 | 13 | 14 | class TestSimple(unittest.TestCase, AssertStringMixin): 15 | 16 | def test_nested_context(self): 17 | renderer = Renderer() 18 | view = NestedContext(renderer) 19 | view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}' 20 | 21 | actual = renderer.render(view) 22 | self.assertString(actual, "one and foo and two") 23 | 24 | def test_looping_and_negation_context(self): 25 | template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' 26 | context = Complex() 27 | 28 | renderer = Renderer() 29 | actual = renderer.render(template, context) 30 | self.assertEqual(actual, "Colors: red Colors: green Colors: blue ") 31 | 32 | def test_empty_context(self): 33 | template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' 34 | self.assertEqual(pystache.Renderer().render(template), "Should see me") 35 | 36 | def test_callables(self): 37 | view = Lambdas() 38 | view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}' 39 | 40 | renderer = Renderer() 41 | actual = renderer.render(view) 42 | self.assertString(actual, 'bar != bar. oh, it does!') 43 | 44 | def test_rendering_partial(self): 45 | renderer = Renderer(search_dirs=EXAMPLES_DIR) 46 | 47 | view = TemplatePartial(renderer=renderer) 48 | view.template = '{{>inner_partial}}' 49 | 50 | actual = renderer.render(view) 51 | self.assertString(actual, 'Again, Welcome!') 52 | 53 | view.template = '{{#looping}}{{>inner_partial}} {{/looping}}' 54 | actual = renderer.render(view) 55 | self.assertString(actual, "Again, Welcome! Again, Welcome! Again, Welcome! ") 56 | 57 | def test_non_existent_value_renders_blank(self): 58 | view = Simple() 59 | template = '{{not_set}} {{blank}}' 60 | self.assertEqual(pystache.Renderer().render(template), ' ') 61 | 62 | 63 | def test_template_partial_extension(self): 64 | """ 65 | Side note: 66 | 67 | From the spec-- 68 | 69 | Partial tags SHOULD be treated as standalone when appropriate. 70 | 71 | In particular, this means that trailing newlines should be removed. 72 | 73 | """ 74 | renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') 75 | 76 | view = TemplatePartial(renderer=renderer) 77 | 78 | actual = renderer.render(view) 79 | self.assertString(actual, """Welcome 80 | ------- 81 | 82 | ## Again, Welcome! ##""") 83 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /test_pystache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Runs project tests. 6 | 7 | This script is a substitute for running-- 8 | 9 | python -m pystache.commands.test 10 | 11 | It is useful in Python 2.4 because the -m flag does not accept subpackages 12 | in Python 2.4: 13 | 14 | http://docs.python.org/using/cmdline.html#cmdoption-m 15 | 16 | """ 17 | 18 | import sys 19 | 20 | from pystache.commands import test 21 | from pystache.tests.main import FROM_SOURCE_OPTION 22 | 23 | 24 | def main(sys_argv=sys.argv): 25 | sys.argv.insert(1, FROM_SOURCE_OPTION) 26 | test.main() 27 | 28 | 29 | if __name__=='__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3{8,9,10,11,12,13}-{linux,macos,windows} 3 | skip_missing_interpreters = true 4 | isolated_build = true 5 | skipsdist = true 6 | 7 | [gh-actions] 8 | python = 9 | 3.8: py38 10 | 3.9: py39 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | 3.13: py313 15 | 16 | [gh-actions:env] 17 | PLATFORM = 18 | ubuntu-20.04: linux 19 | macos-latest: macos 20 | windows-latest: windows 21 | 22 | [base] 23 | deps = 24 | pip>=21.1 25 | setuptools_scm 26 | 27 | [testenv] 28 | passenv = CI,PYTHON,PYTHONIOENCODING 29 | allowlist_externals = bash 30 | 31 | deps = 32 | {[base]deps} 33 | .[test,cov] 34 | 35 | commands = 36 | python -m pytest . --capture=no --doctest-modules --doctest-glob="*.rst" --doctest-glob="*.py" --cov pystache --cov-branch --cov-report term-missing 37 | coverage xml 38 | bash -c './gh/fix_pkg_name.sh' 39 | 40 | [testenv:bare] 41 | # Change the working directory so that we don't import the pystache located 42 | # in the original location. 43 | deps = 44 | {[base]deps} 45 | -e . 46 | 47 | changedir = 48 | {envbindir} 49 | 50 | commands = 51 | pystache-test 52 | 53 | [testenv:bench] 54 | passenv = CI,PYTHON,PYTHONIOENCODING 55 | 56 | deps = 57 | {[base]deps} 58 | # uncomment for comparison, posargs expects a number, eg, 10000 59 | #chevron 60 | 61 | commands_pre = 62 | pip install . 63 | 64 | commands = 65 | python pystache/tests/benchmark.py {posargs:10000} 66 | 67 | [testenv:setup] 68 | passenv = CI,PYTHON,PYTHONIOENCODING 69 | 70 | deps = 71 | {[base]deps} 72 | pyyaml 73 | 74 | # this is becoming even more deprecated (possibly failing) BUT, 75 | # to run the spec tests, first init the git submodule, and then run 76 | # something like: tox -e setup ext/spec/specs 77 | commands = 78 | python -m pip install . 79 | pystache-test {toxinidir} {posargs} 80 | 81 | [testenv:lint] 82 | passenv = CI,PYTHON,PYTHONIOENCODING 83 | 84 | deps = 85 | {[base]deps} 86 | pylint 87 | 88 | commands_pre = 89 | # need to generate version info in a fresh checkout 90 | python setup.py egg_info 91 | 92 | commands = 93 | pylint --rcfile={toxinidir}/.pylintrc --fail-under=8.00 pystache/ 94 | 95 | [testenv:style] 96 | passenv = CI,PYTHON,PYTHONIOENCODING 97 | 98 | deps = 99 | {[base]deps} 100 | flake8 101 | 102 | commands = 103 | flake8 pystache/ 104 | 105 | [testenv:build] 106 | passenv = CI,PYTHON,PYTHONIOENCODING 107 | 108 | deps = 109 | pip>=22.1 110 | build 111 | twine 112 | 113 | commands = 114 | python -m build . 115 | twine check dist/* 116 | 117 | [testenv:check] 118 | passenv = CI,PYTHON,PYTHONIOENCODING 119 | skip_install = true 120 | 121 | allowlist_externals = bash 122 | 123 | deps = 124 | pip>=22.1 125 | 126 | commands = 127 | pip install pystache --force-reinstall --pre --prefer-binary -f dist/ 128 | pystache-test 129 | 130 | [testenv:docs] 131 | skip_install = true 132 | allowlist_externals = 133 | bash 134 | make 135 | 136 | deps = 137 | {[base]deps} 138 | .[doc] 139 | 140 | commands_pre = 141 | # need to generate version info in a fresh checkout 142 | bash -c '[[ -f pystache/_version.py ]] || python setup.py egg_info' 143 | 144 | commands = make -C docs html 145 | 146 | [testenv:docs-lint] 147 | skip_install = true 148 | allowlist_externals = 149 | {[testenv:docs]allowlist_externals} 150 | 151 | deps = 152 | {[testenv:docs]deps} 153 | 154 | commands_pre = 155 | {[testenv:docs]commands_pre} 156 | 157 | commands = make -C docs linkcheck 158 | 159 | [testenv:changes] 160 | skip_install = true 161 | allowlist_externals = 162 | {[testenv:docs]allowlist_externals} 163 | 164 | passenv = 165 | CI 166 | OS 167 | PIP_DOWNLOAD_CACHE 168 | 169 | setenv = 170 | VERSION = {env:VERSION} 171 | 172 | deps = 173 | {[base]deps} 174 | git+https://github.com/sarnold/gitchangelog.git@master 175 | 176 | commands = 177 | bash -c 'gitchangelog {posargs} > CHANGELOG.rst' 178 | 179 | [testenv:sec] 180 | passenv = CI,PYTHON,PYTHONIOENCODING 181 | skip_install = true 182 | 183 | deps = 184 | {[base]deps} 185 | bandit 186 | 187 | commands_pre = 188 | # need to generate version info in a fresh checkout 189 | python setup.py egg_info 190 | 191 | commands = 192 | bandit -r pystache/ -x pystache/tests 193 | 194 | [testenv:isort] 195 | skip_install = true 196 | 197 | setenv = PYTHONPATH = {toxinidir} 198 | 199 | deps = 200 | {[base]deps} 201 | isort 202 | 203 | commands = 204 | python -m isort pystache/ 205 | 206 | [testenv:clean] 207 | skip_install = true 208 | allowlist_externals = bash 209 | 210 | deps = 211 | pip>=21.1 212 | 213 | commands = 214 | bash -c 'make -C docs/ clean' 215 | bash -c 'rm -rf build/ dist/ pystache.egg-info/ pystache/_version.py docs/source/api/' 216 | --------------------------------------------------------------------------------