pax_global_header00006660000000000000000000000064141370101620014505gustar00rootroot0000000000000052 comment=e71b77734f68d620544b6119afe08e35da41f76f pystache-0.6.0/000077500000000000000000000000001413701016200133305ustar00rootroot00000000000000pystache-0.6.0/.coveragerc000066400000000000000000000012671413701016200154570ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] branch = True source = pystache omit = .tox/* setup.py pystache/tests/* plugins = coverage_python_version [report] # must set this to True to see missing show_missing = True # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: ignore_errors = True [html] directory = coverage pystache-0.6.0/.flake8000066400000000000000000000006241413701016200145050ustar00rootroot00000000000000[flake8] exclude = .git, __pycache__, pystache/tests, test_pystache.py, build, dist max-line-length = 110 max-complexity = 25 addons = file,open,basestring,xrange,unicode,long,cmp ignore = E266, # too many leading '#' for block comment E731, # do not assign a lambda expression, use a def E203, # whitespace before ':' E221, # multiple spaces before operator pystache-0.6.0/.gitchangelog.rc000066400000000000000000000240151413701016200163710ustar00rootroot00000000000000# -*- coding: utf-8; mode: python -*- ## ## Format ## ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] ## ## Description ## ## ACTION is one of 'chg', 'fix', 'new' ## ## Is WHAT the change is about. ## ## 'chg' is for refactor, small improvement, cosmetic changes... ## 'fix' is for bug fixes ## 'new' is for new features, big improvement ## ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' ## ## Is WHO is concerned by the change. ## ## 'dev' is for developpers (API changes, refactors...) ## 'usr' is for final users (UI changes) ## 'pkg' is for packagers (packaging changes) ## 'test' is for testers (test only related changes) ## 'doc' is for doc guys (doc only changes) ## ## COMMIT_MSG is ... well ... the commit message itself. ## ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' ## ## They are preceded with a '!' or a '@' (prefer the former, as the ## latter is wrongly interpreted in github.) Commonly used tags are: ## ## 'refactor' is obviously for refactoring code only ## 'minor' is for a very meaningless change (a typo, adding a comment) ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) ## 'wip' is for partial functionality but complete subfunctionality. ## ## Example: ## ## new: usr: support of bazaar implemented ## chg: re-indentend some lines !cosmetic ## new: dev: updated code to be compatible with last version of killer lib. ## fix: pkg: updated year of licence coverage. ## new: test: added a bunch of test around user usability of feature X. ## fix: typo in spelling my name in comment. !minor ## ## Please note that multi-line commit message are supported, and only the ## first line will be considered as the "summary" of the commit message. So ## tags, and other rules only applies to the summary. The body of the commit ## message will be displayed in the changelog without reformatting. ## ## ``ignore_regexps`` is a line of regexps ## ## Any commit having its full commit message matching any regexp listed here ## will be ignored and won't be reported in the changelog. ## ignore_regexps = [ r'@minor', r'!minor', r'@cosmetic', r'!cosmetic', r'@refactor', r'!refactor', r'@wip', r'!wip', r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', r'^$', ## ignore commits with empty messages ] ## ``section_regexps`` is a list of 2-tuples associating a string label and a ## list of regexp ## ## Commit messages will be classified in sections thanks to this. Section ## titles are the label, and a commit is classified under this section if any ## of the regexps associated is matching. ## ## Please note that ``section_regexps`` will only classify commits and won't ## make any changes to the contents. So you'll probably want to go check ## ``subject_process`` (or ``body_process``) to do some changes to the subject, ## whenever you are tweaking this variable. ## section_regexps = [ ('New', [ r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Features', [ r'^([nN]ew|[fF]eat)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Changes', [ r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Fixes', [ r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Other', None ## Match all lines ), ] ## ``body_process`` is a callable ## ## This callable will be given the original body and result will ## be used in the changelog. ## ## Available constructs are: ## ## - any python callable that take one txt argument and return txt argument. ## ## - ReSub(pattern, replacement): will apply regexp substitution. ## ## - Indent(chars=" "): will indent the text with the prefix ## Please remember that template engines gets also to modify the text and ## will usually indent themselves the text if needed. ## ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns ## ## - noop: do nothing ## ## - ucfirst: ensure the first letter is uppercase. ## (usually used in the ``subject_process`` pipeline) ## ## - final_dot: ensure text finishes with a dot ## (usually used in the ``subject_process`` pipeline) ## ## - strip: remove any spaces before or after the content of the string ## ## - SetIfEmpty(msg="No commit message."): will set the text to ## whatever given ``msg`` if the current text is empty. ## ## Additionally, you can `pipe` the provided filters, for instance: #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') #body_process = noop body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip ## ``subject_process`` is a callable ## ## This callable will be given the original subject and result will ## be used in the changelog. ## ## Available constructs are those listed in ``body_process`` doc. subject_process = (strip | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | SetIfEmpty("No commit message.") | ucfirst | final_dot) ## ``tag_filter_regexp`` is a regexp ## ## Tags that will be used for the changelog must match this regexp. ## #tag_filter_regexp = r'^v?[0-9]+\.[0-9]+(\.[0-9]+)?$' tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' ## ``unreleased_version_label`` is a string or a callable that outputs a string ## ## This label will be used as the changelog Title of the last set of changes ## between last valid tag and HEAD if any. unreleased_version_label = "(unreleased)" #unreleased_version_label = lambda: swrap( # ["git", "describe", "--tags"], #shell=False) ## ``output_engine`` is a callable ## ## This will change the output format of the generated changelog file ## ## Available choices are: ## ## - rest_py ## ## Legacy pure python engine, outputs ReSTructured text. ## This is the default. ## ## - mustache() ## ## Template name could be any of the available templates in ## ``templates/mustache/*.tpl``. ## Requires python package ``pystache``. ## Examples: ## - mustache("markdown") ## - mustache("restructuredtext") ## ## - makotemplate() ## ## Template name could be any of the available templates in ## ``templates/mako/*.tpl``. ## Requires python package ``mako``. ## Examples: ## - makotemplate("restructuredtext") ## #output_engine = rest_py #output_engine = mustache("restructuredtext") output_engine = mustache("markdown") #output_engine = makotemplate("restructuredtext") ## ``include_merge`` is a boolean ## ## This option tells git-log whether to include merge commits in the log. ## The default is to include them. include_merge = True ## ``log_encoding`` is a string identifier ## ## This option tells gitchangelog what encoding is outputed by ``git log``. ## The default is to be clever about it: it checks ``git config`` for ## ``i18n.logOutputEncoding``, and if not found will default to git's own ## default: ``utf-8``. #log_encoding = 'utf-8' ## ``publish`` is a callable ## ## Sets what ``gitchangelog`` should do with the output generated by ## the output engine. ``publish`` is a callable taking one argument ## that is an interator on lines from the output engine. ## ## Some helper callable are provided: ## ## Available choices are: ## ## - stdout ## ## Outputs directly to standard output ## (This is the default) ## ## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start(), flags) ## ## Creates a callable that will parse given file for the given ## regex pattern and will insert the output in the file. ## ``idx`` is a callable that receive the matching object and ## must return a integer index point where to insert the ## the output in the file. Default is to return the position of ## the start of the matched string. ## ## - FileRegexSubst(file, pattern, replace, flags) ## ## Apply a replace inplace in the given file. Your regex pattern must ## take care of everything and might be more complex. Check the README ## for a complete copy-pastable example. ## # publish = FileInsertIntoFirstRegexMatch( # "CHANGELOG.rst", # r'/(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/', # idx=lambda m: m.start(1) # ) #publish = stdout ## ``revs`` is a list of callable or a list of string ## ## callable will be called to resolve as strings and allow dynamical ## computation of these. The result will be used as revisions for ## gitchangelog (as if directly stated on the command line). This allows ## to filter exaclty which commits will be read by gitchangelog. ## ## To get a full documentation on the format of these strings, please ## refer to the ``git rev-list`` arguments. There are many examples. ## ## Using callables is especially useful, for instance, if you ## are using gitchangelog to generate incrementally your changelog. ## ## Some helpers are provided, you can use them:: ## ## - FileFirstRegexMatch(file, pattern): will return a callable that will ## return the first string match for the given pattern in the given file. ## If you use named sub-patterns in your regex pattern, it'll output only ## the string matching the regex pattern named "rev". ## ## - Caret(rev): will return the rev prefixed by a "^", which is a ## way to remove the given revision and all its ancestor. ## ## Please note that if you provide a rev-list on the command line, it'll ## replace this value (which will then be ignored). ## ## If empty, then ``gitchangelog`` will act as it had to generate a full ## changelog. ## ## The default is to use all commits to make the changelog. #revs = ["^1.0.3", ] #revs = [ # Caret( # FileFirstRegexMatch( # "CHANGELOG.rst", # r"(?P[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")), # "HEAD" #] revs = [] pystache-0.6.0/.github/000077500000000000000000000000001413701016200146705ustar00rootroot00000000000000pystache-0.6.0/.github/workflows/000077500000000000000000000000001413701016200167255ustar00rootroot00000000000000pystache-0.6.0/.github/workflows/bandit.yml000066400000000000000000000006721413701016200207160ustar00rootroot00000000000000name: Security check - Bandit on: workflow_dispatch: pull_request: push: branches: - master jobs: build: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Run bandit #uses: ioggstream/bandit-report-artifacts@v0.0.2 uses: VCTLabs/bandit-report-artifacts@master with: project_path: pystache ignore_failure: false baseline_file: bandit_baseline.json pystache-0.6.0/.github/workflows/ci.yml000066400000000000000000000027051413701016200200470ustar00rootroot00000000000000name: ci on: workflow_dispatch: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ${{ matrix.os }} defaults: run: shell: bash env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} PYTHONIOENCODING: utf-8 PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache strategy: fail-fast: false matrix: os: [ubuntu-20.04, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9] steps: - name: Set git crlf/eol run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@v2 with: submodules: True - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox tox-gh-actions - name: Run full (internal) test suite run: | tox env: PLATFORM: ${{ matrix.os }} - name: Test with specs and pystache-test run: | tox -e setup . ext/spec/specs - name: Check pkg builds run: | tox -e deploy,check - name: Check docs if: runner.os == 'Linux' run: | sudo apt-get -qq update sudo apt-get install -y pandoc tox -e docs pystache-0.6.0/.github/workflows/conda.yml000066400000000000000000000023221413701016200205330ustar00rootroot00000000000000name: Conda on: workflow_dispatch: #pull_request: push: branches: - master jobs: build: strategy: fail-fast: false matrix: platform: [ubuntu-18.04, windows-latest, macos-latest] python-version: [3.7, 3.9] runs-on: ${{ matrix.platform }} # The setup-miniconda action needs this to activate miniconda defaults: run: shell: "bash -l {0}" steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Cache conda uses: actions/cache@v1 with: path: ~/conda_pkgs_dir key: ${{matrix.os}}-conda-pkgs-${{hashFiles('**/conda/meta.yaml')}} - name: Get conda uses: conda-incubator/setup-miniconda@v2 with: python-version: ${{ matrix.python-version }} channels: conda-forge channel-priority: strict use-only-tar-bz2: true auto-activate-base: true - name: Prepare run: conda install conda-build conda-verify - name: Build run: conda build conda - name: Install run: conda install -c ${CONDA_PREFIX}/conda-bld/ pystache - name: Test run: python test_pystache.py pystache-0.6.0/.github/workflows/coverage.yml000066400000000000000000000253021413701016200212450ustar00rootroot00000000000000# internal coverage PR comment and badge v0.0.2 # badge and comment job logic should be tuned for personal vs org use # (see badge job comments below) name: Coverage on: workflow_dispatch: pull_request: push: branches: - master jobs: pre_ci: name: Prepare CI environment runs-on: ubuntu-20.04 outputs: commit_message: ${{ steps.get_commit_message.outputs.commit_message }} branch: ${{ steps.extract_branch.outputs.branch }} steps: - name: Checkout Project uses: actions/checkout@v2 with: # We need to fetch with a depth of 2 for pull_request so we can do HEAD^2 fetch-depth: 2 - name: Environment run: | bash -c set - name: "Get commit message" id: get_commit_message env: COMMIT_PUSH: ${{ github.event.head_commit.message }} run: | COMMIT_MESSAGE="${COMMIT_PUSH:-$(git log --format=%B -n 1 HEAD^2)}" echo "::set-output name=commit_message::${COMMIT_MESSAGE}" - name: Extract branch name id: extract_branch shell: bash run: | TMP_PULL_HEAD_REF="${{ github.head_ref }}" TMP_GITHUB_REF="${GITHUB_REF#refs/heads/}" EXPORT_VALUE="" if [ "${TMP_PULL_HEAD_REF}" != "" ] then EXPORT_VALUE="${TMP_PULL_HEAD_REF}" else EXPORT_VALUE="${TMP_GITHUB_REF}" fi echo "##[set-output name=branch;]${EXPORT_VALUE}" base: name: Base coverage runs-on: ubuntu-20.04 outputs: base_branch: ${{ steps.get_base.outputs.base_branch }} base_cov: ${{ steps.get_base.outputs.base_cov }} steps: - uses: actions/checkout@v2 with: ref: badges path: badges - name: Get base ref and coverage score id: get_base env: FILE: 'test-coverage.txt' working-directory: ./badges shell: bash run: | TMP_PULL_BASE_REF="${{ github.base_ref }}" TMP_GITHUB_REF="${GITHUB_REF#refs/heads/}" EXPORT_VALUE="" if [ "${TMP_PULL_BASE_REF}" != "" ] then EXPORT_VALUE="${TMP_PULL_BASE_REF}" else EXPORT_VALUE="${TMP_GITHUB_REF}" fi echo "##[set-output name=base_branch;]${EXPORT_VALUE}" if [ -f "${EXPORT_VALUE}/${FILE}" ] then echo "Base coverage found on ${EXPORT_VALUE}" BASE_COV=$(cat "${EXPORT_VALUE}/${FILE}") echo "Base coverage is: ${BASE_COV}" echo "##[set-output name=base_cov;]${BASE_COV}" else echo "Base coverage NOT found on ${EXPORT_VALUE}!!" fi check: name: Pre CI check runs-on: ubuntu-20.04 needs: [pre_ci, base] steps: - name: Check github variables # NOTE base coverage env var may be empty here env: COMMIT_MESSAGE: ${{ needs.pre_ci.outputs.commit_message }} EXPORT_VALUE: ${{ needs.pre_ci.outputs.branch }} BASE_BRANCH: ${{ needs.base.outputs.base_branch }} BASE_COVERAGE: ${{ needs.base.outputs.base_cov }} run: | echo "Commit message: ${COMMIT_MESSAGE}" echo "Export value (head_ref): ${EXPORT_VALUE}" echo "Base value (base_ref): ${BASE_BRANCH}" echo "Base coverage (percent): ${{ env.BASE_COVERAGE }}" cov_data: name: Generate test coverage data runs-on: ubuntu-20.04 needs: [check] defaults: run: shell: bash outputs: coverage: ${{ steps.coverage.outputs.coverage }} coverage-rounded-display: ${{ steps.coverage.outputs.coverage-rounded-display }} env: PLATFORM: ubuntu-20.04 PYTHON: 3.9 PYTHONIOENCODING: utf-8 PIP_DOWNLOAD_CACHE: ${{ github.workspace }}/../.pip_download_cache steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: actions/setup-python@v2 with: python-version: ${{ env.PYTHON }} - name: Add python requirements run: | python -m pip install --upgrade pip pip install tox - name: Generate coverage and fix pkg name run: | tox - name: Code Coverage Summary Report (Text & Value) uses: irongut/CodeCoverageSummary@v1.0.5 with: filename: coverage.xml output: 'both' - name: Check code coverage id: coverage env: VALUE: "Branch Rate" run: | COVERAGE="$( cat code-coverage-results.txt | egrep "^${VALUE}" | sed -e 's#^.* \([0-9]*\)%.*#\1#' )" echo "##[set-output name=coverage;]${COVERAGE}" echo "##[set-output name=coverage-rounded-display;]${COVERAGE}%" - name: Code Coverage Summary Report uses: irongut/CodeCoverageSummary@v1.0.5 if: ${{ github.event_name == 'pull_request' }} with: filename: coverage.xml format: 'markdown' output: 'both' - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2.1.1 if: ${{ github.event_name == 'pull_request' }} with: header: coverage recreate: true path: code-coverage-results.md test: name: Coverage check runs-on: ubuntu-20.04 needs: [cov_data, base] outputs: coverage: ${{ needs.cov_data.outputs.coverage }} coverage-base: ${{ needs.base.outputs.base_cov }} coverage-rounded-display: ${{ needs.cov_data.outputs.coverage-rounded-display }} steps: - name: Check test coverage env: COVERAGE: ${{ needs.cov_data.outputs.coverage }} COVERAGE_ROUNDED: ${{ needs.cov_data.outputs.coverage-rounded-display }} BASE_COVERAGE: ${{ needs.base.outputs.base_cov }} run: | echo "Coverage: ${COVERAGE}" echo "Coverage Rounded: ${COVERAGE_ROUNDED}" echo "Coverage on Base Branch: ${BASE_COVERAGE}" comment_cov_change: name: Comment on PR with coverage delta runs-on: ubuntu-20.04 needs: [test, base] steps: - name: Environment run: | bash -c set - name: Set whether base coverage was found shell: bash env: BASE: ${{ needs.test.outputs.coverage-base }} run: | if [ -n "${BASE}" ] then BASE_RESULT="true" else BASE_RESULT="false" fi echo "HAVE_BASE_COVERAGE is ${BASE_RESULT}" echo "HAVE_BASE_COVERAGE=${BASE_RESULT}" >> $GITHUB_ENV echo "BASE_COVERAGE=${BASE}" >> $GITHUB_ENV - name: Collect variables and construct comment for delta message if: env.HAVE_BASE_COVERAGE == 'true' shell: bash env: BASE_BRANCH: ${{ needs.base.outputs.base_branch }} COVERAGE: ${{ needs.test.outputs.coverage }} BASE_COVERAGE: ${{ needs.test.outputs.coverage-base }} DELTA_WORD: "not change" RATE: "Branch Rate" run: | if [ "${COVERAGE}" -gt "${BASE_COVERAGE}" ] then DELTA_WORD="increase" elif [ "${COVERAGE}" -lt "${BASE_COVERAGE}" ] then DELTA_WORD="decrease" fi CHG=$(( COVERAGE - BASE_COVERAGE )) CHG="${CHG/-/}" echo "" > coverage-delta.md echo "Hello @${{ github.actor }}! Thanks for opening this PR. We found the following information based on analysis of the coverage report:" >> coverage-delta.md echo "" >> coverage-delta.md echo "__Base__ ${RATE} coverage is __${BASE_COVERAGE}%__" >> coverage-delta.md if [ "${CHG}" = "0" ] then echo "Merging ${{ github.sha }} into ${BASE_BRANCH} will __${DELTA_WORD}__ coverage" >> coverage-delta.md else echo "Merging ${{ github.sha }} into ${BASE_BRANCH} will __${DELTA_WORD}__ coverage by __${CHG}%__" >> coverage-delta.md fi if ! [ "${DELTA_WORD}" = "decrease" ] then echo "" >> coverage-delta.md echo "Nice work, @${{ github.actor }}. Cheers! :beers:" >> coverage-delta.md fi - name: Comment PR with test coverage delta uses: marocchino/sticky-pull-request-comment@v2.1.1 if: env.HAVE_BASE_COVERAGE == 'true' with: header: delta recreate: true path: coverage-delta.md badge: # Only generate and publish if these conditions are met: # - The test step ended successfully # - One of these is met: # - This is a push event and the push event is on branch 'master' or 'develop' # Note: if this repo is personal (ie, not an org repo) then you can # use the following to change the scope of the next 2 jobs # instead of running on branch push as shown below: # - This is a pull request event and the pull actor is the same as the repo owner # if: ${{ ( github.event_name == 'pull_request' && github.actor == github.repository_owner ) || github.ref == 'refs/heads/master' }} name: Generate badge image with test coverage value runs-on: ubuntu-20.04 needs: [test, pre_ci] if: ${{ github.event_name == 'push' }} outputs: url: ${{ steps.url.outputs.url }} markdown: ${{ steps.url.outputs.markdown }} steps: - uses: actions/checkout@v2 with: ref: badges path: badges # Use the output from the `coverage` step - name: Generate the badge SVG image uses: emibcn/badge-action@v1 id: badge with: label: 'Branch Coverage' status: ${{ needs.test.outputs.coverage-rounded-display }} color: ${{ needs.test.outputs.coverage > 90 && 'green' || needs.test.outputs.coverage > 80 && 'yellow,green' || needs.test.outputs.coverage > 70 && 'yellow' || needs.test.outputs.coverage > 60 && 'orange,yellow' || needs.test.outputs.coverage > 50 && 'orange' || needs.test.outputs.coverage > 40 && 'red,orange' || needs.test.outputs.coverage > 30 && 'red,red,orange' || needs.test.outputs.coverage > 20 && 'red,red,red,orange' || 'red' }} path: badges/test-coverage.svg - name: Commit badge and data env: BRANCH: ${{ needs.pre_ci.outputs.branch }} COVERAGE: ${{ needs.test.outputs.coverage }} FILE: 'test-coverage.svg' DATA: 'test-coverage.txt' working-directory: ./badges run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" mkdir -p "${BRANCH}" mv "${FILE}" "${BRANCH}" echo "${COVERAGE}" > "${BRANCH}/${DATA}" git add "${BRANCH}/${FILE}" "${BRANCH}/${DATA}" # Will give error if badge has not changed git commit -m "Add/Update badge" || true - name: Push badge commit uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: badges directory: badges pystache-0.6.0/.github/workflows/release.yml000066400000000000000000000042061413701016200210720ustar00rootroot00000000000000name: Release on: push: # release on tag push tags: - '*' jobs: wheels: runs-on: ${{ matrix.os }} defaults: run: shell: bash env: PYTHONIOENCODING: utf-8 strategy: fail-fast: false matrix: os: [ubuntu-20.04, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9] exclude: - os: windows-latest python-version: 2.7 steps: - name: Set git crlf/eol run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel pip install tox tox-gh-actions - name: Build dist pkgs run: | tox -e deploy - name: Upload artifacts if: matrix.python-version == 3.7 && runner.os == 'Linux' uses: actions/upload-artifact@v2 with: name: wheels path: ./dist/*.whl create_release: name: Create Release needs: [wheels] runs-on: ubuntu-18.04 steps: - name: Get version id: get_version run: | echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV echo ${{ env.VERSION }} - uses: actions/checkout@v2 with: fetch-depth: 0 # download all artifacts to project dir - uses: actions/download-artifact@v2 - name: Generate changes file uses: sarnold/gitchangelog-action@master with: github_token: ${{ secrets.GITHUB_TOKEN}} - name: Create release id: create_release uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ env.VERSION }} name: Release v${{ env.VERSION }} body_path: CHANGES.md draft: false prerelease: false files: | wheels/pystache*.whl pystache-0.6.0/.gitignore000066400000000000000000000006711413701016200153240ustar00rootroot00000000000000*.pyc .DS_Store # Tox support. See: http://pypi.python.org/pypi/tox .tox # Our tox runs convert the doctests in *.rst files to Python 3 prior to # running tests. Ignore these temporary files. *.temp2to3.rst # The setup.py "prep" command converts *.md to *.temp.rst (via *.temp.md). *.temp.md *.temp.rst # TextMate project file *.tmproj # Distribution-related folders and files. build dist MANIFEST pystache.egg-info coverage.xml .coverage pystache-0.6.0/.gitmodules000066400000000000000000000001241413701016200155020ustar00rootroot00000000000000[submodule "ext/spec"] path = ext/spec url = https://github.com/mustache/spec.git pystache-0.6.0/.pep8speaks.yml000066400000000000000000000013671413701016200162230ustar00rootroot00000000000000scanner: diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned. linter: flake8 # Other option is pycodestyle no_blank_comment: False # If True, no comment is made on PR without any errors. descending_issues_order: True # If True, PEP 8 issues in message will be displayed in descending order of line numbers in the file pycodestyle: # Same as scanner.linter value. Other option is flake8 max-line-length: 110 # Default is 79 in PEP 8 flake8: max-line-length: 110 # Default is 79 in PEP 8 ignore: # Errors and warnings to ignore - E266 - E731 - E203 - E221 exclude: - pystache/tests - test_pystache.py pystache-0.6.0/.pre-commit-config.yaml000066400000000000000000000066141413701016200176200ustar00rootroot00000000000000# To install the git pre-commit hook run: # pre-commit install # To update the pre-commit hooks run: # pre-commit install-hooks exclude: '^(.tox/|.*\.mustache$)' repos: - repo: meta hooks: - id: check-useless-excludes - id: check-hooks-apply - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' - id: mixed-line-ending args: [--fix=lf] - id: check-toml - id: check-json - id: check-yaml exclude: '(conda/meta.yaml|.pep8speaks.yml)' # use ffffff (black fork) for single quote normalization # (otherwise switch to black for double quotes) - repo: https://github.com/grktsh/ffffff rev: v2020.8.31 hooks: - id: ffffff name: "Format code (ffffff)" exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' language_version: python3 # - repo: "https://github.com/psf/black" # rev: "21.9b0" # hooks: # - id: "black" # language_version: python3 # name: "Format code (black)" # exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_pystache.py$)' - repo: "https://github.com/asottile/blacken-docs" rev: "v1.11.0" hooks: - id: "blacken-docs" name: "Format docs (blacken-docs)" args: ["-l", "64"] additional_dependencies: - "black==21.9b0" - repo: https://github.com/PyCQA/doc8 rev: 0.9.1 hooks: - id: doc8 args: - '--max-line-length=90' - '--ignore=D001' - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 hooks: - id: rst-backticks # exclude: ChangeLog\.rst$ - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/myint/autoflake rev: v1.4 hooks: - id: autoflake exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' args: - --in-place - --remove-all-unused-imports - --remove-duplicate-keys - --remove-unused-variables - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' additional_dependencies: ["flake8-bugbear"] - repo: https://github.com/PyCQA/bandit rev: 1.7.0 hooks: - id: bandit args: ["-ll", "-b", "bandit_baseline.json"] - repo: https://github.com/PyCQA/pylint rev: v2.11.1 hooks: - id: pylint exclude: '(.*tests/.*|.*test.py$|^setup.py$|^test_.*.py$)' args: [ "-rn", "-sn", "--fail-under=8.10", ] - repo: https://github.com/lovesegfault/beautysh rev: v6.2.1 hooks: - id: beautysh ci: autofix_commit_msg: | [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci autofix_prs: false autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' autoupdate_schedule: weekly skip: [] submodules: false # re-running a pull request: you can trigger a re-run on a pull request by # commenting pre-commit.ci run (must appear on a line by itself). # skipping push runs: skip a run by putting [skip ci], [ci skip], # [skip pre-commit.ci], or [pre-commit.ci skip] in the commit message. pystache-0.6.0/.pylintrc000066400000000000000000000004551413701016200152010ustar00rootroot00000000000000# https://github.com/cmheisel/pylintrcs/blob/master/pylintrc [MASTER] #ignore=pystache/tests ignore-paths= .*tests/, .*test.py$, profile=no jobs=1 suggestion-mode=yes [MESSAGES CONTROL] disable= too-few-public-methods, [REPORTS] output-format=colorized [FORMAT] max-line-length=110 pystache-0.6.0/HISTORY.md000066400000000000000000000161561413701016200150240ustar00rootroot00000000000000History ======= **Note:** Official support for Python 2.7 will end with Pystache version 0.6.0. 0.6.0 (2021-03-04) ------------------ - Bump spec versions to latest => v1.1.3 - Modernize python and CI tools, update docs/doctests - Update unicode conversion test for py3-only - Add pep8speaks cfg, cleanup warnings - Remove superfluous setup test/unused imports - Add conda recipe/CI build 0.5.6 (2021-02-28) ------------------ - Use correct wheel name in release workflow, limit wheels - Add install check/test of downloaded wheel - Update/add ci workflows and tox cfg, bump to next dev0 version 0.5.5 (2020-12-16) ------------------ - fix document processing, update pandoc args and history - add release.yml to CI, test env settings - fix bogus commit message, update versions and tox cf - add post-test steps for building pkgs with/without doc updates - add CI build check, fix MANIFEST.in pruning 0.5.4-2 (2020-11-09) -------------------- - Merge pull request #1 from sarnold/rebase-up - Bugfix: test_specloader.py: fix test_find__with_directory on other OSs - Bugfix: pystache/loader.py: remove stray windows line-endings - fix crufty (and insecure) http urls - Bugfix: modernize python versions (keep py27) and fix spec_test load cmd 0.5.4 (2014-07-11) ------------------ - Bugfix: made test with filenames OS agnostic (issue \#162). 0.5.3 (2012-11-03) ------------------ - Added ability to customize string coercion (e.g. to have None render as `''`) (issue \#130). - Added Renderer.render_name() to render a template by name (issue \#122). - Added TemplateSpec.template_path to specify an absolute path to a template (issue \#41). - Added option of raising errors on missing tags/partials: `Renderer(missing_tags='strict')` (issue \#110). - Added support for finding and loading templates by file name in addition to by template name (issue \#127). [xgecko] - Added a `parse()` function that yields a printable, pre-compiled parse tree. - Added support for rendering pre-compiled templates. - Added Python 3.3 to the list of supported versions. - Added support for [PyPy](http://pypy.org/) (issue \#125). - Added support for [Travis CI](http://travis-ci.org) (issue \#124). [msabramo] - Bugfix: `defaults.DELIMITERS` can now be changed at runtime (issue \#135). [bennoleslie] - Bugfix: exceptions raised from a property are no longer swallowed when getting a key from a context stack (issue \#110). - Bugfix: lambda section values can now return non-ascii, non-unicode strings (issue \#118). - Bugfix: allow `test_pystache.py` and `tox` to pass when run from a downloaded sdist (i.e. without the spec test directory). - Convert HISTORY and README files from reST to Markdown. - More robust handling of byte strings in Python 3. - Added Creative Commons license for David Phillips's logo. 0.5.2 (2012-05-03) ------------------ - Added support for dot notation and version 1.1.2 of the spec (issue \#99). [rbp] - Missing partials now render as empty string per latest version of spec (issue \#115). - Bugfix: falsey values now coerced to strings using str(). - Bugfix: lambda return values for sections no longer pushed onto context stack (issue \#113). - Bugfix: lists of lambdas for sections were not rendered (issue \#114). 0.5.1 (2012-04-24) ------------------ - Added support for Python 3.1 and 3.2. - Added tox support to test multiple Python versions. - Added test script entry point: pystache-test. - Added \_\_version\_\_ package attribute. - Test harness now supports both YAML and JSON forms of Mustache spec. - Test harness no longer requires nose. 0.5.0 (2012-04-03) ------------------ This version represents a major rewrite and refactoring of the code base that also adds features and fixes many bugs. All functionality and nearly all unit tests have been preserved. However, some backwards incompatible changes to the API have been made. Below is a selection of some of the changes (not exhaustive). Highlights: - Pystache now passes all tests in version 1.0.3 of the [Mustache spec](https://github.com/mustache/spec). [pvande] - Removed View class: it is no longer necessary to subclass from View or from any other class to create a view. - Replaced Template with Renderer class: template rendering behavior can be modified via the Renderer constructor or by setting attributes on a Renderer instance. - Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. - Introduced separation of concerns and removed circular dependencies (e.g. between Template and View classes, cf. [issue \#13](https://github.com/defunkt/pystache/issues/13)). - Unicode now used consistently throughout the rendering process. - Expanded test coverage: nosetests now runs doctests and \~105 test cases from the Mustache spec (increasing the number of tests from 56 to \~315). - Added a rudimentary benchmarking script to gauge performance while refactoring. - Extensive documentation added (e.g. docstrings). Other changes: - Added a command-line interface. [vrde] - The main rendering class now accepts a custom partial loader (e.g. a dictionary) and a custom escape function. - Non-ascii characters in str strings are now supported while rendering. - Added string encoding, file encoding, and errors options for decoding to unicode. - Removed the output encoding option. - Removed the use of markupsafe. Bug fixes: - Context values no longer processed as template strings. [jakearchibald] - Whitespace surrounding sections is no longer altered, per the spec. [heliodor] - Zeroes now render correctly when using PyPy. [alex] - Multline comments now permitted. [fczuardi] - Extensionless template files are now supported. - Passing `**kwargs` to `Template()` no longer modifies the context. - Passing `**kwargs` to `Template()` with no context no longer raises an exception. 0.4.1 (2012-03-25) ------------------ - Added support for Python 2.4. [wangtz, jvantuyl] 0.4.0 (2011-01-12) ------------------ - Add support for nested contexts (within template and view) - Add support for inverted lists - Decoupled template loading 0.3.1 (2010-05-07) ------------------ - Fix package 0.3.0 (2010-05-03) ------------------ - View.template\_path can now hold a list of path - Add {{& blah}} as an alias for {{{ blah }}} - Higher Order Sections - Inverted sections 0.2.0 (2010-02-15) ------------------ - Bugfix: Methods returning False or None are not rendered - Bugfix: Don't render an empty string when a tag's value is 0. [enaeseth] - Add support for using non-callables as View attributes. [joshthecoder] - Allow using View instances as attributes. [joshthecoder] - Support for Unicode and non-ASCII-encoded bytestring output. [enaeseth] - Template file encoding awareness. [enaeseth] 0.1.1 (2009-11-13) ------------------ - Ensure we're dealing with strings, always - Tests can be run by executing the test file directly 0.1.0 (2009-11-12) ------------------ - First release pystache-0.6.0/LICENSE000066400000000000000000000021351413701016200143360ustar00rootroot00000000000000Copyright (C) 2012 Chris Jerdonek. All rights reserved. Copyright (c) 2009 Chris Wanstrath Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pystache-0.6.0/MANIFEST.in000066400000000000000000000007471413701016200150760ustar00rootroot00000000000000include README.md HISTORY.md TODO.md LICENSE include setup_description.rst include tox.ini include test_pystache.py # You cannot use package_data, for example, to include data files in a # source distribution when using Distribute. recursive-include pystache/tests *.mustache *.txt # We deliberately exclude the gh/ directory because it contains copies # of resources needed only for the web page hosted on GitHub (via the # gh-pages branch). exclude *.ini *travis* prune gh prune .git* pystache-0.6.0/README.md000066400000000000000000000255541413701016200146220ustar00rootroot00000000000000Pystache ======== [![CI](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/ci.yml/badge.svg)](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/ci.yml) [![Conda](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/conda.yml/badge.svg)](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/conda.yml) [![Coverage](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/coverage.yml/badge.svg)](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/coverage.yml) [![Security check - Bandit](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/bandit.yml/badge.svg)](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/bandit.yml) [![Release](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/release.yml/badge.svg)](https://github.com/PennyDreadfulMTG/pystache/actions/workflows/release.yml) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) [![Maintainability](https://api.codeclimate.com/v1/badges/a8fa1bf4638bfc6581b6/maintainability)](https://codeclimate.com/github/PennyDreadfulMTG/pystache/maintainability) [![Latest release](https://img.shields.io/github/v/release/PennyDreadfulMTG/pystache?include_prereleases)](https://github.com/PennyDreadfulMTG/pystache/releases/latest) [![License](https://img.shields.io/github/license/PennyDreadfulMTG/pystache)](https://github.com/PennyDreadfulMTG/pystache/blob/master/LICENSE) [![Python](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) This updated fork of Pystache is currently tested on Python 3.6+ and in Conda, on Linux, Macos, and Windows (Python 2.7 support has been removed). ![](gh/images/logo_phillips_small.png "mustachioed, monocled snake by David Phillips") [Pystache](http://PennyDreadfulMTG.github.com/pystache) is a Python implementation of [Mustache](http://mustache.github.com/). Mustache is a framework-agnostic, logic-free templating system inspired by [ctemplate](https://code.google.com/p/google-ctemplate/) and et by Ivan Formichev. Like ctemplate, Mustache "emphasizes separating logic from presentation: it is impossible to embed application logic in this template language." The [mustache(5)](https://mustache.github.com/mustache.5.html) man page provides a good introduction to Mustache's syntax. For a more complete (and more current) description of Mustache's behavior, see the official [Mustache spec](https://github.com/mustache/spec). Pystache is [semantically versioned](http://semver.org) and older versions can still be found on [PyPI](http://pypi.python.org/pypi/pystache). This version of Pystache now passes all tests in [version 1.1.3](https://github.com/mustache/spec/tree/v1.1.3) of the spec. Making Changes & Contributing ----------------------------- This repo is now pre-commit enabled for various linting and format checks (and in many cases, automatic fixes). The checks run on commit/push and will fail the commit (if not clean) with some checks performing simple file corrections. Simply review the changes and adjust (if needed) then `git add` the files and continue. If other checks fail on commit, the failure display should explain the error types and line numbers. Note you must fix any fatal errors for the commit to succeed; some errors should be fixed automatically (use `git status` and ` git diff` to review any changes). Note `pylint` is the primary check that requires your own input, as well as a decision as to the appropriate fix action. You must fix any `pylint` warnings (relative to the baseline config score) for the commit to succeed. See the pre-commit docs under `docs/dev/` for more information. - [pre-commit-config](docs/dev/pre-commit-config.rst) - [pre-commit-usage](docs/dev/pre-commit-usage.rst) You will need to install pre-commit before contributing any changes; installing it using your system's package manager is recommended, otherwise install with pip into your local user's virtual environment using something like: $ sudo emerge pre-commit --or-- $ pip install pre-commit then install it into the repo you just cloned: $ git clone https://github.com/PennyDreadfulMTG/pystache $ cd pystache $ pre-commit install $ pre-commit install-hooks It's usually a good idea to update the hooks to the latest version: pre-commit autoupdate Requirements ------------ Pystache is tested with-- - Python 3.6 - Python 3.7 - Python 3.8 - Python 3.9 - Conda (py36-py39) [Distribute](http://packages.python.org/distribute/) (the setuptools fork) is no longer required over [setuptools](http://pypi.python.org/pypi/setuptools), as the current packaging is now PEP517-compliant. JSON support is needed only for the command-line interface and to run the spec tests; PyYAML can still be used (see the Develop section). Official support for Python 2 will end with Pystache version 0.6.0. Install It ---------- pip install -U pystache -f https://github.com/PennyDreadfulMTG/pystache/releases/ And test it: pystache-test To install and test from source (e.g. from GitHub), see the Develop section. Use It ------ >>> import pystache >>> print(pystache.render('Hi {{person}}!', {'person': 'Mom'})) Hi Mom! You can also create dedicated view classes to hold your view logic. Here's your view class (in ../pystache/tests/examples/readme.py): class SayHello(object): def to(self): return "Pizza" Instantiating like so: >>> from pystache.tests.examples.readme import SayHello >>> hello = SayHello() Then your template, say\_hello.mustache (by default in the same directory as your class definition): Hello, {{to}}! Pull it together: >>> renderer = pystache.Renderer() >>> print(renderer.render(hello)) Hello, Pizza! For greater control over rendering (e.g. to specify a custom template directory), use the `Renderer` class like above. One can pass attributes to the Renderer class constructor or set them on a Renderer instance. To customize template loading on a per-view basis, subclass `TemplateSpec`. See the docstrings of the [Renderer](https://github.com/PennyDreadfulMTG/pystache/blob/master/pystache/renderer.py) class and [TemplateSpec](https://github.com/PennyDreadfulMTG/pystache/blob/master/pystache/template_spec.py) class for more information. You can also pre-parse a template: >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") >>> print(parsed) ['Hey ', _SectionNode(key='who', index_begin=12, index_end=18, parsed=[_EscapeNode(key='.'), '!'])] And then: >>> print(renderer.render(parsed, {'who': 'Pops'})) Hey Pops! >>> print(renderer.render(parsed, {'who': 'you'})) Hey you! Unicode ------- This section describes how Pystache handles unicode, strings, and encodings. Internally, Pystache uses [only unicode strings](https://docs.python.org/howto/unicode.html#tips-for-writing-unicode-aware-programs). For input, Pystache accepts both unicode strings and byte strings. For output, Pystache's template rendering methods return only unicode. Pystache's `Renderer` class supports a number of attributes to control how Pystache converts byte strings to unicode on input. These include the `file_encoding`, `string_encoding`, and `decode_errors` attributes. The `file_encoding` attribute is the encoding the renderer uses to convert to unicode any files read from the file system. Similarly, `string_encoding` is the encoding the renderer uses to convert any other byte strings encountered during the rendering process into unicode (e.g. context values that are encoded byte strings). The `decode_errors` attribute is what the renderer passes as the `errors` argument to `str()`. The valid values for this argument are `strict`, `ignore`, and `replace`. Each of these attributes can be set via the `Renderer` class's constructor using a keyword argument of the same name. See the Renderer class's docstrings for further details. In addition, the `file_encoding` attribute can be controlled on a per-view basis by subclassing the `TemplateSpec` class. When not specified explicitly, these attributes default to values set in Pystache's `defaults` module. Develop ------- To test from a source distribution (without installing)-- python test_pystache.py To test Pystache with multiple versions of Python (with a single command!) and different platforms, you can use [tox](http://pypi.python.org/pypi/tox): pip install tox tox -e setup To run tests on multiple versions with coverage, run: tox -e py38-linux,py39-linux # for example (substitute your platform above, eg, macos or windows) The source distribution tests also include doctests and tests from the Mustache spec. To include tests from the Mustache spec in your test runs: git submodule update --init The test harness parses the spec's (more human-readable) yaml files if [PyYAML](https://pypi.python.org/pypi/PyYAML) is present. Otherwise, it parses the json files. To install PyYAML-- pip install pyyaml Once the submodule is available, you can run the full test set with: tox -e setup . ext/spec/specs To run a subset of the tests, you can use [nose](https://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html): pip install nose nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present Mailing List (old) ------------------ There is(was) a [mailing list](http://librelist.com/browser/pystache/). Note that there is a bit of a delay between posting a message and seeing it appear in the mailing list archive. Credits ------- >>> import pystache >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek','refurbisher': 'Steve Arnold', 'new_maintainer': 'Thomas David Baker' } >>> print(pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}\nRefurbisher: {{refurbisher}}\nNew maintainer: {{new_maintainer}}", context)) Author: Chris Wanstrath Maintainer: Chris Jerdonek Refurbisher: Steve Arnold New maintainer: Thomas David Baker Pystache logo by [David Phillips](https://davidphillips.us/) is licensed under a [Creative Commons Attribution-ShareAlike 3.0 Unported License](https://creativecommons.org/licenses/by-sa/3.0/deed.en_US). ![](https://i.creativecommons.org/l/by-sa/3.0/88x31.png "Creative Commons Attribution-ShareAlike 3.0 Unported License") pystache-0.6.0/TODO.md000066400000000000000000000012351413701016200144200ustar00rootroot00000000000000TODO ==== In development branch: * Figure out a way to suppress center alignment of images in reST output. * Add a unit test for the change made in 7ea8e7180c41. This is with regard to not requiring spec tests when running tests from a downloaded sdist. * End support for Python 2.7 (done as of 03/03/21 - SA) * Release 0.6.0 on github, make a pypi account (SA) * Turn the benchmarking script at pystache/tests/benchmark.py into a command in pystache/commands, or make it a subcommand of one of the existing commands (i.e. using a command argument). * Provide support for logging in at least one of the commands. * Combine pystache-test with the main command. pystache-0.6.0/bandit_baseline.json000066400000000000000000000441351413701016200173350ustar00rootroot00000000000000{ "errors": [], "generated_at": "2021-10-09T22:51:02Z", "metrics": { "_totals": { "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, "loc": 4377, "nosec": 0 }, "pystache/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/commands/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 3, "nosec": 0 }, "pystache/commands/render.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 41, "nosec": 0 }, "pystache/commands/test.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 9, "nosec": 0 }, "pystache/common.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 33, "nosec": 0 }, "pystache/context.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 218, "nosec": 0 }, "pystache/defaults.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 20, "nosec": 0 }, "pystache/init.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 12, "nosec": 0 }, "pystache/loader.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 114, "nosec": 0 }, "pystache/locator.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 113, "nosec": 0 }, "pystache/parsed.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 33, "nosec": 0 }, "pystache/parser.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 252, "nosec": 0 }, "pystache/renderengine.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 114, "nosec": 0 }, "pystache/renderer.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 331, "nosec": 0 }, "pystache/specloader.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 55, "nosec": 0 }, "pystache/template_spec.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 35, "nosec": 0 }, "pystache/tests/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 3, "nosec": 0 }, "pystache/tests/benchmark.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 71, "nosec": 0 }, "pystache/tests/common.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 151, "nosec": 0 }, "pystache/tests/data/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 3, "nosec": 0 }, "pystache/tests/data/locator/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 3, "nosec": 0 }, "pystache/tests/data/views.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 11, "nosec": 0 }, "pystache/tests/doctesting.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 51, "nosec": 0 }, "pystache/tests/examples/__init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 3, "nosec": 0 }, "pystache/tests/examples/comments.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/tests/examples/complex.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 18, "nosec": 0 }, "pystache/tests/examples/delimiters.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 10, "nosec": 0 }, "pystache/tests/examples/double_section.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 8, "nosec": 0 }, "pystache/tests/examples/escaped.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/tests/examples/inverted.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 21, "nosec": 0 }, "pystache/tests/examples/lambdas.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 26, "nosec": 0 }, "pystache/tests/examples/nested_context.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 21, "nosec": 0 }, "pystache/tests/examples/partials_with_lambdas.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 7, "nosec": 0 }, "pystache/tests/examples/readme.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/tests/examples/simple.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 9, "nosec": 0 }, "pystache/tests/examples/template_partial.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 17, "nosec": 0 }, "pystache/tests/examples/unescaped.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/tests/examples/unicode_input.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 8, "nosec": 0 }, "pystache/tests/examples/unicode_output.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 6, "nosec": 0 }, "pystache/tests/main.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 89, "nosec": 0 }, "pystache/tests/spectesting.py": { "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, "loc": 161, "nosec": 0 }, "pystache/tests/test___init__.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 22, "nosec": 0 }, "pystache/tests/test_commands.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 27, "nosec": 0 }, "pystache/tests/test_context.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 344, "nosec": 0 }, "pystache/tests/test_defaults.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 49, "nosec": 0 }, "pystache/tests/test_examples.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 76, "nosec": 0 }, "pystache/tests/test_loader.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 141, "nosec": 0 }, "pystache/tests/test_locator.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 109, "nosec": 0 }, "pystache/tests/test_parser.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 15, "nosec": 0 }, "pystache/tests/test_pystache.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 93, "nosec": 0 }, "pystache/tests/test_renderengine.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 526, "nosec": 0 }, "pystache/tests/test_renderer.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 505, "nosec": 0 }, "pystache/tests/test_simple.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 57, "nosec": 0 }, "pystache/tests/test_specloader.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, "loc": 303, "nosec": 0 } }, "results": [ { "code": "239 # ast.literal_eval will not work here => lambda expression\n240 return eval(value['python'], {})\n241 \n", "filename": "pystache/tests/spectesting.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of possibly insecure function - consider using safer ast.literal_eval.", "line_number": 240, "line_range": [ 240 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b307-eval", "test_id": "B307", "test_name": "blacklist" } ] } pystache-0.6.0/conda/000077500000000000000000000000001413701016200144145ustar00rootroot00000000000000pystache-0.6.0/conda/meta.yaml000066400000000000000000000015241413701016200162300ustar00rootroot00000000000000{% set name = "pystache" %} {% set version = "0.6.0.dev0" %} package: name: {{ name|lower }} version: {{ version }} source: path: .. build: number: 0 script: {{ PYTHON }} -m pip install . --no-deps --ignore-installed -vvv noarch: python entry_points: - pystache = pystache.commands.render:main - pystache-test = pystache.commands.test:main requirements: build: - python - setuptools run: - python test: imports: - pystache - pystache.commands - pystache.tests - pystache.tests.data - pystache.tests.data.locator - pystache.tests.examples commands: - pystache --help - pystache-test about: home: https://github.com/sarnold/pystache license: MIT license_family: MIT license_file: LICENSE summary: Mustache for Python extra: recipe-maintainers: - sarnold pystache-0.6.0/docs/000077500000000000000000000000001413701016200142605ustar00rootroot00000000000000pystache-0.6.0/docs/dev/000077500000000000000000000000001413701016200150365ustar00rootroot00000000000000pystache-0.6.0/docs/dev/generate_changelog.rst000066400000000000000000000073001413701016200213710ustar00rootroot00000000000000Changelog Generation ==================== Changelogs help document important changes. We use an updated version of gitchangelog_ to produce a nice Github Release page (or just generate a shell SVD-style document) using the gitchangelog-action_ in the Release workflow. .. _gitchangelog: https://github.com/sarnold/gitchangelog .. _gitchangelog-action: https://github.com/marketplace/actions/gitchangelog-action To generate a (full) changelog from the repository root, run: .. code-block:: bash (venv) $ gitchangelog You can use ``gitchangelog`` to create the changelog automatically. It examines git commit history and uses custom "filters" to produce its output. The configuration for this is in the file ``.gitchangelog.rc``. To make your changelog even more useful/readable, you should use good commit messages and consider using the gitchangelog message modifiers. Since the ``.gitchangelog.rc`` is actually written in Python, it becomes quite dynamic, thus the configured modifiers and associated documentation are usually documented in the file itself (unless someone strips out all the comments). For this config, the message format uses 3 types of modifier:: Message Format ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] Description ACTION is one of 'chg', 'fix', 'new' Is WHAT the change is about. 'chg' is for refactor, small improvement, cosmetic changes... 'fix' is for bug fixes 'new' is for new features, big improvement AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' Is WHO is concerned by the change. 'dev' is for developers (API changes, refactors...) 'usr' is for final users (UI changes) 'pkg' is for packagers (packaging changes) 'test' is for testers (test only related changes) 'doc' is for doc guys (doc only changes) COMMIT_MSG is ... well ... the commit message itself. TAGs are additional adjective as 'refactor' 'minor' 'cosmetic' They are preceded with a '!' or a '@' (prefer the former, as the latter is wrongly interpreted in github.) Commonly used tags are: 'refactor' is obviously for refactoring code only 'minor' is for a very meaningless change (a typo, adding a comment) 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 'wip' is for partial functionality but complete sub-functionality. Example: new: usr: support of bazaar implemented chg: re-indented some lines !cosmetic new: dev: updated code to be compatible with last version of killer lib. fix: pkg: updated year of license coverage. new: test: added a bunch of test around user usability of feature X. fix: typo in spelling my name in comment. !minor See the current `.gitchangelog.rc`_ in the repo for more details. Read more about ``gitchangelog`` here_. .. _.gitchangelog.rc: https://github.com/VCTLabs/redis-ipc/blob/develop/.gitchangelog.rc .. _here: https://github.com/sarnold/gitchangelog Git Tags -------- Git tags are a way to bookmark commits, and come in two varieties: lightweight and signed/annotated. Both signed and annotated tags contain author information and when used they will help organize the changelog. To create an annotated tag for a version ``0.1.1`` release: .. code-block:: bash $ git tag -a v0.1.1 -m "v0.1.1" Using tags like this will break the changelog into sections based on versions. If you forgot to make a tag you can checkout an old commit and make the tag (don't forget to adjust the date - you may want to google this...) Sections -------- The sections in the changelog are created from the git log commit messages, and are parsed using the regex defined in the ``.gitchangelog.rc`` configuration file. pystache-0.6.0/docs/dev/pre-commit-config.rst000066400000000000000000000103401413701016200211050ustar00rootroot00000000000000================================================== Contents of the ``.pre-commit-config.yaml`` file ================================================== The file ``.pre-commit-config.yaml`` is used to configure the program ``pre-commit``, which controls the setup and execution of `Git hooks`_. The ``.pre-commit-config.yaml`` file has a list of git repos, each repo may define one or more hooks. In this document we will review the various hooks. Some of the hooks will modify files, some will not. .. _pre-commit: https://pre-commit.com .. _Git hooks: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks Hook Descriptions ================= Basic warning checks include: * ``check-added-large-files`` * ``check-case-conflict`` * ``check-executables-have-shebangs`` * ``check-shebang-scripts-are-executable`` * ``check-merge-conflict`` * ``detect-private-key`` ``end-of-file-fixer`` --------------------- This will modify files by making sure that each file ends in a blank line. If a commit fails due to this hook, just commit again. ``trailing-whitespace`` ----------------------- This will modify files by ensuring there is no trailing whitespace on any line. If a commit fails due to this hook, just commit again. ``mixed-line-ending`` --------------------- This will modify files by ensuring there are no mixed line endings in any file. If a commit fails due to this hook, just commit again. ``check-yaml`` -------------- This will NOT modify files. It will examine YAML files and report any issues. The rules for its configuration are defined in ``.pre-commit-config.yaml`` in the ``exclude`` section. If a commit fails due to this hook, all reported issues must be manually fixed before committing again. ``ffffff`` ---------- (fork of ``black`` with single-quote normalization) This will modify Python files by re-formatting the code. The rules for the formatting are defined in ``.pre-commit-config.yaml`` in the ``args`` section, and should match the rules in ``pyproject.toml`` (for example the line-length must be the same). If a commit fails due to this hook, just commit again. ``flake8`` ---------- This will NOT modify files. It will examine Python files for adherence to PEP8 and report any issues. Typically ``black`` will correct any issues that ``flake8`` may find. The rules for this are defined in ``.flake8``, and must be carefully selected to be compatible with ``black``. If a commit fails due to this hook, all reported issues must be manually fixed before committing again (if not corrected by black/ffffff). ``autoflake`` ------------- This will modify Python files by re-formatting the code. The rules for the formatting are defined in ``.pre-commit-config.yaml`` in the ``args`` section. ``pylint`` ---------- This will NOT modify files. It will examine Python files for errors and code smells, and offer suggestions for refactoring. The rules for the formatting and minimum score are defined in ``.pre-commit-config.yaml`` in the ``args`` section. If the score falls below the minimum, the commit will fail and you must correct it manually before committing again. ``bandit`` ---------- This will NOT modify files. It will examine Python files for security issues and report any potential problems. There is currently one allowed issue (in the baseline.json file) in the spec testing code. Any issues found in non-test code must be resolved manually before committing again. ``beautysh`` ------------ This will modify files. It will examine shell files and fix some formatting issues. The rules for its configuration are defined in ``.pre-commit-config.yaml`` in the ``args`` section. If a commit fails due to this hook, review the proposed changes in the console, and check the files using ``git diff ...`` Doc formatting (.rst files) --------------------------- * blacken-docs * doc8 * pygrep - rst-backticks - rst-directive-colons - rst-inline-touching-normal The blacken-docs tool will check for (and correct) any issues with python code blocks in documentation files; the latter checks will NOT modify files. They will examine all RST files (except ChangeLog.rst) and report any issues. If a commit fails due to the (non)blacken-docs hooks, all reported issues must be manually fixed before committing again. pystache-0.6.0/docs/dev/pre-commit-usage.rst000066400000000000000000000040111413701016200207420ustar00rootroot00000000000000================== Using Pre-Commit ================== `pre-commit`_ is a program used to configure and run Git hooks. These hooks can be triggered in different Git stages, though typically we use them in only commit and push stages. See the `pre-commit config contents`_ document for descriptions of the current hooks. Each of the hooks will run in its own small virtual environment. .. _pre-commit: https://pre-commit.com .. _pre-commit config contents: pre-commit-config.rst Setup ----- The program must be installed and the hooks must be configured. The program should be installed in your usual virtual environment, for example, "venv" (this could also be a conda environment). After activating your environment, run the following commands: .. code-block:: bash (venv) $ pip install pre-commit (venv) $ pre-commit install (venv) $ pre-commit install-hooks (venv) $ pre-commit autoupdate Automatic Usage --------------- In normal usage, ``pre-commit`` will trigger with every ``git commit`` and every ``git push``. The hooks that trigger in each stage can be configured by editing the ``.pre-commit-config.yaml`` file. The files that have changed will be passed to the various hooks before the git operation completes. If one of the hooks exits with a non-zero exit-code, then the commit (or push) will fail. Manual Usage ------------ To manually trigger ``pre-commit`` to run all hooks on CHANGED files: .. code-block:: bash (venv) $ pre-commit run To manually trigger ``pre-commit`` to run all hooks on ALL files, regardless if they are changed or not: .. code-block:: bash (venv) $ pre-commit run --all-files To manually trigger ``pre-commit`` to run a single hook on changed files: .. code-block:: bash (venv) $ pre-commit run To manually trigger ``pre-commit`` to run a single hook on all files: .. code-block:: bash (venv) $ pre-commit run --all-files For example, to run ``pylint`` on all files: .. code-block:: bash (venv) $ pre-commit run pylint --all-files pystache-0.6.0/ext/000077500000000000000000000000001413701016200141305ustar00rootroot00000000000000pystache-0.6.0/ext/spec/000077500000000000000000000000001413701016200150625ustar00rootroot00000000000000pystache-0.6.0/gh/000077500000000000000000000000001413701016200137265ustar00rootroot00000000000000pystache-0.6.0/gh/fix_pkg_name.sh000077500000000000000000000017211413701016200167150ustar00rootroot00000000000000#!/usr/bin/env bash # # This fixes package name="." in coverage.xml or another coverage filename # as the only optional argument: ./fix_pkg_name.sh other-name.xml # We default to grepping pkg name from (python) setup.cfg # otherwise you should set the REAL_NAME environment override, eg: # # REAL_NAME="re2" ./fix_pkg_name.sh # # or export it first in your shell env. set -euo pipefail failures=0 trap 'failures=$((failures+1))' ERR COV_FILE=${1:-coverage.xml} REAL_NAME=${REAL_NAME:-""} VERBOSE="false" # set to "true" for extra output NAME_CHECK=$(grep -o 'name="."' "${COV_FILE}" || true) [[ -z "$NAME_CHECK" ]] && echo "Nothing to fix ..." && exit 0 [[ -n $REAL_NAME ]] || REAL_NAME=$(grep ^name setup.cfg | cut -d" " -f3) [[ -n $REAL_NAME ]] && sed -i -e "s|name=\".\"|name=\"${REAL_NAME}\"|" $COV_FILE [[ -n $REAL_NAME ]] && echo "Replaced \".\" with ${REAL_NAME} in ${COV_FILE} ..." if ((failures != 0)); then echo "Something went wrong !!!" exit 1 fi pystache-0.6.0/gh/images/000077500000000000000000000000001413701016200151735ustar00rootroot00000000000000pystache-0.6.0/gh/images/logo_phillips.png000066400000000000000000005230331413701016200205530ustar00rootroot00000000000000PNG  IHDRsְtEXtSoftwareAdobe ImageReadyqe<"iTXtXML:com.adobe.xmp IDATx\Wu.9SnU/,Ye[ݖ+6H x/K ɟ bJB(!y`lmjc\dY)g׮ksf\^IMsgwo1!ux80uz9}\~U{l䜧ov9}+/>f{]{̽|Oݴ799>C:aU=fs}oMLL p/߻odƷƊ}=ǭ_cyc)o3o?~APx|ڿ?q6`" v|6\~^z}y)8x1<<8es ̏r0/e0g>e?vpNbg'r38:k=W?K?Xv@uMNN ϵr ~ry 皑# l;BewJ`ѢmgyO]seYdD%t/C |~^ [0W\8n\oF掤C@.i>?{}O+k׬YC̏r0O lЍw{߿vLyxn/B9; {I,IN}O{MnpC#kjj*[|kk0 ^7 3y'򚵫x?|U|h!t(^};v X4+QR ,Yd^|󓯹;g+̏r0 Qj4~w:Ə />0 бiH-^f)BE)w1uR. TZ%nK>}~V#-VV_`~9(j-]TT}9^#hR >po(4SNA1J-i'sy- G _b]0;\Ӥ (T KGpGv^1^Z KS$q/֪[E]G3̖ #Nx'U&.ݿG)MB&LMC%}ʱI-e.܃yN?escy Y (0S%5@^Qъv,PWz*5 |\֝Nt\~w]:*G$s=C%,)}2w Ac|L$4OCkrJj6n %켔;>Na܃;a9v9`@.`j%p+p*Mu|\vRXtNش<{N=y{<:ZkB3`~ySmϻ{{&vK0ɉqhMTcڒ7Z-455Y)bj7Q ⼄yu0vhR+zEkj6O6( ǂjE>O5g#XN\_rzijO5>+Kxg (io,Az\:OMA1d͆<)@ 3%UG0 XDV0 E" XI㑠@.ܙ `AtLPߙKm w:L퀄!.Wyԡ$LTG%)o}ۧlgos\pޙ[lG< ܎$Ko;둙lF ?<}cp[{[udv-A|&19 ӓhO3i~^ +ˢ ˥su(Ds1L"]eЪcSbb%$ݤxy9=gBv5⎰!wvn/Ǣ}hƞkS&_3b\h6:EZkd&?'^,c?Oz38ֳyBz,a DD:t31ja02jΑ; ExR/aܼd'jP$Z7Wf`Lw0@-,PC}Ϟ=_{ŷW N8o_~z39:>p{bMI#G`@>+,d&SN<)5Fƭ ;wzxrߖ+lywY.=ʛ/<{<y9^V~(a 8=4&}&HCjr]o(z(gpn9rs\5uÑ}$\gH&*<#$ԭ=Sp$HS<2L $?ȣ|7Ot{UWn u*`Gf<[Uv(L X.7\cfbTn BTJV좠[X67z:7s焁m:7G(V;j[9_Gf(|fAsԚLʓnyؽp@ܖCI& yv<ӟ?/q[/HPiOy`v%yE ?<;З*m1LL -yR-Vjà41L5MU*a_0qd3FҐ|`+G/{br5trS6uރz:Os)H9 ƔAh0@6Cq0 tv{}.Y)O/}?v꽇b[r;9.쨍<2s,,C:γbQEFŷ9G<vpم]uŒDXko7g%w縖;E bٺ4RYA…+h"xhSs1ynk`RH1y^cN:^޳w_|%Ƴ}l>LKC2[DN.}:5DrFv,.չ< ⨷%v/e0w3E~.<):5+gȐO ?ÎQ.O9+g4, %$cY(agH`QV}燯nqן7Eܡw6h-_l|}{ܗ\n0摕/ĕ"x k-;qqs2t|LVɁ;%nrx  Pwά AܩٝV$8]=ɉ:;v2]8 Y;z3r+fXz ڼhOiV~gOtt ,B#%Lz/[Â4/a"sCkUݤ#3N_:`J@۝k o;_l6+G2G0 L;tفLKe)tx.`gCJpf : g\ܚhlg51`Sf?k13(@ L=]C>R=y-'7쌂0pQvyG>=5H^n93)nڸ^fN^^;/mA53^}fPulΫnt3R->Y #/Uk4xB" 6["z FY^hgM {@8=he,ub͙X.s{5UA2b]ީV*5]5qnVL)u^"(LS9ڰg_o|i<f^hҩ[29uhZ,:ozBƹI Lkta$+p윀pm.6EAv^he6AlgP坻S.x[o}_HYB\" ^>؍T?ŒmfDl!30Y9h&%Tk}*NQ ЗH G`g\YOXH( Vc_z&w@%(&YE>[:ᐬpgVu?~*+`vjFʦӃtq ~_ ЮY f\pkG=7*!L\ҡW>Ĩ#f6K;#cw73Q [̳*TҶn}e./g |٣k0<; ?9|77G0k!%F;lʰ0sAaaLée%LA2 }W*UtgcY)eE' `R8͜@Ewu_,Q+%;Ta$|Y;ut_~#32/`Xۡ}7P.^tŕW\ Q3kAtlܪf Aˁ|h0̗N2 l >]4a^DOWKm L*Ix0-'jREkX Unz6G_)ۙ?W,JQcpIai[%y\ q-2j !]+w0/2^l4LM@1i2IcWP7F%.Qdqژ0?NWcOTb-:\<lY`(!x3NR Ƙ) 9^0˂h9SzF aJ!7Ùv~Ȃ[$y08P4@^suޙYx\~uV 0rwޑ_o|^G0kF{mv=ꍙ͝'Tc=L4I X`$bW2uHA=xR `mbvr{[nv Vʱmȫ5Va$:2&P2vhM~72;/;G0ktyL-9̧B:H/DCަS$4&ji_rafٹX e=gRv#J*yOWrjعOBfC ;>A!#$ >rÍ~֡q-/m^=Cn%,}&酛fԛ^2_$M%4Ȉ-lQHkmoEӄF$n3wV4kv^3;* C&yYAOg]0=oIf4nׯۿR|`B[;$7A2v~8\ g%ꂺ-7Z>o31ݚRL]kVQL:`KS#h7D#U;42>Ou'hZ`^AqR|@/ _;ב,@[bPz>Pq-80![eɉFM,Hs׿k[Zx`>RM vŴ< % B\SY/n/ |8e,ֆT6 2yJk7d$1URhB#c[WЏWY;fא^1zz31PphAр.(_o'7n`B[K[L{}\$&737oSgrm8Qzў q3[J)?\`IJֿK;K]˾S7rIhÁ^7Klȉ=bi|Մ =M D'vR:]m+?x/㻿`y\qZ<:>Nt:gGҲa]b" bH+zzbal,8v1[5VOku蓗Ai{\a,t* Я+/Ү")?dc&*rHbɎ$~&g^1;(eSu{xJYkג)&⬉-A[ 2`'T8Bu3)&&^a0") mUT$W꺀Yrr% 'tWפy#[ }uaQhC142p0ѐw_uAsά7K5>>q죏Xnݚ}ki~]/qq=6DLV/U MH3cm 32PM!j"TW2 {>hehV Z1UZ1\5q @u@#vG+GYtFہ Y˝֮,`W\s֏NF0BA]g2%CZM|aF-0SUbxL\vVϕ(U?a+fm:O%x+G`u cg؉PMKhZEK>::|#λ~і._BW=pQZHkxb utA$-DM+E n!?gh<҅mKjc_ !ytut zIDEP=P@jL+fmYmtQA #d 㚥VCL *&YG%[6ˌh=a^nbFqJi1P1:IoQVLk+8}K%^};. 0* TDDCX>t@'5-[r$H`|w$y\ iٷٙΚE8U(v !u(#'@ G`|4-::/T|v|v,|Bnng,GH|T[,d-G6gUq - (`hFx!3(Ww co Df`b SPױ6Z%d\y@V+k }Z0k>ujZ[ˣv7EFAgaЉ<ǘtW胿+?;ΡOFnikNHN}D角KIfQǝHzOArܧ&e?Ĥr61;\т_Ň/y\ f7NsݟvŹrN"ޓRZS٥(7l= woicxၬN`ZZ!1 -@ BDoAk1vdnKQgш+q ` ZL[r8|Z/#hiȸ\pp*W5&euL샳6-\=\_q-d4Czۀbo2FaG rXvcYyX2D!ζҲ׀S0vBurfmR39x咔|NjYNI. ];G|iUy1^ U^Ut +{b_J\s˟v[b_,zl v拜ڒ k^te]wv7Qm̀ 0*bLL\%<*&^UZU1sVa \mJ6Y'5,p3X>2ώȈFv ٖd΍VH.eZm \VJj=GՖ:N+`dfa8t`p? _q-59:g||R84nokSbfG\%=mI9+`NO.NآOǽ]#j{fIrVfDô#+}5r啚4[3#;<vHŧVrot:@;"R;"t#B2^%HZ s)B~>1wibއqpy)zđW +՚Woꃋի՗4Ps"סZOT#hƇ0F`" %te^faae Pe7y;bY&PWhJӅY8>M9$D'SRS(F^b}PA+yHG0k!5w2&Ή ht,:-2K_g6(SD>C]%KiWvq-ucG-1fr o' HHG, z WRtΕ狛av.O3|8qX;1D2L;ٱrZt<(zKv#(,\uw֬ʑ%`Q$>c}& 6@-ur=#ΕFf:Oʾ{)| %, )3AK>u:IhNXyoE=_o֭[۾ ~~#uԯ}.h6FNumsyc:K.0K;,rX\po! aLbၤ%D#"#TC_]+X|j5<w|ɳm3ݱvph?|,\N>]3h.*@f#⋞l(sގEf$SXdw*z^ z/N>~G=nqk#|Zbf -3n69,evD 8ym_ƿ@Nf ҊHXVάqg `'t8<Zm;GX8YTZb 6|av~ sX气2s)H;a9,|E+q4DsZyZRXO望-?z\G:u=E;' "gT=nrXXyy sXfs+y+bεoy<}#u4~1m񳍹,+p:ۜXxIsُ?+ G:q'&ӱpy+b/9,{"*Zk:E2W“/ۗ=y\G횘l]mn4mgjkw"/O*ɹa)&$ʜ䰐غ9,۳ݠ4 V[Q^VaxCY[< ~?' '@';"+ :gGl-!#`,X憕ME֛/Num]z?:ZGn^̼A YW[[,sXXR:pbhO9,0/ X>nt\j"֊hW̟zihE`W\GȒ@rʯlWbcfTa:eeϹDa 9}`9,<qkO,n,A7)F" q'++b皕?ެZj_GV*d 'R ?C@43q6p@ܒV:0`XmG:1EOlVD,zǟ1y\ bmyhϯONLB݄݀6o Ceg?*vrXJ•t )wa0߳;\$VnVq-+ heбSi-' q"/.9Ò G%^l(E9X!qpAH+WVĊn =n:_ѾVKz[' eY%c)C%trX䰄cD0|D9,;<5AHiXTj]NhE̯J:{~9կz休ck}teLM7MNN& lTI۫W 1kV7O }d ;6_tx]]Ѓ&h@켅&"A}Р;0~9,9,,w w9,]Mfڽ4I@GbP_իVp<qju=x >3ZFn)> * Y1TTR^ȣ۟; kZ+ǭ_7;'^uپ{1h^ޖFkb-Q 懐2X9,툳aˁ氄cDa,\YyI>ԐbIìrlݯ/?ze4y\ f}Klx{?wyk٠ #m > :r'!72oTQ!4`;?>^Zn͍v-W\v|],NOO@ِ >^2ɶrP(gKda3?^sXX88p0>0߳PMCgl۷:y*<1AZfuVmGK;ώm4y\]>n f0]>!JUĬr/4eq7tѿ}7ٳ Antu!gdi&F4 2ߊuae90sӡg|F,/>{{U7kP׶xqγz#v&rsɚ[mmg"{ :rXp^Mac;cuaaR#:hKVF"V~IF/8돦 !"jUX}g"nSc4 c{ `m7P[CTRKAO@FBh S@d$S2M W*N:i$4ٜdm,kkPnt_͈,%" Cޙ9Oٜs@%+ |g?%5FfL`9vЪ;scLق+4r *=`NU䔀]Ca_r냪1hpp -^W~ҊMldq_wmZwKK{0wp2)}GYCnЉܠ܁RNٻ2WUFFjͦ%,s"V9v3?+EGsn< `O=9,,:ijf!۶O}ضhq֛yף7W1%UJdګ=._ {< voTLޓe:+\n9a)3ORЫrÝݯrsd߶vrp?Œ99+@נ*TSٌvDhGrv|%>6AR\,6L˶ۈ[ڶ%'hv#g< oypsw>:`Ֆ+27p*,^۶ q\qn"4*h Rd\/jx4lOiA,볥S( EPÝcBe^MR0>pbzP9,Y;ppޱ}#nc+aZıIVZtω-5y\G:笓wŏm߱e_-oS!t6ȡ%+%WĈtZڨS7fA .kaOx/&w(Ǩf$SUDV5UǧqZ8 ̲FvYA(x3acza☷n9,ejG.R$Œ9,saZ)z1L >5s=24/~MM?_彏bjݼw)_&6j<$IVm6dgj$Z 9M=hITX8v0$6VD0ZO5]o^1xѺlzZ睽q5ܵ6g-Xf [56t$I) H4k+axں޸$2Rؒ}UʭÛu.z%E.<)T'͠juej+6_e~޹\1-EKr1ҦF=C< 48`=hV:XɣVS,JFrq@I \`ݑq{AXY`<n9,p3 Yy>= <*DܺZ"8ڷqW>W\| Kx)hXz`M!nY<EK٘&NbQu BrB9hC`IuhX};\N~Pc|,\n218BG6X\3PގY{#V]BbRW^S`bWG" wi[n{˄IA1y0n, ]2ejO p`堝gT~)?M,PoDX䰔!t`g ffD1@ݱU+[_ a;`ל_w mlbح|z 62MJmugf3V[V厕ض?{LsyB#5'm{*> M,1\> ar陙nS^; Pq @{0`ȵD=yԕazYpOsKv=VyV:|7KVތ`W\X/`\ Brʧ mQ]䈿9pu04 %BX6qHC9.str6ecȾGr>+Xy%d嵁aX[ i`A蓗vۭEa.F,ZBGrݺo3`,b4qIɁv+@pB'00KE JcO$rʁ%X8'&?7cr-a^+.4eq?| DbV|*@M n;:`)cBʂDߊY8(2* >Ƨ2Z/v gNOǛ$ sKpeq]sX暕F!cusʏ;ng}B#u@}Ư펝=0$/ܦ  -} Zn\FZͩBZ)nQꖱҷuӴ)(ޭn@0:e Gޞ_3ܰdF.DsXX8A8 wua ٬|ҁ_~Ѧ<`W\r}='sƟ"L͘a^@ۻ.g# 9N}pڍr;%yb&6P9cpx Gn"Y1O=,D;GUYsn3\t(?CB?T5āua)c9 70sɈnϜeUӗ<)7=?=Yf#:ĜiYlN Ihl/Kޏk|ʅH } ptլbrn/[/MM9n1)# KP@ԃP)ʞ ҮA162 Uv,ys4`Q)3 *a0sF;bQVB+"Tuyj*ٞW\t2+`׬}ٷ}+Mۗ<]Im\zsj4N3n=a^ X4'L25`T$LJE?E&@a񌇬5x=@zE p60sHU2 H2|EvLS1(văa ~a .ի=,qj=雿?0w'>ol}cUꑇDH H r=5t]>Lnc~R09[7ݜɆZS{̇sۜa^ 5~fr`-2ɋ@O[m_o8w55;$AmX8 CKOΤX9WI2eZ5m-m32f|9w%OVlq؋ 'UɘMSݪӘΒ z_Dvw+ ݈6e(h颤* _I-8[2Z$ޯ? 9uMŧv\9/•ekGb剖Vה|/_޾uq|){y=':x)+@Oz4zRbQjV/I<d\69bI KDRi9-$hGQ u9) Hnh?H0d>.8˸|m1 V8V9V:IF+Ek/8}t"#uMwrp(ˠӹ<*8:|A[ɡ?pj9Pa2w!D:v.L0\5Ńg\PiC &y,qX wh LΘNZQbA׫ ^ZE%F0k ǭW5˖ )]-"VͰs{–ud ^+S17,9؜To4!|SG.A2s@KN)Usb1)V^8Ddd>!߮gmi$7X^^1ĪIJpH.:o V<9Z+/?yW-(tHO<t ' \˫Aק>!ܴo>++H|pk@wl ]qܻXQvRͿf45mJ\ĭn 1W5[^RV͕ᅟܺkU {FawKc '*}tNe 2jsxDŖdxqkHmce";ܐfV/̓r0=I)3 \%Qn'ׅ! z"OH,Mw-L*H*@]A ٯ $d}u'`-[_hm;5L] =T6*+ogfkO~frC}Or[A> hAD0bgJVR-..0A t v|irPgN@'g8rq&unfL)Йwo>'rqsXНydj$KRP7FkGT aVsL9j,[uqѺ{ &.VhN:^Ӡ^Y,~rF yZ*!N6}DJޏ`Y`aa4I H@۫yj}-T}vΘȱt/->W4<\o&Y~h-b0oB(\ta jrïG ,|1GA1ƙ5EP]PO yk[9\,څ ̃nRi >mIԍ7'廫6~ǣ x6 elӸF0 W5~+O{X R`UK,CK4X4 .vs4$kvy+φN'Wv"XuH.f&Sà|18 =4\SFiDyO0/0&L] 5Hط3\&1-oi -+ۆ;q-$(Let}50h <|011sSEFΙbc]3*@4%ߌ؅!GN"+k$~צuA$@v3&y5M~N‡v)yƋ/zp>|0a{-| o\ES rongqk`c[>?31sq߫]-ܣ#ZKGs%`jiR|fμ3"t::c-:pJzRGibkRֺH}G(sVImaQ~>'SEKhcu u]T ƗLӲy nLLTn N# leeǎԴJc(T>qk`AH+?19O{,]9Tn|Sb&F/Ҡ>=+-ӯ o_ !_~|nGGyhA8]r:z_.t8@A-'# z.D|g,3G$k?7ݰ($pGGClwt(/L? (CP(l0gK3&ږxmP"#uk/>G~&2i+\ESG4GFӒ|6S MjS_.[:x{՘ f:]I0mGPڎgݳaQЁ%3IjL̽nd =5Pw`(P֝\W֤.*c:m*mA [uNbYw/B VՋȿ}zZQIg~GV*#5˅ 22)];c=m!V%XAaSfsJ>`6'Zdg&% >&_[* }UJ>y^$urCK(N.EӰgPZ14ѐ.Ic)?)v߃)ٸt#w#9䀜v;\#ie`̝̂O˸UF0k }7|7}7bmi KM=M;>S}l*ڿG^ hLɟj\ O [Zw6'r>x~xlC/Gǖ L%G,ʔbșa`;\Vc:@?ߢt?p$`UBLWZ/S ݎ˔4tl$o|jJ9/:䥇^C Ylv+Ÿd1o۾R7αs9R]8j(4!V.?s9> :B-[1q'~9nocxm*2#T4O܂Qg٫yʵ<ϵ(R0DQ_oLJ-v8]f^~$'C@_x9 _ [ a @P.XXؿw'Z pT3[AYH=gRYzY1jVݣx`.#KLHxZh(a[ d1F iX@7 U.} t6_)!D~_oeJ4 >qX}C7nMIf>:*OvЮ\7x٦0lg2yeiцm=091X7xf\˙mNO˂h]wR#l Q{ !rҿ(Ͽ$k ĝ$>s߱`xx),Yz [b77<-6y\% -ٟ|Zm3TAKȲSY$!~XS+$A_ 1 }dD=i`o{ච?tcOc^&O*:(+}kfN;vyK36)m vI(Z t*#)nJr[Q*90(r[>:Ev0R2:;+>k}TAdW(Ƚ ׿ uʮv_),ɯ?رGl}]i Ͼq"5}(8*zt5s%^V@x}_qFIui].S:X4mH|tAl #ࢥb>:Wzn&ү"1F5YO _ȣ'2;pvb"*<@AC| VaE0*08"dF*n%Vˣ_w~V7O:q`Yٛ/Yk%DPNIJ%xT& $^R+2 3.VYkMr ׃?fStO-&:};MS%M1@@FLC1e"W\ F( 2,UZ=s߾G<7\w*K~ygwѧ?٪.>JNBddKD:ȨW:jk1ނ"QdqcVu *YRH^fE& `$)Sqpi>%yd j4'M bH|C,\E[5v % z2-{l Shn#}G_mӈ=M)os9wr@nh1%Vs?]30I7gn{Nlt.HnՐBT5< ?y-=z&//$D;ga._^"{k$r3̊lmNn *i}]|]NS:ExNw[޴h6hG/*N@~P$KW8JhXͫ\ UbJݏiZ@$K{v::qr'@~d׮|ӵfWu{_2G!62KdvZ輦)4vm5\hzMR/~Xinxٯs$ "ͽ0/gVo?11@ΤWu垲:s;h4dFS ;/d+s$6GD"$ c%unCSDUR]9 Z]zsaj4RèLm/u&8S)1܊eF0. +R.H|,]Еvu %M;P׭߽wi7pwa~Wph॑0T/3er#jeQ g3M)f|>]Di)[9mnӖMF'4X&R'7ϼ-7m>"s;vl+\&yt=_|;u]y_GCݨH!KCpVQ\?IZ `E6XcY)mK^^*?.\05υw>|X jsUe؁}Xλ7 ˨ y #Έ xQD= mzs1^`:vϳNAM}Ge]>]'M([Յ>+Q"pCuk̻4X\Ǟ4W{7ޮ |~ϻn? gll|cӢe%GNS.;>~i7 ܾV ր͹-/Q"dDSʗ&(HYG|aL,qF{G:(forv$I_4^7t wwZԊtfa󃽡ZbmڌqƀF}GrRMYdzn?>yGOl~띯g[>'?wjT.ȉPcC@~YD^BUբ7('XP0v3u+ئIa,x#tQ-rk̙Vac,+([ίzh/ܻFbQ7r’tNtrQ{l-7CDm[`Ѥs#K1%x+:%(7ݝ p ytSO}{nKTtMlٙŲz8LS,q̓}K67b3A}5X[o5?K^˗?{\h c)VEd nI5whuն!  ["Vsfj D 3q@@./g ϒ%nZN$Vy쯚."L0ݽ(jE,->On1,2msxm;%MxV>&c^#M2Ip/SQz(f|-NYu(**+Avtn~<mt4sh7]9ܺqNx8.݆|ԋ_l.FLGЁcguG;h(嬼GUd ݊hm#CN 8`K^?|qMLbI 7ӗ @ѶE'IĿSBǣO]||\?|?hm~˃{ŹN۹Yڊc)G<2""3 QJ\{O+ E>yjK Z[.ۅ'{axH$^9!^Gr355J(kMrFQ+wr*"lp*_椰 8`ٱo^\"t?ʈ霮g?ϥ/TsC/(;׻u*^sKXǦ4i[zl,(2G8 9bC^~f?.?2 Œ7̎nB婕pщcMmal=2eɧOMoxo,-30-?=v ?ְ5jizw1bbq9"hIMk9BRZI'rlDR 9o#uP:`8J6ب$1,쨌-˓4=(=iai %j92[)@8QsݝțBkjFܺ:n7D@))%4ƣ7y!S06ԍx%;mjqmof`>[.ԙG?Sl'pϵE AXa)` Y!"~G(rq] PWeCCFޠOg ~ipHA7[ƅrݕ$QJ#~4d?LD0"wWE_X ;Gҁ8<};mQ:'xB bT 77* 4hL$Ӿ7UaMo9nJt+7Lzmg9w0*Dwʛ~cϖj~'ϝi[nipR?(YX_來vIuݶRԨVx&w!gGjK2b߳5*G!9ENȳ7˴@)JT.xE|&]ps}WcME1iyuhdUKYN3P4yAۓ@h5ϩEwg+3mf1yԢ.WNѹRt>&^s'8_HL'N2Cpڈ+yVik/S9/_0co(DcFQNN Ut(O &MltOpSqXֈQ$DGC1p߽unݷRh&2F*r3t1X?\o d=_ixO|ZE~f4=INyLq$ywJ|Jxv?'s\h %bA[_?oϮYd>[.?έ?.?ne(OPAPiR ՚(: }8# 7Rꅁ/t E|)/*b_^\Ъ-_(^r)@⠗G#$E+ 5 ot0DK i|Yl>F;DAH}*Z(zno DԦs3xTU3Ե *U~=O|N灃&C#7Ci8wTDYut,oennk})2_zz?E7w@,Ct_/E!!tMY,0 laA`@4Q}igHpnqqk?Q?xʥ5Qu'dH%Tg ?wVxU -3}29-@ Gbڵ*ZsΞ|Ξ9&V.:2qK_t(*\BO1jŦi VGnGg`>[^~lRn!RtsD߹!km+LCf8$rSNs8DV \yD*]a_Hk0P@)T]>"f0=2yuЙv)'r-vh+G ;6V_u}Uoܫr~5:Hgu;JPN|AK[ #].-J~+;Gڻ-yk]k:XVbi+4\֦CD9i{{gAK5>Re]V*Nca[J42CzۂyjdHiVpPIPorazyFjp{noܱw.:O4bN}BK(l :sqTw4J'4kiuu~.آlz*>FQçOcŢvOvFUU !ol#}K<[>:?(|+7jv9fkuF*՗]NoQ*SRM}hԻ#GBVr1$+2@DLc<|(sd+#ep7ݍ떖đ#|ariǻ9G<*Wv5*NDI^ͱR;@k{!ATʮ-ӧg)UG4Dh:rO7P{Qg<2 ھ<~1:mpwQzHurrv1/ډi sFu輖XM.}n~ ER__|h~="n=G=/M ̝^vN7sLx->m\~L.x9~@JiמC{UC:q .;IJͤrƺS-{&rvDz,QڐzGMF* r^3ly.|H@J}\fWhJ䍮?IWH-)R }$BHhy)EBGswAGа֭ 뼽Q3D|Lf 1Bej"*@p\<$DLp7#ǎAxDž4ʐA0^QPÁzY=}vuwIVQA{\O]ft>Cí t/f׃zKnʀ:K\M|Q /@ԭNu{s8~#lDg5Itg`>[^ݻrW?SQ/) T&> U›FcFgNxR\(:unړwo2{.Jpo(;Mӏ4a  0LL209Fֵ{߂I_sѳG 9Z 0/uuUnwc{5UR=p ìHV}磵}_9sY\?), wة>[omځAvQT@hxrս^;D oЁ30-/峟EŶk#){%p> _;t$݉|;mkҀ=ED;-nmsl lG l|W*vx|(7{;*E(Ѯ>7=Ex Oą7KxYob^E<\(IMI bJDĹڳ4e!Q ̴kEBbƑP*?JH8P_\tPIZk>7DkS>Xr};OYר*P090U?N -S1P^}C'#C*cw'WL-rq"h~e:;Bհ?^ڽ*"z$>y׻zN9\>[1'9&*w.Of +=ػ[o[ݹŽ2O\{7["<ަ7wCUBeNLFۣgWQ-qG~0XueR`?;x/%ley޿]ՐH݁9TgFJ;29J`|蕩p^h:!ߑh<sLvnR2P]@r|%RD4dJ1T D6C26Mc0 E~~;N.ISxz]>+\zqe~0W?CgN}E=_}W\fbPh){hWYՀv Mk-4 c;gg R#_, M;;Vވ0IT}w 2&I \0BsgoHYKyxŏPZH"HsG~o{o| 30{Wo} S4V;\5.@%uO=!.OGNld$*@}e:tPj-ՇtͼT 5 bGՑF,Ѩ?a4.Fح g1JBnqg7g!*O9̻sE>IศjcC{7 'XLp2 s4ԇ}dWuʁ?wDZ Y+3u3y>|!x5~% !6]x0h=-QKZo93MXKw=ptBIQ:Ih|ۭ7Oݫ">2Z{w^T>䃍"#V,_^(=q!t[=:Qۊ6TM2UJ uzv~)2SVj5T(TuLezz]NpPDtp+.þ~H('<;s땁Wp!$MY~tPp4eq.Ǿ[wm_7K>AuqMO6S:cY>>.; _t!ܢ{""?uvj|m 87@}7*׀x@eMN7q|42ɇ.r;[^y}xo}]Kf`~yEwvB+x@(iӈTEbXU|82ޒ.H&uKϡH,m#hHQ#0ԍ\+WJ ي "-χDCWbHo@KhIz{Ep$]d}j/faVB}DOP08T q NPg;7! /. G^y\TP;3-ܪ$IE}Z{kzFꎲԊ4|||IT9+ C&(G:7J'a0n ~gC-B m4jEt+1!\=ukh~.3q]^ iH-Ֆ>/GI{rM^RUYh@5tsiVc.XB= *q8^椽kծ7')kA 0ɕ/ Eőuj(( <1y ` #RM|UQΟH9bеA>݃;W.8:{`'"o2爓ύBX҂:{ dcf;τ{H;<.g~&h?'`>].tV3 06{KOՕ-\6 We1xW_QiM!3@]𼲥VÖ7<HKQSOɊɶ ]U4QuxT*h,nsѺ4ݎF6e*^  A4ŧ'2/|.rCin |Y[7GQ/=@t0U]s=u7?}DyNI"r]f@x`(v4V/rB k ɗ, T 0Nyy

a@Cl ~pƺJ6H8y:_süx5 bN)$tUD6z~O_{c+k_gg2X~o8:hR"qD$PV*^Y$*ϝGM׮s"EP $b"4à."4j`WYO<ШH]ǖ:*(jv9V1(\"x>G:pclTY= qjE")S*=l;dg>X܉|GڲNrty4 c}QS `!G2hpy*syXfp|SBMD`C^;gl1ԞOh4n'7(*` iV)I(j-H>jaZ2~(1BHm3g$H0Ή/WV6iP,|9mPE PA eh2yF2iJ5Bgȍ`㥨Qhͯ~8w3Vf3n C!?ωU2pփP<2Ws=A#\KQWf^.炯U N|,0y['~w>|"Ui2EB(7 "m=; Xm`NV}9aG΅含Eq*I9[R$kJ0v KToc~涱wa]hXA ?5g`~-<9>Yŀdh)\u@kN*Ap{0Ku|W8M#MdfRSo'BU'//P)$%7$j"7Zy/-ᾅ{fPP 5*j煞Oc nW>NDa8IB/_?DSiEM%E" 8U%ڼEBhg|ޠT QxdqϿiR0 J:\fF2X\غ'|yL_>+k45,~kQ S:!nN^7lB;M$7&9rQq $9HtنO ]$j30b#4Y1TD'p݇P$J2["NRhfC6 U$2cHt+uNGU|,_@dBX x&$FYhVs&Vu}P'S.,zIM'%2 yú~WYk;_VT%o6ǚopW& k3p*IzeD3b05 3$|,h>!J?sgWnu?Z"&FP^ceP7xL{V5Ur{څl,mjS? ˺mi p". IB"+" P(M^18,܀bLS 2 -]E:H{Bب]P<PJv"#T'38EԶ*p+#}sD@ WJo0@-;r*m`0d~Dƅ3V{t,TIdq)t=R_xw?]30rZ/Hš^ɥB%h&jU=Vۖ6O JwND]ksB>"gUJi?4=ǨFԉ{/)QvC Rܿu/D?$R(vqz`7*j tSUDsD y#",䜈c7*2>]<00ų6e\y?MoAa->B/;, 7 /@(eBk=?>~ 2O/󍱺&NNb,vG30,fIzƉ6u ITJQujjMjJJMp 7j$e$ M91nDwXò;xoC9\w'N+zfv@T'<.TH[,j L%NJ"3B1&\83Bqxݐ6NL2j/ wam$M-* M"KH(~G;p0SXސjB ߎˣ jTbjb33G]9uggH0~QWU9M$Qdr&3bM ƱACW4=9ksXH-$7R/y$[|J{;1q,M"(nw'u6Yx-vW~@э<[ zDfy)$DL `r31\T/IզL+lJ$ aYe] N&LhR+[.PDyN6a OV<264 BٶzA*M‰1,"E4e)m Evͷ| f`~_~Me̜`Br*#r3Ⱦh.!5Id oм3`IK$9$99OŁjdJB_ۡΨ3تbςAh=wRRLefG =s g`~ғzV^6 |YJQEd/i)" Rk20#A+Z8lx$ 'K|-Wp8 ܏#Y2(x܍=ś&"y\o{ ݨv-}{tAztx+>S/srԆyTDeGSb9cзzoD͠lU}IA&YmL=;nr"1}w;y՛fg`~P-O '搀LO *f%U-DMtNB-9E Bel8WvX'vה ",TK Ú612ܘD|\GDtzY>sEsِ%-1FDQ\k;SП|qDhMo'MwAIED#8j&YM=&xD&̸]gR#ܨ0XVǐ#BX^X:]"NE^nK\U0CR&⯎ytQh2SagcKP-`|ϼf_ Ҿg&W)m„,sq2olKK8 U 0B\KcX(l 9G|D`SɅ sYDi|L\DJ鉓tas]>Ӂ]k)~Ė}u俞y_8 : "؀*'PZ&g[1=\93RJ'7:XkJ1ubꭦT=էǎpV_Kwp@7~yxOidchgFn:fZ{nDQ'=48蠅;4Јf {Lk\Q =EW;PMeO߿L&sqw$@w,@2*Iz1H6VrwD(t@(v5$$FoT\"j[_rܱ'@j)Xr5(ν(ug*kT6G*$QX|/ʅF%Ynm:uGc5dQHKSXc*B Jsr-psy?8٢R:5OwLw_@S$w; )> w9uWPQ\4m<9Aɠ&L5Oq@A7*Wph**},I\jE*l$2T+y#FmjviB\xYiK"-F"x4(Ji␛6h4e(Çz{Cayb8{]4/N;vtZ]ԡout|i %_ےo+į9`ݘt$uZt^]zJg0's5fϘvj]j;D6͜vL8qP-c`c$ ̖Ƞ/M+#={3^D*MƥVqibQ@oRjUc46ѱ&Wo,d$cM9qz+];;Fb5f޾pHrhk8.&Muz&)2HPB3#9&>}ǫ 9'Y+m|*<7SraS|$1o ʤSӆ[Ͷ#֫=/x@jgzNjk?DZJ=67o;Y3| g`~ѯoId"9HI ^lP..v7Ggl:ߠPF@Ty' څ|CkʙbgzЁ]oo)X?MRC8Z-`I⨱>QHQE/OөH0; m=:G_ 2qTD)=j {L ^ʨ w %Ӯ.  \P.pf Nf~'$-cה`ͪQdʫq$R_@4͕D*{L$zܓYn^w\Z8I2,9o*k` Ѣ0 PSP1R:v]ʾӹ\JGN,Uv]]ȋrħ;L :1 D ھEOD>yh?FÍZܶciļ}SM#60R}> ڨxL-ٲ#nm&FAFؐBSPbU(41iHkm(:_ j-|<ƀ䣵<ɽ|//~ D,uIOP,PKdqA4D7H7Wv}=אC>:nZ]MtA'=G^./Vp(* `$?dc~~$e@H$n$CdqŴٻl 'tO=m z8OMyRI!Uys^9dݬM]k5k25Ȫ9~b XK7Hr {3ε4a&!q cO}eWu= w} +ʣ/jD\TűQTn1(b LA‰7^ߣ;3k[h > E,;%EЌK3Mr>=ՔnK2:x-w9G7s[f#|9U/|)|a ̈́F:gdsQM էtLGD+ɧ$V"'xH[=j4\ƹPx+5Ȁ*2EIg=p g`~^w@N:U&fR85)Xҋ/B.1|mӛnhiIM=xm E~{e Dwi20Pp,<1.:[l'|m0{˗nI,uG.%6I3{^#0+-ܬyc`R~P#64L/S$p1ʪ~{srD*jʯUnLS%eH(sR\@b[TG=w!nke ]mUq+9@Xt%rBaл^;GYWxkO>޷=>p_.JǴ+$};b%Z0T!9  K4l>@P=%11u=҅Ƭ N{J4_t4N$"vOK@fΞ7LG0I``m ]DJX yRDM'<"L7zE=P&Y]dvr?p!De/^t0W\ ͩa*$J:ۺoRLY!gBJy{kK2`㸚8lqSY/l'v(CIB!a(΍|A3#w7J!77AX(W0֯?u:lݟ c*:/tmޕn4rT?f,g%[o˴9-~ݴg5.ʺG;u1ݼͥ)=a]DvD' a\UG]@nw1'CL*iS&_JHpuسFb #b$ob|ͻiۛ k%=[j9M5Q&jIw!R9o6ݎX/dkU+c'{ f`~Goz,ύsUi5sMy"jWڎApÃK% BcQrCk+JXW޷]r)&A5O;ԥMHnO$*SD˲Z7"B("IҀdod /n,g f+>9}lqnZ cxp~4c4Q6fF;[h@!Q 6my4>ږ_'Q :2B5?=J2N4FFH_\vqt^KF?~CfQ /{@ONum8do𧑨 ]L[8` ocQ [z ٨'V}os +_,  nh _D3E8“w:m9N͒Zм#}tfj$s}?^y [{'fu>DžhZ:2tI'Q{$2)E>8w\<ߕf2D?V,·S*>0ȑ UB5BO}WTMqhUVAD26n((*%2{=GGVS:z{havzQOG%,˼mmn\}ۦ=6^G`/M?qP2R(-z1}R-vx-KiPCVX2w*- t H&qɤT[W&j'(PP %m;NfW U?1p/1 wJ 9T. Bi];;d| g`~E;wzQmxLՕC=7}6-QFشz+LccSM  ~m)..Ӟ=T )Z> bɜyw֝;(??[.Oh`B G ̯Μw>.p4:ZF@ rAf0E&^7Pȉoh+.J|=G9:g޼]AM)X3ԋ#Qcs`F+cHV5C-1BT"y(lr&Odqt͞,*M:CBԮ,-`=exIsm}6;}9`,-8^8Vii?*u ~gR}9aMO*2Ѿ}s+5_/epTGFUMqFZB8kgcZ^o_o̹ CYsp\ǢN"ջY2ƍkdtX,_iaxЩe_[50'Ƥ萋-j[i$V%70,-:b|L8⠉4x,$/H&L@aAԙuլ,;*μM1۳wЗYD$*O$ϑI:3|/{TE)n)[S4C{*..]M_[ꐮy0g^tG}>ji'p$?Q ^G?;A8)"p|~}1(P-:P/hnځ]lB'rDne0FɛD+^qĤ Ґb :qZ x_CѰs1Q -9wd˒Π /w?~l4^>B!$k*Jմ5;ާnQ!G\ ęǛ'HO>ܹc7e4P+M+Pß5_gdb4[7!6>]p!\8*_N\7ɽ5ݤ_0o8 O8 9q(T(zR氬v?[ tjgs\xG7> cMmBc7k[lqKf¹YZ=X#؟}+7]E^@>c. o'DOj¹/nS |cv…+J@MunZy/p?Wz7: yŹ$oRRpKM*iT] ]{Z%#%Ym-B>q>Պ6N [Pd>A2"/mfAnҲP#r# Et!DIЧdz: O*-nB=3TEİ{daM&6-xX~=XdZ9џ ztGϜ9Jwo6_N@>Y$(?~;~#ћ~0r9±Czq|;ogRY*mkvWv#G}G?클*#7 2/?v$ (DgޗeGۑ O=}ny]#NR˫ee> :kIY=GhɆ(ZʕBwS; r ]{$!m><L <6cMrWU uբ|aAd0V:xz*ۇIZEUk 3|旽/]\B%pC)ՅNtwnw1JeVyOџ~WE;IJ"I$ jA?xllE/^59_"[Ȭ*QKVb}+|{O[BVxy|]j!"ϑ՛sc3Hq4x_9&A\Mmn6,%٧ȍNΟ, ѽU6x}l @_oۃytw,]S> ?7EJRς6^$X͠]O u; Lt>ԃ/{I28}}6#jJ )P$R^˚j^%03,=:̺m/~4&:x)QQtb?iFSZ?mwe6M82$ae g [#co-):N}kDwC)KtߟWǠq7(5ߺoMӞ{T|W~nw!UJ}ilZxUz#Ņ1єF.Zz ":Ow_7O4[+:.nUCQd^5Mn&:~]$$CDX7EB<(sM~BwΪ'VFH#TPa-OgU#rCbȫ0-!gu{HtV էSM'x Try֩MRXB ȭ\м.jr9J$nmgY\om @>Yf%?Dx(Z&6ѼQ0cwZf/|4upC[F9s~kϪ)E(8Rj,3z1(_/k 1'k:w/te|/C1$((8֒{7K(R~d +/ ߪsj,ae(a0J50*vEGS"LwH'/o{cҲLt#9BiXr&G7BJ̗KSROHWqQ͚&m{) m9j:M7r]I@>+?oT5} U 8uefA8u۽n*<7/ksG Nǎ k4 yeh#Á" Hl4o7eskND홁u07{g1$ͱGMqPQ)WLdžEsc+g ։}<Msqe^#Ў71ӊr58fvR_JXM2 `b6xyw|9dD+D(tgy#s;DA\.O+QL%V­TNtCd\yוFog`~_{*m.Jk"b$ =G\6;GCE|-@~[oJNtyzOS9Y;g=!:/DY1KƾلNr8#׸UZvD\z%6JUbyEh)itX܂OZ #5@ǔ wDظbvƥs)}8߸ 3L&† g.Fr OO_G&7PԒy@.Hk{QZMhSϽw2)4oMzcj7:8@PpEG/_%u\k S6 N<Rk==3=0;3 * 1JܾD?TBIE&!ݘ5Du`{ny9=]U]>>|o ƣLNDIgJBhW$HVjLxf^ ; F!7:;( rǑ)b}3N6h a|ōb5:-h7DO1k'!> hk)cf4lJr 0V#BFF ҂"f{>زʳ`>K}_|^s'oQ,=\+ w̍i<"}"f> 9Ē˺1 5Ivm`I%$Q_n}_HMOF^^ML?Zb(PfwnFVf!kF9*7'MDiƑ bgrft.fEݞ  p6he5_n.v07q\[>t|6Q4[8˶.]|_r+,, JGq؂JPZ3TWm-P,#׳z)hvf9j@ pnWשꪌ02%HRF !ސz03-T%7Ќguur^u>Ҏ.0J:43Xc!j͌y3?Ϗ}<3#d:5C0 o.'g:pk~<(_4RKuD,?z,ڟ)Ye:t X:._Bah yah|DZtogr,yMq+ѝ'7<:3|B(Vf(y4ǓS9e0Ќ:aҩVdwKvDkYL=jEy%2y$>HO7T9Nؒ|\CK+3#Y30zԞu#Qk^-8ὦdң+T6ױM4ƺ@ײF[yFlKơ/yF쟏*xO<+3θ mkdIud*b#3gwאY>〾z9{V|)qU͝g'1C;”% 6"X٘t0t S(6HWigi3s^̧5k2mШ#ҡ$mO䫰f\Q{AI&@S$?C /cC)>GG&% B#2QxPOy (Őٮ,lj`'[Qٓ)xr+xŬՍ3& R&:1IXrzyy9Y95=?.){d!߸l}D]93T:o]EvNV{>HtATwq@ yel4CD!/{ 򺅞}с Zcx#+ sC`-p ND1 |K8tƩW$1С0%Iq'hJG%zvvm'*z'Pe+gfiSL#%%l''aWifںB!97DzkӤ!NG§'j\HoyDa5NƵ ӪEBMֹk-F&+N+[<]Po'&]̬F&4e(I}P-y6Z53r+F`qњyPΔl3>w~{7ooOjƳkgpьq{vߝ|CC}422"79綻ezD`IgRD$|VӲt*Ulq7mFr2vMJJZr8TȰs7̂?O`1+cӎ8#X*UMKX65 '֮[rh1qb#xG[iϔuӎ`cJ,a^}. #Rc*mZ7LOft]q)R,,cn)1=\L&[1go,?Yeé7Yӽ7Gw`‡ Tl`w^ԧ֦>ʇT*s\NKL'f+i#vz] dӓ#rUМ)mZM R{͸cJ3ZZ>:@ ňN<V@J=XC&2ż2yL#v;j+W;2m]T/}`#.c1U<<ȅU )=mߘP1G\aa4[4BIT EI6>ݰ&6$~ݷ|<-l缒83[N}ϺjNhK }6̴Ét)t%_){BCGcy~x85E<ҿeϠ\=೔ ^&cUe;$T %2+ U*x pQ@>4H]AE5Gã Y N,P&j`B]n hinp>zs.WU2%CoĪDMa-˅7 ׅ{.QX|7'A-Q#ÚdDҏ9C 賨:փ}eKNk^_G:7XP&X7ivtqh#%3 VB Obz46Zڲ^ڱA2%(A}69,>:KG) (^~pCPȣBU %L%ZC>* RbA13?EP暸zQAUe#tue=)w;r|th$0媑qjd7 5.g9:st*X|Z yJ ЀF9*ݽ0Ohh.eeIGlJxf/+Ww-pT@Іy S}x7\ܻ~rqg!Ǧ.҅6Wx,E>MLFs9ret *ԑ@#+y#}Tȋ΢,@a5qdLerA$\sH+_@ڝTT6~xsJ%NVžqϒY \D;iJʁ:B[IH'jRMTW m9yPV{b~jl//ɚ: "wnRҬPzKS.PV YL'_d\lmj,#ݜnT1ߴ34k&ǃzP]1.[e^"dR/ޟ׳uly =JF^N^d[~xo]M4q[ĢSFKJpDt11-=3@.>aFME=A[;~˺ ]+ y8X0r0tVkdDXf/&a 赔IH"sڏu^m::nn~֨[`k.reb@4ל-mF< iʡ޽[:ò 6':؛v^;9_g:3nNPUőbeϔaZ2VyI0{kԕ+^OI;`qǿ2(QjX88 MCѝRZw 0X`EP̐:9^ġ1ehN?TI #ڸ1Ty|O˽R+8 t2E_ u\xOR)|`decX}ԙBG'֍ߢ|s]zٛkb(|Ν{8@gݛ~L'n{ok]'?S QF Br*g*[/y(O1hl<I0$4qD7v(S`qz`FYE&6+ѳuh|`?_arbXn^6㏍ŏecdF^ ,xQ)(zOO<GtP @Ug/hM8ı+W,%T3۩']vv{dT 0oYk!o=v^8Oi+k/tU`QرCtH.yf': Xy=s7f憡R?E\Iu@=pb:@kdXz4 )6~xCfiWt5gakфlhE᦬H{"*r;|jkO%h ߾}u8]`[mClK5z\8 (>{|2v-Pp"AY_plԱq|DYè3~Ӗ STE:bCfidܳXO!2ٶFJ1Uw^ӚΒ |)Ks.]lжOdYr7ԹQAJԿC}{ȑ}Wf(Q tO贗I ?fl (efY=Mdu#s+@o"͝;V3G{HEXM7,Y#2 GT ޹29'QyQWF@%kvێ^q 2E9g>@t-xj_e^xe ቧ־F|3c s"$"1KS K44ڝ$nn[E/9_&Zl7 16Gh`pP:pp0ԏ|XJRCU]{U򑘪C1TW4wrRǦYo!M,{ٮsюm"1ڱldơds!0=f'k<..L-jX3BѮ/ԓ` 7md. /{;]zYVUxkQ{{Y}=WI~nsSsI=ձ=z+1MD& BOmaK{|y OҖwh^ʘй ˍEEOĦqp~~~=l8|woCuS5À91 Ƈ6~Y5Kp*<1(N”Sݸ^\rr"[]aƜLA |#7tۓt?_ ƌO0h,Ԥ-2 QlYxGC-èD/8+Go|sQ>qʴ:P(NxkO;c:;oۛwv2?|6rKO;a>,- 1 wG7Ѣy 8J}{s?2e6=,Q卙{v Ӑ`& $tz%C˯8BW6`D G߇d1 .S-Yy)g,<{3Ƕ͂oV;WOO&?my+м>Zkֱ/!#m_Upv~j.sUGYp ;9jkC֩Xtol2Ryϋ<@6laj.*9Lb̝C=tq2'tL?Wo r~c=Hu5S{Mgvn`\C ޾m~dst w0a= tyLK{ l6+ICx>u~ B&3`vCH?˂۩M0Ī`X,-߼=zC*ZەF "kDb0wʶ 8uMGk"/.Wt4qbIYŊ3n%.^!*e=$4|A@;68C\Wq/]`)'}JJ䂆!D⎎ 8A{H(bȟV*g\wgV:::`\=Ar(|Kogk׺y&?䣲W6KfǙ?e ݳTR;dyye9@\d %q6ل^rVzŋF*@6$EȑQz^pi8 @o荎(ԔGu˺{OڬqqBd}kZΔc3C_y':8yTj= +#_8ϡqTTSy &txe,bnD`"!|kI398r@Zb cy"Rd%N/SI?@OGF._;/l}r]\ze-d{%ԮQr:6p: ҹʺ:J/<ך药8  ;b¿4oeB'i0%'-eW//frzk!\g밼RWd淞jiYY'sh72`Z6r$z0hw,V7jj٧PPv:}Sq ȫ2^M)3-}TdOjk.D~pʉOoN?Y9s4¼+>/^\LI4)O/:;nzrCڰsYy""K5]w\l~Z jel%{wt`ˎ֮ Evt (Dwu}?"w*S@YJ1yTNtwfe#6 !b"7EWc%̅Z%?s&}`>eq0C/|곯]_-R"VJt.&:ny&eROoΥG6j3z}zt;SJGcp?, o=ӹvI@AHNʘ؜/Ҿ(J \90*kmYEPJV1G.[=T!*A^/i1OjFW8R/\_O|x/IAIL>q3eQ,8Q M93^|&{ 0LJ+W*#}vRVX(*`Oh9ጣs곋tdt>=XEwPH^o s;N[ĩ5KeZ,?+R MOt!1'yyY9#Ql*72MEZ]§:*1G,0uǺ̈7jcl9oj9ji\C\7%]2°;L.xS99ԗ]2-`@v,_G1:oCHZ7"GAMok[{.h|IclaQ̏Y"k[^%zNyQ'# ңUi ,N7Er1@ f+Wv8QXyx{Bo1A@Y׈1 jh nw5Ʈ)8rQRYA57;OU?R LXi-Td^ĸ8#Ҙ!3}(.Kk֝Apf0q4oh!i%8"Jф g o&iL-4 :ᛯڃ3-O[Z3r\;~̶b/4AEZeShp=mS } Ѧ5=]"oG8XDI塀dx Z9ߑl<ÏZU| ɖӖ@>"z~)ќyƑ+Al++crfjrH-0ɆxY )Ɍb6?_<.sa|n67<4R7,9ɟg9 r M0Z:0z,ĉ|nU샎a7IiptY/#ɚ-_׋0}هO;ҭسj gJ;K}~}>O{)~` uQk֔gJ HhOUe6@m=p+Z,#fU(UuUqVMi)uh֓#d[rJU~bh#Xiƛ 9eg-<'5qN跖8u\`JN7.wXM&q;JyPF:βurf;98k7f?Pzg O'3'z ܲw4KyB%rbMU;w&'tD>~dhr:\9|hK[sfQRKk@-QَTf\H#б8x`HG I#Gj+pB5fĚqoגULH'}tzl&&kא34Oc NqDw:ZTC^؀rZD{&."es7omo*I6>[ 0LJ-uW//J6X37_3I)ebxC[+cvbŢDzz^o@iѨ<:O4ж v7S-GJ,GeEul"d8RdZCyfȬI㪴2PK &T$3RObnDw;xr_Xw-9%i~j>cUb@9ck[hrTlau@.· iF bA[setqۇ_=&~8$FqЉ@ocq\bo{̬|뢧[V>cWΡT 9O'g$-5+7YH.H23W_/H X)HF"ag Obq8LJ"wj,)^76-9!cty:oC[}%rY\`uBl'RGV<(I i_aq#{D][ObYMy ;|9: (6N]j.lVc\:lkmf[(kleU"(`ZYZFnqmю*c.Q\7F:lb&Y {oYںLaD7@ 2_gK8ˣny䘅SyR=[]2\4X+kX ^h xX;_|:Y̏R~\Ǹj.8o:a ೅6ݼ泔O 5-jڑ]50pj@yQTŖeއO/;-RrG:޵oS1w'jiU plu]ɔ1혦r8D.fc|uɋA-0ޏ;SFMjNP=OF`7d$4ڵ-h6;voSrN<JFbľx& vpf j%KZ툉sg[{{i?lXІ2+##GV!qg@Ay vV$fĉAG;!p;`9dcǸqBٺh˓$:AiL"@2q,M1` jNsHgqj aXY`< ±p{j뛼y3Ӧb3FIǰYA71'1K5VB&Vgu"){tm'OWVi0!Oτ|&.k \8ܤg VT2+s/P؏Ut8;ȸ䴓/>zL` fސZJAt2 P/櫂%Z[[ #SV>SF='eϩVڴT#%8d&F1qf`}kAny UWgZ| 9`bǖk cZ+ؿG |qӓ\rqC GcVo1X:[4mY5iv =e!Nr\왁2##˱"{Zwq/[^9P)e\X`>@|RˁD"`p÷1$નZz!ECeW(*F( P"8ɬ0iSKS I>E8sq&W7x:=@s+橩2cOR=JC:TTO# >eͽ(c+jdڥ~u2 y\T:Bs2G] ~u5߽a&Cⳙ7Y.4 "[ZZ)6_g?qbn@% Zq^xqjKrtY!@,߼%_Bƈ ^h1N;LȇQ;w%(T5g.QwcRŁU"i>-Y옃 ^H6+ǟ=|gB*s1G)uX{gt '1FF'6["2hiE}(H/l|Lk7'xǾxlԲKRLAgpLm,=؉u8h#?[}ɤy Os(@EPh@.-bQ P2K);_y73#@-[mVEӅG (|H*;4QSSE'Ǹ>`yt<_dP[۷֎H,fu&٥=?sC+cxl-vA3mn& G^s埼5\{Lk g7  R(Ú@@X\L*Ծ7M>@Q.SKg?vo$РR$ORvsC5!zd u񎋜$C3O_6uQ8;yRYÃ(Z98,seHvQ.wrhmɥރrD6GJ{/4WiQDKjFulbf7v&VgJ~=7= vA7o,Z'sLܼ(,ҕZ $SG~Љ = ͥS,8I#fEaS F/; mGcc5Yр^Pcț&$0mP4z:,f$ ~M'U:8u),P Ξ(iG șbv g@ (?6pH,*Z0L_k?Pt™JA 6ob>Wr|W[sLgPo,+Τ2S~sк' ;z}ڏH)`#UK}:wOƴeg(Eg| fsdʕ9jDgU"`fЍ%+.4)ICƲ0F` 8\wج' 3EML=i&c(}&AViNf `Qu̚m f̬8N+|T0{VX (8 2>EVa=cg ok/xp'~&ne.ye&i(Ԅ\mk\Z9\3m{2] @1Z[cb* AܢG9u̎A˙:;OFvgW!'.-'i7)K@nJ93VV]%!+AZY\*shxwPܒ[$ y ʾlR G= 1Zqqp9__4>Wap\~ߜ7}-MO3oAgGo *{OB5u4+yzw!Pʪx)С9礭s `Q_x"&57%4TR>s:`DM/P]7BULt(^$g-PX>kܽI!&Gxt`@9/%~w':nn<$f/6f2 =mVtjbqֱ8;b:K{O[F9ypAKr>uTkQ/GcpbZsK:PDDrBH0~"xɧJD# yHV$Ch]2o.rNU~|j3Jhh,?JC!3Hwʰ)֍=jk3@0(a6փy=O%[z8*`MHc?lfƆ# s+-Yi ]U/Υ/ϗ-9ah (~6y: e:^hE qׇF%DOqIuFI5Ѻ5ұ+ڬoX\?E\A sD.+gti'oM :4hc;z2,| :@XD5e 8s8k{__*?P`>?`%78tlFd#ZdgiFs5qj_T@Ce O|iȤzaZjF0qVUH*N>RJw^׷s1?|67~Ύlx?:g ss —\GS*@yeAPlsS)v]q7{GVcc`DJ-VH ܥvLU0m(ֹ-.ebkc'cfu9sT>Fyi67EL#ה S(6)2 @\&(_`Mp1 lr[׮z tsriktB^K)rBM</W>~ YLaWfR]нf2t{ţđ[O"J&.=q>WeBͪ!H@ux 1ecybr(<%a G iE(:= %*げTP_\YXo6-؝-Oϋhʄ$r~k}` -~~+ e3Ӵ{*L|%G{,AetYY|^ܜD.@9/CM&D]槌9`S7灡2Aݢh@n.SEP;Z:YUc2xuoH Qhd!UX#Otŭ=Ib{DԷ;3gv^ٯˋm]ƀ|W~k_L`ӌ3=>no'.-F^SZ*Ne0OtVM wHÚ`NB٪>GHF|Qw  "FR#GȘ'vj&#]xmʫ!a+iȜtG=M`ȅz wbq6 NB#ZH6b갲!UDҞ[n9o`;X﷣p?O~;_Wniie6+ nY̧+L蹿.`ՠaC qlR@22KjF93*zq_K[LW}łEL+{c3|`HL9 \Mz]d3ptt-XG'#w.z_c΋/:t.R-\wuryX,hd燢p13o0)qk]?:' 0LOd;U:+˂  UaST<j#H@#/ ݔOOQVoQϛնAt/+":Dz3R m8s.gT\byPwO"ޡv'-cLi>5ߤ16j%p(;}[yuOw=y7⮻zWza#@ݎe"x?gmo{f7m@tۏ<ٕ~*Mtѩ J'7(DFdaR+1Z,#meQ˨0Қn̈́G0ĉV..t trqpd4yME%qq]`ک;NPF[:ѮlĬ3O0y5Zvt"_ OGBҁ׽T 0_mmu>;0p/7p'l~z-O֭;o\}/@.tvp㷾{]S~>Odoae_6bɋ7 ; ) rW6p@/b{tv, sx=&tOК7 ɤOdDw)[-_N2E}U uj0u tU 7 }uysTg%@U*c<`{C; g@`=?E2J:=H YݨE ѹCk-b֗{ۺzk^W_+L&?+![;gw }xx%'/ xOOOob5yZfSMKyJ& )yB8#])Ӟ>9ϧxv@ J  Pr, =D3Á(LG{[f ۉ?H VZ/Lȋ-yn$EN(Nfk`GG `m~sMf [xZ`l=s߼[##޾c_x饗-7n,#L1 f3afmo̼$#@(BUlRG`x=YzGd舅9:ա} )P<7Cqѣ2*3.mҘ"{Z^SAI.}0nPwQ`uADwy#[m f^׼~V,㵚[t",2x=ܞ q5yrq n.$$v\e1$WM.Roѧ $ 5BiݾY='q}Q3`4V:8 ڪtn~`9 A|DTT^ z#;Tl`꘎@Gsd@'w4/{%7- K|v-\vD ~>[]wyܙ̼{f~8A?9ǜy"zU?-X:}:{O 8lޡrD 22>733#/qG}We,x_ ] I5gSE`- T!:).O k{tʋ񚹢5֔"ܪqdlYC;o2zg<D b#8M7hx˖ܹsk_җvTo=;5r [>O١t ~(@|$%3e_T#vn#Z#h,u59 ddRf*9-F4\si`MmcEHփC.'kٮ$X.2)QZPD*y,`oIhʫ&XMha U%2u}_:T)W'$WkEoh_λ.qs[6nng3-4y?+4sVzՏ%zYJK r"jU<^B.S Tu/O! ȅF#d͜n_d3)kP >]]q7om/~s"$[!Пxk뮹 |z$Sf~ {^QoЮo45Q~@0cԷMODwK-a:8bx,u#2a NJs`eP-@ig,PV[k;LxX/0/]U^HU=^^e,2c5#}^wuSNmt;׬[U#by Kn ,dq&žgHp<|q]Ԗ-\vX9]p[ީ22a3`cf^s5kX"9SKn?sН)P}GI\ 5%'m*#VZP{?Y&rԳzcbk2].` mJg!3K"~8/{˾|M6UNGFFNY~n>3m9L}2Y%OyDlo-v/4#e.-iqShLa)Dv$4|,.u $MSq;{K{쥕3V7яe' i\źoiRX Z~^mhxziQ+9M]%Ǡ Y2 Ϟ< źyDk GEꨤC[V{kc_qVZ59s/| onbӕ[`뻆CBlye=~OcrkvwhNAuZ/b}ojzF@nkc٭Yb3 Lb-pHh\+һ:_48(n AHO(Aeut-ߍ<={쏙c{{cz=>:A&%3c~)3^kn] &OS"NUawƙԞ(iqcjhVm-Ȼ =5V҂!¶Pm(B'셸,YB&Xg{_p۽諟CHd亃qN[g@oic_e(lB*tGO+XAjg|OuI&Rs7 S{ws >:A8Ԕr ŋc~*K.Y ^?f|ѢE;EknM7a3Y<#ϙrLQS9cgP߱cDžwVM]LP,t)g;FW0hP^^Ý5XHGTtіmyN JTRSl=MNdEN"j`XN\3i'/t_D4gg0lWwvIWvj ׉x@Ug{򚝣 Rg^#9or47C^t͠h7Mpͦh~2StjB3O7!_wuꩧb z~ddɒotttS2;8`l{.mƖJ" Q=Y%X˥%Rah-)SpcfMT",EggM%gi% OODK]m2G8Id?#$#I908v친( B\U;VyyȅPݡ7$Gk0 ]o44tn80L{޵7"Bmg 6AܰaCL<EpY&?G?շ~'p§N>ď |_:<tU蘲(V>wQ[)fq[{ 6;ڍ W"  ^j' MsP_ۄݝNs ChJNd/Ed0V^&|Ь7A]4xZvVcsZyy[D΃'ž[r Zz˺_]nX9.OK-^;tt*6yP_ <r+7?;o޼ ?n^'^ tv=x^~tT(kLWIpC].Шw(9b9BRU2v03sX~5b4}"s͐f j*"2Էe T?Y. K.Yӕ pF ]C yڑ8W!yZ$۱PUw=i(g#3?V}tH-SO?VȷMSS#+ʀ~wn& Z=Xl'6|:N7|iO?k/~'?ɟ9qm͚58 Pq08~luK. B+·G [? \H+"9C^Tߺۉ<%X6>N1S5C+[ _~§['VN{ +By{zĜwO7,.S\M1rΚk`g,"~C9/TNoS7QX8}4#8\[Qޅb,?b;Q3aߓi*Dp4y̿o.rt޾8s//^tϺsmZGFU5(R/ѦvCSd#I\]۶C\ |`CY:"vU]t9k `EldmPr7 ߹y}&y4.xZ2+ Ku(H;mۛ tc(?: N `s[Yxo_hP$o:ɩqKop)uɰrޮ暟4Q!?zv.-;`dPIo0n~뭷gϞsCM7tþ>';3e:?)1nxCBAqH[+PXE Ͱ@-sHK[S_JWBD+d˸`BMMs)P$=^H!@̧7.W1WI}kq):˳dydW:Yڥ,L䙜&դl@EB>R`Υ3R`vXҶG/8/xdddX9/_>z;-{g^t4Hbx>ϾEh__+Vv^|FU;74ضer0?U$5k#CeU8 Ovۡ ,y]sZN_SKN0o'`@= ['rV 3fg}dB}]_v+H 9E2N \ѬXsN! Zb|ko"{R[fMIP`ka?j&y{ vGαmڴPk̈́P+ 0E~wwoذ*[+|Ygc2_bUϋX|l>룒UtkU !Z`ާxp+Đf/!0{Q .S5੽:@8:JُxmfVY]MhZi ԝrvc[#ҠG~AgSN9n"]p]` rxkh $<5?WG3tY1yGNN ox{&b(23;L[n9 >Y83tS'ejnn,~`D?6`XxŒ=VZ=K6oQ:rMc``'tc̜ XBo#=l8LwTc @')qxޱR+?ORf\ĩU Ȥ 9-`BX<ߤqT3ET`MCAd/x~&n ELشfnHupeX,MSa|zK_z·M|7o\-z4=[' 5|FGGZ]mGe|f͚StL26rcl#`+cUZrrRE:|"ܩOCl*)Q`ڷGdzԿMf² θG37D`ya)FZw`B){|z9GsㆩD#qy`rY#>{Ѭ]2tdyWBTX9oЇSDs=v8';b-`^%o{{bO/~1cƌT5@nfSfߙ<YΩۛmbK3ۏ dB hԸ慶\nsxZM&BOEP:f_s9e Bcx#Y-@]L_HM!($몋g@tS9'7Nw9%OQ\G@.dܮnXVN_~PŷH7ڵ봱'~>cxæ;vx?=fkfKLO(Cۏ׍MUckȴ(8iGWL= (3@GP6ҲXYb@nvܠ1۹Cp7P<.F&|{xI l܂}G wCt6H>r%, LPȼ=W4Ԃ~se_^4Y.KMg/f?\9厩Զbe,]vi\DK7Ը'Hxo{ϲ`6qm;.; :sqU,,:jY &{[{}.ГU0 .k.4":6J"Xcg>TK`Mx"_p>R+ D+tt<芀W 9@rbM D@5k5y4n?fy'4.'b__0T|^QR6f#=.t~mʕ>34HPpJ/wL^xܬ뮻Z6r.ls"(Ѭtwargnc VפBVQD3H.g5ӉdpO2 o2 $\} T,L[rwm~sөj#'AJvDC%ɖv.\-ߦ(K-*}WRL.j$ݨh_ j,K_߹sZ9sR9vl4A2&!#sƫrs&> +>lY&]?? tl 5?uMqE8|@Ԝ$~r8]HWts:TN~z=R UR|ֺK-lUī%E3Z3) ȋGz^OO2JYJ9)Gb]]f4M󜞺pvZ:vNrz!P =ŞۇY<K^ڡy&OmXv#DL͟lѳ@#l@. u[&}xO-Dx'1 e=xZ#7otmۿL<7{R<\Nwwc)DZ?P;-[n][TRj\:3%/Gӡj%JKk=*-1KR`:5b{5]r.hNJZJT :cϺ[&uvl!m~CPUЖxE|DT\ ~, pTExͰBug\ѠlzXW;:RG[4p}'M@n(VMtAlL_vwȇ\nPw{`4Y:tYYy5XxZ.fJuH:<퓜-u(ՐX6e- X;NeS~ge&h-G"90?%jy,KJجĂK5e ).c׷$Bp™N_DEIC0ZplëOOudgp-窓n5ooolXgȤ?gaDT{Xcp?1s+"8~m.˘?QgzC?$F]JNFuB+ c#yMs[2e(_3ލ[Gb5\|\npts槕ʫ>^Ilə(F Q żV!c35ڶb˖‘Κ5n^(=b쇥XGxѪ-':n;xE(hG4ӎYHHlӞ<;W? @Mv7HFv09&}}1쁶%Z-(  "ȘE8ٗ $`J8ǬM[";,9UDud4HT"}L҉#l%HL+֖w[z)H)k{Éy}WkbXGz-}YUtb_c\Aon-'x8z5yXz!'CCmyФv֭;v\MWQ*Շ^ o^iK/̙W\QDRXLw?d51ϾTGƜpmzs Ci}`Ҥ2cZi:iӳ~%n0Yvh+ d2l+ǟ wx()^vM lڵ|aV6T6kB(i |H˥ pW$ɅMg<GTy7@+#\Q!`m0@OP$<v4Uzs||YLfuHw˖-;2Zr ŀ 0#ıY.x#ag`޽㞞.]iӦJafC}PFP?seu Oؼenj5th)ղ1܌p6է ;B1FKKԄ{ 7_Afaab=2P+pZ.8N B{2u~KĊE]*.;b2:0 уThV!COXhúDxϗ8p`矿] 6>ҘI3S缴'ç5ִ^ p+}=!+j$\Єէ|8D(`_oaP{*p\:=s>+Y7XFڔЦϥ싀$'M-) ocSQ6K.nNvGWc*dc!V kel,93 1{`sec-Z{1\>V&iu Eh^+4Qݴm9_T }1sg}=h?oZ"\("WYx?}}pr9 n[[-[\+a& ^lJ͛77g?52+ju8C'O;GoqTj>`h9y1Ze%xdn:i&}/~󉩏VVljs+.Cku'ePNpJ~1 7MSf)@52Thy f)Sgz̚ota0zTfĥu% 6y@/O=vtþa\b*7u\;foaDϚŋnԂ"\H.QfѰךIMF&/|ѝ1#^inp׿ĀslW% N>BA;~i]8G/ŸUVx Vʞ(a#D׬Ys-bS6o޼w[NNR3Iwy]דh@SSٳgo9s~v---Tpso@q4UP@d̔n3 !Tk. 6܆,UplѬ$撚x"-$3CS]TnExG2had{5갈-A6lRMKA'ino:dMvVT7l3N)ή&)^k]*'@2>s1PvA۲]^?|ŝ>=^P.\8/|}x@^s"\x-RCgvȫ`ocd@^@=ߤI2^f/av&ZZP9COWzD~Zhys `QUk EDpuD5`IA&4C" ' 3b0#VLZ-6ѠztܖD#ؘ lu\d^uq@c ptS~uDtb;v\:TZo&gB-|tji`jZ$;={\ n2vj/U|4 ahLh 44VBsy>Uw:1#4gxl4wӦM_!λ 6|?c_Mqݭe˖=z,3rr$JJy:x5ٹ]c#@OJ4.!k"G++4># m$4Aovc: AgV} M, ;M»#7:^M`3xzꡩvC#46L h:!b%9rPk!Cq7W6еeKěh% -(#(I7k'YWab~p WlSJ:=X`;|#[~^>G뮻ZzϏW#|_y/3V>tR +KGRi^) /6Y-vad`z獤sC6zN45febz<a5 nW<臮@\g1i^3MA*makm~7! )Cu)FMFopx4l@ V_^y#{] b)(kaKOG,2 PٸCjh \,_~kE'\U4p{&Ed ۓӟ􋌡vMkf2ٟgv3C+ܼy*Z_xm'OΌWV^^RZȶ]a=rAR9*vvw լ*o P0XMЉlLtvXg/z02xГ'(% хOZ:oX=3gZOM6i~D2]Db ػM اA8@uF7LOL-h/d"&R8|BcQ%H9WA!dş5|!9xo7YDC5wT}R25WSyY$V0뮻n޽˖C]O|bw}K%'߶mۇ)>ZxK%Xz}896R{wmXuڨy2A`wsnd }b`Ý/B&=g å)NwxpKc׳k8NrgROlC:Cwo{}=Q4h4)Ղ͍S1<Wj$+/:,dGrt[QNX6M(0!M/ ,Z,tC}]wse 7xc*dyDb!x=묳׆8WFYkJR,^;Yh?:םϫBRQV 5Tt>l^Q01hd)BS7!DG='$bbW ◰岂;J@l55Pɫ&$#K"1& aoD} l]=$- E{PABaߨ,OjG({/|*Ե(l#ibvt؆"w".sB1.fASmGK~_cW^ ֭|?s\&\Bg}jX,u5ŴkWj*uhc1=& s߿OU) HֺC I+A,Ξ+!dz#TVS}r{ 꾖h&jv^EⶀpA>yU\;m)f$}MRHC&1m ſPpVy7zǗ͝;3f$JY'?Ge};ʫ- VtTYۑXz-}浰*Kar=l''zΤL m3!mgu-AJfԄK {0xIG<*2yM!%AaRIj%-:]W|& Ul Iʕ%yc27AeM>*QErcU U) j R+UN+=?dzV;w{) A.93gTCf)_׭_ tq??>馛n- {7?k֬~JjQ,p JO8}_~4^NKNϼ@^{egc\0S*.g`#n4`KlL{]B2Ki bF"2I ؈`{6ƽ09ǒ:sєq,o"ϡbVT;+ &F4ש ]!I&i?J6IJ99[@ܮg]_~# Ѯh7g #>(|lLn rtڻrٚh W/==3˵t K.ٹ|%羍Wd,;vI\ Ne]qM >,Zg)\ {b\ˌZpVl- F4 ڰ7CCi:cCa/D e!CB_ǔz,}FM8A :k4I6?Eh$ yM󃎼w)R bB8/:-0زh Nh M,/6؎̂C rեbנ:-p {S 硇5хL@5տ6\cK&V+W˞8RE9?W'd C0CCzu!/ SǖlAf¨BU*8Ò ׂĀ44fή ~*n_=as|;Z4 Hu'%TDT(SiZG;=v }]%$M rO@hsp!CStZд/n'3k5{`tZxg"dCy |#WġBx㍏\r߷ouڇ>TLı@R7=q hl% :-zrD`gh'MA2Dae'fqyCIk^*UXV=jCH.|#"gۢuxK'WKBT):_ HUn$fLR$/sWsڶ_/_,AwKjBWW*Dw_|S` Jy+Y`9A@ Jj7 uzwfZ`R;!vc (mےvmyRlPnǠ3Qm!RU$0{ћX K^\0BBěE}> 4T^K2$6Eb~$r/ߙ[hMD0Z-ւ],-uۛRhK+%2K5غwVDN?EQv3o7yl̂~,3mX+eB(TC H`@xۆd +-eWj"" ~qVOWsTmpN\+"!) .՛@'/3UoY{ mR rH7Q|V,^$yJ;] unRy-uJ`)6Yɞceh4vNkuGhKL#N`x^]lc@<2SXߺ3Uh XšȫBx]pۚ/XRM!׵uێOl ~|oeHZyt?[v@5P7wD9[S6;W;:fn ɠ[cfsꈕrYF>Reh$6.A=4aN^]GXd0v|w^l4F0Dg D PWJ)W#Ny"I:㗰ims?q+32vN]IwPw~4R ]p;J+4ģ"V~?F.^rΜus^.g06*XOςZWue},Ál-xY, QfA OyJlUg`3TtHٕ BwYM@: N*d!gXB نqp#+%d?!oQG믈d.(k~#q(0pԻuЭis6Nk=g#vw|ׄbXP 'DN}(uՉ`oyE䄆;gO?g'GshH']VnC1\i)w„W(yysvlަn*"jJʏue"=@kUe,dj9RJeˇ *hL[a2 iyHYNj⤮㗀u{۰Tjb2Di]~NA=ZYQWX'mί%xX8}v8OOK4F~.M 2X9Y (vPT"8_qZ ֌c Z.x 2K9 ^pmJ8UJ«aAR$SgؖA"qOdž*L6' !Us-HPmQepԼDDK\"R"+2QuQw%S~ VfD" V% u#ޔ 21N ML#pQeZX/ǡq^9Mx,'^-b:(9WdQeN>PKģMfLj& UżRǖQc˝-O[B3:aqCl۹`T x~MH'(c #GC^EXy̬Te\j)ׅg?/烹̼'SYPh?}syxACjD|&-,KR{ >0庛Ɠ\P?,7K|V+QhĮD@l>&pۯ/Yҕl''=TAb<+՜Ԣ\v,%bG=EA! ;s716F{NC*#F }<O 6.~1zڸ*⚺3$]*dEųxcڧxU}]p #tW?J&킸zϬCp,dy1IeIdIVbdiE~YDUpQja.^K5O¡nH*HJSU^sy)] 3v}6A/1g!w: }N5lZs2pb>͛G\ X- E4jP~٣qW<])=]*µ6#40nĒ lHۢkQ;`sS^Q^z:.^k.}YnmUBvvQDaӸn NKQ7unPv$PJnRRVJ/':azϐe ՙd ܰUh2f/Հw0t̾NBR:Nؖ-S湭c΍RdQ`5|"Z,HL:=~ى2NߦC9WeY/~skƌCc0@MA̱с/e9|/to|ŊM͏&'Ѣf)5Z {/k_jr(7K5հ% aW @\ ,lR6Wy^c #_5KN D 1pZ7.g _gяejOBFOP >"olej!/?$J2`*dbqb!b *+"\~Q\.sdħzĘ<ܹ]żZ&>Тxhi%#jSORҋLa 0@3T#P/Y0庰'm::N:`uҐe@- SoL]Oh{L읧ߋ\S0 Ђe곧ۤpF[W^X[9.YcAF]mϞjj=tBqVRpV/{pv|$11: ֮䂖 /#SW8`}-FwtHf ;A; a[B~DV85ƒŐzjD  }OAE@U#}Mpj| Gc]#SS 'nJ`>pЮ(,(uuA]cU 㴩\x۶łvBd)UW%\InrJXzy%,˱V²kh>jk€*EP>]z_K3t'gc8Rw"[$t{oVDЦ z+$:brq`8.t? }W1e{Phg4Nf򘡘{{;_Spdqo$m qCEAQk}m)ݶ O>Ю$#or%eK]LJ3j|# sReH*2+%FRH1ƆEpv KN A&F іGQZVmVn(H.'e \0G䢭]ۙ;%P7[]] x?*f2бB@״u^)ձXƛ߼Ա%w\]X6le]ݵ:lF@g}Pymb$s!Q fV4>xHpd55̸?뷆CV3L5'ѡd9hظBZ*q/O|Kb86j2:skonط?C={&Ws~J|<j({kk{6lIB:y~Pf܅fc逸vk~P[rDlbk(e\EEgCox/7n^Z,ڹ;l2C<1Զg-+R̀0v>i?pLW +])Ex0/`T6dϖ`Tuͯ躒E\s B熡=GF 7Œ)=04>ZߴU<3bӄ#g0 '.$uEDկHok{g2'N|lmښXmyҥ,M//GC/vb.=1EoX'I&y3 Nbn"v&cGexO!u Y[ITub,rTK򙏵ZK5,t*IlV.zX8aӌm=|]8+5Bv% ĵmAu9~>sQʊLO1!Dfo:~^ 'Nu]KkB;+Rqaƌ/]  IrɦR`2@;LK(TDIQ}}zhݺoOMPǺV V2RQؑUJR/2K5$J<%?Ɉm{vD2MoLuy%ItO~\!(I5-csUhTxڼ#tw |4P3gf 4OQP 8#כ2eJʭ~PɔBf.X+7&>cRhst͛[k!|%;~G%R%"TCrYlk֬$^^BUB9YȟI:6[y1C$=BAFEn| l\ ^jȨbbeP+UPT䠐y%\/*/(5pD:]I\-JlzټC`GX6u 2\bg,ꌽSFb^!_NMGs))Y ؏Es$ ,x @凁-Q-{S}R] L\-q*`%ŅT [=9n l)#i"6s*9GL{% iXIЮ+v:|xo. ̼K$oeq]Qk [w/>OT]ԦN^Xli?ܲ0-כc\>$3R)&>2K?V@^ .9Ŷ'_Fu;A訯dl/K)ĺY|1tGtz _~ڻvUK&iy=zy`nuh`Ϟk=vbŋRbײ'h1W.Ǜ^^To>w:c߾6Сl s>00aŽK{W >[pSϪU$VWd 78N$<9PON^QZaҤW"G[ V }gq`f^i^M&>,J0j;4󱄑ncDq KTYl_A-ٌk>qm]K#E$%aPNQ| \Q Q 7)`9E팘85$w- h p兂zzWDb 6 5=AH-]#ug[Ֆ_C(2|J)c+>غ  X* c2+rKw-ΏTz_alDRy# :D]&G\MCδ [W.oLl>a! IWGQupLP$"2G kyƱhO~r1</.\0F@l_p5Gn7?k(&syxթf{zbK;v5k^6p=Z).3 btj%%-]yyy =u-mjdφCʾN~sֺu|%Pa!H (tob3C{Sl m~-& 4/~͠C v?'/ҥ;Ky?8>gy+W7GkPcбNgnu)TEװ]- ` izv0 5Xma1}E'؂}S-glB㘢V*(Zs">刚28.]W3o|[pW';C{>R_,%j3`osYfW#*Ahe2tΘ$LL0 @9Zj} I'T$G*EL>$2Gz=R#~TD,B^:Bue7WEڵ9s57n<5t$rTu˖if*GR 7FsM SNQFͅvvqx<[q#vDZ `6g2}9pkSsC4W/7@E~jfO %F\䥉j|෣+Ll<_U}=y5J7].+~ nAw5$ut4'wGn7 wrN31<7[X<1:k2Y9)qxB]2--k*x$ḟڵc:tjoLs0)oyrO)AjJ+9 ͼ`] @^򷕐j|<]dv|,PuĩeCW.092H~ql4sU$ho:wz o" _yET\߂-2E 7f窪::ao̭[/T,%Wm1:Aټ9w_C qxA`.?(W /6X9ϣȫ)<-﹛vB{z!џ@?wKRJļFlX5~'uaU"II~[W4p:mAsZ/foBI?ѳe͟\.s/ۻZmo߫XZ%|Љs;}1#=o}+f?} -w}(pn\% 8d(vz2)8fLAcBqy-%i. Qm?csrQ`;kCJu!q$֢u RlyTP2A$eUWt0rBꉷ`s6uj[p;5^xDpN̬[{wjw%,9z> #h;p:+ +fr&j'0Meֽq!@gZ *8WPt5#pea[hRr؂AwJk(Dd؃9XY. zB2ZN2tBI@U G-Y/ss<#3NV[)ͼ\k%R uJuT2Ҷb+:[ =vp)f ܲy L)^-L$ 2Ơ#Z#"(V[vNh EKBW<,rhE7tB2qKʋFXhz91qn˛fX-}W NäRK9ﭘczZmժk-2ws3k} wJ-Vz7* ts45^R dj퉭[W6һEܢꉺ(u-&C)oF!Ǚ9(6i2AI;&W)r ; ; O|oר u].fT&P:Nj: K̇i'_Ÿagŋ׊ʾf@Bo,&ZLpm8[j^`h.&@ M>\[{۲:l;_Gj6&բqIdR&@A8ǐ$#m$N@I "FQ"b"#Q۔nvou?p{Y_{Uo}^{-?̷tӡSf/-3wl,G2Wǩp]]/:tTх97EG˕RuT#y~%h?ڇy7:!zc^Vה(WY[ ]~YYkRr7o.wNrC*ܽBQ5>㬙#ϭankOm xLDrȷ'Z2/7j#*&Bw @:kק(t~5-wg~j{(`{X@/J}Z} mۖ'FS8::Rho:mِ'RY$1$+4=@NF%1zN 5ӞL~η1p ;y2}|Z"/(-ܛ#}v񙏬?Y拠/?*(O2k4KUa.YW{PZQQgJ bq݆~r^vW+pҨ B -ڸDYr%~c0r- j|R(Gnx$?~IC`(̮9T]Tmw||Q(ESZ__y$rԎ?1g3?쵴 m+ǝb% a@?*hVhvw 寍6-Q42E;qlS Lk7 DWXp L1+Sgv66G  +i_ŝ / xz+|jrm6B0U]i6jx9l_je;]Q-qtij v[AD_ 4]5q7N?* ħx*ϭ4p|ZS)E iIU=gL"Ђp?d(;- gH5nqaxW]zKe? 6h,m5d LB^\ zHgOĴTDKI+q\UYpsQt "i0AƘ vFKOGΖl>K=.l˯Hlwܚ~~QqZy]+mvΛbBo ݩ"Jz_SSv@ߪ+]AhU]~9Klp|jVUCztDg9&p+p< k*C!7, |} ް R WW,{nDμ jmeFܑfY,VmR+]5:v ~ix|jx}/6X7ixrz̟˷b9*PU}֌M$"1KhjxQql 'AѨ iI(5a-G ҋbc 3II˻g]w]Kܻ6@gɗw=ImJ™3^J}dDlsUW(kš#•? < @^L ΐr5J&.k/TȀ̟̱GR9kJB8ZҬ^82}", Irifi%PȻ4j3E e}zE̯M"zU@SUF]bdw!3S @7iiRF3r4Zg845[DmR5oU_ *oqSg6xd@_Vm5BVу7S[zXJ2? ),eFCQ8Zތ\A2Rw41TZז{ qIy1 QX*&g/e͑,VɠQR?\j_iۿ/>I̻(,hoY$=}m4BVm1fet='{! +2El/$͝WhAȋFOAx ި#V0/<;m&h|7 zVQumM+!ir  qQQ`)rz~+yMyABapfX\^zn݂tѷ3EɈ)* pDЀ/Τǃ_N*Wux z4KrmUwqf(X}qZ@|6V>rQoHj0l@S8=Uϥ bj%@ }S`gZ'%y}?ӹOe.N$(䓶PYcgQ ?2xg(߳ǹ 5F̻j|ޗfik_DENpj.\vAɴ@ÀՐ`rXwPH,T2Μy=k`:<\#eRWE+H.N sQj'ܢ|Rji*ӤE]:ϊu#4Ab횸(ē+?yx4Wg her@h_'j&&TcJ㥝1mL㪺f(NfT@tQ0 t0<zGpH%~g:&͒F֥( >GGlxov ԳGșУYq:rFg_%=+μ^)Ylkrͭ9o*{5SlV,vOʽFgH:DG{GBw8gh:渾/l5.]fBVC Vځx+RGzJ_*?سES֏c0}nqy,a|(yo\:Rέ|h@(rW R;iq G*5 td|B˒C3PBdh(\V0 t`㋱}YQ)avIdcIœW_Ԡʁ祚tbj;j;]oLQhSOk€>_ _w^9yNvǙ3Ke*YswdǓW%jhSHJ^Al&&W 饭}ZUɏ+ Ҫ:s*tc0W>C-wS/F]mB0O`p)%W'Zc7:~ZRooΓ#ԻWfU@8qY=ѕƼ+ ?,G%ggXpSp)+ <_ѧ/TVUU\mݨ<#dZ)榙-]ʰQUy.ZٛLĕ8́4:΅QF2"8M||[L&ǣ_ESf͗w7r|g~*u"S5Ϛ3!4_wC~]ӗ^_v8,Bda" BRHQ;S.RCx8@Pg!eFu\[s@ qr1xHHb෷bvht8WڮTioR@Q'=ȜΜξnۄ8Q%," D3}RFm{G5BW/(T||F i Q)] yŸS-qѮ¦0XH@49`~| zs6|?~`AxJo?j\,lL]UBLˀ0]c7Aڝ\qElgilY(:Z9B ̸(d )ŞqD 2t- <mdJ\4XX tcb#ۛIE4޿hr]Q.ɹ4K׍ЃV](Gm~փA,fznIJUŖ"Fš ڼoP#Ed@+_r,W̙h ^}<3;R: ~&SG2o/]0~O?bvWhyEͲ :*ăQw@{c1/+՗A.nKW+̜ 6dE7Bw!d.BF$kݩZIz@nEB rxKw a5 ժo((#)U\.)WəϢ z$eQ?Zin3;Jf.հOfrzTթ41$dEet(m7#ZMRі (S|t)xRI%ѕamnb`Eho/a?T"ȗ~ÝsEYZy|ay?՘A(VóHz`Qy^_9k\ϯ%[d[x`FS۝V<:RT*jFΛ:4=K{T.(dBo;\{ǹgF,PC:$B>?[_LB͗ϊ^YDμ*gE 8lSs#@<0@jJA[n`w&;Q5D- 4P3*Yh~kW-<0EK)Cn!c,FAg.@P2Fe1CoXȢq^}~,4 Ɠ&hT臢Y%Tb_h^Ng­uLܠ*eW9ӳoj8RT_?9nѰWqk'oLF2s;o#L4mTġ9B]4Ք Чא piJi:u?S=j]^|FR9^/w|~\zǍ3oxꥋf[ٝ^52 }_e9N[C@Wmn.UrH_YV69̲<.STǥh84:Q|[S:yED*w&`(RHrE3}LVmvVv=%;)`ͫ>MГΙyW0J "oZY$37 =?q0sB p@nt{++nU=y`|$WGhxd*qLeޘԳ8pX&bkin(LFRI;2q=XċH&:$fnҎo6ACC͗I: {튼)m+ D RAM` RV_$Υ y87AKY'v5\2 q" |A^^R0=06|c%:ujmBV|>8u Ēg@%Gk'~M.蝎/"_~\|E-šA,p']:X2qW ~6 r#yHq"-`+ pYd@!X9o!3W "vJBo>DkgQ4lgc$>-T`bYrSWиcaڅ҈5vO'[3 e%>j|uYEȻ ys~;pH?Kكt#]*u@EP:pfHlocQ{0&Kzڦ/s 'S2Й'w3QP0#oyq,ScR!ήr, lSpD _!Mt_k[2 `|޴˼C+Kܕf9iʢW4N:ʄ-9[0+BP%>SU[ Ni˞  -8rtT 9 G&D\)滛O!!,e߈\@2pkt-N ]W6+#?Rl+  wYK}nv36[^sǴyx. Je$1U*Wĭ1}"ٝ8yC#ҍwK_jK#b /`љ֠7#iTh[A}QSfI|2qESZ5?~ܿ UYڵ44_k: Ia,J8 zIBy(_J\Z]%&ZB`xS(8_sv)L*qR3j`g_x7G/o۬:? %wH~bfřE-"ЍS#x&9.N75Bպ$Un" > ƽ@#{J\|Vad4 GBHsߧ[h8>@=w gj# ,g;??:/;8;.=7~ΗϚ^g~To5>C̪"@:(ϻPA&%ۯSJXC 7OB2*`]^*A z\-]žZ +5yzЗK՛U@.9x]/?k_%L+*-KRC2Jsi6Q4KMIʣ͕ mqˏ g6BHʼgr7 Af AX}¢6q)yY, 4yU=?_U؛afӤ˰WLᖴuV>ϥJGH@t 'GduL2$h,JcI$Ϯ"/%T&S6[ƙ 3_ Z9H}\+y|h5#?KBdIkc=Oק+;3*5DR(%6JrU}02 j^/ђ;ECstWPzV! ѫsmO dB50ʧ,oeJcj)dIym,=|e#tQzQ)C,Ȼ f/pۦZ,aPT {ύtH//VbiL5~MGHOۣ+4IՔJH$>-78ɵy'{t&M4jB@yq9wpX5 ot7:CZ:Y,p^D J2.'p,j||N rol-?tE=q9njˊk)AA?m_3CF}zeA*+b/F@(P'% ȆSGC)REucܟH 182D:%)PiŁq"CBwFiy^NsF2@mQ7nykڊDžd*4^>N{bUq*zi84O)AtWD#a][ӗ0mm>WUicd3/prXz&m6mMAmx\+MvwDas2տkj g޸%urx}K$Jm$ ?_4 W@>IE*o+<5Tn(Q~ k@K&"R"˕oo~;̗ϫMeTGY[ضqr⋪` w|8XF5C2ց`T!&ť¡ǡ=1L@Oz͎@5׈Ø?vs-!ruIY((QGBC2 C"͔)e?*(oB Q+ %܂+ywژ98y2]?]6-%g~.4hWυivq~x.y)XB1ۆ+?Գ5 Ffc1V" F6]WQ»Ww1)tĥf0O)7FÓnQz?tSwZQdicQ%;[4}L՗5s*B/XLwwfI}#dܦpUw[ꖃ|ڢ m=5]t|t@Wst8GIVBDZu!Xg*2m4yYͼ5\ 'bŕgR&IR4GaePx{,ď=c2iТ%"qr,R#ջ*թӌ5h{VoFVmssܱzf蝀m ,],;ayy~|Yf?KAPjB!FNiRS?Oɫ.'NBWj&?ܘ#PP)9qnG#3Uύ}[g,Z1TT,Fm"\"ϻj)RKRU7 U}ޒn=ymUm+ǁbf9lݦ|{AtVa]87fͩ/adOs#e0cKôCqp7i<}|zXTE:$S 褥7)mS RD_9;4'(HKoscׁt )#hp̃Ve#<$Fg<*Eq@JcJN;Xs4m]x2G_*kZ;R 7" Y@ WC+Q SJM)9*m_]G $mF-qA~D5c>-ysPt,*n̕A4ҔZBc~`ZVhUm~qGiGrܸ%>BT&Mv4OxB51[*[H1$ʢ0g`j#Y, @cr4&E4 T(g~ξ87')Q[G?HacJr_`i5 AU1;tL?FUwel“d>E٢$Jw,>|?$Iv^9=Z4vr wMYF[E'wIt &Y\Fa|Dt19G6_Fs? ѪW?01~ ;_9 2:-itDg<@)8..SijP ,rO=GaBim2эNnaY<;\N32A_S]JzQ<6Vd}盜6Bˁt̢mz/=j Y xV=CQdCOWzƄ9 j,xEf7@*+c\L{ЁT(; :'ӣ6ԡ#4U ,I#mdzz F`[@Ӵrn`z3]<8A}ь:&*-hxz@fs K[ ѵ7;5ZJ|Uۺ2P 0* 7.aTfo;.]٦dR8oP*aT +}Í@7(XA* y`R <>V(\䅈b3w2} ':[ b˽VB&C㶿th0?j>o?y4BȏBմmrL|yW%7|i}E(q#r *0g%KP#Tbλ^FkM3UG?$NL]AQx?$"bUS>7^yh'Bxin2`4' yCOpB$r Kyc~ͦV!yrGҦ*v)/o=.y(}MΓ~WK[ʬWevjVOt™ѐ;W@#RƁ"߯1QF qQUTѐ恗W(ô)b{)L^"6 BCu 5>/r"4^Iv qsXŃS1S3ghY[N`4w dyFO5s9y]ƏwAI̊/yϼrPq_ Р"So-&NKɋћ|d]}5b$"œ3<մ!TT?ae$AV/]`/9(ڴcrKO"R*0xl^V_xuGKEA\HA&H Ib2U OE@g4C@->r#qheLF^ Rl>0߀_θW| <7 PɻeyaΒ+u~75_6>R /2Ι}Õ "NUԺPBf8R+o @^zP ϔVl̰QYcl>`^γBQ:ov lE#$Y]\v3j5&cFriQaD&d'X3`k5y#sěvEcM Ijyvixdž k(pKxmN44MGjvm)p +UrWqK #KZ(88R+vcL~E!"lnQ17SXtr\W,0iQrTZkޝt*o*< E8N={$+GCg1t$Uw;>x:AC:wv@~ːzuJ\P\zPhv2XAhңH s)H! FHn*  3OX.>ԫ,lNb%oXՒ44Nr:z D8E) Z|wn:cm*]+]6;ۤXőB> mr6~To}]AA42B뤅;ɽA%!S  Ur׃X9o/:RݽdqJ$ڮMtb% R MaZb"8\1+jeʬA}VҖ4 }Mζ%]B?3o{ÍcLTsˁ49 ҃W =[}il\pFP~h @&>av?C Hs J^pY\&УBC"Ӟ(b@]ݖ)O,FZ ZGzD$F6ǘZۤ_z4qM.RCE_cWÃ4vuԛBUP0˧z?ihKv@k/'PdPAFPƸydi5[.閝6ֱ-`4 #D#Z7MK ClQpT:y2.* 91?qU;&4{/cF;gޤaEDJ4Lhzk_El͏/o~LxcaU|xtibC(XINGnl +:ݴC).nV@5<-̶H)?^'%Czfee?o(_@"4 0 [9 ҂i*gRD8Ʀ_sHAW[y}<(ӳ5?NB,wgM̚?n+?+OD]ӰY:9R8 r%>!m ¦_*+L4^)1nI TN+y(n<$j#ZTݧ~$AyU6z,y ܪo@fUz|=v /Ak> |y@AyJ Gv쾭~|*y.WDш\ yP')ا:5`Ĵ߾}k_3|ԟN'Kϲy*[n|2G ZMbȥ,+ Ofp&Ru L]WJ*C)ODtDIJI<&6¹PEL ThU_h`t# V{IJ9zF>B>|PcWzyʼ S g.N<~n'O맊o)LVl(X;u[H .FBޑ&*/,%32d'GVI]2t$z1XJ26})[7U^mo^] ]L¥:>^9r\E""."m. ǒF= yJjZzY"%aV V+b8ɏUSд3ϥ7NJm=I2܃E&ӠM/r ( -2o7[KS[/mt'6^e=/eʃoپȷlõk2DjJa4#j)$*iQp"x(a1ݍ&I6&u>Jֵc 0t`U 1~m骢kGmzc~_rUl늜A|&z,IRv/9Z\ϕ32kK$ U(8pvlc b(3~Ddmx9ݩ4gZsrwh w!RE=ӯva=P'h$%( (Oϲy(Yw4KM.]zdg/Mwty1xR%J#[ؓ~bc/g%*Gpmghk񺶮ٖ$<8RmV !suّYU_$j8'^ڥ~~"͛m.SϾW z.N-|īc NiT;+YXYsBi-'5jF=7tJ?֑zhuzSp+nî/p{T(8`{cmY'/D\a&4Us#Muvw"g[~,T Dnmg(ˢq謕(LG{_zl4ׁԠ,R/H%ܰGiunsiȠɶ=4BoOqo֌qQUF) 4\^ڙɏs+ns~o][ݺ[r'/,r8Y giCpvT?ڻbGӦ7SgqhU4*b7(ׅ\71q^VgPJOX PdBɄ;c?]>?hjPyӛ377v*x&,.r\ ˸n;,q =lL>=ch  WYSy+ʰ>6p0c1Yt AXz=Ns:dQ6$h݂"4ꀆ%|3o کcRw]ܤ'"moǛ;/}+Y x f$FsȳB\d*!/s֓:yP'[c&T:Җs; xQR( \Pzay .MW>k*S}|Ol~{={&\bsrBpbh}j> POWePw:}-K0wDY_"}A. Rh=4]Uw 9t{L]]֗[>}i|+tW/N[6w~0EqUnת$Mo'~Ӏl=s*3rP5M/C@ X)ʘ*ȿEùcs*R'/N^ 9ҽ@o"Ǘ=.}X ,X`YV⋬diGmQ{\\ԛ6xvrl~#-$ IH?dIkaDa g`eԲL~=nbz5ʩ`js<"XDCNV(C<Zx`]@M'?R/0`oQ9ϙ^\~XgO$qsai6&M>X~$]!EXMrm5 H!4˼aw_cOlrI2wjT P*4TlU[~pF"+rD9R+/"`zpwR_Sijˍk#(XA7MT[r^H?)8c={L\GLCڭ*GPnݘLK+5ؔR)QȯC?$;.T֗ʶ~vNtWX:2b&] GccTˍyJF}Thżܫow^;P,|<x)k<'SZ *u鉂."Yǻ.zʼ-W ^c}F"ņ^7rr{#V,YR+\x 8EQ{?'#oF"<`l<&#YYrcIytmVqq֓Z C o|SY@lu޺א-l\}@񲡝JѯRгtls =-= /[, ~y?~hj^Ng_h,2H;~C82 m,S]5'ǡ.qA̅F%[3 dQ/)\;޸+UD=ӥ$q"q82ۓۏ_W6*M~v:/ު5e; BP314TrxgcHPIׁ`=Ppo5W՛j-KΡ( xD(Xz*R?E ʦ 'y\שHSkH!/?I@vl*_}h(d*p@1JZx/iEs`}m0knU㳥ǔm`*9Š^Q}z[߫E4p =j8҈缘!NJŃsF;ͥҘc:]Zx'[_?-KMbo)* h=zE*Mҫ}ۢPbfHAqY|;cEo[J0Ȱ@MD<1x LRj[10cQ] PKHaSUU^޽>%T- jo_Лҵ 9W9$D7ͿǝA6),LgeJ5V04x /#.N48gsmUu8Ty&uQI)3o jf9ΉGEYSCe&o~;߻ #,E JUO-aX$, Ig߈Jm ZPnC B&4vdy:xc#/t؛½ "o u#Cb|KYq2~ /h4oۍMV[5gϯ{NS0BݖE|^^.]a]+kܥ،0¯JmDuQ]N iB98@iԋ.7 U~L7hWx@y{5~ jP?1Rqcқ,`FzWIdq~V*/Cު UqpW^Tn'*CAbhHX4kȁǢNɐ?2IE،ý_D\aٝ(2i`fY#yK~KXOb)?fsiEucV er4ϵyN]QF,DqQ }ߍXZ.Nśa'qzXᡬPjz8EFܓu@ОFS\IF [UOC" Tbӥ_|IMrv# ]%{ֲNA(itquݱPS䦦uᕭ\,Z&[~B㪢U=&lB*Q\Dy}W=[gGCL^'h,0f(h@5{@n||O qIDIJs/IM7 d4SufD zL i̋#7-fx6ʼnpR& =ݮSRu#P@^EqO`;S!NX\y$nQ=XV@[7$ۯRLU[z9"i*d:vh/ 5'/$6)Vs*h6\īDodSb owze L[hP3gE'r ze(Ek.„]m4;/^w}lđm*`c;W I) |F"喡N*ac@G ٌN,ΪKf|2٫Kat!0^b y$/,u/ y;թvy鉏w ቍpaUqNz&x (YZ!5}jMhP]MЏ.F ;O =H^ c<&@buStQ-Q#/q<_ M3\FEHo~#pI׺7t9VԘoD- rhꇪ̃#sOA<pac|x9t]3,DꆈB+tTh^5܋ե/-7d~ٛ:\2N]xTk r,}ӕ~m"lMoЄ@ /c#$i# z27He@i~_E>]+tL:檛7 Tf{ FYj胁1liNpèC !7x+4^d [4 uF Q^UB rBb_;9DNxd7xޮ5͔Es=J|V~RmsT.=&2ˈr_ǟ{56ғ9ZŪ q3捑.qz!}[ M3 -(ޛZ\aU{8ܡu|8(@2Ij0(:,YR4E6d@?2% H1dV"0IH%4=>|SOw8g]U9Ug}>~\v3U $ vC8R»b y9v7& C?'(bso}*|q"|̙cc|XVة$Fdn#?vj E}CՆ`id`r5%l3B4ݙn]Sl唺xb'x7%rZ+p PD'mui}2mf 3{lcpRzO^`N=t3sXg3B$i/+ l CI5p!/.AczٍR1}o(YrNtaT܅~zx&!V(tVqZQe4KL;b,w3~~ݝWaՂ"9@ S0hV 0QO otۍ.C~l|rh"~sb%ed4$mٰQ.=#b$QyGN]b Ÿ,2o6jT! _y8F:>lPEE*`]HO!u_ ynf>N{sv]=}geovхU)ѨW=ăUը ?dMYU8z(èC^ty={ٟ!q0n^So'>}vhRѵ*TGnϯ3;3]~u}v tx̵svh0*ify5hY#]ܢ|F%Fd<'GCQf2Cl-ҍgBtw#ܯG]ocnbE($ݷwfޫ,BW9RaQvs a28Crdk >Ja< iw{"@^?>9Ϡg klFTݒ4so>8mSqI)gIp3SϴE`%VBxlH~Vvf2CC:+Itsq˛ zX.CT-ſmX@ ˢ@,fhRA9R 帀R xl|w3&żEΙ`pQӏzqb]]~uW>_EX'M7}q]X]/(oKJP{f\u!c,ucU_ďљ+^{[Z$ߩ83^pG' v4)`\5uu(jm`b]ַn%r;0 P˭*Ego+ow6׉A,]srRsCHˉ8XY|aXu/DEa 5<`NG,(+Io.f_K'rl o X?n y GN1 x' )1bm^#Et-dǐ[yqp6Ƨ$[?>.>>L{|qڗ:IxOBAj嫡';"@2e͉ۨPpf/Suu!As\L";Y</Kr%.k%hPz GY #.֪Bt,`]DSc6x!wM;E ]׿&TjHRwN I;n4f'B̲w㵋 %X:[4TprŝX3Bn``SMOV1D1 DقrB#!ߊ,;xueĮ}ENKWbxj]ИU~ xvҰӦEm15/օ{ÂɂN gK wN)QY;$c7#; v*g#|u nYEc+ I(O<;6{Z0~iҎܴ`l!>'ٓ<Pcx c@<ۋ1V u ďof͈Ӛ@2eO:eŽ8Kq+d^xN(P2)( x`O_ϬD!=jGצzeym̘M.bEӋ lTW22LO;R"#M{7ecu!dׅƮߓ h7=+nC7WZFY4J 5-k3$ 4$lc&IwF˜EƋb Xgo,fNGZcN %>Gx̄B B0oU5e0Jޕ(ef]1V. _9l|,m5ֈȩX7bb䒹Alj2zĂV (C)DAIkKpaw=*)Ri{U2tjM.X>Hg;M3Y6H%1D;=dvͥ_)D: ή'e#F&)tQd>/6-T rPhrA',*x?;_r@bl=btXc=z9c7u!ǁ*KG2x6( 1gmE]f&5Z 3Wf3($5e fywj P~7ܿVB2bڶLKDf kc, , ږENa%IP))r숬W$B;ܑ F z_m${- ԭS|0wUFl =/v^dz 1OEF-}ǃMĜiFEݒ!|fᘠZDz+hjgFvwRUѫ:b,n#M4CI7ܰ;墧"at ďƓmW (d:L0D@-c@.2P1⢴(5JUҩϬZnćcRI3{3>~l7~]ȇu8b ɕ Q!}[F̧1AYv9&̆pN@C+ߓ ?q__9{ +Qvzre)P<,x2mR8z='pNaf .0%]z'>ˣDf1_NGGϺoL'N@ ofzUgN;YI'{P",0FxX`@FDX91%N4leHaJ'c =7F5K@gq]p|Ľ>غέv:!gׯ>sKi?nY7S.4okaF d0"P>EktovAa.]DX9j@#P'p"L$ )~'R`N6qW$ʬ*!abG,LA҆cĎnl.f[eq <~֊<6B|dPГ~7_?Z6D)NUnֈU92X*z4f^DzpܙIH.pu傅AR~`U`{Ѡ +zq!0W X@pw|4|0ICC&EcZ>eW3|t=f^/l\3u|sք,CH1eQ74LIT]LbbV?_QcxCF1xTi#LHVb y$<1T`j#pڒ )[CzB%CKR8_ c݁Nfww,h2 CL? E'όBPSyPƈ1* 3e/  F8Z)k+8yʎE:@kasWb>P:D=(0crM3LYY>[?>mf1g .~ivׯJz9zU8N2+xFF:3f;c+ P.r2B XvWo"xtRXۗFf8?o: _4ȼ+N Jh0M:"x7z?w7zݵeXr]_U#@xJ@OL^ogGЀ[֒)yWF3fjɘ UՐ8%5(xAA3FD#. 1B#^a):I7Am*VYW{/汽}|q]:MH)/+S)8 [XW*qn"u. {l4By&}2Cd:;032ܮcXIA T@̪SI" G`dr:!ZB9߮nuW]y5mtٻchPlużauڨP Km5μ'F !a@ud0`.F~TZfwrm#D]إn:mptȕ.<.8^/!0KLJDkUؐAbu"C^hv_Jr}w7a Ŵ}: u(*ݞ3S 0A5aΒDщc痊|Ckxs;59T'KUS'A;#PXHeWjI9=WwrwBaS/EgY7l:h)^V\ݍ ㎲5cH6x€\kpPrW[rʱ͝}`7\h C]ިOՎ36*sXJj H8EQ%?Y4 mI]bz y[V̞_:pK*_t ?ؐ^&`+x^m9lK^N6(g L5LJ?Bg5!,SS ޔ ڨ tpY9ʉ]Y[ۨ`lS6&.pZγdM‡EP6U2S ^Q3pdpPT7F$*$ "SQYR!אN© Pv0;@()GX*xMTVaP?X ~j*vr?HpI:iё.w%̶h+8yNxLUj M&pz[Vy5(:bEt,8nЪ9F}-e-AP:- ^7x D Sd9B \3̄,1oK!ԅmPeeEͪ[7J}GB_[O9Ԑ, a '}羫Fi7BaѿUŻx-k9=wl=D50V&Y`| NV{n݈r-R^h;O('pxb 99Y؋:D&d!jYho!|\Xinji\Y"8ԔvtYRr Mgiؘ :NA2E/y%x~Ǎ @(P6mzG镱K$A*IxLlCE;f|rY[ %, }-.cFCD/KB_txR~X !=XWݗ~᯼̅'wr\x;nix=E'@Rp)fł53b9= }LK<~KCY. :=j?鼙G(q1q0 `y<*WVTx~}Sٝ3T*Μ jN&25e0 U-zZ‏\m}Mi[y}-1wN{.$(Ё ;X):e2hGJ}T%( T\,ASxy")X(}tds;ڱ-*m[zf\?ƥC=^؉.=vKLtڸz @x: 6M~n:̜|>pns5|8c{/37"忖zfaG!򛍣7r}SxONXLsğ@TD`$b./1Ofjhz?L̑"P$=d;R*dه:`Oko&V6JM/ aRz1BAXÄ bށq߻r&жnd`;Bj&IoȽآ20g,sY0#,xu-g33 i#IȈ:P*{ L&a\v@A)X*UJ9]Yx*8ۂ:1w'Za[r;p)]y] jlj@'[P88/\rK8Um=`lp^g Oa|bi $p$ sӜQ)(q:"Tٵ6e!P-?&J#jNCjX #*^;*fe0A*(2FnLdJ*)Vó3+@n ׾r;KU86,<.5 ?G-0]ONG7Fy2JB&^:vfB)tƺ'sFR\Ȗї0 蘢*%l %9`/{q P!#5G-V=߁_, _X%(11NlOyW:U֎F?ĖvGǼtvؼ;pgS af .F໎P(pIC%$5;6`&{Q<>-QɖB)p|CW$ N4RX- OBb_~XFva7ڱ_. \I{a*WE"xF!*vIzAJ',XQCi+ Se]yW6.N{j#(;}=ObbTL( Si bu} 9ܼ#'1P05̎/񹟿o.HMםu~O\s> қh}jg>G;/OҙzS[K>ȔDq6vEh f.Oi-pA<|A}3qwoV̔jfIkI(q;*W.-yGX P('L ` F7p' ,j;z XMhQa.<\@^9.$K={ 5F-j'Yp'cpbFd`3GJ[d(Y 6*^'1 ^/`C?Y&[-7?sjNhn]?v]vm8xZv9\A&ƇQ wRjK7J픚EsJK !K1&E9JBe DG-_/7S '\\E=/'_X7{/ ?suK3mu1\z}cd&[TDz>\w%|C"@ H r"e] akd&h^n70bjI&ݶZV3 l$uWcLE )K궑$\;yԸrAɅy*xA"dCFמw'I1#bK#0PX ^P&.__<'?ϗc|RwW_?^,W ri} EUu]z  TR ^ɉi( ;C,FG+ SHV @O>Ql.@f`èvr.l3v^(~(b# YF! n9YТ{UcN/^':}QT!novg%aF!&7cwJpA%DAVSWY3`{h7^#iqFqgxf"]?uAE;?Vsu巳wp>]%zEE ՎMie5+-: 0<…+(9^v jH%?C>X$nu񾥝_g@Z@l8H%H;N/ԉȝ@/5MZ+_Ȣq,o:d—`CM{D$lr M2Eދ=,F<H]it5j(-ȳLqnIS2ͯ'B%Ac<<ȽBCl@֑ܘ2xD8($G0N"FX07 27]$Z얻~ADls 5D9RAFt~BYUbv^jj}cr+{]xǏ2Dž3c_gŁij(pxy?/{~퟾_K!TP~܊a;t8 As7_wwmg~ϤսUHJ^G7h<^{z!Qp]U$<^xDNO.#!p0 K⏧a* +NՁqqH XmJߙ`)C{ABS֖7HP:Sao@CPx%-h!øHfA&[b^yە=瀗9< g ё= lXL. 39H=B`2<s"R->b|_v6+exP3v͙}9!OEN7~A!̾轠6 I)M熿_\kVșYRsŷ ׸z=:v>¹!)u p:~6pq(n4A%P;"JgDžظQxȳ[AtrIj$ꅁמ ; ?0iPtnՃ )YڛLG cH hēWj#9-Gm1.W3 lUb xcߞ)j b݃ EĭZ0;R @Pr,iq x۠.XD f&pXXeW; O9N58LfW<`¦ig|}]0>GhQW!U|}⫯ҧ>g_:ԬmTD搃F3|U.UxǯuᨩQԹSM̗G6/pr{w(6`ˍHcP˰hiYp ɂpdKPǢ݀ *k$c(c+}D Į. *AA. `vaܩ v .Iķ>Xt0eL=9@(ExP|,;$&Jk/` #I3#mJȈY9.h#&EA".{O/ JY*0v5"%6 4ts5s(BnBN]sQ >)}C >.?@‘^$ݠL#p1uSܦPPB;zepsf4,:F0o}a}oDنsB^w?(((Q5\0<$Åq1Q%hK |.w,,#mϓyaeLicGܦky>`6B[-"+qă!(|9" ޖŰNQtrxrWF}B850`BI/tRh1mJs/ FyI rv ȱkǻAd"'`P:-xQ03ӤPW pCr} Z VKZts}O`,"'dX:`)F{_ SKmxOlV|p[?E}#>/g=t] Լr3$=X}01 3*1F&P2.7S@{Fy+W/7֬QFKGC"C@pU[J2&A6c7n>HQwm JPƣ91`n{uD-Dэ[lP_I1i&[9!Nt=I3HqZ s3j0ql :NaÆ=鱥@?@CB_9etvde"<$]CQ-+!/ieW 8B'8y\+l.Z7/6ӑC ƿIs^5^}}L^68@\N m<yϟ]xk6.9 +GQhpr 0ѕ]xkf]:wxy!9mӴŏW)ĆRk FF3nJ,EF Lu;&Touڹ"qב4^%J hC|_.x-̾^E1+1}?'׬‰^# Oy32>AC&zz8n~9nPޟN= @*NI0EI;dZa~9j5g 8%;!2ECzKD NS9ݻgv6BYG`p\k ɠcpM7;=0l+XOU.ߣc?PւzZzv.?S?y;gυ޶ځ^ϸZ1٠U.CMP42Ћ\īIӂP!r+ Ϡ0V^S'3hLT:_{"#eHI s{!V gA!Hli4Ah5x(}Gxǘuef8(T#+?C# vq+X-$mL Ձ` ^`Bv#q3R{4dspGRJIX sx B9쒈xcGl7tO|_6w=gzlV2+ݽ(lޒk BEb8/O~4WzYC+;η(o`/?3>Fc L;q$h̗yTO$m%;NE;਱}vhG$B橓SE" đݏ{,P( Q-5  t@OP {O–jWhLheG v:NW^Cz o1L; AQ0b V!itƴ.M=N=',|sﰵ/4i6"X'JD\d>*y8`؉sGǺs51T@tmqay{dMH(`΀v+âcnT^[ʬ o o >mZu˝y=6E¡az"sp6N^'hmS~2̰`5R J`J!L,"R gIXd:cCe'V[ʙ;x A+N7(ikJ5\!l}ѠΟ}s;. ܁{C\_`5.A 'DfYKνX7 3Mx6 ` Y̷A(54/mmUXTkOt ,сЙC$("|j@\㴃fJ I:E#%;Ul as,0Py3m]91oOܷuڠPLmmjӮӂoVqμŶc̓/}y~F3%T%E (Cm;9 #3&VE,>{}+~pqiNsS}֬xȶ.<| l+۠[x fֽ3= @X@ 't(@4d'FU VKPQdHf7ꕁ P 0f0=lQ @bpu5AP&M4VK ^h Epxa ٣J-(/.elTڵX-|ƪ!=uҜnȃH5f丂?% G%a."} M dI|e&}C0 d%ˮ|u&AxsFϛACh7N:z]p<=hb6O>`_eGPK|ozۥ_ge]+A\:1d0FKqkO pvTxPlJ!h8 @'Cdb(s]4 Mȼ8-v䇃| م.~;M7)wB{63|'k xYlmvIm~l? AM|%nuVhC&9]rR cTdDHҝW~'m;-A:'lꄣIדyWc>OxūG -A'80FI%D`QJZZmȘ?]@BA!B-ydL NfH^40YjW&]$ ȺrfDPLTk%{39i% VL ?%:cwYX#RUeE/Xl5 ^  Ǒ?G/:exp%vb1S7; ^{-igxObJ ԷنVN^9%e#;B[[7JS6$:B:kϋ__ԥ| t64|]m6k'FGEş`׻mwXԡ'REp}K̏XVXCIWDAc#oK14%΅~@!4|V!Qfx>\wfU` 'V"̜il/ " y] 3{Fc;;V| u8 KͬDZpxClVg4@D$9؂55,ՇV0t$iɁ?Ly!'pHZ n ?b߭5 qʶ|Ĕ9߶KEzb|-3' I/E^lvg׼ڐY.bIX*rk "fn@}$⼷ɰQxhCY)53}I >ҽ҃AEؒ4G躙54Ȣ$'0E0,a49nk8S!MW ZQ^tUuӨuh,yA {,\:4]lRHB=%R@Ub;|V_oۆs7{` :uPt1h*7*Cgkm{_~T ]pA;$GٽfgvÅ@.tqΛۊȌ*vjhGU4 )V;p[cQBP gvC->u` tmICD%Pé2o- Z9;9b[ ؾڒՁc O^C_M4Zጸ-u FUZu'|[fkomI2AG:*Җ0xp a]@|j{g:S vLdƆAqr K{aƌBE=ń'ğŮg9~ygغ?QGm /Έեm.' O>n`e)ض!kMad+xqɟ,;0}_ 5D#l]p`I`5e&szf UrrupZ-KANz]'HNZ2(.|P/ -+a](,a1cnHt03} w/??~.N@x/09Hŀ-@J\(-r-5 eeܰzD])WaS+v4ΰOCC8o5:kkr"*dC!]vq]}dBx%uO U7Xz%.Nx|߱.<2[ه]߫UF6z`68Xx׆q5Lw 4fߥt^bkTE壯wDirKz7b^Oq[k`sQ̹?~cH ӷjec+ڧƳah[3/;w}o?I̽~]>ԥS'k.k.Pэט~?ᓶQ|#CSo"1Gz:P FAC4n-GT3:I;ܩ"uG@ѽ#-/N<8%!۠kșrx`^&{hqÅ-V uRH 3L5u WYX jIʾ,lH4SgBl(ӆ?Czn Z0;(S =>0O#;oL xWq'gDM^ـ\ ю!R,v1Z.cf9 rLTbg>'e{>zZr S{s}Oo,CϷ=}CϯǏ7|G^f~"SbGٛ-XQHT¶3W%vpN$]R4fzrGfTXxӐ p&܅WtCN΂M*f򆔳SP*DAjc:N^`1fp(u~d$Z)t}e",^è 12 ծ\a$ą/9⨣~yOv8_WZ^ Q`L^jb?5̈NԔ 8 憑 1^}w|Ƽ޿|7K? 9Ƹ z;sW1v~,>733_ !$+%!=H6RWE$8ID;ULD ,1*@hC~`O,:9qqB4@o7–բ3ӡ|(>ÝBc&X*{B4D9;s$1qGh`hx}vM Ixq| %5~Oxm>=q=FQyr=4}|Z  (7N|gz:V)cOrGY@8, BXڝ(|{3ؿ٦}ߪ¾MQzbk1^[ocɓ,Ng.۷s2\w.T8Q6FVh DFyx=X !1*BwreFFG @U@2FqlCJPxʹ4eaS%JP OXăZ\Kkc*5_)*ɕ8c!C[Sqز0hP/Η7 |ʳ'NEN/?_|:sO{͛Vt枮 pA 3e!)A ЌAftՔ.Jy9_؂ܾu[>QѭP'׶@s8C+ܱo{ksy:9֋ěj}Q`T\w!0$}Y{/SQ7F`0TaEf)לؖCGot߾SWO!X̐mn5Z!`Sfѫc|+Fcdfc(ൂ;Bi[tx1uS_yJL^ ۅ7xL#֖;X+3CU케O?+'O+yp_/;JwDFay;tak#FnI9Q!5k}Cx+?6+ۊzVeZx'Z?B"s'`~ssw[sj(9Vv.5Z&'F!b+<7BΛpUA5`=< f^NZ#N[N*ӽEΓ߿%<}q?¢B@ݱ)/XHED,PS髮0>z9 򂿚(('L5T 2B2<9TWňE0WP|eYjF[{sT[ebY;Bujp"/hJNSHs)NӋ8?PPcJ=7xrgzf|t'Mycʃ#^wϡw56v;q̕" w\IU'PJ]`0DŽ'c|zOJ+ifKQM4ڕv׶J4f;wQc›x'z' U][mםSډHiŐv}u%y%o\l~~^|̗^p v]w @{#_N_ʴ{D Jw dΪ!(bXXQ0$M9Z{AGH  -J33(" idhn `wEEnX|W gNHwV,H#NYz0849BXɚ l-F1Qv؎ێ{ Jk u%/m{Xo?"ٞC"f]5 7ø9 OP_bpf*#&ġ37{I@|sgA۴Ǣ??rtx8ۆGkm X(vlCaK%qn 9 ײ,/1-lL79xZzgn&.j=~"?_`sߥt g5V{W_},j^qɕtkJ[.[@nଅ ([ +ya[`lXC}<]sns.|^`J@4?E!" |nD_܊wi'v"s!Qޙ`jXuZWȍc7q̾>K7?8VKL,b&K_z}X_f~b=e$hZ= ÍK@;P#Isu p({z F/I_i1 ~;7{'NOg4 STvµQH.uwAs/.'v_ҋ"C&X&v ^[ZICOex&|(=v qGÎ\.Er{džAc`u`Imczb -^w Pm 2%q5rKPda=#\Ibi(wF$'&'7hyrWz)~;ȼPrp@]WКw8hPF!2q/ܹ%l v0w_\7'8)5g~};CE<1sT±E/E?W]uՙ)Х?vڍazeB5$v?ўrQh3k6 |t"O!B{w\f3Q&%YHޜAC¿'n {  mb%V%'x "6`7.P%r{%ʃS4JfNnNLgÐ:_~\ZC:y\IöN *e;k{ i}Fk>VcfL]^ /|Űb90t|]{fK9x1N/{u\m|q1CBBb?^ >ԝcz.U,v#(jFd \ovyM6*>]$ސaf Qh6 '%vó(^❸HpZ_iA?º֭sEJC#M?g@9Lb7vrto.Fi  gmd_*pG0Nj?r CҞpSU{i}젵ha9+Vؾg_&'!x/\C1Qn(U9t RmknyvQ7y ?'TB#EC8Yc;pw=Gz  ;\Q.lbQ}'P|Z6'` _$?u#?8"vL뷤l9J򮺣 f':{ݪQke)U셞J +q [x'v6+ʎqɾ~Dˑ\szVe$STXq, zV,EljIzbtss QDD6H50yx,G{TAǏc߳DΊCfV&j()yavň rH!&>0vN u﷢WXWDL`waUIx$`lCjfUn=o?x}^RP/db+S!2̶]Dϸo-c(]g I.jEwA jlsqAguTy{Jݿv| 4K͐tb]b BS/g>u?Fyn۴op^~okm%tܬTP\5S(cx{8J}޹ư 7P0G DĞ2T/#ZȽȅ%xrO]{0|ޝI,fO>ɘvC<BZCy7f_sN+%J wԭW=άBmR8wfESͧ_w|%Jĥ`"^ O% o>ۀ[<ɳO߷sF[ ~`.XAX);p=xU`HxјCh!n 7L>c/Ev>/ЩCg}1Ǣ~(=Tv1+tH7c‰w\ |;<lPw|PYˢA>㝞?gzhXy8+*oEݞN {w[MqTOvPwu#*AjLdf d(XF9 ]~%yieƉ:m.lr8ab1I@F!~LsqmCS]/Eɱ3B&9 L7.Ą駘 za-'{սĔ(hF, Sr|n8̙q,)%1#0^PtH:srBɖ? ?w&GD#-tmsث|EiW0ϲzvs4ʵ|@k[X&:]Ohhb) R-~>Ffmr!jmpERAS.u6EkʔyXtQׅM 86l5:gDukHWT#TW%/dJ>-|4vV~&(QfA:wvS*n= BUQ.|Bg&JRh(E 9< EUg$qK~GBcK`JOx+.?^(B@ͷ?wA'&{ۻSܞ(KNX"~2rq{{W$f5sq~&5h[pɗ;>̛`ʼլo%?|g I |d$H X꥟\U)":`~2Vb^^sTYs 5fPYO-K,e>8e^' 23Xmš9\t#f"p&o ŗL3C5,/K<:iE.AfQ+q. %ŭ>-n6϶nT`D%:ozg3)*i|掶bgur7%eZVzᮛ4І*xωF8Ktc&+Gj勾Шz4B|hXoW_ĢY?߭Mz6 r#} edAm^E PYj¤Gnhzt_vR7?R]$8RZ;w&(qB|M(m 욡S"'wf]>v\mcTvzr޷Kܸw-n0?˅Tam?"}ЈB`} XvJk–09 |daŧuj4> me܆w.JC0ֳ#]t9?]zSN!d2ka"P^0a`y[hsi:]H\qsQ4Ww.'=]D[ntg7=~e(/FڵBS0p#< d9gAy33ā=ִk=Vėb@:}ݦ O1s&{kn.G5 -8 0Gqv1y|&W6AQ.pJ4̯9V?=@)_$t-MFJo T {F7^&YI|g&ms.T_t?8]c7P^)}ƚwh^~t8]U7AJs7^@irYSHs7(0gQax9ߍ#B6_֒"f/Ay"QιgAyWQ/rA{RO 6S%x(*h0q^-,@ nAcwNDi|˅g/2hKގ!~NFJ(w߇)?2a^tK[R:Ui K6psY^\Wf3(Fo7$(GU?GU\Ӏ, v"[ɻz4jSloDa}wὺ~>'WR Ei׾1D d@UJXC6 avYa8 !9 5`qj2"7\:1E;-GGFӥ6wUa=8b)܉Ga.fY~(s.+q~R֤ y6Z@̖ 2ASynvf`CnX4EX2v[ߨVF _̱i+tޠW5-U*207d:Wl@H"_ȑ NG8N $=45A;&'׋麟 ER4W׿~~[ו un0.3mȌmm/nUCu>^~gj&C)ޤ= F4zkdpkÅyA>b~&ɛ~F[-꺁:]v"{m1׮")$ GQ7WA"!c%okinٜݦ`w~޻RP_GQX|BUU k뷊H_l fsvy @'EyVΒxkq4S'[|^T@efJg#rwCP+G75|P}㿅ai_|3+H(w'/1fH]nYvx)i. !d>Z z\4ӢOWgO36m4~.cyŤ0RE]5FW7 Eʩ0qMs؃r ȋ T?F R-qЉyL7}cAmlp.e . g?ٕbrϷEL@- FTUkzkc~uuxzMT[όpV636J5sclg34t|Hb7~yo.3@8oh!YX qw{.?Ow뵍iգ۾C~g~ 7cX9 ʢi稬Wt@]]1Pח{0<8ɕmyW2Sw%̔41jW1 1^M9̮iŐ)b8J2zdm`]#[0tUnƕbr+ԑӏɔϳ#jTzr࠾ HTkJ$ q]PsvjrUVN.xK<"NJ٠wlC߳: xQB+ iBD24α o]5tRr+qz? cgXsTjz[P%=^R6^-AG8<[C'*9, xks;'QP of1ֱ̂h}5g7=Pm_\)+ʱrP1Ǥj>T?͚ɇ>?{\j]3zywXeP؀Ϲ}ݿ_Am8w?.%\y"*#(lo5>̟?N~'?߅~Gk{_,o_e^65? q"8p*?})nhڿV w+?|5yoyfy yUs/R)'}a75kwo׽n7"SJ.NiF,/ ,V 0 4yI]~[  )Q(ҹ"eYJ R`RIp4U8i=CVR9qcxxlN١V95PeVf]Ӣ1I_6+!Fut3#lTёHsxݖujU)u{<;EUL.ui+΃ugXrO,KJAf7볜ZԽT986$XMڮ)Ip 1f/sN$`*,iCrM:Rt]xk˲W4M'OURZo%ܱ6lyv^lh@_K$/M+]fwm2y*> KFobsuo6X~hv,!ѶT{Z3&K2AVN{NL+&_,(ki$f*F28ROd@ݳ#GU-vN)>Vd^"4cS7-Q52LXw½%߶ӕ|ne4U89WvB;eO(VK,-n0Jqu &jN~]/%ly2XtAɷ&殇LW7϶H;n Tb0+He?%ak*- N觶PAO^4# C ui%U/1nfQ DIT]GRZ~+8 *y{֭Ka*@;!dʟ"S(JX.y}PbHGko,ۤ(B0|H?htE=g3B$њ,2H dc?/I"zWuKpP]GosIu{!< a ]u3† #rJUC}{٠*?3To.K{_eIX(~wh 8ȀVcf%2gs$Ms:pGȎq Gh @CEQjdz,<])e,D\V'rV=+ӽ;iS^ t9G} 7`.]mҎ9]2i/]pD v ~TlLMԃR!jt空2Sܥݒ>9-fL}abu*nY]]N^]ӧpMTѹs[ms0jصurB> GA`hiOT*N`SޞU_\`00Њ Ek` 6X5Z.e$LTVeG!g~@Hw2BߎvH!6.g% ȳŽF b:Ck9@ܧfAC7a& E٭찱?}`ÓD B`Rj2pA{Nn.SaG2LjjyU upΌ*CF[[$m%NWaAP{.vd'd|zdH3r(P~8a KH8^u}058+0; m-!Z$K+|mTp D8ȺlC4,NTbь,l AԂP3$DcYAx!u)SMN>sۗsmTl8KZP;I0R´4Y ;NLcoӂt7mHg!e.V kzE9}`i`HJ1^i*y< 9cFA>AWZB,R*Cch2L'SFC YMa1ft.muX`蔐 uP)5RPjc` )vdLBn=Md̥]n=htc4Pˆe>J{4ʹ7X [g͂-e$q4i%4#~ &F)=}| iTXtXML:com.adobe.xmp @XGsBIT|d IDATxͽy|W}>̳j,ɶ;;{ $) Eeh~ZZJ{BR~45,-ʖam $bI>913#;՜&ci433=#x訽/|wfW_{ޏ e==_OL"CuJj=|HUwx]xo{]8CCzߍ Syg$P--CYAg-`)uGW/}_sε~ب}߸}ȡ!9>>nS T]$ pa[|ggOo( DhfRQ,P(vjDQ& GZu ZfOi.wfxwg?jll<3=#E(5XcQr7b'*hM1ZDQ@jr.l\$B)uwPooys_HV<);%EGPfsj? '!H)J"RThC) VfB4k B5 $:X$?_W P۷E3I :dR[j9(j154QKV0h`R?9R ˙Tʟ[_82<}Ձ{7qu>_Mcq" QA1fHN$?lR:JibjT )SB"_`ĺGȈ` ȝz|Ϗy#m1&v3 ]4UYLGABC,rb^?K!S{D>BHV hGQb}C˖~񗢙9::sA%2FFF=ܰvVO' T+ӄA#{L5qi0i2KYYgS#5.֙$[?Ěu';Ocg]F-?]׽յbi +. [Xkx)$a[H4p'pi Ru*M*-jiZ:qγ?7+ϕk֟ͣy7 n}KX2sttԎ ݼr.E lrB }\'NkN(*cGqÅ($h5U'(X@*E._f`p\R,atd=:JڷmGy+C{~")AP'hJ.Q$R \=˓PF A^Qy՛K["LSO/0@'{/=$W[.~=B0qH)p6a=!Ia"L)q Mv)ڷ+7uȈ}rq)*÷vP(V+X4ꁟXTz*yp's3># QTRPy<Q%0)9۷Vlک/|o\4XNXRԈC Oim`F]zWP:xo#df(a@vIϚG 5DLsyqgipBC g߸CCZӢ&8Zody6\u+:\Hˣ|A`z13u?]=}/QZ9Rˬ$\"M5IzLS ~^v[5āshhH۷PoCn;Ǝr %,QLهR2j 0q 㒀[¬1If%^e)+uλĎ*uNiv@ntz1~i)vCcgYo/}CHU"$P2q3ӵE@iM܃G:g^gXg(n}`/x-y>"#( JPY ro`n130R5Q-b9T)^ F)Bk' ~ AS/)*AQz:]YP偧jYenKOҬUWu:sf^iϟԝIݤgrP씹CC#z߾%QڽG|)R6mu"mUH Q_mYQ817 \8zhӓQNF 8*(lV=6`&v&Ǝ1:nȗj{; )c,ua\"ˬ$o98Dm/!h P :Zf: (EeB^JpֱfV6{-]-TgvNjR8'8W8vVX Kk9n$rr/uam7Eq,#-Qg{fg;O#Vĩg4qBcJ#2`K#Je sXVzM23ӖǨWѨCk!5)IIGggs{w7z Pq&:1N,4h@VP.C3Xttڍ` QU9Z Gfi4,A :lͽ \?coxgRF%$ΉWWgݜ/`ed~.=f+1AmA!. mT(V@Z7b)Dj6YǵꜪt5pf)* [*@'n„M >$8q~W؝'_&ࡃM~͘8.d5Y6lMkQB}&k&&M 7ވ9#oqJC{qeG㱮u(OHu;p.nx_'4wb[vqz"y>)ˁk5.mP-e/cO!X>j׵鷄sC#?)>aDkR1'; њ۾\vE@%R'sPq Uy7903! իQ kc$c)j~w*"];W(?dddAbt Q(A¡$2]16a gb'PJ߇b˖ӏ3qhs⍽NcUO6l(@2~NLq#]RYaDf9q\-ncèӧ&XΝGG=>W,[( M ,:` YBB%q f&:#?h&1ȢbEh O_Q!ǒᜲ,N]&cW1 Þc56Q&'jp+ԏy%::=ӅuѻFl(՗Y#?O$@eizQɲgEfiFP)[6 9ZS0;-x!}$T3iRI.Z-GA375-W?PV;Vx+aI8K\eZkl9}e$tt :=F>K|0ҁ#ry_٣o:ÆpxA&uGl^A =ZH.X]k>ȅϞa`: 'Z슼5/u˖no`b:t⩩H3IQ###X,~\~Es!p##Wwwm+X :)7oü3䕎M9 7qNγarB.Ιr˖-{!iM7/B7Xk*Bm3|{fr'〡".t8v~I~Ӽ5RVMJ֭Q !n]{#wEAj ¯徴L[,on뛝s,K/bFFFI8=96;)t;ƿK\ı^l# :m[`n#]"R_(VYy^OwwsPjbq`x8_g%G'GqꅛrE~5o*I-j=t^P vy!X9p%7 pb"g E  UBZZ> _ǎlHŎ;80B7"O~p23PU)^.CXagr&)kA px9j*!Ys9Ϥι|]`"#df ıN6TRsuy,jKPosrLsD(ki<{V0 +OmrOl6J)_""Oܓo3/yoկ{_jx[ oCca+ f0 -PWN 6f%-[8fZ911qnX,X,/G=@AdX|EþS\~EOOEr§n/T۵W |MMT9?`X_)Pԃ䲣Y9MB4ӺH+Ahd!e[*vZk{ ߝgRjzrwjv !k5_]^v[~ _r;GeC(?ɚiKme*sИKV0 xeŀ1wE{ljrra>UY>|hŊ}LG_؇JZ-:* ?\fl:Ʊ|a㧫dm!ިj+"Ѳ0K˺V \g !DR;6CQJwbHtk8n{I`rna}G񍗭oS<4=tȲ~Jđ))-Yp %j&~j&HZÚ+Ă\~}ѯE]sO000h4V ]ɜQ12 guS9fYb*KdЊհf`EPa1%MkAl,&!fx0Մ n"M׀ٳ'lZϗRZo3ju5O:vmۂUy KVBo*o\R,B #m*L,+8WrN[jD^.? T? IDAT96 4nl{zWTEц;SB,6`cBhm)BZ-B)XBl9?AԔ׾/- : EsU  Ʋϋz}1H8lKY|kqMWNr٨oͻ,\!# s9>:r9(xbװa)gizX*[ ~vPD:@9R؜C9'4Q[~m qqwzzlV{G_x-ӊK]÷mvO(z&Y&.6gPY蚟Uv0-/woXxǍެ=~ޙY{MF |;533 |%AjP2b>enԫߞn/ڷ/k:}]&%ic8R[u |!eTɒa5 ϮN7-??۵+7w_7[k_8|ry9KT_VG_M^K3RbJ.gh]͂C{qw! B]{xѮ]ژJ9޼yIOk,i6%ģ.qssn~%̖i\">uA<כ͏et?~F6^;WV6fI(*cw7`Kn-rD˙?=koǎ'%j)liu^>kmH=_1銱p[Ox>n4i/1J ]+%-Q:FĩMqkXͶzg&n]WM RB`ٞHMAj&ҳ&ubEEO{,U(@-G[_rQ8uhSG1oo;dU63rXF[c;{ 졃 U\ٜ7 `RɒZ!$F5>85Yuƒ՗8)`}Gr5[#t#L>Q|;܉cɛ*W7WMwLֺy^/srTƒ3tBP?N+/uHڧHc]v@n;GvǎKv]0XK1Si/sWOER`y~7߾?=cnZ]]yk;]_4dmP%`i/YXpM1{oKNFm/J5`VwRLb.O>ұT\39F7ޘPToCU/Lvx}.Y5Ŗ*4*тBJl<]35~+rFhX"ᝁ5eaҾ?}Ow,%E 9Uq@/Fy^m%m%[p"7ÒSTSco!ZqmAgHX['be%V'KiBѭJ+jvB(\JE/B q i Bw#(uY0BlǼb}=*\>x )K)s8ߧ~KԶ3ElgQOgI%1Cr8fOe,Y|g&K ju/]FVzVz%ʁ7ΙkNJ8ĨH>IBBV.=ן/&NjMי416 ~lonxϖO;CԦ\?vp' gɨ,6*pz^Ӿ'G/Z6oaZ.%@RN)QGx^F6Z}@d0EkgchHdط+_:̛JI +,J:L HՂ!\WkbTނlt:NST)b$-̚i$t~mH |=$N;z(KP*-BSCD<ƕ{M !-f{X=39R18 Lk"5[o~3޲|級D"%(fJCW7$Y.'vFU$ \Ztڦywr@MD01t4{>!otmܠEyxYO8SF͏LtZO/e[8#%R/)66"klHU"pPTj+SJa*5FԳ|aaO& ,TV7xdE Xu+)NhSzMH}LJ1Ql4*<]&)8noH THE;IfYfWD.4f OdvYeQB"H&{I /&"kuy%ƹf^,Ț(Na!.GNl&YjD&d]_Aްkpl;&S|ր2Q0)I͕H'#ۀTЋ4 6$h,\7ٵL1m]Z#B6s v?y[0g.Aژ8k)cYx8q"/.m<;6pd)^2[2@&.Jk=;E260Zxe&}8eE}yqn9DTY(ni`qwD?pDܻAgEs&]#n#y'mڔ=lڷ׶iFѦT6LJAƣm눳f|\%&qw@g ur0 t9|RzҼz (YhĦdűETgmj?B%HhF KSabD6eh+[Xv&Rm/dzX)N&gmM\mdbٶɤ0(PP:h$$fҹLEB=<؁;C@m8dK=;'Ȋ^p+Xhg,f@a.̈́ھc WL氠C/xk}2oۆ`ٿq3˳麶L# x Lg]X|DyLi5ښ6qB"Mř͗{\b7/nތ瞧o*-%o璘t aOFq6?YG yXʟf{JEU+>Ҷ~b+.c z0_ 4/21Hgn &/g.nba&NhjZ b/_7޸f_R3ٔ Rcؚ\m]AĝR$C ljrnX?}K6[]ȴ`VsbP֝(2s"ȸT L"qj[y^74%zVvq{tCa'#y9jD+ 04=̼;< nć$󑾏R[B0I69o*dvYh E([şE>ɞ1׊e."?B9Z }Y[O}*PFYV_{}+EZ9Dl:Lj YbJX6!uZ,_qA lLbdU^uǗgW[VfqlBTf~#\?2gG},CC}~V|B梭zz~Ȭ[!BdMbh6"Ɖo22hqqOB$o? c^Maec+SSYfPR p]z訰ټa~˖'w mm/旽ъtf+h$m[&@%FKT_A18DZg9"/:*ؐ:V*LlDIM)Aʟ`T>& K,>~ֵ`Hi^(ϑM~EEO~]{ܚy?r~dbnkԸ<>N[ xR8O{6 pXl@FSXs%yǗ|dJP #lI3 C?l{l\)Y+%}щ /%9@8˄<,1V'}}fAكxX]sx%VI6*-EnW 9ǩXq/Q{U94۲ob8xpNFM@Os! p9ֱE*.psRqgE g C;+sq!'ZiIENDB`pystache-0.6.0/pyproject.toml000066400000000000000000000007461413701016200162530ustar00rootroot00000000000000[build-system] requires = ["setuptools>=40.8.0", "wheel"] build-backend = "setuptools.build_meta" [tool.coverage.run] branch = true source = ["pystache"] [tool.coverage.paths] source = ["pystache"] [tool.coverage.report] fail_under = 95 show_missing = true [tool.black] line-length = 110 skip-string-normalization = true include = '\.py$' exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist | pystache/tests )/ ''' pystache-0.6.0/pystache/000077500000000000000000000000001413701016200151505ustar00rootroot00000000000000pystache-0.6.0/pystache/__init__.py000066400000000000000000000003541413701016200172630ustar00rootroot00000000000000""" TODO: add a docstring. """ # We keep all initialization code in a separate module. from pystache.init import parse, render, Renderer, TemplateSpec __all__ = ['parse', 'render', 'Renderer', 'TemplateSpec'] __version__ = '0.6.0' pystache-0.6.0/pystache/commands/000077500000000000000000000000001413701016200167515ustar00rootroot00000000000000pystache-0.6.0/pystache/commands/__init__.py000066400000000000000000000000401413701016200210540ustar00rootroot00000000000000""" TODO: add a docstring. """ pystache-0.6.0/pystache/commands/render.py000066400000000000000000000035311413701016200206040ustar00rootroot00000000000000# coding: utf-8 """ This module provides command-line access to pystache. Run this script using the -h option for command-line help. """ import json # TODO: switch to argparse already, sheesh... # The optparse module is deprecated in Python 2.7 in favor of argparse. # However, argparse is not available in Python 2.6 and earlier. from optparse import OptionParser import sys # We use absolute imports here to allow use of this script from its # location in source control (e.g. for development purposes). # Otherwise, the following error occurs: # # ValueError: Attempted relative import in non-package # from pystache.common import TemplateNotFoundError from pystache.renderer import Renderer USAGE = """\ %prog [-h] template context Render a mustache template with the given context. positional arguments: template A filename or template string. context A filename or JSON string.""" def parse_args(sys_argv, usage): """ Return an OptionParser for the script. """ args = sys_argv[1:] parser = OptionParser(usage=usage) options, args = parser.parse_args(args) template, context = args return template, context # TODO: verify whether the setup() method's entry_points argument # supports passing arguments to main: # # http://packages.python.org/distribute/setuptools.html#automatic-script-creation # def main(sys_argv=sys.argv): template, context = parse_args(sys_argv, USAGE) if template.endswith('.mustache'): template = template[:-9] renderer = Renderer() try: template = renderer.load_template(template) except TemplateNotFoundError: pass try: context = json.load(open(context)) except IOError: context = json.loads(context) rendered = renderer.render(template, context) print(rendered) if __name__ == '__main__': main() pystache-0.6.0/pystache/commands/test.py000066400000000000000000000004161413701016200203030ustar00rootroot00000000000000# coding: utf-8 """ This module provides a command to test pystache (unit tests, doctests, etc). """ import sys from pystache.tests.main import main as run_tests def main(sys_argv=sys.argv): run_tests(sys_argv=sys_argv) if __name__ == '__main__': main() pystache-0.6.0/pystache/common.py000066400000000000000000000031251413701016200170130ustar00rootroot00000000000000# coding: utf-8 """ Exposes functionality needed throughout the project. """ def _get_string_types(): """ Return the Python3 string type (no more python2) """ return (str, type('a'.encode('utf-8'))) _STRING_TYPES = _get_string_types() def is_string(obj): """ Return whether the given object is a byte string or unicode string. This function is provided for compatibility with both Python 2 and 3 when using 2to3. """ return isinstance(obj, _STRING_TYPES) # This function was designed to be portable across Python versions -- both # with older versions and with Python 3 after applying 2to3. def read(path): """ Return the contents of a text file as a byte string. """ # Opening in binary mode is necessary for compatibility across Python # 2 and 3. In both Python 2 and 3, open() defaults to opening files in # text mode. However, in Python 2, open() returns file objects whose # read() method returns byte strings (strings of type `str` in Python 2), # whereas in Python 3, the file object returns unicode strings (strings # of type `str` in Python 3). f = open(path, 'rb') # We avoid use of the with keyword for Python 2.4 support. try: return f.read() finally: f.close() class MissingTags(object): """Contains the valid values for Renderer.missing_tags.""" ignore = 'ignore' strict = 'strict' class PystacheError(Exception): """Base class for Pystache exceptions.""" class TemplateNotFoundError(PystacheError): """An exception raised when a template is not found.""" pystache-0.6.0/pystache/context.py000066400000000000000000000267601413701016200172210ustar00rootroot00000000000000# coding: utf-8 """ Exposes a ContextStack class. The Mustache spec makes a special distinction between two types of context stack elements: hashes and objects. For the purposes of interpreting the spec, we define these categories mutually exclusively as follows: (1) Hash: an item whose type is a subclass of dict. (2) Object: an item that is neither a hash nor an instance of a built-in type. """ from pystache.common import PystacheError # This equals '__builtin__' in Python 2 and 'builtins' in Python 3. _BUILTIN_MODULE = type(0).__module__ # We use this private global variable as a return value to represent a key # not being found on lookup. This lets us distinguish between the case # of a key's value being None with the case of a key not being found -- # without having to rely on exceptions (e.g. KeyError) for flow control. # # TODO: eliminate the need for a private global variable, e.g. by using the # preferred Python approach of "easier to ask for forgiveness than permission": # http://docs.python.org/glossary.html#term-eafp class NotFound(object): pass _NOT_FOUND = NotFound() def _get_value(context, key): """ Retrieve a key's value from a context item. Returns _NOT_FOUND if the key does not exist. The ContextStack.get() docstring documents this function's intended behavior. """ if isinstance(context, dict): # Then we consider the argument a "hash" for the purposes of the spec. # # We do a membership test to avoid using exceptions for flow control # (e.g. catching KeyError). if key in context: return context[key] elif type(context).__module__ != _BUILTIN_MODULE: # Then we consider the argument an "object" for the purposes of # the spec. # # The elif test above lets us avoid treating instances of built-in # types like integers and strings as objects (cf. issue #81). # Instances of user-defined classes on the other hand, for example, # are considered objects by the test above. try: attr = getattr(context, key) except AttributeError: # TODO: distinguish the case of the attribute not existing from # an AttributeError being raised by the call to the attribute. # See the following issue for implementation ideas: # http://bugs.python.org/issue7559 pass else: # TODO: consider using EAFP here instead. # http://docs.python.org/glossary.html#term-eafp if callable(attr): return attr() return attr return _NOT_FOUND class KeyNotFoundError(PystacheError): """ An exception raised when a key is not found in a context stack. """ def __init__(self, key, details): self.key = key self.details = details def __str__(self): return 'Key %s not found: %s' % (repr(self.key), self.details) class ContextStack(object): """ Provides dictionary-like access to a stack of zero or more items. Instances of this class are meant to act as the rendering context when rendering Mustache templates in accordance with mustache(5) and the Mustache spec. Instances encapsulate a private stack of hashes, objects, and built-in type instances. Querying the stack for the value of a key queries the items in the stack in order from last-added objects to first (last in, first out). Caution: this class does not currently support recursive nesting in that items in the stack cannot themselves be ContextStack instances. See the docstrings of the methods of this class for more details. """ # We reserve keyword arguments for future options (e.g. a "strict=True" # option for enabling a strict mode). def __init__(self, *items): """ Construct an instance, and initialize the private stack. The *items arguments are the items with which to populate the initial stack. Items in the argument list are added to the stack in order so that, in particular, items at the end of the argument list are queried first when querying the stack. Caution: items should not themselves be ContextStack instances, as recursive nesting does not behave as one might expect. """ self._stack = list(items) def __repr__(self): """ Return a string representation of the instance. For example-- >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123}) >>> repr(context) "ContextStack({'alpha': 'abc'}, {'numeric': 123})" """ return '%s%s' % (self.__class__.__name__, tuple(self._stack)) @staticmethod def create(*context, **kwargs): """ Build a ContextStack instance from a sequence of context-like items. This factory-style method is more general than the ContextStack class's constructor in that, unlike the constructor, the argument list can itself contain ContextStack instances. Here is an example illustrating various aspects of this method: >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'} >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'}) >>> >>> context = ContextStack.create(obj1, None, obj2, mineral='gold') >>> >>> context.get('animal') 'cat' >>> context.get('vegetable') 'spinach' >>> context.get('mineral') 'gold' Arguments: *context: zero or more dictionaries, ContextStack instances, or objects with which to populate the initial context stack. None arguments will be skipped. Items in the *context list are added to the stack in order so that later items in the argument list take precedence over earlier items. This behavior is the same as the constructor's. **kwargs: additional key-value data to add to the context stack. As these arguments appear after all items in the *context list, in the case of key conflicts these values take precedence over all items in the *context list. This behavior is the same as the constructor's. """ items = context context = ContextStack() for item in items: if item is None: continue if isinstance(item, ContextStack): context._stack.extend(item._stack) else: context.push(item) if kwargs: context.push(kwargs) return context # TODO: add more unit tests for this. # TODO: update the docstring for dotted names. def get(self, name): """ Resolve a dotted name against the current context stack. This function follows the rules outlined in the section of the spec regarding tag interpolation. This function returns the value as is and does not coerce the return value to a string. Arguments: name: a dotted or non-dotted name. default: the value to return if name resolution fails at any point. Defaults to the empty string per the Mustache spec. This method queries items in the stack in order from last-added objects to first (last in, first out). The value returned is the value of the key in the first item that contains the key. If the key is not found in any item in the stack, then the default value is returned. The default value defaults to None. In accordance with the spec, this method queries items in the stack for a key differently depending on whether the item is a hash, object, or neither (as defined in the module docstring): (1) Hash: if the item is a hash, then the key's value is the dictionary value of the key. If the dictionary doesn't contain the key, then the key is considered not found. (2) Object: if the item is an an object, then the method looks for an attribute with the same name as the key. If an attribute with that name exists, the value of the attribute is returned. If the attribute is callable, however (i.e. if the attribute is a method), then the attribute is called with no arguments and that value is returned. If there is no attribute with the same name as the key, then the key is considered not found. (3) Neither: if the item is neither a hash nor an object, then the key is considered not found. *Caution*: Callables are handled differently depending on whether they are dictionary values, as in (1) above, or attributes, as in (2). The former are returned as-is, while the latter are first called and that value returned. Here is an example to illustrate: >>> def greet(): ... return "Hi Bob!" >>> >>> class Greeter(object): ... greet = None >>> >>> dct = {'greet': greet} >>> obj = Greeter() >>> obj.greet = greet >>> >>> dct['greet'] is obj.greet True >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS >>> ContextStack(obj).get('greet') 'Hi Bob!' TODO: explain the rationale for this difference in treatment. """ if name == '.': try: return self.top() except IndexError: raise KeyNotFoundError('.', 'empty context stack') parts = name.split('.') try: result = self._get_simple(parts[0]) except KeyNotFoundError: raise KeyNotFoundError(name, 'first part') for part in parts[1:]: # The full context stack is not used to resolve the remaining parts. # From the spec-- # # 5) If any name parts were retained in step 1, each should be # resolved against a context stack containing only the result # from the former resolution. If any part fails resolution, the # result should be considered falsey, and should interpolate as # the empty string. # # TODO: make sure we have a test case for the above point. result = _get_value(result, part) # TODO: consider using EAFP here instead. # http://docs.python.org/glossary.html#term-eafp if result is _NOT_FOUND: raise KeyNotFoundError(name, 'missing %s' % repr(part)) return result def _get_simple(self, name): """ Query the stack for a non-dotted name. """ for item in reversed(self._stack): result = _get_value(item, name) if result is not _NOT_FOUND: return result raise KeyNotFoundError(name, 'part missing') def push(self, item): """ Push an item onto the stack. """ self._stack.append(item) def pop(self): """ Pop an item off of the stack, and return it. """ return self._stack.pop() def top(self): """ Return the item last added to the stack. """ return self._stack[-1] def copy(self): """ Return a copy of this instance. """ return ContextStack(*self._stack) pystache-0.6.0/pystache/defaults.py000066400000000000000000000037351413701016200173410ustar00rootroot00000000000000# coding: utf-8 """ This module provides a central location for defining default behavior. Throughout the package, these defaults take effect only when the user does not otherwise specify a value. """ try: # Python 3.2 adds html.escape() and deprecates cgi.escape(). from html import escape except ImportError: from cgi import escape import os import sys from pystache.common import MissingTags # How to handle encoding errors when decoding strings from str to unicode. # # This value is passed as the "errors" argument to Python's built-in # unicode() function: # # http://docs.python.org/library/functions.html#unicode # DECODE_ERRORS = 'strict' # The name of the encoding to use when converting to unicode any strings of # type str encountered during the rendering process. STRING_ENCODING = sys.getdefaultencoding() # The name of the encoding to use when converting file contents to unicode. # This default takes precedence over the STRING_ENCODING default for # strings that arise from files. FILE_ENCODING = sys.getdefaultencoding() # The delimiters to start with when parsing. DELIMITERS = ('{{', '}}') # How to handle missing tags when rendering a template. MISSING_TAGS = MissingTags.ignore # The starting list of directories in which to search for templates when # loading a template by file name. SEARCH_DIRS = [os.curdir] # i.e. ['.'] # The escape function to apply to strings that require escaping when # rendering templates (e.g. for tags enclosed in double braces). # Only unicode strings will be passed to this function. # # The quote=True argument causes double but not single quotes to be escaped # in Python 3.1 and earlier, and both double and single quotes to be # escaped in Python 3.2 and later: # # http://docs.python.org/library/cgi.html#cgi.escape # http://docs.python.org/dev/library/html.html#html.escape # TAG_ESCAPE = lambda u: escape(u, quote=True) # noqa # The default template extension, without the leading dot. TEMPLATE_EXTENSION = 'mustache' pystache-0.6.0/pystache/init.py000066400000000000000000000007651413701016200164750ustar00rootroot00000000000000# encoding: utf-8 """ This module contains the initialization logic called by __init__.py. """ # do not let linter tools remove any imports !! from pystache.parser import parse # noqa from pystache.renderer import Renderer from pystache.template_spec import TemplateSpec # noqa def render(template, context=None, **kwargs): """ Return the given template string rendered using the given context. """ renderer = Renderer() return renderer.render(template, context, **kwargs) pystache-0.6.0/pystache/loader.py000066400000000000000000000114171413701016200167740ustar00rootroot00000000000000# coding: utf-8 """ This module provides a Loader class for locating and reading templates. """ import platform from pystache import common from pystache import defaults from pystache.locator import Locator # We make a function so that the current defaults take effect. # TODO: revisit whether this is necessary. def _make_to_unicode(): def to_unicode(s, encoding=None): """ Raises a TypeError exception if the given string is already unicode. """ if encoding is None: encoding = defaults.STRING_ENCODING return str(s, encoding, defaults.DECODE_ERRORS) return to_unicode class Loader(object): """ Loads the template associated to a name or user-defined object. All load_*() methods return the template as a unicode string. """ def __init__( self, file_encoding=None, extension=None, to_unicode=None, search_dirs=None, ): """ Construct a template loader instance. Arguments: extension: the template file extension, without the leading dot. Pass False for no extension (e.g. to use extensionless template files). Defaults to the package default. file_encoding: the name of the encoding to use when converting file contents to unicode. Defaults to the package default. search_dirs: the list of directories in which to search when loading a template by name or file name. Defaults to the package default. to_unicode: the function to use when converting strings of type str to unicode. The function should have the signature: to_unicode(s, encoding=None) It should accept a string of type str and an optional encoding name and return a string of type unicode. Defaults to calling Python's built-in function unicode() using the package string encoding and decode errors defaults. """ if extension is None: extension = defaults.TEMPLATE_EXTENSION if file_encoding is None: file_encoding = defaults.FILE_ENCODING if search_dirs is None: search_dirs = defaults.SEARCH_DIRS if to_unicode is None: to_unicode = _make_to_unicode() self.extension = extension self.file_encoding = file_encoding # TODO: unit test setting this attribute. self.search_dirs = search_dirs self.to_unicode = to_unicode def _make_locator(self): return Locator(extension=self.extension) def str(self, s, encoding=None): """ Convert a string to unicode using the given encoding, and return it. This function uses the underlying to_unicode attribute. Arguments: s: a basestring instance to convert to unicode. Unlike Python's built-in unicode() function, it is okay to pass unicode strings to this function. (Passing a unicode string to Python's unicode() with the encoding argument throws the error, "TypeError: decoding Unicode is not supported.") encoding: the encoding to pass to the to_unicode attribute. Defaults to None. """ if isinstance(s, str): return str(s) return self.to_unicode(s, encoding) def read(self, path, encoding=None): """ Read the template at the given path, and return it as a unicode string. """ b = common.read(path) if encoding is None: encoding = self.file_encoding if platform.system() == 'Windows': return self.str(b, encoding).replace('\r', '') return self.str(b, encoding) def load_file(self, file_name): """ Find and return the template with the given file name. Arguments: file_name: the file name of the template. """ locator = self._make_locator() path = locator.find_file(file_name, self.search_dirs) return self.read(path) def load_name(self, name): """ Find and return the template with the given template name. Arguments: name: the name of the template. """ locator = self._make_locator() path = locator.find_name(name, self.search_dirs) return self.read(path) # TODO: unit-test this method. def load_object(self, obj): """ Find and return the template associated to the given object. Arguments: obj: an instance of a user-defined class. search_dirs: the list of directories in which to search. """ locator = self._make_locator() path = locator.find_object(obj, self.search_dirs) return self.read(path) pystache-0.6.0/pystache/locator.py000066400000000000000000000110411413701016200171620ustar00rootroot00000000000000# coding: utf-8 """ This module provides a Locator class for finding template files. """ import os import re import sys from pystache.common import TemplateNotFoundError from pystache import defaults class Locator(object): def __init__(self, extension=None): """ Construct a template locator. Arguments: extension: the template file extension, without the leading dot. Pass False for no extension (e.g. to use extensionless template files). Defaults to the package default. """ if extension is None: extension = defaults.TEMPLATE_EXTENSION self.template_extension = extension def get_object_directory(self, obj): """ Return the directory containing an object's defining class. Returns None if there is no such directory, for example if the class was defined in an interactive Python session, or in a doctest that appears in a text file (rather than a Python file). """ if not hasattr(obj, '__module__'): return None module = sys.modules[obj.__module__] if not hasattr(module, '__file__'): # TODO: add a unit test for this case. return None path = module.__file__ return os.path.dirname(path) def make_template_name(self, obj): """ Return the canonical template name for an object instance. This method converts Python-style class names (PEP 8's recommended CamelCase, aka CapWords) to lower_case_with_underscords. Here is an example with code: >>> class HelloWorld(object): ... pass >>> hi = HelloWorld() >>> >>> locator = Locator() >>> locator.make_template_name(hi) 'hello_world' """ template_name = obj.__class__.__name__ def repl(match): return '_' + match.group(0).lower() return re.sub('[A-Z]', repl, template_name)[1:] def make_file_name(self, template_name, template_extension=None): """ Generate and return the file name for the given template name. Arguments: template_extension: defaults to the instance's extension. """ file_name = template_name if template_extension is None: template_extension = self.template_extension if template_extension is not False: file_name += os.path.extsep + template_extension return file_name def _find_path(self, search_dirs, file_name): """ Search for the given file, and return the path. Returns None if the file is not found. """ for dir_path in search_dirs: file_path = os.path.join(dir_path, file_name) if os.path.exists(file_path): return file_path return None def _find_path_required(self, search_dirs, file_name): """ Return the path to a template with the given file name. """ path = self._find_path(search_dirs, file_name) if path is None: raise TemplateNotFoundError( 'File %s not found in dirs: %s' % (repr(file_name), repr(search_dirs)) ) return path def find_file(self, file_name, search_dirs): """ Return the path to a template with the given file name. Arguments: file_name: the file name of the template. search_dirs: the list of directories in which to search. """ return self._find_path_required(search_dirs, file_name) def find_name(self, template_name, search_dirs): """ Return the path to a template with the given name. Arguments: template_name: the name of the template. search_dirs: the list of directories in which to search. """ file_name = self.make_file_name(template_name) return self._find_path_required(search_dirs, file_name) def find_object(self, obj, search_dirs, file_name=None): """ Return the path to a template associated with the given object. """ if file_name is None: # TODO: should we define a make_file_name() method? template_name = self.make_template_name(obj) file_name = self.make_file_name(template_name) dir_path = self.get_object_directory(obj) if dir_path is not None: search_dirs = [dir_path] + search_dirs path = self._find_path_required(search_dirs, file_name) return path pystache-0.6.0/pystache/parsed.py000066400000000000000000000022421413701016200170000ustar00rootroot00000000000000# coding: utf-8 """ Exposes a class that represents a parsed (or compiled) template. """ class ParsedTemplate(object): """ Represents a parsed or compiled template. An instance wraps a list of unicode strings and node objects. A node object must have a `render(engine, stack)` method that accepts a RenderEngine instance and a ContextStack instance and returns a unicode string. """ def __init__(self): self._parse_tree = [] def __repr__(self): return repr(self._parse_tree) def add(self, node): """ Arguments: node: a unicode string or node object instance. See the class docstring for information. """ self._parse_tree.append(node) def render(self, engine, context): """ Returns: a string of type unicode. """ # We avoid use of the ternary operator for Python 2.4 support. def get_unicode(node): if type(node) is str: return node return node.render(engine, context) parts = list(map(get_unicode, self._parse_tree)) s = ''.join(parts) return str(s) pystache-0.6.0/pystache/parser.py000066400000000000000000000271431413701016200170250ustar00rootroot00000000000000# coding: utf-8 """ Exposes a parse() function to parse template strings. """ import re from pystache import defaults from pystache.parsed import ParsedTemplate END_OF_LINE_CHARACTERS = ['\r', '\n'] NON_BLANK_RE = re.compile(r'^(.)', re.M) # TODO: add some unit tests for this. # TODO: add a test case that checks for spurious spaces. # TODO: add test cases for delimiters. def parse(template, delimiters=None): """ Parse a unicode template string and return a ParsedTemplate instance. Arguments: template: a unicode template string. delimiters: a 2-tuple of delimiters. Defaults to the package default. Examples: >>> parsed = parse("Hey {{#who}}{{name}}!{{/who}}") >>> print(str(parsed).replace('u', '')) # This is an old hack. ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])] """ if type(template) is not str: raise Exception('Template is not unicode: %s' % type(template)) parser = _Parser(delimiters) return parser.parse(template) def _compile_template_re(delimiters): """ Return a regular expression object (re.RegexObject) instance. """ # The possible tag type characters following the opening tag, # excluding "=" and "{". tag_types = '!>&/#^' # TODO: are we following this in the spec? # # The tag's content MUST be a non-whitespace character sequence # NOT containing the current closing delimiter. # tag = r""" (?P[\ \t]*) %(otag)s \s* (?: (?P=) \s* (?P.+?) \s* = | (?P{) \s* (?P.+?) \s* } | (?P[%(tag_types)s]?) \s* (?P[\s\S]+?) ) \s* %(ctag)s """ % { 'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1]), } return re.compile(tag, re.VERBOSE) class ParsingError(Exception): pass ## Node types def _format(obj, exclude=None): if exclude is None: exclude = [] exclude.append('key') attrs = obj.__dict__ names = list(set(attrs.keys()) - set(exclude)) names.sort() names.insert(0, 'key') args = ['%s=%s' % (name, repr(attrs[name])) for name in names] return '%s(%s)' % (obj.__class__.__name__, ', '.join(args)) class _CommentNode(object): def __repr__(self): return _format(self) def render(self, engine, context): return '' class _ChangeNode(object): def __init__(self, delimiters): self.delimiters = delimiters def __repr__(self): return _format(self) def render(self, engine, context): return '' class _EscapeNode(object): def __init__(self, key): self.key = key def __repr__(self): return _format(self) def render(self, engine, context): s = engine.fetch_string(context, self.key) return engine.escape(s) class _LiteralNode(object): def __init__(self, key): self.key = key def __repr__(self): return _format(self) def render(self, engine, context): s = engine.fetch_string(context, self.key) return engine.literal(s) class _PartialNode(object): def __init__(self, key, indent): self.key = key self.indent = indent def __repr__(self): return _format(self) def render(self, engine, context): template = engine.resolve_partial(self.key) # Indent before rendering. template = re.sub(NON_BLANK_RE, self.indent + r'\1', template) return engine.render(template, context) class _InvertedNode(object): def __init__(self, key, parsed_section): self.key = key self.parsed_section = parsed_section def __repr__(self): return _format(self) def render(self, engine, context): # TODO: is there a bug because we are not using the same # logic as in fetch_string()? data = engine.resolve_context(context, self.key) # Note that lambdas are considered truthy for inverted sections # per the spec. if data: return '' return self.parsed_section.render(engine, context) class _SectionNode(object): # TODO: the template_ and parsed_template_ arguments don't both seem # to be necessary. Can we remove one of them? For example, if # callable(data) is True, then the initial parsed_template isn't used. def __init__(self, key, parsed, delimiters, template, index_begin, index_end): self.delimiters = delimiters self.key = key self.parsed = parsed self.template = template self.index_begin = index_begin self.index_end = index_end def __repr__(self): return _format(self, exclude=['delimiters', 'template']) def render(self, engine, context): values = engine.fetch_section_data(context, self.key) parts = [] for val in values: if callable(val): # Lambdas special case section rendering and bypass pushing # the data value onto the context stack. From the spec-- # # When used as the data value for a Section tag, the # lambda MUST be treatable as an arity 1 function, and # invoked as such (passing a String containing the # unprocessed section contents). The returned value # MUST be rendered against the current delimiters, then # interpolated in place of the section. # # Also see-- # # https://github.com/defunkt/pystache/issues/113 # # TODO: should we check the arity? val = val(self.template[self.index_begin : self.index_end]) val = engine._render_value(val, context, delimiters=self.delimiters) parts.append(val) continue context.push(val) parts.append(self.parsed.render(engine, context)) context.pop() return str(''.join(parts)) class _Parser(object): _delimiters = None _template_re = None def __init__(self, delimiters=None): if delimiters is None: delimiters = defaults.DELIMITERS self._delimiters = delimiters def _compile_delimiters(self): self._template_re = _compile_template_re(self._delimiters) def _change_delimiters(self, delimiters): self._delimiters = delimiters self._compile_delimiters() def parse(self, template): """ Parse a template string starting at some index. This method uses the current tag delimiter. Arguments: template: a unicode string that is the template to parse. index: the index at which to start parsing. Returns: a ParsedTemplate instance. """ self._compile_delimiters() start_index = 0 parsed_section, section_key = None, None parsed_template = ParsedTemplate() states = [] while True: match = self._template_re.search(template, start_index) if match is None: break match_index = match.start() end_index = match.end() matches = match.groupdict() # Normalize the matches dictionary. if matches['change'] is not None: matches.update(tag='=', tag_key=matches['delims']) elif matches['raw'] is not None: matches.update(tag='&', tag_key=matches['raw_name']) tag_type = matches['tag'] tag_key = matches['tag_key'] leading_whitespace = matches['whitespace'] # Standalone (non-interpolation) tags consume the entire line, # both leading whitespace and trailing newline. did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS is_tag_interpolating = tag_type in ['', '&'] if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating: if end_index < len(template): end_index += template[end_index] == '\r' and 1 or 0 if end_index < len(template): end_index += template[end_index] == '\n' and 1 or 0 elif leading_whitespace: match_index += len(leading_whitespace) leading_whitespace = '' # Avoid adding spurious empty strings to the parse tree. if start_index != match_index: parsed_template.add(template[start_index:match_index]) start_index = end_index if tag_type in ('#', '^'): # Cache current state. state = (tag_type, end_index, section_key, parsed_template) states.append(state) # Initialize new state section_key, parsed_template = tag_key, ParsedTemplate() continue if tag_type == '/': if tag_key != section_key: raise ParsingError('Section end tag mismatch: %s != %s' % (tag_key, section_key)) # Restore previous state with newly found section data. parsed_section = parsed_template ( tag_type, section_start_index, section_key, parsed_template, ) = states.pop() node = self._make_section_node( template, tag_type, tag_key, parsed_section, section_start_index, match_index, ) else: node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace) parsed_template.add(node) # Avoid adding spurious empty strings to the parse tree. if start_index != len(template): parsed_template.add(template[start_index:]) return parsed_template def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace): """ Create and return a non-section node for the parse tree. """ # TODO: switch to using a dictionary instead of a bunch of ifs and elifs. if tag_type == '!': return _CommentNode() if tag_type == '=': delimiters = tag_key.split() self._change_delimiters(delimiters) return _ChangeNode(delimiters) if tag_type == '': return _EscapeNode(tag_key) if tag_type == '&': return _LiteralNode(tag_key) if tag_type == '>': return _PartialNode(tag_key, leading_whitespace) raise Exception('Invalid symbol for interpolation tag: %s' % repr(tag_type)) def _make_section_node( self, template, tag_type, tag_key, parsed_section, section_start_index, section_end_index, ): """ Create and return a section node for the parse tree. """ if tag_type == '#': return _SectionNode( tag_key, parsed_section, self._delimiters, template, section_start_index, section_end_index, ) if tag_type == '^': return _InvertedNode(tag_key, parsed_section) raise Exception('Invalid symbol for section tag: %s' % repr(tag_type)) pystache-0.6.0/pystache/renderengine.py000066400000000000000000000151521413701016200201730ustar00rootroot00000000000000# coding: utf-8 """ Defines a class responsible for rendering logic. """ from pystache.common import is_string from pystache.parser import parse def context_get(stack, name): """ Find and return a name from a ContextStack instance. """ return stack.get(name) class RenderEngine(object): """ Provides a render() method. This class is meant only for internal use. As a rule, the code in this class operates on unicode strings where possible rather than, say, strings of type str or markupsafe.Markup. This means that strings obtained from "external" sources like partials and variable tag values are immediately converted to unicode (or escaped and converted to unicode) before being operated on further. This makes maintaining, reasoning about, and testing the correctness of the code much simpler. In particular, it keeps the implementation of this class independent of the API details of one (or possibly more) unicode subclasses (e.g. markupsafe.Markup). """ # TODO: it would probably be better for the constructor to accept # and set as an attribute a single RenderResolver instance # that encapsulates the customizable aspects of converting # strings and resolving partials and names from context. def __init__( self, literal=None, escape=None, resolve_context=None, resolve_partial=None, to_str=None, ): """ Arguments: literal: the function used to convert unescaped variable tag values to unicode, e.g. the value corresponding to a tag "{{{name}}}". The function should accept a string of type str or unicode (or a subclass) and return a string of type unicode (but not a proper subclass of unicode). This class will only pass basestring instances to this function. For example, it will call str() on integer variable values prior to passing them to this function. escape: the function used to escape and convert variable tag values to unicode, e.g. the value corresponding to a tag "{{name}}". The function should obey the same properties described above for the "literal" function argument. This function should take care to convert any str arguments to unicode just as the literal function should, as this class will not pass tag values to literal prior to passing them to this function. This allows for more flexibility, for example using a custom escape function that handles incoming strings of type markupsafe.Markup differently from plain unicode strings. resolve_context: the function to call to resolve a name against a context stack. The function should accept two positional arguments: a ContextStack instance and a name to resolve. resolve_partial: the function to call when loading a partial. The function should accept a template name string and return a template string of type unicode (not a subclass). to_str: a function that accepts an object and returns a string (e.g. the built-in function str). This function is used for string coercion whenever a string is required (e.g. for converting None or 0 to a string). """ self.escape = escape self.literal = literal self.resolve_context = resolve_context self.resolve_partial = resolve_partial self.to_str = to_str # TODO: Rename context to stack throughout this module. # From the spec: # # When used as the data value for an Interpolation tag, the lambda # MUST be treatable as an arity 0 function, and invoked as such. # The returned value MUST be rendered against the default delimiters, # then interpolated in place of the lambda. # def fetch_string(self, context, name): """ Get a value from the given context as a basestring instance. """ val = self.resolve_context(context, name) if callable(val): # Return because _render_value() is already a string. return self._render_value(val(), context) if not is_string(val): return self.to_str(val) return val def fetch_section_data(self, context, name): """ Fetch the value of a section as a list. """ data = self.resolve_context(context, name) # From the spec: # # If the data is not of a list type, it is coerced into a list # as follows: if the data is truthy (e.g. `!!data == true`), # use a single-element list containing the data, otherwise use # an empty list. # if not data: data = [] else: # The least brittle way to determine whether something # supports iteration is by trying to call iter() on it: # # http://docs.python.org/library/functions.html#iter # # It is not sufficient, for example, to check whether the item # implements __iter__ () (the iteration protocol). There is # also __getitem__() (the sequence protocol). In Python 2, # strings do not implement __iter__(), but in Python 3 they do. try: iter(data) except TypeError: # Then the value does not support iteration. data = [data] else: if is_string(data) or isinstance(data, dict): # Do not treat strings and dicts (which are iterable) as lists. data = [data] # Otherwise, treat the value as a list. return data def _render_value(self, val, context, delimiters=None): """ Render an arbitrary value. """ if not is_string(val): # In case the template is an integer, for example. val = self.to_str(val) if type(val) is not str: val = self.literal(val) return self.render(val, context, delimiters) def render(self, template, context_stack, delimiters=None): """ Render a unicode template string, and return as unicode. Arguments: template: a template string of type unicode (but not a proper subclass of unicode). context_stack: a ContextStack instance. """ parsed_template = parse(template, delimiters) return parsed_template.render(self, context_stack) pystache-0.6.0/pystache/renderer.py000066400000000000000000000402251413701016200173330ustar00rootroot00000000000000# coding: utf-8 """ This module provides a Renderer class to render templates. """ from pystache import defaults from pystache.common import TemplateNotFoundError, MissingTags, is_string from pystache.context import ContextStack, KeyNotFoundError from pystache.loader import Loader from pystache.parsed import ParsedTemplate from pystache.renderengine import context_get, RenderEngine from pystache.specloader import SpecLoader from pystache.template_spec import TemplateSpec class Renderer(object): """ A class for rendering mustache templates. This class supports several rendering options which are described in the constructor's docstring. Other behavior can be customized by subclassing this class. For example, one can pass a string-string dictionary to the constructor to bypass loading partials from the file system: >>> partials = {'partial': 'Hello, {{thing}}!'} >>> renderer = Renderer(partials=partials) >>> # We apply print to make the test work in Python 3 after 2to3. >>> print(renderer.render('{{>partial}}', {'thing': 'world'})) Hello, world! To customize string coercion (e.g. to render False values as ''), one can subclass this class. For example: class MyRenderer(Renderer): def str_coerce(self, val): if not val: return '' else: return str(val) """ def __init__( self, file_encoding=None, string_encoding=None, decode_errors=None, search_dirs=None, file_extension=None, escape=None, partials=None, missing_tags=None, ): """ Construct an instance. Arguments: file_encoding: the name of the encoding to use by default when reading template files. All templates are converted to unicode prior to parsing. Defaults to the package default. string_encoding: the name of the encoding to use when converting to unicode any byte strings (type str in Python 2) encountered during the rendering process. This name will be passed as the encoding argument to the built-in function unicode(). Defaults to the package default. decode_errors: the string to pass as the errors argument to the built-in function unicode() when converting byte strings to unicode. Defaults to the package default. search_dirs: the list of directories in which to search when loading a template by name or file name. If given a string, the method interprets the string as a single directory. Defaults to the package default. file_extension: the template file extension. Pass False for no extension (i.e. to use extensionless template files). Defaults to the package default. partials: an object (e.g. a dictionary) for custom partial loading during the rendering process. The object should have a get() method that accepts a string and returns the corresponding template as a string, preferably as a unicode string. If there is no template with that name, the get() method should either return None (as dict.get() does) or raise an exception. If this argument is None, the rendering process will use the normal procedure of locating and reading templates from the file system -- using relevant instance attributes like search_dirs, file_encoding, etc. escape: the function used to escape variable tag values when rendering a template. The function should accept a unicode string (or subclass of unicode) and return an escaped string that is again unicode (or a subclass of unicode). This function need not handle strings of type `str` because this class will only pass it unicode strings. The constructor assigns this function to the constructed instance's escape() method. To disable escaping entirely, one can pass `lambda u: u` as the escape function, for example. One may also wish to consider using markupsafe's escape function: markupsafe.escape(). This argument defaults to the package default. missing_tags: a string specifying how to handle missing tags. If 'strict', an error is raised on a missing tag. If 'ignore', the value of the tag is the empty string. Defaults to the package default. """ if decode_errors is None: decode_errors = defaults.DECODE_ERRORS if escape is None: escape = defaults.TAG_ESCAPE if file_encoding is None: file_encoding = defaults.FILE_ENCODING if file_extension is None: file_extension = defaults.TEMPLATE_EXTENSION if missing_tags is None: missing_tags = defaults.MISSING_TAGS if search_dirs is None: search_dirs = defaults.SEARCH_DIRS if string_encoding is None: string_encoding = defaults.STRING_ENCODING if isinstance(search_dirs, str): search_dirs = [search_dirs] self._context = None self.decode_errors = decode_errors self.escape = escape self.file_encoding = file_encoding self.file_extension = file_extension self.missing_tags = missing_tags self.partials = partials self.search_dirs = search_dirs self.string_encoding = string_encoding # This is an experimental way of giving views access to the current context. # TODO: consider another approach of not giving access via a property, # but instead letting the caller pass the initial context to the # main render() method by reference. This approach would probably # be less likely to be misused. @property def context(self): """ Return the current rendering context [experimental]. """ return self._context # We could not choose str() as the name because 2to3 renames the unicode() # method of this class to str(). def str_coerce(self, val): """ Coerce a non-string value to a string. This method is called whenever a non-string is encountered during the rendering process when a string is needed (e.g. if a context value for string interpolation is not a string). To customize string coercion, you can override this method. """ return str(val) def _to_unicode_soft(self, s): """ Convert a basestring to unicode, preserving any unicode subclass. """ # We type-check to avoid "TypeError: decoding Unicode is not supported". # We avoid the Python ternary operator for Python 2.4 support. if isinstance(s, str): return s return self.str(s) def _to_unicode_hard(self, s): """ Convert a basestring to a string with type unicode (not subclass). """ return str(self._to_unicode_soft(s)) def _escape_to_unicode(self, s): """ Convert a basestring to unicode (preserving any unicode subclass), and escape it. Returns a unicode string (not subclass). """ return str(self.escape(self._to_unicode_soft(s))) def str(self, b, encoding=None): """ Convert a byte string to unicode, using string_encoding and decode_errors. Arguments: b: a byte string. encoding: the name of an encoding. Defaults to the string_encoding attribute for this instance. Raises: TypeError: Because this method calls Python's built-in unicode() function, this method raises the following exception if the given string is already unicode: TypeError: decoding Unicode is not supported """ if encoding is None: encoding = self.string_encoding # TODO: Wrap UnicodeDecodeErrors with a message about setting # the string_encoding and decode_errors attributes. return str(b, encoding, self.decode_errors) def _make_loader(self): """ Create a Loader instance using current attributes. """ return Loader( file_encoding=self.file_encoding, extension=self.file_extension, to_unicode=self.str, search_dirs=self.search_dirs, ) def _make_load_template(self): """ Return a function that loads a template by name. """ loader = self._make_loader() def load_template(template_name): return loader.load_name(template_name) return load_template def _make_load_partial(self): """ Return a function that loads a partial by name. """ if self.partials is None: return self._make_load_template() # Otherwise, create a function from the custom partial loader. partials = self.partials def load_partial(name): # TODO: consider using EAFP here instead. # http://docs.python.org/glossary.html#term-eafp # This would mean requiring that the custom partial loader # raise a KeyError on name not found. template = partials.get(name) if template is None: raise TemplateNotFoundError( 'Name %s not found in partials: %s' % (repr(name), type(partials)) ) # RenderEngine requires that the return value be unicode. return self._to_unicode_hard(template) return load_partial def _is_missing_tags_strict(self): """ Return whether missing_tags is set to strict. """ val = self.missing_tags if val == MissingTags.strict: return True elif val == MissingTags.ignore: return False raise Exception("Unsupported 'missing_tags' value: %s" % repr(val)) def _make_resolve_partial(self): """ Return the resolve_partial function to pass to RenderEngine.__init__(). """ load_partial = self._make_load_partial() if self._is_missing_tags_strict(): return load_partial # Otherwise, ignore missing tags. def resolve_partial(name): try: return load_partial(name) except TemplateNotFoundError: return '' return resolve_partial def _make_resolve_context(self): """ Return the resolve_context function to pass to RenderEngine.__init__(). """ if self._is_missing_tags_strict(): return context_get # Otherwise, ignore missing tags. def resolve_context(stack, name): try: return context_get(stack, name) except KeyNotFoundError: return '' return resolve_context def _make_render_engine(self): """ Return a RenderEngine instance for rendering. """ resolve_context = self._make_resolve_context() resolve_partial = self._make_resolve_partial() engine = RenderEngine( literal=self._to_unicode_hard, escape=self._escape_to_unicode, resolve_context=resolve_context, resolve_partial=resolve_partial, to_str=self.str_coerce, ) return engine # TODO: add unit tests for this method. def load_template(self, template_name): """ Load a template by name from the file system. """ load_template = self._make_load_template() return load_template(template_name) def _render_object(self, obj, *context, **kwargs): """ Render the template associated with the given object. """ loader = self._make_loader() # TODO: consider an approach that does not require using an if # block here. For example, perhaps this class's loader can be # a SpecLoader in all cases, and the SpecLoader instance can # check the object's type. Or perhaps Loader and SpecLoader # can be refactored to implement the same interface. if isinstance(obj, TemplateSpec): loader = SpecLoader(loader) template = loader.load(obj) else: template = loader.load_object(obj) context = [obj] + list(context) return self._render_string(template, *context, **kwargs) def render_name(self, template_name, *context, **kwargs): """ Render the template with the given name using the given context. See the render() docstring for more information. """ loader = self._make_loader() template = loader.load_name(template_name) return self._render_string(template, *context, **kwargs) def render_path(self, template_path, *context, **kwargs): """ Render the template at the given path using the given context. Read the render() docstring for more information. """ loader = self._make_loader() template = loader.read(template_path) return self._render_string(template, *context, **kwargs) def _render_string(self, template, *context, **kwargs): """ Render the given template string using the given context. """ # RenderEngine.render() requires that the template string be unicode. template = self._to_unicode_hard(template) render_func = lambda engine, stack: engine.render(template, stack) # noqa return self._render_final(render_func, *context, **kwargs) # All calls to render() should end here because it prepares the # context stack correctly. def _render_final(self, render_func, *context, **kwargs): """ Arguments: render_func: a function that accepts a RenderEngine and ContextStack instance and returns a template rendering as a unicode string. """ stack = ContextStack.create(*context, **kwargs) self._context = stack engine = self._make_render_engine() return render_func(engine, stack) def render(self, template, *context, **kwargs): """ Render the given template string, view template, or parsed template. Returns a unicode string. Prior to rendering, this method will convert a template that is a byte string (type str in Python 2) to unicode using the string_encoding and decode_errors attributes. See the constructor docstring for more information. Arguments: template: a template string that is unicode or a byte string, a ParsedTemplate instance, or another object instance. In the final case, the function first looks for the template associated to the object by calling this class's get_associated_template() method. The rendering process also uses the passed object as the first element of the context stack when rendering. *context: zero or more dictionaries, ContextStack instances, or objects with which to populate the initial context stack. None arguments are skipped. Items in the *context list are added to the context stack in order so that later items in the argument list take precedence over earlier items. **kwargs: additional key-value data to add to the context stack. As these arguments appear after all items in the *context list, in the case of key conflicts these values take precedence over all items in the *context list. """ if is_string(template): return self._render_string(template, *context, **kwargs) if isinstance(template, ParsedTemplate): render_func = lambda engine, stack: template.render(engine, stack) # noqa return self._render_final(render_func, *context, **kwargs) # Otherwise, we assume the template is an object. return self._render_object(template, *context, **kwargs) pystache-0.6.0/pystache/specloader.py000066400000000000000000000047231413701016200176510ustar00rootroot00000000000000# coding: utf-8 """ This module supports customized (aka special or specified) template loading. """ import os.path from pystache.loader import Loader # TODO: add test cases for this class. class SpecLoader(object): """ Supports loading custom-specified templates (from TemplateSpec instances). """ def __init__(self, loader=None): if loader is None: loader = Loader() self.loader = loader def _find_relative(self, spec): """ Return the path to the template as a relative (dir, file_name) pair. The directory returned is relative to the directory containing the class definition of the given object. The method returns None for this directory if the directory is unknown without first searching the search directories. """ if spec.template_rel_path is not None: return os.path.split(spec.template_rel_path) # Otherwise, determine the file name separately. locator = self.loader._make_locator() # We do not use the ternary operator for Python 2.4 support. if spec.template_name is not None: template_name = spec.template_name else: template_name = locator.make_template_name(spec) file_name = locator.make_file_name(template_name, spec.template_extension) return (spec.template_rel_directory, file_name) def _find(self, spec): """ Find and return the path to the template associated to the instance. """ if spec.template_path is not None: return spec.template_path dir_path, file_name = self._find_relative(spec) locator = self.loader._make_locator() if dir_path is None: # Then we need to search for the path. path = locator.find_object(spec, self.loader.search_dirs, file_name=file_name) else: obj_dir = locator.get_object_directory(spec) path = os.path.join(obj_dir, dir_path, file_name) return path def load(self, spec): """ Find and return the template associated to a TemplateSpec instance. Returns the template as a unicode string. Arguments: spec: a TemplateSpec instance. """ if spec.template is not None: return self.loader.str(spec.template, spec.template_encoding) path = self._find(spec) return self.loader.read(path, spec.template_encoding) pystache-0.6.0/pystache/template_spec.py000066400000000000000000000032761413701016200203570ustar00rootroot00000000000000# coding: utf-8 """ Provides a class to customize template information on a per-view basis. To customize template properties for a particular view, create that view from a class that subclasses TemplateSpec. The "spec" in TemplateSpec stands for "special" or "specified" template information. """ class TemplateSpec(object): """ A mixin or interface for specifying custom template information. The "spec" in TemplateSpec can be taken to mean that the template information is either "specified" or "special." A view should subclass this class only if customized template loading is needed. The following attributes allow one to customize/override template information on a per view basis. A None value means to use default behavior for that value and perform no customization. All attributes are initialized to None. Attributes: template: the template as a string. template_encoding: the encoding used by the template. template_extension: the template file extension. Defaults to "mustache". Pass False for no extension (i.e. extensionless template files). template_name: the name of the template. template_path: absolute path to the template. template_rel_directory: the directory containing the template file, relative to the directory containing the module defining the class. template_rel_path: the path to the template file, relative to the directory containing the module defining the class. """ template = None template_encoding = None template_extension = None template_name = None template_path = None template_rel_directory = None template_rel_path = None pystache-0.6.0/pystache/tests/000077500000000000000000000000001413701016200163125ustar00rootroot00000000000000pystache-0.6.0/pystache/tests/__init__.py000066400000000000000000000000401413701016200204150ustar00rootroot00000000000000""" TODO: add a docstring. """ pystache-0.6.0/pystache/tests/benchmark.py000077500000000000000000000041171413701016200206240ustar00rootroot00000000000000#!/usr/bin/env python # coding: utf-8 """ A rudimentary backward- and forward-compatible script to benchmark pystache. Usage: tests/benchmark.py 10000 """ import sys from timeit import Timer try: import chevron as pystache print('Using module: chevron') except (ImportError): import pystache print('Using module: pystache') import pystache # TODO: make the example realistic. examples = [ # Test case: 1 ("""{{#person}}Hi {{name}}{{/person}}""", {"person": {"name": "Jon"}}, "Hi Jon"), # Test case: 2 ("""\

{{header}}

    {{#comments}}
  • {{name}}

    {{body}}

  • {{/comments}}
""", {'header': "My Post Comments", 'comments': [ {'name': "Joe", 'body': "Thanks for this post!"}, {'name': "Sam", 'body': "Thanks for this post!"}, {'name': "Heather", 'body': "Thanks for this post!"}, {'name': "Kathy", 'body': "Thanks for this post!"}, {'name': "George", 'body': "Thanks for this post!"}]}, """\

My Post Comments

  • Joe

    Thanks for this post!

  • Sam

    Thanks for this post!

  • Heather

    Thanks for this post!

  • Kathy

    Thanks for this post!

  • George

    Thanks for this post!

"""), ] def make_test_function(example): template, context, expected = example def test(): actual = pystache.render(template, context) if actual != expected: raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual)) return test def main(sys_argv): args = sys_argv[1:] count = int(args[0]) print("Benchmarking: %sx" % count) print() for example in examples: test = make_test_function(example) t = Timer(test,) print(min(t.repeat(repeat=3, number=count))) print("Done") if __name__ == '__main__': main(sys.argv) pystache-0.6.0/pystache/tests/common.py000066400000000000000000000156321413701016200201630ustar00rootroot00000000000000# coding: utf-8 """ Provides test-related code that can be used by all tests. """ import os import pystache from pystache import defaults from pystache.tests import examples # Save a reference to the original function to avoid recursion. _DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE _TESTS_DIR = os.path.dirname(pystache.tests.__file__) DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'. EXAMPLES_DIR = os.path.dirname(examples.__file__) PACKAGE_DIR = os.path.dirname(pystache.__file__) PROJECT_DIR = os.path.join(PACKAGE_DIR, '..') # TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files) # containing doctests. The paths should be relative to the project directory. TEXT_DOCTEST_PATHS = ['README.md'] UNITTEST_FILE_PREFIX = "test_" def get_spec_test_dir(project_dir): return os.path.join(project_dir, 'ext', 'spec', 'specs') def html_escape(u): """ An html escape function that behaves the same in both Python 2 and 3. This function is needed because single quotes are escaped in Python 3 (to '''), but not in Python 2. The global defaults.TAG_ESCAPE can be set to this function in the setUp() and tearDown() of unittest test cases, for example, for consistent test results. """ u = _DEFAULT_TAG_ESCAPE(u) return u.replace("'", ''') def get_data_path(file_name=None): """Return the path to a file in the test data directory.""" if file_name is None: file_name = "" return os.path.join(DATA_DIR, file_name) # Functions related to get_module_names(). def _find_files(root_dir, should_include): """ Return a list of paths to all modules below the given directory. Arguments: should_include: a function that accepts a file path and returns True or False. """ paths = [] # Return value. is_module = lambda path: path.endswith(".py") # os.walk() is new in Python 2.3 # http://docs.python.org/library/os.html#os.walk for dir_path, dir_names, file_names in os.walk(root_dir): new_paths = [os.path.join(dir_path, file_name) for file_name in file_names] new_paths = list(filter(is_module, new_paths)) new_paths = list(filter(should_include, new_paths)) paths.extend(new_paths) return paths def _make_module_names(package_dir, paths): """ Return a list of fully-qualified module names given a list of module paths. """ package_dir = os.path.abspath(package_dir) package_name = os.path.split(package_dir)[1] prefix_length = len(package_dir) module_names = [] for path in paths: path = os.path.abspath(path) # for example /subpackage/module.py rel_path = path[prefix_length:] # for example /subpackage/module.py rel_path = os.path.splitext(rel_path)[0] # for example /subpackage/module parts = [] while True: (rel_path, tail) = os.path.split(rel_path) if not tail: break parts.insert(0, tail) # We now have, for example, ['subpackage', 'module']. parts.insert(0, package_name) module = ".".join(parts) module_names.append(module) return module_names def get_module_names(package_dir=None, should_include=None): """ Return a list of fully-qualified module names in the given package. """ if package_dir is None: package_dir = PACKAGE_DIR if should_include is None: should_include = lambda path: True paths = _find_files(package_dir, should_include) names = _make_module_names(package_dir, paths) names.sort() return names class AssertStringMixin: """A unittest.TestCase mixin to check string equality.""" def assertString(self, actual, expected, format=None): """ Assert that the given strings are equal and have the same type. Arguments: format: a format string containing a single conversion specifier %s. Defaults to "%s". """ if format is None: format = "%s" # Show both friendly and literal versions. details = """String mismatch: %%s Expected: \"""%s\""" Actual: \"""%s\""" Expected: %s Actual: %s""" % (expected, actual, repr(expected), repr(actual)) def make_message(reason): description = details % reason return format % description self.assertEqual(actual, expected, make_message("different characters")) reason = "types different: %s != %s (actual)" % (repr(type(expected)), repr(type(actual))) self.assertEqual(type(expected), type(actual), make_message(reason)) class AssertIsMixin: """A unittest.TestCase mixin adding assertIs().""" # unittest.assertIs() is not available until Python 2.7: # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone def assertIs(self, first, second): self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second))) class AssertExceptionMixin: """A unittest.TestCase mixin adding assertException().""" # unittest.assertRaisesRegexp() is not available until Python 2.7: # http://docs.python.org/library/unittest.html#unittest.TestCase.assertRaisesRegexp def assertException(self, exception_type, msg, callable, *args, **kwds): try: callable(*args, **kwds) raise Exception("Expected exception: %s: %s" % (exception_type, repr(msg))) except exception_type as err: self.assertEqual(str(err), msg) class SetupDefaults(object): """ Mix this class in to a unittest.TestCase for standard defaults. This class allows for consistent test results across Python 2/3. """ def setup_defaults(self): self.original_decode_errors = defaults.DECODE_ERRORS self.original_file_encoding = defaults.FILE_ENCODING self.original_string_encoding = defaults.STRING_ENCODING defaults.DECODE_ERRORS = 'strict' defaults.FILE_ENCODING = 'ascii' defaults.STRING_ENCODING = 'ascii' def teardown_defaults(self): defaults.DECODE_ERRORS = self.original_decode_errors defaults.FILE_ENCODING = self.original_file_encoding defaults.STRING_ENCODING = self.original_string_encoding class Attachable(object): """ A class that attaches all constructor named parameters as attributes. For example-- >>> obj = Attachable(foo=42, size="of the universe") >>> repr(obj) "Attachable(foo=42, size='of the universe')" >>> obj.foo 42 >>> obj.size 'of the universe' """ def __init__(self, **kwargs): self.__args__ = kwargs for arg, value in kwargs.items(): setattr(self, arg, value) def __repr__(self): return "%s(%s)" % (self.__class__.__name__, ", ".join("%s=%s" % (k, repr(v)) for k, v in self.__args__.items())) pystache-0.6.0/pystache/tests/data/000077500000000000000000000000001413701016200172235ustar00rootroot00000000000000pystache-0.6.0/pystache/tests/data/__init__.py000066400000000000000000000000401413701016200213260ustar00rootroot00000000000000""" TODO: add a docstring. """ pystache-0.6.0/pystache/tests/data/ascii.mustache000066400000000000000000000000121413701016200220370ustar00rootroot00000000000000ascii: abcpystache-0.6.0/pystache/tests/data/duplicate.mustache000066400000000000000000000000671413701016200227330ustar00rootroot00000000000000This file is used to test locate_path()'s search order.pystache-0.6.0/pystache/tests/data/locator/000077500000000000000000000000001413701016200206665ustar00rootroot00000000000000pystache-0.6.0/pystache/tests/data/locator/__init__.py000066400000000000000000000000401413701016200227710ustar00rootroot00000000000000""" TODO: add a docstring. """ pystache-0.6.0/pystache/tests/data/locator/duplicate.mustache000066400000000000000000000000671413701016200243760ustar00rootroot00000000000000This file is used to test locate_path()'s search order.pystache-0.6.0/pystache/tests/data/locator/template.txt000066400000000000000000000000231413701016200232350ustar00rootroot00000000000000Test template file pystache-0.6.0/pystache/tests/data/non_ascii.mustache000066400000000000000000000000151413701016200227140ustar00rootroot00000000000000non-ascii: épystache-0.6.0/pystache/tests/data/sample_view.mustache000066400000000000000000000000121413701016200232620ustar00rootroot00000000000000ascii: abcpystache-0.6.0/pystache/tests/data/say_hello.mustache000066400000000000000000000000151413701016200227310ustar00rootroot00000000000000Hello, {{to}}pystache-0.6.0/pystache/tests/data/views.py000066400000000000000000000003531413701016200207330ustar00rootroot00000000000000# coding: utf-8 """ TODO: add a docstring. """ from pystache import TemplateSpec class SayHello(object): def to(self): return "World" class SampleView(TemplateSpec): pass class NonAscii(TemplateSpec): pass pystache-0.6.0/pystache/tests/doctesting.py000066400000000000000000000046561413701016200210420ustar00rootroot00000000000000# coding: utf-8 """ Exposes a get_doctests() function for the project's test harness. """ import doctest import os import pkgutil import sys import traceback if sys.version_info >= (3,): # Then pull in modules needed for 2to3 conversion. The modules # below are not necessarily available in older versions of Python. from lib2to3.main import main as lib2to3main # new in Python 2.6? from shutil import copyfile from pystache.tests.common import TEXT_DOCTEST_PATHS from pystache.tests.common import get_module_names # This module follows the guidance documented here: # # http://docs.python.org/library/doctest.html#unittest-api # def get_doctests(text_file_dir): """ Return a list of TestSuite instances for all doctests in the project. Arguments: text_file_dir: the directory in which to search for all text files (i.e. non-module files) containing doctests. """ # Since module_relative is False in our calls to DocFileSuite below, # paths should be OS-specific. See the following for more info-- # # http://docs.python.org/library/doctest.html#doctest.DocFileSuite # paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS] if sys.version_info >= (3,): # Skip the README doctests in Python 3 for now because examples # rendering to unicode do not give consistent results # (e.g. 'foo' vs u'foo'). # paths = _convert_paths(paths) paths = [] suites = [] for path in paths: suite = doctest.DocFileSuite(path, module_relative=False) suites.append(suite) modules = get_module_names() for module in modules: suite = doctest.DocTestSuite(module) suites.append(suite) return suites def _convert_2to3(path): """ Convert the given file, and return the path to the converted files. """ base, ext = os.path.splitext(path) # For example, "README.temp2to3.rst". new_path = "%s.temp2to3%s" % (base, ext) copyfile(path, new_path) args = ['--doctests_only', '--no-diffs', '--write', '--nobackups', new_path] lib2to3main("lib2to3.fixes", args=args) return new_path def _convert_paths(paths): """ Convert the given files, and return the paths to the converted files. """ new_paths = [] for path in paths: new_path = _convert_2to3(path) new_paths.append(new_path) return new_paths pystache-0.6.0/pystache/tests/examples/000077500000000000000000000000001413701016200201305ustar00rootroot00000000000000pystache-0.6.0/pystache/tests/examples/__init__.py000066400000000000000000000000401413701016200222330ustar00rootroot00000000000000""" TODO: add a docstring. """ pystache-0.6.0/pystache/tests/examples/comments.mustache000066400000000000000000000001011413701016200235000ustar00rootroot00000000000000

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

pystache-0.6.0/pystache/tests/examples/comments.py000066400000000000000000000001641413701016200223300ustar00rootroot00000000000000 """ TODO: add a docstring. """ class Comments(object): def title(self): return "A Comedy of Errors" pystache-0.6.0/pystache/tests/examples/complex.mustache000066400000000000000000000003531413701016200233330ustar00rootroot00000000000000

{{ header }}

{{#list}}
    {{#item}}{{# current }}
  • {{name}}
  • {{/ current }}{{#link}}
  • {{name}}
  • {{/link}}{{/item}}
{{/list}}{{#empty}}

The list is empty.

{{/empty}}pystache-0.6.0/pystache/tests/examples/complex.py000066400000000000000000000010331413701016200221460ustar00rootroot00000000000000 """ TODO: add a docstring. """ class Complex(object): def header(self): return "Colors" def item(self): items = [] items.append({ 'name': 'red', 'current': True, 'url': '#Red' }) items.append({ 'name': 'green', 'link': True, 'url': '#Green' }) items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' }) return items def list(self): return not self.empty() def empty(self): return len(self.item()) == 0 def empty_list(self): return []; pystache-0.6.0/pystache/tests/examples/delimiters.mustache000066400000000000000000000001111413701016200240150ustar00rootroot00000000000000{{=<% %>=}} * <% first %> <%=| |=%> * | second | |={{ }}=| * {{ third }} pystache-0.6.0/pystache/tests/examples/delimiters.py000066400000000000000000000004311413701016200226410ustar00rootroot00000000000000 """ TODO: add a docstring. """ class Delimiters(object): def first(self): return "It worked the first time." def second(self): return "And it worked the second time." def third(self): return "Then, surprisingly, it worked the third time." pystache-0.6.0/pystache/tests/examples/double_section.mustache000066400000000000000000000000611413701016200246560ustar00rootroot00000000000000{{#t}}* first{{/t}} * {{two}} {{#t}}* third{{/t}}pystache-0.6.0/pystache/tests/examples/double_section.py000066400000000000000000000002211413701016200234730ustar00rootroot00000000000000 """ TODO: add a docstring. """ class DoubleSection(object): def t(self): return True def two(self): return "second" pystache-0.6.0/pystache/tests/examples/escaped.mustache000066400000000000000000000000221413701016200232610ustar00rootroot00000000000000

{{title}}

pystache-0.6.0/pystache/tests/examples/escaped.py000066400000000000000000000001551413701016200221070ustar00rootroot00000000000000 """ TODO: add a docstring. """ class Escaped(object): def title(self): return "Bear > Shark" pystache-0.6.0/pystache/tests/examples/extensionless000066400000000000000000000000321413701016200227510ustar00rootroot00000000000000No file extension: {{foo}}pystache-0.6.0/pystache/tests/examples/inner_partial.mustache000066400000000000000000000000211413701016200245030ustar00rootroot00000000000000Again, {{title}}!pystache-0.6.0/pystache/tests/examples/inner_partial.txt000066400000000000000000000000271413701016200235170ustar00rootroot00000000000000## Again, {{title}}! ##pystache-0.6.0/pystache/tests/examples/inverted.mustache000066400000000000000000000002421413701016200235010ustar00rootroot00000000000000{{^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-0.6.0/pystache/tests/examples/inverted.py000066400000000000000000000007311413701016200223230ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec class Inverted(object): def t(self): return True def f(self): return False def two(self): return 'two' def empty_list(self): return [] def populated_list(self): return ['some_value'] class InvertedLists(Inverted, TemplateSpec): template_name = 'inverted' def t(self): return [0, 1, 2] def f(self): return [] pystache-0.6.0/pystache/tests/examples/lambdas.mustache000066400000000000000000000001121413701016200232600ustar00rootroot00000000000000{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}pystache-0.6.0/pystache/tests/examples/lambdas.py000066400000000000000000000014361413701016200221110ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec def rot(s, n=13): r = "" for c in s: cc = c if cc.isalpha(): cc = cc.lower() o = ord(cc) ro = (o+n) % 122 if ro == 0: ro = 122 if ro < 97: ro += 96 cc = chr(ro) r = ''.join((r,cc)) return r def replace(subject, this='foo', with_this='bar'): return subject.replace(this, with_this) # This class subclasses TemplateSpec because at least one unit test # sets the template attribute. class Lambdas(TemplateSpec): def replace_foo_with_bar(self, text=None): return replace def rot13(self, text=None): return rot def sort(self, text=None): return lambda text: ''.join(sorted(text)) pystache-0.6.0/pystache/tests/examples/looping_partial.mustache000066400000000000000000000000311413701016200250400ustar00rootroot00000000000000Looping partial {{item}}!pystache-0.6.0/pystache/tests/examples/nested_context.mustache000066400000000000000000000001251413701016200247070ustar00rootroot00000000000000{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}pystache-0.6.0/pystache/tests/examples/nested_context.py000066400000000000000000000011761413701016200235350ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec class NestedContext(TemplateSpec): def __init__(self, renderer): self.renderer = renderer def _context_get(self, key): return self.renderer.context.get(key) def outer_thing(self): return "two" def foo(self): return {'thing1': 'one', 'thing2': 'foo'} def derp(self): return [{'inner': 'car'}] def herp(self): return [{'outer': 'car'}] def nested_context_in_view(self): if self._context_get('outer') == self._context_get('inner'): return 'it works!' return '' pystache-0.6.0/pystache/tests/examples/partial_in_partial.mustache000066400000000000000000000000131413701016200255130ustar00rootroot00000000000000{{>simple}}pystache-0.6.0/pystache/tests/examples/partial_with_lambda.mustache000066400000000000000000000000411413701016200256450ustar00rootroot00000000000000{{#rot13}}abcdefghijklm{{/rot13}}pystache-0.6.0/pystache/tests/examples/partial_with_partial_and_lambda.mustache000066400000000000000000000000711413701016200302060ustar00rootroot00000000000000{{>partial_with_lambda}}{{#rot13}}abcdefghijklm{{/rot13}}pystache-0.6.0/pystache/tests/examples/partials_with_lambdas.py000066400000000000000000000002351413701016200250370ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache.tests.examples.lambdas import rot class PartialsWithLambdas(object): def rot(self): return rot pystache-0.6.0/pystache/tests/examples/readme.py000066400000000000000000000001431413701016200217350ustar00rootroot00000000000000 """ TODO: add a docstring. """ class SayHello(object): def to(self): return "Pizza" pystache-0.6.0/pystache/tests/examples/say_hello.mustache000066400000000000000000000000161413701016200236370ustar00rootroot00000000000000Hello, {{to}}!pystache-0.6.0/pystache/tests/examples/simple.mustache000066400000000000000000000000261413701016200231520ustar00rootroot00000000000000Hi {{thing}}!{{blank}}pystache-0.6.0/pystache/tests/examples/simple.py000066400000000000000000000002661413701016200217770ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec class Simple(TemplateSpec): def thing(self): return "pizza" def blank(self): return '' pystache-0.6.0/pystache/tests/examples/tagless.mustache000066400000000000000000000000121413701016200233160ustar00rootroot00000000000000No tags...pystache-0.6.0/pystache/tests/examples/template_partial.mustache000066400000000000000000000000451413701016200252110ustar00rootroot00000000000000

{{title}}

{{>inner_partial}}pystache-0.6.0/pystache/tests/examples/template_partial.py000066400000000000000000000010131413701016200240240ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec class TemplatePartial(TemplateSpec): def __init__(self, renderer): self.renderer = renderer def _context_get(self, key): return self.renderer.context.get(key) def title(self): return "Welcome" def title_bars(self): return '-' * len(self.title()) def looping(self): return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}] def thing(self): return self._context_get('prop') pystache-0.6.0/pystache/tests/examples/template_partial.txt000066400000000000000000000000551413701016200242200ustar00rootroot00000000000000{{title}} {{title_bars}} {{>inner_partial}} pystache-0.6.0/pystache/tests/examples/unescaped.mustache000066400000000000000000000000241413701016200236260ustar00rootroot00000000000000

{{{title}}}

pystache-0.6.0/pystache/tests/examples/unescaped.py000066400000000000000000000001571413701016200224540ustar00rootroot00000000000000 """ TODO: add a docstring. """ class Unescaped(object): def title(self): return "Bear > Shark" pystache-0.6.0/pystache/tests/examples/unicode_input.mustache000066400000000000000000000000061413701016200245240ustar00rootroot00000000000000abcdépystache-0.6.0/pystache/tests/examples/unicode_input.py000066400000000000000000000002561413701016200233520ustar00rootroot00000000000000 """ TODO: add a docstring. """ from pystache import TemplateSpec class UnicodeInput(TemplateSpec): template_encoding = 'utf8' def age(self): return 156 pystache-0.6.0/pystache/tests/examples/unicode_output.mustache000066400000000000000000000000251413701016200247260ustar00rootroot00000000000000

Name: {{name}}

pystache-0.6.0/pystache/tests/examples/unicode_output.py000066400000000000000000000002071413701016200235470ustar00rootroot00000000000000# encoding: utf-8 """ TODO: add a docstring. """ class UnicodeOutput(object): def name(self): return 'Henri Poincaré' pystache-0.6.0/pystache/tests/main.py000066400000000000000000000122761413701016200176200ustar00rootroot00000000000000# coding: utf-8 """ Exposes a main() function that runs all tests in the project. This module is for our test console script. """ import os import sys import unittest from unittest import TestCase, TestProgram import pystache from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, UNITTEST_FILE_PREFIX from pystache.tests.common import get_module_names, get_spec_test_dir from pystache.tests.doctesting import get_doctests from pystache.tests.spectesting import get_spec_tests # If this command option is present, then the spec test and doctest directories # will be inserted if not provided. FROM_SOURCE_OPTION = "--from-source" def make_extra_tests(text_doctest_dir, spec_test_dir): tests = [] if text_doctest_dir is not None: doctest_suites = get_doctests(text_doctest_dir) tests.extend(doctest_suites) if spec_test_dir is not None: spec_testcases = get_spec_tests(spec_test_dir) tests.extend(spec_testcases) return unittest.TestSuite(tests) def make_test_program_class(extra_tests): """ Return a subclass of unittest.TestProgram. """ # The function unittest.main() is an alias for unittest.TestProgram's # constructor. TestProgram's constructor does the following: # # 1. calls self.parseArgs(argv), # 2. which in turn calls self.createTests(). # 3. then the constructor calls self.runTests(). # # The createTests() method sets the self.test attribute by calling one # of self.testLoader's "loadTests" methods. Each loadTest method returns # a unittest.TestSuite instance. Thus, self.test is set to a TestSuite # instance prior to calling runTests(). class PystacheTestProgram(TestProgram): """ Instantiating an instance of this class runs all tests. """ def createTests(self): """ Load tests and set self.test to a unittest.TestSuite instance Compare-- http://docs.python.org/library/unittest.html#unittest.TestSuite """ super(PystacheTestProgram, self).createTests() self.test.addTests(extra_tests) return PystacheTestProgram # Do not include "test" in this function's name to avoid it getting # picked up by nosetests. def main(sys_argv): """ Run all tests in the project. Arguments: sys_argv: a reference to sys.argv. """ # TODO: use logging module print("pystache: running tests: argv: %s" % repr(sys_argv)) should_source_exist = False spec_test_dir = None project_dir = None if len(sys_argv) > 1 and sys_argv[1] == FROM_SOURCE_OPTION: # This usually means the test_pystache.py convenience script # in the source directory was run. should_source_exist = True sys_argv.pop(1) try: # TODO: use optparse command options instead. project_dir = sys_argv[1] sys_argv.pop(1) except IndexError: if should_source_exist: project_dir = PROJECT_DIR try: # TODO: use optparse command options instead. spec_test_dir = sys_argv[1] sys_argv.pop(1) except IndexError: if project_dir is not None: # Then auto-detect the spec test directory. _spec_test_dir = get_spec_test_dir(project_dir) if not os.path.exists(_spec_test_dir): # Then the user is probably using a downloaded sdist rather # than a repository clone (since the sdist does not include # the spec test directory). print("pystache: skipping spec tests: spec test directory " "not found") else: spec_test_dir = _spec_test_dir if len(sys_argv) <= 1 or sys_argv[-1].startswith("-"): # Then no explicit module or test names were provided, so # auto-detect all unit tests. module_names = _discover_test_modules(PACKAGE_DIR) sys_argv.extend(module_names) if project_dir is not None: # Add the current module for unit tests contained here sys_argv.append(__name__) extra_tests = make_extra_tests(project_dir, spec_test_dir) test_program_class = make_test_program_class(extra_tests) # We pass None for the module because we do not want the unittest # module to resolve module names relative to a given module. # (This would require importing all of the unittest modules from # this module.) See the loadTestsFromName() method of the # unittest.TestLoader class for more details on this parameter. test_program_class(argv=sys_argv, module=None) # No need to return since unitttest.main() exits. def _discover_test_modules(package_dir): """ Discover and return a sorted list of the names of unit-test modules. """ def is_unittest_module(path): file_name = os.path.basename(path) return file_name.startswith(UNITTEST_FILE_PREFIX) names = get_module_names(package_dir=package_dir, should_include=is_unittest_module) # This is a sanity check to ensure that the unit-test discovery # methods are working. if len(names) < 1: raise Exception("No unit-test modules found--\n in %s" % package_dir) return names pystache-0.6.0/pystache/tests/spectesting.py000066400000000000000000000201661413701016200212210ustar00rootroot00000000000000# coding: utf-8 """ Exposes a get_spec_tests() function for the project's test harness. Creates a unittest.TestCase for the tests defined in the mustache spec. """ # TODO: this module can be cleaned up somewhat. # TODO: move all of this code to pystache/tests/spectesting.py and # have it expose a get_spec_tests(spec_test_dir) function. FILE_ENCODING = 'utf-8' # the encoding of the spec test files. yaml = None try: # We try yaml first since it is more convenient when adding and modifying # test cases by hand (since the YAML is human-readable and is the master # from which the JSON format is generated). import yaml except ImportError: try: import json except: # The module json is not available prior to Python 2.6, whereas # simplejson is. The simplejson package dropped support for Python 2.4 # in simplejson v2.1.0, so Python 2.4 requires a simplejson install # older than the most recent version. try: import simplejson as json except ImportError: # Raise an error with a type different from ImportError as a hack around # this issue: # http://bugs.python.org/issue7559 from sys import exc_info ex_type, ex_value, tb = exc_info() new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value)) raise new_ex.__class__(new_ex).with_traceback(tb) file_extension = 'json' parser = json else: file_extension = 'yml' parser = yaml import ast import codecs import glob import os.path import unittest import pystache from pystache import common from pystache.renderer import Renderer from pystache.tests.common import AssertStringMixin def get_spec_tests(spec_test_dir): """ Return a list of unittest.TestCase instances. """ # TODO: use logging module instead. print("pystache: spec tests: using %s" % _get_parser_info()) cases = [] # Make this absolute for easier diagnosis in case of error. spec_test_dir = os.path.abspath(spec_test_dir) spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension)) for path in spec_paths: new_cases = _read_spec_tests(path) cases.extend(new_cases) # Store this as a value so that CheckSpecTestsFound is not checking # a reference to cases that contains itself. spec_test_count = len(cases) # This test case lets us alert the user that spec tests are missing. class CheckSpecTestsFound(unittest.TestCase): def runTest(self): if spec_test_count > 0: return raise Exception("Spec tests not found--\n in %s\n" " Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir)) case = CheckSpecTestsFound() cases.append(case) return cases def _get_parser_info(): return "%s (version %s)" % (parser.__name__, parser.__version__) def _read_spec_tests(path): """ Return a list of unittest.TestCase instances. """ b = common.read(path) u = str(b, encoding=FILE_ENCODING) spec_data = parse(u) tests = spec_data['tests'] cases = [] for data in tests: case = _deserialize_spec_test(data, path) cases.append(case) return cases # TODO: simplify the implementation of this function. def _convert_children(node): """ Recursively convert to functions all "code strings" below the node. This function is needed only for the json format. """ if not isinstance(node, (list, dict)): # Then there is nothing to iterate over and recurse. return if isinstance(node, list): for child in node: _convert_children(child) return # Otherwise, node is a dict, so attempt the conversion. for key in list(node.keys()): val = node[key] if not isinstance(val, dict) or val.get('__tag__') != 'code': _convert_children(val) continue # Otherwise, we are at a "leaf" node. val = ast.literal_eval(val['python']) node[key] = val continue def _deserialize_spec_test(data, file_path): """ Return a unittest.TestCase instance representing a spec test. Arguments: data: the dictionary of attributes for a single test. """ context = data['data'] description = data['desc'] # PyYAML seems to leave ASCII strings as byte strings. expected = str(data['expected']) # TODO: switch to using dict.get(). partials = 'partials' in data and data['partials'] or {} template = data['template'] test_name = data['name'] _convert_children(context) test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path) return test_case def _make_spec_test(expected, template, context, partials, description, test_name, file_path): """ Return a unittest.TestCase instance representing a spec test. """ file_name = os.path.basename(file_path) test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name)) # We subclass SpecTestBase in order to control the test method name (for # the purposes of improved reporting). class SpecTest(SpecTestBase): pass def run_test(self): self._runTest() # TODO: should we restore this logic somewhere? # If we don't convert unicode to str, we get the following error: # "TypeError: __name__ must be set to a string object" # test.__name__ = str(name) setattr(SpecTest, test_method_name, run_test) case = SpecTest(test_method_name) case._context = context case._description = description case._expected = expected case._file_path = file_path case._partials = partials case._template = template case._test_name = test_name return case def parse(u): """ Parse the contents of a spec test file, and return a dict. Arguments: u: a unicode string. """ # TODO: find a cleaner mechanism for choosing between the two. if yaml is None: # Then use json. # The only way to get the simplejson module to return unicode strings # is to pass it unicode. See, for example-- # # http://code.google.com/p/simplejson/issues/detail?id=40 # # and the documentation of simplejson.loads(): # # "If s is a str then decoded JSON strings that contain only ASCII # characters may be parsed as str for performance and memory reasons. # If your code expects only unicode the appropriate solution is # decode s to unicode prior to calling loads." # return json.loads(u) # Otherwise, yaml. def code_constructor(loader, node): value = loader.construct_mapping(node) # ast.literal_eval will not work here => lambda expression return eval(value['python'], {}) yaml.add_constructor('!code', code_constructor) return yaml.full_load(u) class SpecTestBase(unittest.TestCase, AssertStringMixin): def _runTest(self): context = self._context description = self._description expected = self._expected file_path = self._file_path partials = self._partials template = self._template test_name = self._test_name renderer = Renderer(partials=partials) actual = renderer.render(template, context) # We need to escape the strings that occur in our format string because # they can contain % symbols, for example (in delimiters.yml)-- # # "template: '{{=<% %>=}}(<%text%>)'" # def escape(s): return s.replace("%", "%%") parser_info = _get_parser_info() subs = [repr(test_name), description, os.path.abspath(file_path), template, repr(context), parser_info] subs = tuple([escape(sub) for sub in subs]) # We include the parsing module version info to help with troubleshooting # yaml/json/simplejson issues. message = """%s: %s File: %s Template: \"""%s\""" Context: %s %%s [using %s] """ % subs self.assertString(actual, expected, format=message) pystache-0.6.0/pystache/tests/test___init__.py000066400000000000000000000014531413701016200214650ustar00rootroot00000000000000# coding: utf-8 """ Tests of __init__.py. """ # Calling "import *" is allowed only at the module level. GLOBALS_INITIAL = list(globals().keys()) from pystache import * GLOBALS_PYSTACHE_IMPORTED = list(globals().keys()) import unittest import pystache class InitTests(unittest.TestCase): def test___all__(self): """ Test that "from pystache import *" works as expected. """ actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL) expected = set(['parse', 'render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL']) self.assertEqual(actual, expected) def test_version_defined(self): """ Test that pystache.__version__ is set. """ actual_version = pystache.__version__ self.assertTrue(actual_version) pystache-0.6.0/pystache/tests/test_commands.py000066400000000000000000000014521413701016200215260ustar00rootroot00000000000000# coding: utf-8 """ Unit tests of commands.py. """ import sys import unittest from pystache.commands.render import main ORIGINAL_STDOUT = sys.stdout class MockStdout(object): def __init__(self): self.output = "" def write(self, str): self.output += str class CommandsTestCase(unittest.TestCase): def setUp(self): sys.stdout = MockStdout() def callScript(self, template, context): argv = ['pystache', template, context] main(argv) return sys.stdout.output def testMainSimple(self): """ Test a simple command-line case. """ actual = self.callScript("Hi {{thing}}", '{"thing": "world"}') self.assertEqual(actual, "Hi world\n") def tearDown(self): sys.stdout = ORIGINAL_STDOUT pystache-0.6.0/pystache/tests/test_context.py000066400000000000000000000346361413701016200214230ustar00rootroot00000000000000# coding: utf-8 """ Unit tests of context.py. """ from datetime import datetime import unittest from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable class SimpleObject(object): """A sample class that does not define __getitem__().""" def __init__(self): self.foo = "bar" def foo_callable(self): return "called..." class DictLike(object): """A sample class that implements __getitem__() and __contains__().""" def __init__(self): self._dict = {'foo': 'bar'} self.fuzz = 'buzz' def __contains__(self, key): return key in self._dict def __getitem__(self, key): return self._dict[key] class GetValueTestCase(unittest.TestCase, AssertIsMixin): """Test context._get_value().""" def assertNotFound(self, item, key): """ Assert that a call to _get_value() returns _NOT_FOUND. """ self.assertIs(_get_value(item, key), _NOT_FOUND) ### Case: the item is a dictionary. def test_dictionary__key_present(self): """ Test getting a key from a dictionary. """ item = {"foo": "bar"} self.assertEqual(_get_value(item, "foo"), "bar") def test_dictionary__callable_not_called(self): """ Test that callable values are returned as-is (and in particular not called). """ def foo_callable(self): return "bar" item = {"foo": foo_callable} self.assertNotEqual(_get_value(item, "foo"), "bar") self.assertTrue(_get_value(item, "foo") is foo_callable) def test_dictionary__key_missing(self): """ Test getting a missing key from a dictionary. """ item = {} self.assertNotFound(item, "missing") def test_dictionary__attributes_not_checked(self): """ Test that dictionary attributes are not checked. """ item = {1: 2, 3: 4} # I was not able to find a "public" attribute of dict that is # the same across Python 2/3. attr_name = "__len__" self.assertEqual(getattr(item, attr_name)(), 2) self.assertNotFound(item, attr_name) def test_dictionary__dict_subclass(self): """ Test that subclasses of dict are treated as dictionaries. """ class DictSubclass(dict): pass item = DictSubclass() item["foo"] = "bar" self.assertEqual(_get_value(item, "foo"), "bar") ### Case: the item is an object. def test_object__attribute_present(self): """ Test getting an attribute from an object. """ item = SimpleObject() self.assertEqual(_get_value(item, "foo"), "bar") def test_object__attribute_missing(self): """ Test getting a missing attribute from an object. """ item = SimpleObject() self.assertNotFound(item, "missing") def test_object__attribute_is_callable(self): """ Test getting a callable attribute from an object. """ item = SimpleObject() self.assertEqual(_get_value(item, "foo_callable"), "called...") def test_object__non_built_in_type(self): """ Test getting an attribute from an instance of a type that isn't built-in. """ item = datetime(2012, 1, 2) self.assertEqual(_get_value(item, "day"), 2) def test_object__dict_like(self): """ Test getting a key from a dict-like object (an object that implements '__getitem__'). """ item = DictLike() self.assertEqual(item["foo"], "bar") self.assertNotFound(item, "foo") def test_object__property__raising_exception(self): """ Test getting a property that raises an exception. """ class Foo(object): @property def bar(self): return 1 @property def baz(self): raise ValueError("test") foo = Foo() self.assertEqual(_get_value(foo, 'bar'), 1) self.assertNotFound(foo, 'missing') self.assertRaises(ValueError, _get_value, foo, 'baz') ### Case: the item is an instance of a built-in type. def test_built_in_type__integer(self): """ Test getting from an integer. """ class MyInt(int): pass cust_int = MyInt(10) pure_int = 10 # We have to use a built-in method like __neg__ because "public" # attributes like "real" were not added to Python until Python 2.6, # when the numeric type hierarchy was added: # # http://docs.python.org/library/numbers.html # self.assertEqual(cust_int.__neg__(), -10) self.assertEqual(pure_int.__neg__(), -10) self.assertEqual(_get_value(cust_int, '__neg__'), -10) self.assertNotFound(pure_int, '__neg__') def test_built_in_type__string(self): """ Test getting from a string. """ class MyStr(str): pass item1 = MyStr('abc') item2 = 'abc' self.assertEqual(item1.upper(), 'ABC') self.assertEqual(item2.upper(), 'ABC') self.assertEqual(_get_value(item1, 'upper'), 'ABC') self.assertNotFound(item2, 'upper') def test_built_in_type__list(self): """ Test getting from a list. """ class MyList(list): pass item1 = MyList([1, 2, 3]) item2 = [1, 2, 3] self.assertEqual(item1.pop(), 3) self.assertEqual(item2.pop(), 3) self.assertEqual(_get_value(item1, 'pop'), 2) self.assertNotFound(item2, 'pop') class ContextStackTestCase(unittest.TestCase, AssertIsMixin, AssertStringMixin, AssertExceptionMixin): """ Test the ContextStack class. """ def test_init__no_elements(self): """ Check that passing nothing to __init__() raises no exception. """ context = ContextStack() def test_init__many_elements(self): """ Check that passing more than two items to __init__() raises no exception. """ context = ContextStack({}, {}, {}) def test__repr(self): context = ContextStack() self.assertEqual(repr(context), 'ContextStack()') context = ContextStack({'foo': 'bar'}) self.assertEqual(repr(context), "ContextStack({'foo': 'bar'},)") context = ContextStack({'foo': 'bar'}, {'abc': 123}) self.assertEqual(repr(context), "ContextStack({'foo': 'bar'}, {'abc': 123})") def test__str(self): context = ContextStack() self.assertEqual(str(context), 'ContextStack()') context = ContextStack({'foo': 'bar'}) self.assertEqual(str(context), "ContextStack({'foo': 'bar'},)") context = ContextStack({'foo': 'bar'}, {'abc': 123}) self.assertEqual(str(context), "ContextStack({'foo': 'bar'}, {'abc': 123})") ## Test the static create() method. def test_create__dictionary(self): """ Test passing a dictionary. """ context = ContextStack.create({'foo': 'bar'}) self.assertEqual(context.get('foo'), 'bar') def test_create__none(self): """ Test passing None. """ context = ContextStack.create({'foo': 'bar'}, None) self.assertEqual(context.get('foo'), 'bar') def test_create__object(self): """ Test passing an object. """ class Foo(object): foo = 'bar' context = ContextStack.create(Foo()) self.assertEqual(context.get('foo'), 'bar') def test_create__context(self): """ Test passing a ContextStack instance. """ obj = ContextStack({'foo': 'bar'}) context = ContextStack.create(obj) self.assertEqual(context.get('foo'), 'bar') def test_create__kwarg(self): """ Test passing a keyword argument. """ context = ContextStack.create(foo='bar') self.assertEqual(context.get('foo'), 'bar') def test_create__precedence_positional(self): """ Test precedence of positional arguments. """ context = ContextStack.create({'foo': 'bar'}, {'foo': 'buzz'}) self.assertEqual(context.get('foo'), 'buzz') def test_create__precedence_keyword(self): """ Test precedence of keyword arguments. """ context = ContextStack.create({'foo': 'bar'}, foo='buzz') self.assertEqual(context.get('foo'), 'buzz') ## Test the get() method. def test_get__single_dot(self): """ Test getting a single dot ("."). """ context = ContextStack("a", "b") self.assertEqual(context.get("."), "b") def test_get__single_dot__missing(self): """ Test getting a single dot (".") with an empty context stack. """ context = ContextStack() self.assertException(KeyNotFoundError, "Key '.' not found: empty context stack", context.get, ".") def test_get__key_present(self): """ Test getting a key. """ context = ContextStack({"foo": "bar"}) self.assertEqual(context.get("foo"), "bar") def test_get__key_missing(self): """ Test getting a missing key. """ context = ContextStack() self.assertException(KeyNotFoundError, "Key 'foo' not found: first part", context.get, "foo") def test_get__precedence(self): """ Test that get() respects the order of precedence (later items first). """ context = ContextStack({"foo": "bar"}, {"foo": "buzz"}) self.assertEqual(context.get("foo"), "buzz") def test_get__fallback(self): """ Check that first-added stack items are queried on context misses. """ context = ContextStack({"fuzz": "buzz"}, {"foo": "bar"}) self.assertEqual(context.get("fuzz"), "buzz") def test_push(self): """ Test push(). """ key = "foo" context = ContextStack({key: "bar"}) self.assertEqual(context.get(key), "bar") context.push({key: "buzz"}) self.assertEqual(context.get(key), "buzz") def test_pop(self): """ Test pop(). """ key = "foo" context = ContextStack({key: "bar"}, {key: "buzz"}) self.assertEqual(context.get(key), "buzz") item = context.pop() self.assertEqual(item, {"foo": "buzz"}) self.assertEqual(context.get(key), "bar") def test_top(self): key = "foo" context = ContextStack({key: "bar"}, {key: "buzz"}) self.assertEqual(context.get(key), "buzz") top = context.top() self.assertEqual(top, {"foo": "buzz"}) # Make sure calling top() didn't remove the item from the stack. self.assertEqual(context.get(key), "buzz") def test_copy(self): key = "foo" original = ContextStack({key: "bar"}, {key: "buzz"}) self.assertEqual(original.get(key), "buzz") new = original.copy() # Confirm that the copy behaves the same. self.assertEqual(new.get(key), "buzz") # Change the copy, and confirm it is changed. new.pop() self.assertEqual(new.get(key), "bar") # Confirm the original is unchanged. self.assertEqual(original.get(key), "buzz") def test_dot_notation__dict(self): name = "foo.bar" stack = ContextStack({"foo": {"bar": "baz"}}) self.assertEqual(stack.get(name), "baz") # Works all the way down name = "a.b.c.d.e.f.g" stack = ContextStack({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}}) self.assertEqual(stack.get(name), "w00t!") def test_dot_notation__user_object(self): name = "foo.bar" stack = ContextStack({"foo": Attachable(bar="baz")}) self.assertEqual(stack.get(name), "baz") # Works on multiple levels, too name = "a.b.c.d.e.f.g" A = Attachable stack = ContextStack({"a": A(b=A(c=A(d=A(e=A(f=A(g="w00t!"))))))}) self.assertEqual(stack.get(name), "w00t!") def test_dot_notation__mixed_dict_and_obj(self): name = "foo.bar.baz.bak" stack = ContextStack({"foo": Attachable(bar={"baz": Attachable(bak=42)})}) self.assertEqual(stack.get(name), 42) def test_dot_notation__missing_attr_or_key(self): name = "foo.bar.baz.bak" stack = ContextStack({"foo": {"bar": {}}}) self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name) stack = ContextStack({"foo": Attachable(bar=Attachable())}) self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name) def test_dot_notation__missing_part_terminates_search(self): """ Test that dotted name resolution terminates on a later part not found. Check that if a later dotted name part is not found in the result from the former resolution, then name resolution terminates rather than starting the search over with the next element of the context stack. From the spec (interpolation section)-- 5) If any name parts were retained in step 1, each should be resolved against a context stack containing only the result from the former resolution. If any part fails resolution, the result should be considered falsey, and should interpolate as the empty string. This test case is equivalent to the test case in the following pull request: https://github.com/mustache/spec/pull/48 """ stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'}) self.assertEqual(stack.get('a'), 'A') self.assertException(KeyNotFoundError, "Key 'a.b' not found: missing 'b'", stack.get, "a.b") stack.pop() self.assertEqual(stack.get('a.b'), 'A.B') def test_dot_notation__autocall(self): name = "foo.bar.baz" # When any element in the path is callable, it should be automatically invoked stack = ContextStack({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))}) self.assertEqual(stack.get(name), "Called!") class Foo(object): def bar(self): return Attachable(baz='Baz') stack = ContextStack({"foo": Foo()}) self.assertEqual(stack.get(name), "Baz") pystache-0.6.0/pystache/tests/test_defaults.py000066400000000000000000000041461413701016200215370ustar00rootroot00000000000000# coding: utf-8 """ Unit tests for defaults.py. """ import unittest import pystache from pystache.tests.common import AssertStringMixin # TODO: make sure each default has at least one test. class DefaultsConfigurableTestCase(unittest.TestCase, AssertStringMixin): """Tests that the user can change the defaults at runtime.""" # TODO: switch to using a context manager after 2.4 is deprecated. def setUp(self): """Save the defaults.""" defaults = [ 'DECODE_ERRORS', 'DELIMITERS', 'FILE_ENCODING', 'MISSING_TAGS', 'SEARCH_DIRS', 'STRING_ENCODING', 'TAG_ESCAPE', 'TEMPLATE_EXTENSION' ] self.saved = {} for e in defaults: self.saved[e] = getattr(pystache.defaults, e) def tearDown(self): for key, value in list(self.saved.items()): setattr(pystache.defaults, key, value) def test_tag_escape(self): """Test that changes to defaults.TAG_ESCAPE take effect.""" template = "{{foo}}" context = {'foo': '<'} actual = pystache.render(template, context) self.assertString(actual, "<") pystache.defaults.TAG_ESCAPE = lambda u: u actual = pystache.render(template, context) self.assertString(actual, "<") def test_delimiters(self): """Test that changes to defaults.DELIMITERS take effect.""" template = "[[foo]]{{foo}}" context = {'foo': 'FOO'} actual = pystache.render(template, context) self.assertString(actual, "[[foo]]FOO") pystache.defaults.DELIMITERS = ('[[', ']]') actual = pystache.render(template, context) self.assertString(actual, "FOO{{foo}}") def test_missing_tags(self): """Test that changes to defaults.MISSING_TAGS take effect.""" template = "{{foo}}" context = {} actual = pystache.render(template, context) self.assertString(actual, "") pystache.defaults.MISSING_TAGS = 'strict' self.assertRaises(pystache.context.KeyNotFoundError, pystache.render, template, context) pystache-0.6.0/pystache/tests/test_examples.py000066400000000000000000000063201413701016200215420ustar00rootroot00000000000000# encoding: utf-8 """ TODO: add a docstring. """ import unittest from .examples.comments import Comments from .examples.double_section import DoubleSection from .examples.escaped import Escaped from .examples.unescaped import Unescaped from .examples.template_partial import TemplatePartial from .examples.delimiters import Delimiters from .examples.unicode_output import UnicodeOutput from .examples.unicode_input import UnicodeInput from .examples.nested_context import NestedContext from pystache import Renderer from pystache.tests.common import EXAMPLES_DIR from pystache.tests.common import AssertStringMixin class TestView(unittest.TestCase, AssertStringMixin): def _assert(self, obj, expected): renderer = Renderer() actual = renderer.render(obj) self.assertString(actual, expected) def test_comments(self): self._assert(Comments(), "

A Comedy of Errors

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

Name: Henri Poincaré

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

Bear > Shark

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

Bear > Shark

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

Welcome

Again, Welcome!""") def test_template_partial_extension(self): renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') view = TemplatePartial(renderer=renderer) actual = renderer.render(view) self.assertString(actual, """Welcome ------- ## Again, Welcome! ##""") def test_delimiters(self): renderer = Renderer() actual = renderer.render(Delimiters()) self.assertString(actual, """\ * It worked the first time. * And it worked the second time. * Then, surprisingly, it worked the third time. """) def test_nested_context(self): renderer = Renderer() actual = renderer.render(NestedContext(renderer)) self.assertString(actual, "one and foo and two") def test_nested_context_is_available_in_view(self): renderer = Renderer() view = NestedContext(renderer) view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}' actual = renderer.render(view) self.assertString(actual, 'it works!') def test_partial_in_partial_has_access_to_grand_parent_context(self): renderer = Renderer(search_dirs=EXAMPLES_DIR) view = TemplatePartial(renderer=renderer) view.template = '''{{>partial_in_partial}}''' actual = renderer.render(view, {'prop': 'derp'}) self.assertEqual(actual, 'Hi derp!') if __name__ == '__main__': unittest.main() pystache-0.6.0/pystache/tests/test_loader.py000066400000000000000000000131751413701016200212000ustar00rootroot00000000000000# encoding: utf-8 """ Unit tests of loader.py. """ import os import sys import unittest from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults from pystache import defaults from pystache.loader import Loader # We use the same directory as the locator tests for now. LOADER_DATA_DIR = os.path.join(DATA_DIR, 'locator') class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults): def setUp(self): self.setup_defaults() def tearDown(self): self.teardown_defaults() def test_init__extension(self): loader = Loader(extension='foo') self.assertEqual(loader.extension, 'foo') def test_init__extension__default(self): # Test the default value. loader = Loader() self.assertEqual(loader.extension, 'mustache') def test_init__file_encoding(self): loader = Loader(file_encoding='bar') self.assertEqual(loader.file_encoding, 'bar') def test_init__file_encoding__default(self): file_encoding = defaults.FILE_ENCODING try: defaults.FILE_ENCODING = 'foo' loader = Loader() self.assertEqual(loader.file_encoding, 'foo') finally: defaults.FILE_ENCODING = file_encoding def test_init__to_unicode(self): to_unicode = lambda x: x loader = Loader(to_unicode=to_unicode) self.assertEqual(loader.to_unicode, to_unicode) def test_init__to_unicode__default(self): loader = Loader() self.assertRaises(TypeError, loader.to_unicode, "abc") decode_errors = defaults.DECODE_ERRORS string_encoding = defaults.STRING_ENCODING nonascii = 'abcdé'.encode('utf-8') loader = Loader() self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii) defaults.DECODE_ERRORS = 'ignore' loader = Loader() self.assertString(loader.to_unicode(nonascii), 'abcd') defaults.STRING_ENCODING = 'utf-8' loader = Loader() self.assertString(loader.to_unicode(nonascii), 'abcdé') def _get_path(self, filename): return os.path.join(DATA_DIR, filename) def test_unicode__basic__input_str(self): """ Test unicode(): default arguments with str input. """ loader = Loader() actual = loader.str("foo") self.assertString(actual, "foo") def test_unicode__basic__input_unicode(self): """ Test unicode(): default arguments with unicode input. """ loader = Loader() actual = loader.str("foo") self.assertString(actual, "foo") def test_unicode__basic__input_unicode_subclass(self): """ Test unicode(): default arguments with unicode-subclass input. """ class UnicodeSubclass(str): pass s = UnicodeSubclass("foo") loader = Loader() actual = loader.str(s) self.assertString(actual, "foo") def test_unicode__to_unicode__attribute(self): """ Test unicode(): encoding attribute. """ loader = Loader() non_ascii = 'abcdé'.encode('utf-8') self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) def to_unicode(s, encoding=None): if encoding is None: encoding = 'utf-8' return str(s, encoding) loader.to_unicode = to_unicode self.assertString(loader.str(non_ascii), "abcdé") def test_unicode__encoding_argument(self): """ Test unicode(): encoding argument. """ loader = Loader() non_ascii = 'abcdé'.encode('utf-8') self.assertRaises(UnicodeDecodeError, loader.str, non_ascii) actual = loader.str(non_ascii, encoding='utf-8') self.assertString(actual, 'abcdé') # TODO: check the read() unit tests. def test_read(self): """ Test read(). """ loader = Loader() path = self._get_path('ascii.mustache') actual = loader.read(path) self.assertString(actual, 'ascii: abc') def test_read__file_encoding__attribute(self): """ Test read(): file_encoding attribute respected. """ loader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, loader.read, path) loader.file_encoding = 'utf-8' actual = loader.read(path) self.assertString(actual, 'non-ascii: é') def test_read__encoding__argument(self): """ Test read(): encoding argument respected. """ loader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, loader.read, path) actual = loader.read(path, encoding='utf-8') self.assertString(actual, 'non-ascii: é') def test_read__to_unicode__attribute(self): """ Test read(): to_unicode attribute respected. """ loader = Loader() path = self._get_path('non_ascii.mustache') self.assertRaises(UnicodeDecodeError, loader.read, path) #loader.decode_errors = 'ignore' #actual = loader.read(path) #self.assertString(actual, u'non-ascii: ') def test_load_file(self): loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR]) template = loader.load_file('template.txt') self.assertEqual(template, 'Test template file\n') def test_load_name(self): loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR], extension='txt') template = loader.load_name('template') self.assertEqual(template, 'Test template file\n') pystache-0.6.0/pystache/tests/test_locator.py000066400000000000000000000126551413701016200213770ustar00rootroot00000000000000# encoding: utf-8 """ Unit tests for locator.py. """ from datetime import datetime import os import sys import unittest # TODO: remove this alias. from pystache.common import TemplateNotFoundError from pystache.loader import Loader as Reader from pystache.locator import Locator from pystache.tests.common import DATA_DIR, EXAMPLES_DIR, AssertExceptionMixin from pystache.tests.data.views import SayHello LOCATOR_DATA_DIR = os.path.join(DATA_DIR, 'locator') class LocatorTests(unittest.TestCase, AssertExceptionMixin): def _locator(self): return Locator(search_dirs=DATA_DIR) def test_init__extension(self): # Test the default value. locator = Locator() self.assertEqual(locator.template_extension, 'mustache') locator = Locator(extension='txt') self.assertEqual(locator.template_extension, 'txt') locator = Locator(extension=False) self.assertTrue(locator.template_extension is False) def _assert_paths(self, actual, expected): """ Assert that two paths are the same. """ self.assertEqual(actual, expected) def test_get_object_directory(self): locator = Locator() obj = SayHello() actual = locator.get_object_directory(obj) self._assert_paths(actual, DATA_DIR) def test_get_object_directory__not_hasattr_module(self): locator = Locator() # Previously, we used a genuine object -- a datetime instance -- # because datetime instances did not have the __module__ attribute # in CPython. See, for example-- # # http://bugs.python.org/issue15223 # # However, since datetime instances do have the __module__ attribute # in PyPy, we needed to switch to something else once we added # support for PyPi. This was so that our test runs would pass # in all systems. obj = "abc" self.assertFalse(hasattr(obj, '__module__')) self.assertEqual(locator.get_object_directory(obj), None) self.assertFalse(hasattr(None, '__module__')) self.assertEqual(locator.get_object_directory(None), None) def test_make_file_name(self): locator = Locator() locator.template_extension = 'bar' self.assertEqual(locator.make_file_name('foo'), 'foo.bar') locator.template_extension = False self.assertEqual(locator.make_file_name('foo'), 'foo') locator.template_extension = '' self.assertEqual(locator.make_file_name('foo'), 'foo.') def test_make_file_name__template_extension_argument(self): locator = Locator() self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar') def test_find_file(self): locator = Locator() path = locator.find_file('template.txt', [LOCATOR_DATA_DIR]) expected_path = os.path.join(LOCATOR_DATA_DIR, 'template.txt') self.assertEqual(path, expected_path) def test_find_name(self): locator = Locator() path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple') self.assertEqual(os.path.basename(path), 'simple.mustache') def test_find_name__using_list_of_paths(self): locator = Locator() path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple') self.assertTrue(path) def test_find_name__precedence(self): """ Test the order in which find_name() searches directories. """ locator = Locator() dir1 = DATA_DIR dir2 = LOCATOR_DATA_DIR self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate')) self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate')) path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate') dirpath = os.path.dirname(path) dirname = os.path.split(dirpath)[-1] self.assertEqual(dirname, 'locator') def test_find_name__non_existent_template_fails(self): locator = Locator() self.assertException(TemplateNotFoundError, "File 'doesnt_exist.mustache' not found in dirs: []", locator.find_name, search_dirs=[], template_name='doesnt_exist') def test_find_object(self): locator = Locator() obj = SayHello() actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache') expected = os.path.join(DATA_DIR, 'sample_view.mustache') self._assert_paths(actual, expected) def test_find_object__none_file_name(self): locator = Locator() obj = SayHello() actual = locator.find_object(search_dirs=[], obj=obj) expected = os.path.join(DATA_DIR, 'say_hello.mustache') self.assertEqual(actual, expected) def test_find_object__none_object_directory(self): locator = Locator() obj = None self.assertEqual(None, locator.get_object_directory(obj)) actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache') expected = os.path.join(DATA_DIR, 'say_hello.mustache') self.assertEqual(actual, expected) def test_make_template_name(self): """ Test make_template_name(). """ locator = Locator() class FooBar(object): pass foo = FooBar() self.assertEqual(locator.make_template_name(foo), 'foo_bar') pystache-0.6.0/pystache/tests/test_parser.py000066400000000000000000000010011413701016200212070ustar00rootroot00000000000000# coding: utf-8 """ Unit tests of parser.py. """ import unittest from pystache.defaults import DELIMITERS from pystache.parser import _compile_template_re as make_re class RegularExpressionTestCase(unittest.TestCase): """Tests the regular expression returned by _compile_template_re().""" def test_re(self): """ Test getting a key from a dictionary. """ re = make_re(DELIMITERS) match = re.search("b {{test}}") self.assertEqual(match.start(), 1) pystache-0.6.0/pystache/tests/test_pystache.py000066400000000000000000000112511413701016200215430ustar00rootroot00000000000000# encoding: utf-8 import unittest import pystache from pystache import defaults from pystache import renderer from pystache.tests.common import html_escape class PystacheTests(unittest.TestCase): def setUp(self): self.original_escape = defaults.TAG_ESCAPE defaults.TAG_ESCAPE = html_escape def tearDown(self): defaults.TAG_ESCAPE = self.original_escape def _assert_rendered(self, expected, template, context): actual = pystache.render(template, context) self.assertEqual(actual, expected) def test_basic(self): ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' }) self.assertEqual(ret, "Hi world!") def test_kwargs(self): ret = pystache.render("Hi {{thing}}!", thing='world') self.assertEqual(ret, "Hi world!") def test_less_basic(self): template = "It's a nice day for {{beverage}}, right {{person}}?" context = { 'beverage': 'soda', 'person': 'Bob' } self._assert_rendered("It's a nice day for soda, right Bob?", template, context) def test_even_less_basic(self): template = "I think {{name}} wants a {{thing}}, right {{name}}?" context = { 'name': 'Jon', 'thing': 'racecar' } self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context) def test_ignores_misses(self): template = "I think {{name}} wants a {{thing}}, right {{name}}?" context = { 'name': 'Jon' } self._assert_rendered("I think Jon wants a , right Jon?", template, context) def test_render_zero(self): template = 'My value is {{value}}.' context = { 'value': 0 } self._assert_rendered('My value is 0.', template, context) def test_comments(self): template = "What {{! the }} what?" actual = pystache.render(template) self.assertEqual("What what?", actual) def test_false_sections_are_hidden(self): template = "Ready {{#set}}set {{/set}}go!" context = { 'set': False } self._assert_rendered("Ready go!", template, context) def test_true_sections_are_shown(self): template = "Ready {{#set}}set{{/set}} go!" context = { 'set': True } self._assert_rendered("Ready set go!", template, context) non_strings_expected = """(123 & ['something'])(chris & 0.9)""" def test_non_strings(self): template = "{{#stats}}({{key}} & {{value}}){{/stats}}" stats = [] stats.append({'key': 123, 'value': ['something']}) stats.append({'key': "chris", 'value': 0.900}) context = { 'stats': stats } self._assert_rendered(self.non_strings_expected, template, context) def test_unicode(self): template = 'Name: {{name}}; Age: {{age}}' context = {'name': 'Henri Poincaré', 'age': 156} self._assert_rendered('Name: Henri Poincaré; Age: 156', template, context) def test_sections(self): template = """
    {{#users}}
  • {{name}}
  • {{/users}}
""" context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] } expected = """
  • Chris
  • Tom
  • PJ
""" self._assert_rendered(expected, template, context) def test_implicit_iterator(self): template = """
    {{#users}}
  • {{.}}
  • {{/users}}
""" context = { 'users': [ 'Chris', 'Tom','PJ' ] } expected = """
  • Chris
  • Tom
  • PJ
""" self._assert_rendered(expected, template, context) # The spec says that sections should not alter surrounding whitespace. def test_surrounding_whitepace_not_altered(self): template = "first{{#spacing}} second {{/spacing}}third" context = {"spacing": True} self._assert_rendered("first second third", template, context) def test__section__non_false_value(self): """ Test when a section value is a (non-list) "non-false value". From mustache(5): When the value [of a section key] is non-false but not a list, it will be used as the context for a single rendering of the block. """ template = """{{#person}}Hi {{name}}{{/person}}""" context = {"person": {"name": "Jon"}} self._assert_rendered("Hi Jon", template, context) def test_later_list_section_with_escapable_character(self): """ This is a simple test case intended to cover issue #53. The test case failed with markupsafe enabled, as follows: AssertionError: Markup(u'foo <') != 'foo <' """ template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}""" context = {'s1': True, 's2': [True]} self._assert_rendered("foo <", template, context) pystache-0.6.0/pystache/tests/test_renderengine.py000066400000000000000000000636421413701016200224030ustar00rootroot00000000000000# coding: utf-8 """ Unit tests of renderengine.py. """ import sys import unittest from pystache.context import ContextStack, KeyNotFoundError from pystache import defaults from pystache.parser import ParsingError from pystache.renderer import Renderer from pystache.renderengine import context_get, RenderEngine from pystache.tests.common import AssertStringMixin, AssertExceptionMixin, Attachable def _get_unicode_char(): if sys.version_info < (3, ): return 'u' return '' _UNICODE_CHAR = _get_unicode_char() def mock_literal(s): """ For use as the literal keyword argument to the RenderEngine constructor. Arguments: s: a byte string or unicode string. """ if isinstance(s, str): # Strip off unicode super classes, if present. u = str(s) else: u = str(s, encoding='ascii') # We apply upper() to make sure we are actually using our custom # function in the tests return u.upper() class RenderEngineTestCase(unittest.TestCase): """Test the RenderEngine class.""" def test_init(self): """ Test that __init__() stores all of the arguments correctly. """ # In real-life, these arguments would be functions engine = RenderEngine(resolve_partial="foo", literal="literal", escape="escape", to_str="str") self.assertEqual(engine.escape, "escape") self.assertEqual(engine.literal, "literal") self.assertEqual(engine.resolve_partial, "foo") self.assertEqual(engine.to_str, "str") class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): """ Tests RenderEngine.render(). Explicit spec-test-like tests best go in this class since the RenderEngine class contains all parsing logic. This way, the unit tests will be more focused and fail "closer to the code". """ def _engine(self): """ Create and return a default RenderEngine for testing. """ renderer = Renderer(string_encoding='utf-8', missing_tags='strict') engine = renderer._make_render_engine() return engine def _assert_render(self, expected, template, *context, **kwargs): """ Test rendering the given template using the given context. """ partials = kwargs.get('partials') engine = kwargs.get('engine', self._engine()) if partials is not None: engine.resolve_partial = lambda key: str(partials[key]) context = ContextStack(*context) # RenderEngine.render() only accepts unicode template strings. actual = engine.render(str(template), context) self.assertString(actual=actual, expected=expected) def test_render(self): self._assert_render('Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) def test__resolve_partial(self): """ Test that render() uses the load_template attribute. """ engine = self._engine() partials = {'partial': "{{person}}"} engine.resolve_partial = lambda key: partials[key] self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) def test__literal(self): """ Test that render() uses the literal attribute. """ engine = self._engine() engine.literal = lambda s: s.upper() self._assert_render('BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine) def test_literal__sigil(self): template = "

{{& thing}}

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

Bear > Giraffe

" self._assert_render(expected, template, context) def test__escape(self): """ Test that render() uses the escape attribute. """ engine = self._engine() engine.escape = lambda s: "**" + s self._assert_render('**bar', '{{foo}}', {'foo': 'bar'}, engine=engine) def test__escape_does_not_call_literal(self): """ Test that render() does not call literal before or after calling escape. """ engine = self._engine() engine.literal = lambda s: s.upper() # a test version engine.escape = lambda s: "**" + s template = 'literal: {{{foo}}} escaped: {{foo}}' context = {'foo': 'bar'} self._assert_render('literal: BAR escaped: **bar', template, context, engine=engine) def test__escape_preserves_unicode_subclasses(self): """ Test that render() preserves unicode subclasses when passing to escape. This is useful, for example, if one wants to respect whether a variable value is markupsafe.Markup when escaping. """ class MyUnicode(str): pass def escape(s): if type(s) is MyUnicode: return "**" + s else: return s + "**" engine = self._engine() engine.escape = escape template = '{{foo1}} {{foo2}}' context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'} self._assert_render('**bar bar**', template, context, engine=engine) # Custom to_str for testing purposes. def _to_str(self, val): if not val: return '' else: return str(val) def test_to_str(self): """Test the to_str attribute.""" engine = self._engine() template = '{{value}}' context = {'value': None} self._assert_render('None', template, context, engine=engine) engine.to_str = self._to_str self._assert_render('', template, context, engine=engine) def test_to_str__lambda(self): """Test the to_str attribute for a lambda.""" engine = self._engine() template = '{{value}}' context = {'value': lambda: None} self._assert_render('None', template, context, engine=engine) engine.to_str = self._to_str self._assert_render('', template, context, engine=engine) def test_to_str__section_list(self): """Test the to_str attribute for a section list.""" engine = self._engine() template = '{{#list}}{{.}}{{/list}}' context = {'list': [None, None]} self._assert_render('NoneNone', template, context, engine=engine) engine.to_str = self._to_str self._assert_render('', template, context, engine=engine) def test_to_str__section_lambda(self): # TODO: add a test for a "method with an arity of 1". pass def test__non_basestring__literal_and_escaped(self): """ Test a context value that is not a basestring instance. """ engine = self._engine() engine.escape = mock_literal engine.literal = mock_literal self.assertRaises(TypeError, engine.literal, 100) template = '{{text}} {{int}} {{{int}}}' context = {'int': 100, 'text': 'foo'} self._assert_render('FOO 100 100', template, context, engine=engine) def test_tag__output_not_interpolated(self): """ Context values should not be treated as templates (issue #44). """ template = '{{template}}: {{planet}}' context = {'template': '{{planet}}', 'planet': 'Earth'} self._assert_render('{{planet}}: Earth', template, context) def test_tag__output_not_interpolated__section(self): """ Context values should not be treated as templates (issue #44). """ template = '{{test}}' context = {'test': '{{#hello}}'} self._assert_render('{{#hello}}', template, context) ## Test interpolation with "falsey" values # # In these test cases, we test the part of the spec that says that # "data should be coerced into a string (and escaped, if appropriate) # before interpolation." We test this for data that is "falsey." def test_interpolation__falsey__zero(self): template = '{{.}}' context = 0 self._assert_render('0', template, context) def test_interpolation__falsey__none(self): template = '{{.}}' context = None self._assert_render('None', template, context) def test_interpolation__falsey__zero(self): template = '{{.}}' context = False self._assert_render('False', template, context) # Built-in types: # # Confirm that we not treat instances of built-in types as objects, # for example by calling a method on a built-in type instance when it # has a method whose name matches the current key. # # Each test case puts an instance of a built-in type on top of the # context stack before interpolating a tag whose key matches an # attribute (method or property) of the instance. # def _assert_builtin_attr(self, item, attr_name, expected_attr): self.assertTrue(hasattr(item, attr_name)) actual = getattr(item, attr_name) if callable(actual): actual = actual() self.assertEqual(actual, expected_attr) def _assert_builtin_type(self, item, attr_name, expected_attr, expected_template): self._assert_builtin_attr(item, attr_name, expected_attr) template = '{{#section}}{{%s}}{{/section}}' % attr_name context = {'section': item, attr_name: expected_template} self._assert_render(expected_template, template, context) def test_interpolation__built_in_type__string(self): """ Check tag interpolation with a built-in type: string. """ self._assert_builtin_type('abc', 'upper', 'ABC', 'xyz') def test_interpolation__built_in_type__integer(self): """ Check tag interpolation with a built-in type: integer. """ # Since public attributes weren't added to integers until Python 2.6 # (for example the "real" attribute of the numeric type hierarchy)-- # # http://docs.python.org/library/numbers.html # # we need to resort to built-in attributes (double-underscored) on # the integer type. self._assert_builtin_type(15, '__neg__', -15, '999') def test_interpolation__built_in_type__list(self): """ Check tag interpolation with a built-in type: list. """ item = [[1, 2, 3]] attr_name = 'pop' # Make a copy to prevent changes to item[0]. self._assert_builtin_attr(list(item[0]), attr_name, 3) template = '{{#section}}{{%s}}{{/section}}' % attr_name context = {'section': item, attr_name: 7} self._assert_render('7', template, context) # This test is also important for testing 2to3. def test_interpolation__nonascii_nonunicode(self): """ Test a tag whose value is a non-ascii, non-unicode string. """ template = '{{nonascii}}' context = {'nonascii': 'abcdé'.encode('utf-8')} self._assert_render('abcdé', template, context) def test_implicit_iterator__literal(self): """ Test an implicit iterator in a literal tag. """ template = """{{#test}}{{{.}}}{{/test}}""" context = {'test': ['<', '>']} self._assert_render('<>', template, context) def test_implicit_iterator__escaped(self): """ Test an implicit iterator in a normal tag. """ template = """{{#test}}{{.}}{{/test}}""" context = {'test': ['<', '>']} self._assert_render('<>', template, context) def test_literal__in_section(self): """ Check that literals work in sections. """ template = '{{#test}}1 {{{less_than}}} 2{{/test}}' context = {'test': {'less_than': '<'}} self._assert_render('1 < 2', template, context) def test_literal__in_partial(self): """ Check that literals work in partials. """ template = '{{>partial}}' partials = {'partial': '1 {{{less_than}}} 2'} context = {'less_than': '<'} self._assert_render('1 < 2', template, context, partials=partials) def test_partial(self): partials = {'partial': "{{person}}"} self._assert_render('Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials) def test_partial__context_values(self): """ Test that escape and literal work on context values in partials. """ engine = self._engine() template = '{{>partial}}' partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'} context = {'foo': '<'} self._assert_render( 'unescaped: < escaped: <', template, context, engine=engine, partials=partials) ## Test cases related specifically to lambdas. # This test is also important for testing 2to3. def test_section__nonascii_nonunicode(self): """ Test a section whose value is a non-ascii, non-unicode string. """ template = '{{#nonascii}}{{.}}{{/nonascii}}' context = {'nonascii': 'abcdé'.encode('utf-8')} self._assert_render('abcdé', template, context) # This test is also important for testing 2to3. def test_lambda__returning_nonascii_nonunicode(self): """ Test a lambda tag value returning a non-ascii, non-unicode string. """ template = '{{lambda}}' context = {'lambda': lambda: 'abcdé'.encode('utf-8')} self._assert_render('abcdé', template, context) ## Test cases related specifically to sections. def test_section__end_tag_with_no_start_tag(self): """ Check what happens if there is an end tag with no start tag. """ template = '{{/section}}' try: self._assert_render(None, template) except ParsingError as err: self.assertEqual(str(err), "Section end tag mismatch: section != None") def test_section__end_tag_mismatch(self): """ Check what happens if the end tag doesn't match. """ template = '{{#section_start}}{{/section_end}}' try: self._assert_render(None, template) except ParsingError as err: self.assertEqual(str(err), "Section end tag mismatch: section_end != section_start") def test_section__context_values(self): """ Test that escape and literal work on context values in sections. """ engine = self._engine() template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}' context = {'test': {'foo': '<'}} self._assert_render('unescaped: < escaped: <', template, context, engine=engine) def test_section__context_precedence(self): """ Check that items higher in the context stack take precedence. """ template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}' context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}} self._assert_render('chicken : beans and rice', template, context) def test_section__list_referencing_outer_context(self): """ Check that list items can access the parent context. For sections whose value is a list, check that items in the list have access to the values inherited from the parent context when rendering. """ context = { "greeting": "Hi", "list": [{"name": "Al"}, {"name": "Bob"}], } template = "{{#list}}{{greeting}} {{name}}, {{/list}}" self._assert_render("Hi Al, Hi Bob, ", template, context) def test_section__output_not_interpolated(self): """ Check that rendered section output is not interpolated. """ template = '{{#section}}{{template}}{{/section}}: {{planet}}' context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'} self._assert_render('{{planet}}: Earth', template, context) # TODO: have this test case added to the spec. def test_section__string_values_not_lists(self): """ Check that string section values are not interpreted as lists. """ template = '{{#section}}foo{{/section}}' context = {'section': '123'} # If strings were interpreted as lists, this would give "foofoofoo". self._assert_render('foo', template, context) def test_section__nested_truthy(self): """ Check that "nested truthy" sections get rendered. Test case for issue #24: https://github.com/defunkt/pystache/issues/24 This test is copied from the spec. We explicitly include it to prevent regressions for those who don't pull down the spec tests. """ template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |' context = {'bool': True} self._assert_render('| A B C D E |', template, context) def test_section__nested_with_same_keys(self): """ Check a doubly-nested section with the same context key. Test case for issue #36: https://github.com/defunkt/pystache/issues/36 """ # Start with an easier, working case. template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}' context = {'x': {'z': {'y': 1}}} self._assert_render('1', template, context) template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}' context = {'x': {'x': {'y': 1}}} self._assert_render('1', template, context) def test_section__lambda(self): template = '{{#test}}Mom{{/test}}' context = {'test': (lambda text: 'Hi %s' % text)} self._assert_render('Hi Mom', template, context) # This test is also important for testing 2to3. def test_section__lambda__returning_nonascii_nonunicode(self): """ Test a lambda section value returning a non-ascii, non-unicode string. """ template = '{{#lambda}}{{/lambda}}' context = {'lambda': lambda text: 'abcdé'.encode('utf-8')} self._assert_render('abcdé', template, context) def test_section__lambda__returning_nonstring(self): """ Test a lambda section value returning a non-string. """ template = '{{#lambda}}foo{{/lambda}}' context = {'lambda': lambda text: len(text)} self._assert_render('3', template, context) def test_section__iterable(self): """ Check that objects supporting iteration (aside from dicts) behave like lists. """ template = '{{#iterable}}{{.}}{{/iterable}}' context = {'iterable': (i for i in range(3))} # type 'generator' self._assert_render('012', template, context) context = {'iterable': range(4)} # type 'xrange' self._assert_render('0123', template, context) d = {'foo': 0, 'bar': 0} # We don't know what order of keys we'll be given, but from the # Python documentation: # "If items(), keys(), values(), iteritems(), iterkeys(), and # itervalues() are called with no intervening modifications to # the dictionary, the lists will directly correspond." expected = ''.join(list(d.keys())) context = {'iterable': iter(d.keys())} # type 'dictionary-keyiterator' self._assert_render(expected, template, context) def test_section__lambda__tag_in_output(self): """ Check that callable output is treated as a template string (issue #46). The spec says-- When used as the data value for a Section tag, the lambda MUST be treatable as an arity 1 function, and invoked as such (passing a String containing the unprocessed section contents). The returned value MUST be rendered against the current delimiters, then interpolated in place of the section. """ template = '{{#test}}Hi {{person}}{{/test}}' context = {'person': 'Mom', 'test': (lambda text: text + " :)")} self._assert_render('Hi Mom :)', template, context) def test_section__lambda__list(self): """ Check that lists of lambdas are processed correctly for sections. This test case is equivalent to a test submitted to the Mustache spec here: https://github.com/mustache/spec/pull/47 . """ template = '<{{#lambdas}}foo{{/lambdas}}>' context = {'foo': 'bar', 'lambdas': [lambda text: "~{{%s}}~" % text, lambda text: "#{{%s}}#" % text]} self._assert_render('<~bar~#bar#>', template, context) def test_section__lambda__mixed_list(self): """ Test a mixed list of lambdas and non-lambdas as a section value. This test case is equivalent to a test submitted to the Mustache spec here: https://github.com/mustache/spec/pull/47 . """ template = '<{{#lambdas}}foo{{/lambdas}}>' context = {'foo': 'bar', 'lambdas': [lambda text: "~{{%s}}~" % text, 1]} self._assert_render('<~bar~foo>', template, context) def test_section__lambda__not_on_context_stack(self): """ Check that section lambdas are not pushed onto the context stack. Even though the sections spec says that section data values should be pushed onto the context stack prior to rendering, this does not apply to lambdas. Lambdas obey their own special case. This test case is equivalent to a test submitted to the Mustache spec here: https://github.com/mustache/spec/pull/47 . """ context = {'foo': 'bar', 'lambda': (lambda text: "{{.}}")} template = '{{#foo}}{{#lambda}}blah{{/lambda}}{{/foo}}' self._assert_render('bar', template, context) def test_section__lambda__no_reinterpolation(self): """ Check that section lambda return values are not re-interpolated. This test is a sanity check that the rendered lambda return value is not re-interpolated as could be construed by reading the section part of the Mustache spec. This test case is equivalent to a test submitted to the Mustache spec here: https://github.com/mustache/spec/pull/47 . """ template = '{{#planet}}{{#lambda}}dot{{/lambda}}{{/planet}}' context = {'planet': 'Earth', 'dot': '~{{.}}~', 'lambda': (lambda text: "#{{%s}}#" % text)} self._assert_render('#~{{.}}~#', template, context) def test_comment__multiline(self): """ Check that multiline comments are permitted. """ self._assert_render('foobar', 'foo{{! baz }}bar') self._assert_render('foobar', 'foo{{! \nbaz }}bar') def test_custom_delimiters__sections(self): """ Check that custom delimiters can be used to start a section. Test case for issue #20: https://github.com/defunkt/pystache/issues/20 """ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]' context = {'foo': True} self._assert_render('bar', template, context) def test_custom_delimiters__not_retroactive(self): """ Check that changing custom delimiters back is not "retroactive." Test case for issue #35: https://github.com/defunkt/pystache/issues/35 """ expected = ' {{foo}} ' self._assert_render(expected, '{{=$ $=}} {{foo}} ') self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. def test_dot_notation(self): """ Test simple dot notation cases. Check that we can use dot notation when the variable is a dict, user-defined object, or combination of both. """ template = 'Hello, {{person.name}}. I see you are {{person.details.age}}.' person = Attachable(name='Biggles', details={'age': 42}) context = {'person': person} self._assert_render('Hello, Biggles. I see you are 42.', template, context) def test_dot_notation__multiple_levels(self): """ Test dot notation with multiple levels. """ template = """Hello, Mr. {{person.name.lastname}}. I see you're back from {{person.travels.last.country.city}}.""" expected = """Hello, Mr. Pither. I see you're back from Cornwall.""" context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'}, 'travels': {'last': {'country': {'city': 'Cornwall'}}}, 'details': {'public': 'likes cycling'}}} self._assert_render(expected, template, context) # It should also work with user-defined objects context = {'person': Attachable(name={'firstname': 'unknown', 'lastname': 'Pither'}, travels=Attachable(last=Attachable(country=Attachable(city='Cornwall'))), details=Attachable())} self._assert_render(expected, template, context) def test_dot_notation__missing_part_terminates_search(self): """ Test that dotted name resolution terminates on a later part not found. Check that if a later dotted name part is not found in the result from the former resolution, then name resolution terminates rather than starting the search over with the next element of the context stack. From the spec (interpolation section)-- 5) If any name parts were retained in step 1, each should be resolved against a context stack containing only the result from the former resolution. If any part fails resolution, the result should be considered falsey, and should interpolate as the empty string. This test case is equivalent to the test case in the following pull request: https://github.com/mustache/spec/pull/48 """ context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} } template = '{{a.b}}' self._assert_render('A.B', template, context) template = '{{#c}}{{a}}{{/c}}' self._assert_render('A', template, context) template = '{{#c}}{{a.b}}{{/c}}' self.assertException(KeyNotFoundError, "Key %(unicode)s'a.b' not found: missing %(unicode)s'b'" % {'unicode': _UNICODE_CHAR}, self._assert_render, 'A.B :: (A :: )', template, context) pystache-0.6.0/pystache/tests/test_renderer.py000066400000000000000000000510771413701016200215430ustar00rootroot00000000000000# coding: utf-8 """ Unit tests of template.py. """ import codecs import os import sys import unittest from .examples.simple import Simple from pystache import Renderer from pystache import TemplateSpec from pystache.common import TemplateNotFoundError from pystache.context import ContextStack, KeyNotFoundError from pystache.loader import Loader from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin from pystache.tests.data.views import SayHello def _make_renderer(): """ Return a default Renderer instance for testing purposes. """ renderer = Renderer(string_encoding='ascii', file_encoding='ascii') return renderer def mock_unicode(b, encoding=None): if encoding is None: encoding = 'ascii' u = str(b, encoding=encoding) return u.upper() class RendererInitTestCase(unittest.TestCase): """ Tests the Renderer.__init__() method. """ def test_partials__default(self): """ Test the default value. """ renderer = Renderer() self.assertTrue(renderer.partials is None) def test_partials(self): """ Test that the attribute is set correctly. """ renderer = Renderer(partials={'foo': 'bar'}) self.assertEqual(renderer.partials, {'foo': 'bar'}) def test_escape__default(self): escape = Renderer().escape self.assertEqual(escape(">"), ">") self.assertEqual(escape('"'), """) # Single quotes are escaped only in Python 3.2 and later. if sys.version_info < (3, 2): expected = "'" else: expected = ''' self.assertEqual(escape("'"), expected) def test_escape(self): escape = lambda s: "**" + s renderer = Renderer(escape=escape) self.assertEqual(renderer.escape("bar"), "**bar") def test_decode_errors__default(self): """ Check the default value. """ renderer = Renderer() self.assertEqual(renderer.decode_errors, 'strict') def test_decode_errors(self): """ Check that the constructor sets the attribute correctly. """ renderer = Renderer(decode_errors="foo") self.assertEqual(renderer.decode_errors, "foo") def test_file_encoding__default(self): """ Check the file_encoding default. """ renderer = Renderer() self.assertEqual(renderer.file_encoding, renderer.string_encoding) def test_file_encoding(self): """ Check that the file_encoding attribute is set correctly. """ renderer = Renderer(file_encoding='foo') self.assertEqual(renderer.file_encoding, 'foo') def test_file_extension__default(self): """ Check the file_extension default. """ renderer = Renderer() self.assertEqual(renderer.file_extension, 'mustache') def test_file_extension(self): """ Check that the file_encoding attribute is set correctly. """ renderer = Renderer(file_extension='foo') self.assertEqual(renderer.file_extension, 'foo') def test_missing_tags(self): """ Check that the missing_tags attribute is set correctly. """ renderer = Renderer(missing_tags='foo') self.assertEqual(renderer.missing_tags, 'foo') def test_missing_tags__default(self): """ Check the missing_tags default. """ renderer = Renderer() self.assertEqual(renderer.missing_tags, 'ignore') def test_search_dirs__default(self): """ Check the search_dirs default. """ renderer = Renderer() self.assertEqual(renderer.search_dirs, [os.curdir]) def test_search_dirs__string(self): """ Check that the search_dirs attribute is set correctly when a string. """ renderer = Renderer(search_dirs='foo') self.assertEqual(renderer.search_dirs, ['foo']) def test_search_dirs__list(self): """ Check that the search_dirs attribute is set correctly when a list. """ renderer = Renderer(search_dirs=['foo']) self.assertEqual(renderer.search_dirs, ['foo']) def test_string_encoding__default(self): """ Check the default value. """ renderer = Renderer() self.assertEqual(renderer.string_encoding, sys.getdefaultencoding()) def test_string_encoding(self): """ Check that the constructor sets the attribute correctly. """ renderer = Renderer(string_encoding="foo") self.assertEqual(renderer.string_encoding, "foo") class RendererTests(unittest.TestCase, AssertStringMixin): """Test the Renderer class.""" def _renderer(self): return Renderer() ## Test Renderer.unicode(). def test_unicode__string_encoding(self): """ Test that the string_encoding attribute is respected. """ renderer = self._renderer() b = "é".encode('utf-8') renderer.string_encoding = "ascii" self.assertRaises(UnicodeDecodeError, renderer.str, b) renderer.string_encoding = "utf-8" self.assertEqual(renderer.str(b), "é") def test_unicode__decode_errors(self): """ Test that the decode_errors attribute is respected. """ renderer = self._renderer() renderer.string_encoding = "ascii" b = "déf".encode('utf-8') renderer.decode_errors = "ignore" self.assertEqual(renderer.str(b), "df") renderer.decode_errors = "replace" # U+FFFD is the official Unicode replacement character. self.assertEqual(renderer.str(b), u'd\ufffd\ufffdf') ## Test the _make_loader() method. def test__make_loader__return_type(self): """ Test that _make_loader() returns a Loader. """ renderer = self._renderer() loader = renderer._make_loader() self.assertEqual(type(loader), Loader) def test__make_loader__attributes(self): """ Test that _make_loader() sets all attributes correctly.. """ unicode_ = lambda x: x renderer = self._renderer() renderer.file_encoding = 'enc' renderer.file_extension = 'ext' renderer.str = unicode_ loader = renderer._make_loader() self.assertEqual(loader.extension, 'ext') self.assertEqual(loader.file_encoding, 'enc') self.assertEqual(loader.to_unicode, unicode_) ## Test the render() method. def test_render__return_type(self): """ Check that render() returns a string of type unicode. """ renderer = self._renderer() rendered = renderer.render('foo') self.assertEqual(type(rendered), str) def test_render__unicode(self): renderer = self._renderer() actual = renderer.render('foo') self.assertEqual(actual, 'foo') def test_render__str(self): renderer = self._renderer() actual = renderer.render('foo') self.assertEqual(actual, 'foo') def test_render__non_ascii_character(self): renderer = self._renderer() actual = renderer.render('Poincaré') self.assertEqual(actual, 'Poincaré') def test_render__context(self): """ Test render(): passing a context. """ renderer = self._renderer() self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom') def test_render__context_and_kwargs(self): """ Test render(): passing a context and **kwargs. """ renderer = self._renderer() template = 'Hi {{person1}} and {{person2}}' self.assertEqual(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') def test_render__kwargs_and_no_context(self): """ Test render(): passing **kwargs and no context. """ renderer = self._renderer() self.assertEqual(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom') def test_render__context_and_kwargs__precedence(self): """ Test render(): **kwargs takes precedence over context. """ renderer = self._renderer() self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad') def test_render__kwargs_does_not_modify_context(self): """ Test render(): passing **kwargs does not modify the passed context. """ context = {} renderer = self._renderer() renderer.render('Hi {{person}}', context=context, foo="bar") self.assertEqual(context, {}) def test_render__nonascii_template(self): """ Test passing a non-unicode template with non-ascii characters. """ renderer = _make_renderer() template = "déf".encode("utf-8") # Check that decode_errors and string_encoding are both respected. renderer.decode_errors = 'ignore' renderer.string_encoding = 'ascii' self.assertEqual(renderer.render(template), "df") renderer.string_encoding = 'utf_8' self.assertEqual(renderer.render(template), "déf") def test_make_resolve_partial(self): """ Test the _make_resolve_partial() method. """ renderer = Renderer() renderer.partials = {'foo': 'bar'} resolve_partial = renderer._make_resolve_partial() actual = resolve_partial('foo') self.assertEqual(actual, 'bar') self.assertEqual(type(actual), str, "RenderEngine requires that " "resolve_partial return unicode strings.") def test_make_resolve_partial__unicode(self): """ Test _make_resolve_partial(): that resolve_partial doesn't "double-decode" Unicode. """ renderer = Renderer() renderer.partials = {'partial': 'foo'} resolve_partial = renderer._make_resolve_partial() self.assertEqual(resolve_partial("partial"), "foo") # Now with a value that is already unicode. renderer.partials = {'partial': 'foo'} resolve_partial = renderer._make_resolve_partial() # If the next line failed, we would get the following error: # TypeError: decoding Unicode is not supported self.assertEqual(resolve_partial("partial"), "foo") def test_render_name(self): """Test the render_name() method.""" data_dir = get_data_path() renderer = Renderer(search_dirs=data_dir) actual = renderer.render_name("say_hello", to='foo') self.assertString(actual, "Hello, foo") def test_render_path(self): """ Test the render_path() method. """ renderer = Renderer() path = get_data_path('say_hello.mustache') actual = renderer.render_path(path, to='foo') self.assertEqual(actual, "Hello, foo") def test_render__object(self): """ Test rendering an object instance. """ renderer = Renderer() say_hello = SayHello() actual = renderer.render(say_hello) self.assertEqual('Hello, World', actual) actual = renderer.render(say_hello, to='Mars') self.assertEqual('Hello, Mars', actual) def test_render__template_spec(self): """ Test rendering a TemplateSpec instance. """ renderer = Renderer() class Spec(TemplateSpec): template = "hello, {{to}}" to = 'world' spec = Spec() actual = renderer.render(spec) self.assertString(actual, 'hello, world') def test_render__view(self): """ Test rendering a View instance. """ renderer = Renderer() view = Simple() actual = renderer.render(view) self.assertEqual('Hi pizza!', actual) def test_custom_string_coercion_via_assignment(self): """ Test that string coercion can be customized via attribute assignment. """ renderer = self._renderer() def to_str(val): if not val: return '' else: return str(val) self.assertEqual(renderer.render('{{value}}', value=None), 'None') renderer.str_coerce = to_str self.assertEqual(renderer.render('{{value}}', value=None), '') def test_custom_string_coercion_via_subclassing(self): """ Test that string coercion can be customized via subclassing. """ class MyRenderer(Renderer): def str_coerce(self, val): if not val: return '' else: return str(val) renderer1 = Renderer() renderer2 = MyRenderer() self.assertEqual(renderer1.render('{{value}}', value=None), 'None') self.assertEqual(renderer2.render('{{value}}', value=None), '') # By testing that Renderer.render() constructs the right RenderEngine, # we no longer need to exercise all rendering code paths through # the Renderer. It suffices to test rendering paths through the # RenderEngine for the same amount of code coverage. class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): """ Check the RenderEngine returned by Renderer._make_render_engine(). """ def _make_renderer(self): """ Return a default Renderer instance for testing purposes. """ return _make_renderer() ## Test the engine's resolve_partial attribute. def test__resolve_partial__returns_unicode(self): """ Check that resolve_partial returns unicode (and not a subclass). """ class MyUnicode(str): pass renderer = Renderer() renderer.string_encoding = 'ascii' renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')} engine = renderer._make_render_engine() actual = engine.resolve_partial('str') self.assertEqual(actual, "foo") self.assertEqual(type(actual), str) # Check that unicode subclasses are not preserved. actual = engine.resolve_partial('subclass') self.assertEqual(actual, "abc") self.assertEqual(type(actual), str) def test__resolve_partial__not_found(self): """ Check that resolve_partial returns the empty string when a template is not found. """ renderer = Renderer() engine = renderer._make_render_engine() resolve_partial = engine.resolve_partial self.assertString(resolve_partial('foo'), '') def test__resolve_partial__not_found__missing_tags_strict(self): """ Check that resolve_partial provides a nice message when a template is not found. """ renderer = Renderer() renderer.missing_tags = 'strict' engine = renderer._make_render_engine() resolve_partial = engine.resolve_partial self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']", resolve_partial, "foo") def test__resolve_partial__not_found__partials_dict(self): """ Check that resolve_partial returns the empty string when a template is not found. """ renderer = Renderer() renderer.partials = {} engine = renderer._make_render_engine() resolve_partial = engine.resolve_partial self.assertString(resolve_partial('foo'), '') def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self): """ Check that resolve_partial provides a nice message when a template is not found. """ renderer = Renderer() renderer.missing_tags = 'strict' renderer.partials = {} engine = renderer._make_render_engine() resolve_partial = engine.resolve_partial # Include dict directly since str(dict) is different in Python 2 and 3: # versus , respectively. self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict, resolve_partial, "foo") ## Test the engine's literal attribute. def test__literal__uses_renderer_unicode(self): """ Test that literal uses the renderer's unicode function. """ renderer = self._make_renderer() renderer.str = mock_unicode engine = renderer._make_render_engine() literal = engine.literal b = "foo".encode("ascii") self.assertEqual(literal(b), "FOO") def test__literal__handles_unicode(self): """ Test that literal doesn't try to "double decode" unicode. """ renderer = Renderer() renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() literal = engine.literal self.assertEqual(literal("foo"), "foo") def test__literal__returns_unicode(self): """ Test that literal returns unicode (and not a subclass). """ renderer = Renderer() renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() literal = engine.literal self.assertEqual(type(literal("foo")), str) class MyUnicode(str): pass s = MyUnicode("abc") self.assertEqual(type(s), MyUnicode) self.assertTrue(isinstance(s, str)) self.assertEqual(type(literal(s)), str) ## Test the engine's escape attribute. def test__escape__uses_renderer_escape(self): """ Test that escape uses the renderer's escape function. """ renderer = Renderer() renderer.escape = lambda s: "**" + s engine = renderer._make_render_engine() escape = engine.escape self.assertEqual(escape("foo"), "**foo") def test__escape__uses_renderer_unicode(self): """ Test that escape uses the renderer's unicode function. """ renderer = Renderer() renderer.str = mock_unicode engine = renderer._make_render_engine() escape = engine.escape b = "foo".encode('ascii') self.assertEqual(escape(b), "FOO") def test__escape__has_access_to_original_unicode_subclass(self): """ Test that escape receives strings with the unicode subclass intact. """ renderer = Renderer() renderer.escape = lambda s: str(type(s).__name__) engine = renderer._make_render_engine() escape = engine.escape class MyUnicode(str): pass self.assertEqual(escape("foo".encode('ascii')), str.__name__) self.assertEqual(escape("foo"), str.__name__) self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__) def test__escape__returns_unicode(self): """ Test that literal returns unicode (and not a subclass). """ renderer = Renderer() renderer.string_encoding = 'ascii' engine = renderer._make_render_engine() escape = engine.escape self.assertEqual(type(escape("foo")), str) # Check that literal doesn't preserve unicode subclasses. class MyUnicode(str): pass s = MyUnicode("abc") self.assertEqual(type(s), MyUnicode) self.assertTrue(isinstance(s, str)) self.assertEqual(type(escape(s)), str) ## Test the missing_tags attribute. def test__missing_tags__unknown_value(self): """ Check missing_tags attribute: setting an unknown value. """ renderer = Renderer() renderer.missing_tags = 'foo' self.assertException(Exception, "Unsupported 'missing_tags' value: 'foo'", renderer._make_render_engine) ## Test the engine's resolve_context attribute. def test__resolve_context(self): """ Check resolve_context(): default arguments. """ renderer = Renderer() engine = renderer._make_render_engine() stack = ContextStack({'foo': 'bar'}) self.assertEqual('bar', engine.resolve_context(stack, 'foo')) self.assertString('', engine.resolve_context(stack, 'missing')) def test__resolve_context__missing_tags_strict(self): """ Check resolve_context(): missing_tags 'strict'. """ renderer = Renderer() renderer.missing_tags = 'strict' engine = renderer._make_render_engine() stack = ContextStack({'foo': 'bar'}) self.assertEqual('bar', engine.resolve_context(stack, 'foo')) self.assertException(KeyNotFoundError, "Key 'missing' not found: first part", engine.resolve_context, stack, 'missing') pystache-0.6.0/pystache/tests/test_simple.py000066400000000000000000000053411413701016200212170ustar00rootroot00000000000000import unittest import pystache from pystache import Renderer from .examples.nested_context import NestedContext from .examples.complex import Complex from .examples.lambdas import Lambdas from .examples.template_partial import TemplatePartial from .examples.simple import Simple from pystache.tests.common import EXAMPLES_DIR from pystache.tests.common import AssertStringMixin class TestSimple(unittest.TestCase, AssertStringMixin): def test_nested_context(self): renderer = Renderer() view = NestedContext(renderer) view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}' actual = renderer.render(view) self.assertString(actual, "one and foo and two") def test_looping_and_negation_context(self): template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}' context = Complex() renderer = Renderer() actual = renderer.render(template, context) self.assertEqual(actual, "Colors: red Colors: green Colors: blue ") def test_empty_context(self): template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}' self.assertEqual(pystache.Renderer().render(template), "Should see me") def test_callables(self): view = Lambdas() view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}' renderer = Renderer() actual = renderer.render(view) self.assertString(actual, 'bar != bar. oh, it does!') def test_rendering_partial(self): renderer = Renderer(search_dirs=EXAMPLES_DIR) view = TemplatePartial(renderer=renderer) view.template = '{{>inner_partial}}' actual = renderer.render(view) self.assertString(actual, 'Again, Welcome!') view.template = '{{#looping}}{{>inner_partial}} {{/looping}}' actual = renderer.render(view) self.assertString(actual, "Again, Welcome! Again, Welcome! Again, Welcome! ") def test_non_existent_value_renders_blank(self): view = Simple() template = '{{not_set}} {{blank}}' self.assertEqual(pystache.Renderer().render(template), ' ') def test_template_partial_extension(self): """ Side note: From the spec-- Partial tags SHOULD be treated as standalone when appropriate. In particular, this means that trailing newlines should be removed. """ renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt') view = TemplatePartial(renderer=renderer) actual = renderer.render(view) self.assertString(actual, """Welcome ------- ## Again, Welcome! ##""") pystache-0.6.0/pystache/tests/test_specloader.py000066400000000000000000000315561413701016200220560ustar00rootroot00000000000000# coding: utf-8 """ Unit tests for template_spec.py. """ import os.path import sys import unittest from . import examples from .examples.simple import Simple from .examples.complex import Complex from .examples.lambdas import Lambdas from .examples.inverted import Inverted, InvertedLists from pystache import Renderer from pystache import TemplateSpec from pystache.common import TemplateNotFoundError from pystache.locator import Locator from pystache.loader import Loader from pystache.specloader import SpecLoader from pystache.tests.common import DATA_DIR, EXAMPLES_DIR from pystache.tests.common import AssertIsMixin, AssertStringMixin from pystache.tests.data.views import SampleView from pystache.tests.data.views import NonAscii class Thing(object): pass class AssertPathsMixin: """A unittest.TestCase mixin to check path equality.""" def assertPaths(self, actual, expected): self.assertEqual(actual, expected) class ViewTestCase(unittest.TestCase, AssertStringMixin): def test_template_rel_directory(self): """ Test that View.template_rel_directory is respected. """ class Tagless(TemplateSpec): pass view = Tagless() renderer = Renderer() self.assertRaises(TemplateNotFoundError, renderer.render, view) # TODO: change this test to remove the following brittle line. view.template_rel_directory = "examples" actual = renderer.render(view) self.assertEqual(actual, "No tags...") def test_template_path_for_partials(self): """ Test that View.template_rel_path is respected for partials. """ spec = TemplateSpec() spec.template = "Partial: {{>tagless}}" renderer1 = Renderer() renderer2 = Renderer(search_dirs=EXAMPLES_DIR) actual = renderer1.render(spec) self.assertString(actual, "Partial: ") actual = renderer2.render(spec) self.assertEqual(actual, "Partial: No tags...") def test_basic_method_calls(self): renderer = Renderer() actual = renderer.render(Simple()) self.assertString(actual, "Hi pizza!") def test_non_callable_attributes(self): view = Simple() view.thing = 'Chris' renderer = Renderer() actual = renderer.render(view) self.assertEqual(actual, "Hi Chris!") def test_complex(self): renderer = Renderer() actual = renderer.render(Complex()) self.assertString(actual, """\

Colors

""") def test_higher_order_replace(self): renderer = Renderer() actual = renderer.render(Lambdas()) self.assertEqual(actual, 'bar != bar. oh, it does!') def test_higher_order_rot13(self): view = Lambdas() view.template = '{{#rot13}}abcdefghijklm{{/rot13}}' renderer = Renderer() actual = renderer.render(view) self.assertString(actual, 'nopqrstuvwxyz') def test_higher_order_lambda(self): view = Lambdas() view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}' renderer = Renderer() actual = renderer.render(view) self.assertString(actual, 'abcdefghijklmnopqrstuvwxyz') def test_partials_with_lambda(self): view = Lambdas() view.template = '{{>partial_with_lambda}}' renderer = Renderer(search_dirs=EXAMPLES_DIR) actual = renderer.render(view) self.assertEqual(actual, 'nopqrstuvwxyz') def test_hierarchical_partials_with_lambdas(self): view = Lambdas() view.template = '{{>partial_with_partial_and_lambda}}' renderer = Renderer(search_dirs=EXAMPLES_DIR) actual = renderer.render(view) self.assertString(actual, 'nopqrstuvwxyznopqrstuvwxyz') def test_inverted(self): renderer = Renderer() actual = renderer.render(Inverted()) self.assertString(actual, """one, two, three, empty list""") def test_accessing_properties_on_parent_object_from_child_objects(self): parent = Thing() parent.this = 'derp' parent.children = [Thing()] view = Simple() view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}" renderer = Renderer() actual = renderer.render(view, {'parent': parent}) self.assertString(actual, 'derp') def test_inverted_lists(self): renderer = Renderer() actual = renderer.render(InvertedLists()) self.assertString(actual, """one, two, three, empty list""") def _make_specloader(): """ Return a default SpecLoader instance for testing purposes. """ # Python 2 and 3 have different default encodings. Thus, to have # consistent test results across both versions, we need to specify # the string and file encodings explicitly rather than relying on # the defaults. def to_unicode(s, encoding=None): """ Raises a TypeError exception if the given string is already unicode. """ if encoding is None: encoding = 'ascii' return str(s, encoding, 'strict') loader = Loader(file_encoding='ascii', to_unicode=to_unicode) return SpecLoader(loader=loader) class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin, AssertPathsMixin): """ Tests template_spec.SpecLoader. """ def _make_specloader(self): return _make_specloader() def test_init__defaults(self): spec_loader = SpecLoader() # Check the loader attribute. loader = spec_loader.loader self.assertEqual(loader.extension, 'mustache') self.assertEqual(loader.file_encoding, sys.getdefaultencoding()) # TODO: finish testing the other Loader attributes. to_unicode = loader.to_unicode def test_init__loader(self): loader = Loader() custom = SpecLoader(loader=loader) self.assertIs(custom.loader, loader) # TODO: rename to something like _assert_load(). def _assert_template(self, loader, custom, expected): self.assertString(loader.load(custom), expected) def test_load__template__type_str(self): """ Test the template attribute: str string. """ custom = TemplateSpec() custom.template = "abc" spec_loader = self._make_specloader() self._assert_template(spec_loader, custom, "abc") def test_load__template__type_unicode(self): """ Test the template attribute: unicode string. """ custom = TemplateSpec() custom.template = "abc" spec_loader = self._make_specloader() self._assert_template(spec_loader, custom, "abc") def test_load__template__unicode_non_ascii(self): """ Test the template attribute: non-ascii unicode string. """ custom = TemplateSpec() custom.template = "é" spec_loader = self._make_specloader() self._assert_template(spec_loader, custom, "é") def test_load__template__with_template_encoding(self): """ Test the template attribute: with template encoding attribute. """ custom = TemplateSpec() custom.template = 'é'.encode('utf-8') spec_loader = self._make_specloader() self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, 'é') custom.template_encoding = 'utf-8' self._assert_template(spec_loader, custom, 'é') # TODO: make this test complete. def test_load__template__correct_loader(self): """ Test that reader.unicode() is called correctly. This test tests that the correct reader is called with the correct arguments. This is a catch-all test to supplement the other test cases. It tests SpecLoader.load() independent of reader.unicode() being implemented correctly (and tested). """ class MockLoader(Loader): def __init__(self): self.s = None self.encoding = None # Overrides the existing method. def str(self, s, encoding=None): self.s = s self.encoding = encoding return "foo" loader = MockLoader() custom_loader = SpecLoader() custom_loader.loader = loader view = TemplateSpec() view.template = "template-foo" view.template_encoding = "encoding-foo" # Check that our unicode() above was called. self._assert_template(custom_loader, view, 'foo') self.assertEqual(loader.s, "template-foo") self.assertEqual(loader.encoding, "encoding-foo") def test_find__template_path(self): """Test _find() with TemplateSpec.template_path.""" loader = self._make_specloader() custom = TemplateSpec() custom.template_path = "path/foo" actual = loader._find(custom) self.assertPaths(actual, "path/foo") # TODO: migrate these tests into the SpecLoaderTests class. # TODO: rename the get_template() tests to test load(). # TODO: condense, reorganize, and rename the tests so that it is # clear whether we have full test coverage (e.g. organized by # TemplateSpec attributes or something). class TemplateSpecTests(unittest.TestCase, AssertPathsMixin): def _make_loader(self): return _make_specloader() def _assert_template_location(self, view, expected): loader = self._make_loader() actual = loader._find_relative(view) self.assertEqual(actual, expected) def test_find_relative(self): """ Test _find_relative(): default behavior (no attributes set). """ view = SampleView() self._assert_template_location(view, (None, 'sample_view.mustache')) def test_find_relative__template_rel_path__file_name_only(self): """ Test _find_relative(): template_rel_path attribute. """ view = SampleView() view.template_rel_path = 'template.txt' self._assert_template_location(view, ('', 'template.txt')) def test_find_relative__template_rel_path__file_name_with_directory(self): """ Test _find_relative(): template_rel_path attribute. """ view = SampleView() view.template_rel_path = 'foo/bar/template.txt' self._assert_template_location(view, ('foo/bar', 'template.txt')) def test_find_relative__template_rel_directory(self): """ Test _find_relative(): template_rel_directory attribute. """ view = SampleView() view.template_rel_directory = 'foo' self._assert_template_location(view, ('foo', 'sample_view.mustache')) def test_find_relative__template_name(self): """ Test _find_relative(): template_name attribute. """ view = SampleView() view.template_name = 'new_name' self._assert_template_location(view, (None, 'new_name.mustache')) def test_find_relative__template_extension(self): """ Test _find_relative(): template_extension attribute. """ view = SampleView() view.template_extension = 'txt' self._assert_template_location(view, (None, 'sample_view.txt')) def test_find__with_directory(self): """ Test _find() with a view that has a directory specified. """ loader = self._make_loader() view = SampleView() view.template_rel_path = os.path.join('foo', 'bar.txt') self.assertTrue(loader._find_relative(view)[0] is not None) actual = loader._find(view) expected = os.path.join(DATA_DIR, 'foo', 'bar.txt') self.assertPaths(actual, expected) def test_find__without_directory(self): """ Test _find() with a view that doesn't have a directory specified. """ loader = self._make_loader() view = SampleView() self.assertTrue(loader._find_relative(view)[0] is None) actual = loader._find(view) expected = os.path.join(DATA_DIR, 'sample_view.mustache') self.assertPaths(actual, expected) def _assert_get_template(self, custom, expected): loader = self._make_loader() actual = loader.load(custom) self.assertEqual(type(actual), str) self.assertEqual(actual, expected) def test_get_template(self): """ Test get_template(): default behavior (no attributes set). """ view = SampleView() self._assert_get_template(view, "ascii: abc") def test_get_template__template_encoding(self): """ Test get_template(): template_encoding attribute. """ view = NonAscii() self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo') view.template_encoding = 'utf-8' self._assert_get_template(view, "non-ascii: é") pystache-0.6.0/setup.cfg000066400000000000000000000027071413701016200151570ustar00rootroot00000000000000[metadata] name = pystache version = attr: pystache.__version__ author = Chris Wanstrath author_email = chris@ozmm.org maintainer = Steve Arnold maintainer_email = nerdboy@gentoo.org description = Mustache for Python url = https://github.com/sarnold/pystache license = MIT license_files = LICENSE classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] python_requires = >=3.6 zip_safe = True include_package_data = True packages = find: [options.package_data] * = *.mustache, *.txt [options.entry_points] console_scripts = pystache=pystache.commands.render:main pystache-test=pystache.commands.test:main [options.extras_require] test = nose cov = coverage coverage_python_version [check] metadata = true restructuredtext = true strict = false [bdist_wheel] universal = 0 [check-manifest] ignore = .codeclimate.yml .gitattributes .coveragerc .gitignore .pep8speaks.yml coverage* conda/** [nosetests] traverse-namespace = 1 verbosity = 3 with-coverage = 1 with-doctest = 1 doctest-extension = rst cover-package = pystache cover-xml = 1 # set the default ignore here so we can append on cmdline ignore-files = (?:^\.|^_,|^setup\.py$) pystache-0.6.0/setup.py000066400000000000000000000174571413701016200150600ustar00rootroot00000000000000#!/usr/bin/env python # coding: utf-8 """ This script supports publishing Pystache to PyPI. This docstring contains instructions to Pystache maintainers on how to release a new version of Pystache. (1) Prepare the release. Make sure the code is finalized and merged to master. Bump the version number in setup.py, update the release date in the HISTORY file, etc. Generate the reStructuredText long_description using-- $ python setup.py prep and be sure this new version is checked in. You must have pandoc installed to do this step: http://johnmacfarlane.net/pandoc/ It helps to review this auto-generated file on GitHub prior to uploading because the long description will be sent to PyPI and appear there after publishing. PyPI attempts to convert this string to HTML before displaying it on the PyPI project page. If PyPI finds any issues, it will render it instead as plain-text, which we do not want. To check in advance that PyPI will accept and parse the reST file as HTML, you can use the rst2html.py program installed by the docutils package (http://docutils.sourceforge.net/). To install docutils: $ pip install docutils To check the file, run the following command and confirm that it reports no warnings: $ python setup.py --long-description | rst2html.py -v --no-raw > out.html See here for more information: http://docs.python.org/distutils/uploading.html#pypi-package-display (2) Push to PyPI. To release a new version of Pystache to PyPI-- http://pypi.python.org/pypi/pystache create a PyPI user account if you do not already have one. The user account will need permissions to push to PyPI. A current "Package Index Owner" of Pystache can grant you those permissions. When you have permissions, run the following: python setup.py publish If you get an error like the following-- Upload failed (401): You must be identified to edit package information then add a file called .pyirc to your home directory with the following contents: [server-login] username: password: as described here, for example: http://docs.python.org/release/2.5.2/dist/pypirc.html (3) Tag the release on GitHub. Here are some commands for tagging. List current tags: git tag -l -n3 Create an annotated tag: git tag -a -m "Version 0.5.1" "v0.5.1" Push a tag to GitHub: git push --tags defunkt v0.5.1 """ import os import sys import subprocess as sp from setuptools import setup from shlex import split FILE_ENCODING = 'utf-8' README_PATH = 'README.md' HISTORY_PATH = 'HISTORY.md' LICENSE_PATH = 'LICENSE' RST_DESCRIPTION_PATH = 'setup_description.rst' TEMP_EXTENSION = '.temp' PREP_COMMAND = 'prep' # Comments in reST begin with two dots. RST_LONG_DESCRIPTION_INTRO = """\ .. Do not edit this file. This file is auto-generated for PyPI by setup.py .. using pandoc, so edits should go in the source files rather than here. """ def read(path): """ Read and return the contents of a text file as a unicode string. """ # This function implementation was chosen to be compatible across Python 2/3. f = open(path, 'rb') # We avoid use of the with keyword for Python 2.4 support. try: b = f.read() finally: f.close() return b.decode(FILE_ENCODING) def write(u, path): """ Write a unicode string to a file (as utf-8). """ print(("writing to: %s" % path)) # This function implementation was chosen to be compatible across Python 2/3. f = open(path, "wb") try: b = u.encode(FILE_ENCODING) f.write(b) finally: f.close() def make_temp_path(path, new_ext=None): """ Arguments: new_ext: the new file extension, including the leading dot. Defaults to preserving the existing file extension. """ root, ext = os.path.splitext(path) if new_ext is None: new_ext = ext temp_path = root + TEMP_EXTENSION + new_ext return temp_path def strip_html_comments(text): """Strip HTML comments from a unicode string.""" lines = text.splitlines(True) # preserve line endings. # Remove HTML comments (which we only allow to take a special form). new_lines = [line for line in lines if not line.startswith("{command}") if os.path.exists(rst_temp_path): os.remove(rst_temp_path) sp.check_call(split(command)) if not os.path.exists(rst_temp_path): s = (f"Error running: {command}\n" " Did you install pandoc per the {__file__} docstring?") sys.exit(s) return read(rst_temp_path) # The long_description needs to be formatted as reStructuredText. # See the following for more information: # # http://docs.python.org/distutils/setupscript.html#additional-meta-data # http://docs.python.org/distutils/uploading.html#pypi-package-display # def make_long_description(): """ Generate the reST long_description for setup() from source files. Returns the generated long_description as a unicode string. """ readme_path = README_PATH # Remove our HTML comments because PyPI does not allow it. # See the setup.py docstring for more info on this. readme_md = strip_html_comments(read(readme_path)) history_md = strip_html_comments(read(HISTORY_PATH)) license_md = """\ License ======= """ + read(LICENSE_PATH) sections = [readme_md, history_md, license_md] md_description = '\n\n'.join(sections) # Write the combined Markdown file to a temp path. md_ext = os.path.splitext(readme_path)[1] md_description_path = make_temp_path(RST_DESCRIPTION_PATH, new_ext=md_ext) write(md_description, md_description_path) rst_temp_path = make_temp_path(RST_DESCRIPTION_PATH) long_description = convert_md_to_rst(md_path=md_description_path, rst_temp_path=rst_temp_path) return "\n".join([RST_LONG_DESCRIPTION_INTRO, long_description]) def prep(): """Update the reST long_description file.""" long_description = make_long_description() write(long_description, RST_DESCRIPTION_PATH) def publish(): """Publish this package to PyPI (aka "the Cheeseshop").""" long_description = make_long_description() if long_description != read(RST_DESCRIPTION_PATH): print(("""\ Description file not up-to-date: %s Run the following command and commit the changes-- python setup.py %s """ % (RST_DESCRIPTION_PATH, PREP_COMMAND))) sys.exit() print(("Description up-to-date: %s" % RST_DESCRIPTION_PATH)) answer = input("Are you sure you want to publish to PyPI (yes/no)?") if answer != "yes": exit("Aborted: nothing published") os.system('python setup.py sdist upload') def main(sys_argv): # TODO: use the logging module instead of printing. command = sys_argv[-1] if command == 'publish': publish() sys.exit() elif command == PREP_COMMAND: prep() sys.exit() long_description = read(RST_DESCRIPTION_PATH) setup( long_description=long_description, long_description_content_type='text/x-rst', ) if __name__=='__main__': main(sys.argv) pystache-0.6.0/setup_description.rst000066400000000000000000000437751413701016200176450ustar00rootroot00000000000000.. Do not edit this file. This file is auto-generated for PyPI by setup.py .. using pandoc, so edits should go in the source files rather than here. Pystache ======== |ci| |Conda| |Wheels| |Release| |Python| |Latest release| |License| |Maintainability| |codecov| This updated fork of Pystache is currently tested on Python 3.6+ and in Conda, on Linux, Macos, and Windows (Python 2.7 support has been removed). |image9| `Pystache `__ is a Python implementation of `Mustache `__. Mustache is a framework-agnostic, logic-free templating system inspired by `ctemplate `__ and `et `__. Like ctemplate, Mustache "emphasizes separating logic from presentation: it is impossible to embed application logic in this template language." The `mustache(5) `__ man page provides a good introduction to Mustache's syntax. For a more complete (and more current) description of Mustache's behavior, see the official `Mustache spec `__. Pystache is `semantically versioned `__ and older versions can still be found on `PyPI `__. This version of Pystache now passes all tests in `version 1.1.3 `__ of the spec. Requirements ------------ Pystache is tested with-- - Python 3.6 - Python 3.7 - Python 3.8 - Python 3.9 - Conda (py36-py39) `Distribute `__ (the setuptools fork) is no longer required over `setuptools `__, as the current packaging is now PEP517-compliant. JSON support is needed only for the command-line interface and to run the spec tests; PyYAML can still be used (see the Develop section). Official support for Python 2 will end with Pystache version 0.6.0. Install It ---------- :: pip install -U pystache -f https://github.com/sarnold/pystache/releases/ And test it-- :: pystache-test To install and test from source (e.g. from GitHub), see the Develop section. Use It ------ :: >>> import pystache >>> print(pystache.render('Hi {{person}}!', {'person': 'Mom'})) Hi Mom! You can also create dedicated view classes to hold your view logic. Here's your view class (in ../pystache/tests/examples/readme.py): :: class SayHello(object): def to(self): return "Pizza" Instantiating like so: :: >>> from pystache.tests.examples.readme import SayHello >>> hello = SayHello() Then your template, say_hello.mustache (by default in the same directory as your class definition): :: Hello, {{to}}! Pull it together: :: >>> renderer = pystache.Renderer() >>> print(renderer.render(hello)) Hello, Pizza! For greater control over rendering (e.g. to specify a custom template directory), use the ``Renderer`` class like above. One can pass attributes to the Renderer class constructor or set them on a Renderer instance. To customize template loading on a per-view basis, subclass ``TemplateSpec``. See the docstrings of the `Renderer `__ class and `TemplateSpec `__ class for more information. You can also pre-parse a template: :: >>> parsed = pystache.parse(u"Hey {{#who}}{{.}}!{{/who}}") >>> print(parsed) ['Hey ', _SectionNode(key='who', index_begin=12, index_end=18, parsed=[_EscapeNode(key='.'), '!'])] And then: :: >>> print(renderer.render(parsed, {'who': 'Pops'})) Hey Pops! >>> print(renderer.render(parsed, {'who': 'you'})) Hey you! Python 3 -------- Pystache has supported Python 3 since version 0.5.1. Pystache behaves slightly differently between Python 2 and 3, as follows: - In Python 2, the default html-escape function ``cgi.escape()`` does not escape single quotes. In Python 3, the default escape function ``html.escape()`` does escape single quotes. - In both Python 2 and 3, the string and file encodings default to ``sys.getdefaultencoding()``. However, this function can return different values under Python 2 and 3, even when run from the same system. Check your own system for the behavior on your system, or do not rely on the defaults by passing in the encodings explicitly (e.g. to the ``Renderer`` class). Unicode ------- This section describes how Pystache handles unicode, strings, and encodings. Internally, Pystache uses `only unicode strings `__ (``str`` in Python 3 and ``unicode`` in Python 2). For input, Pystache accepts both unicode strings and byte strings (``bytes`` in Python 3 and ``str`` in Python 2). For output, Pystache's template rendering methods return only unicode. Pystache's ``Renderer`` class supports a number of attributes to control how Pystache converts byte strings to unicode on input. These include the ``file_encoding``, ``string_encoding``, and ``decode_errors`` attributes. The ``file_encoding`` attribute is the encoding the renderer uses to convert to unicode any files read from the file system. Similarly, ``string_encoding`` is the encoding the renderer uses to convert any other byte strings encountered during the rendering process into unicode (e.g. context values that are encoded byte strings). The ``decode_errors`` attribute is what the renderer passes as the ``errors`` argument to Python's built-in unicode-decoding function (``str()`` in Python 3 and ``unicode()`` in Python 2). The valid values for this argument are ``strict``, ``ignore``, and ``replace``. Each of these attributes can be set via the ``Renderer`` class's constructor using a keyword argument of the same name. See the Renderer class's docstrings for further details. In addition, the ``file_encoding`` attribute can be controlled on a per-view basis by subclassing the ``TemplateSpec`` class. When not specified explicitly, these attributes default to values set in Pystache's ``defaults`` module. Develop ------- To test from a source distribution (without installing)-- :: python test_pystache.py To test Pystache with multiple versions of Python (with a single command!) and different platforms, you can use `tox `__: :: pip install tox tox -e setup To run tests on multiple versions with coverage, run: :: tox -e py38-linux,py39-linux # for example (substitute your platform above, eg, macos or windows) The source distribution tests also include doctests and tests from the Mustache spec. To include tests from the Mustache spec in your test runs: :: git submodule init git submodule update The test harness parses the spec's (more human-readable) yaml files if `PyYAML `__ is present. Otherwise, it parses the json files. To install PyYAML-- :: pip install pyyaml Once the submodule is available, you can run the full test set with: :: tox -e setup . ext/spec/specs To run a subset of the tests, you can use `nose `__: :: pip install nose nosetests --tests pystache/tests/test_context.py:GetValueTests.test_dictionary__key_present Mailing List (old) ------------------ There is(was) a `mailing list `__. Note that there is a bit of a delay between posting a message and seeing it appear in the mailing list archive. Credits ------- :: >>> import pystache >>> context = { 'author': 'Chris Wanstrath', 'maintainer': 'Chris Jerdonek','refurbisher': 'Steve Arnold' } >>> print(pystache.render("Author: {{author}}\nMaintainer: {{maintainer}}\nRefurbisher: {{refurbisher}}", context)) Author: Chris Wanstrath Maintainer: Chris Jerdonek Refurbisher: Steve Arnold Pystache logo by `David Phillips `__ is licensed under a `Creative Commons Attribution-ShareAlike 3.0 Unported License `__. |image10| History ======= **Note:** Official support for Python 2.7 will end with Pystache version 0.6.0. 0.6.0 (2021-03-04) ------------------ - Bump spec versions to latest => v1.1.3 - Modernize python and CI tools, update docs/doctests - Update unicode conversion test for py3-only - Add pep8speaks cfg, cleanup warnings - Remove superfluous setup test/unused imports - Add conda recipe/CI build .. _section-1: 0.5.6 (2021-02-28) ------------------ - Use correct wheel name in release workflow, limit wheels - Add install check/test of downloaded wheel - Update/add ci workflows and tox cfg, bump to next dev0 version .. _section-2: 0.5.5 (2020-12-16) ------------------ - fix document processing, update pandoc args and history - add release.yml to CI, test env settings - fix bogus commit message, update versions and tox cf - add post-test steps for building pkgs with/without doc updates - add CI build check, fix MANIFEST.in pruning .. _section-3: 0.5.4-2 (2020-11-09) -------------------- - Merge pull request #1 from sarnold/rebase-up - Bugfix: test_specloader.py: fix test_find__with_directory on other OSs - Bugfix: pystache/loader.py: remove stray windows line-endings - fix crufty (and insecure) http urls - Bugfix: modernize python versions (keep py27) and fix spec_test load cmd .. _section-4: 0.5.4 (2014-07-11) ------------------ - Bugfix: made test with filenames OS agnostic (issue #162). .. _section-5: 0.5.3 (2012-11-03) ------------------ - Added ability to customize string coercion (e.g. to have None render as ``''``) (issue #130). - Added Renderer.render_name() to render a template by name (issue #122). - Added TemplateSpec.template_path to specify an absolute path to a template (issue #41). - Added option of raising errors on missing tags/partials: ``Renderer(missing_tags='strict')`` (issue #110). - Added support for finding and loading templates by file name in addition to by template name (issue #127). [xgecko] - Added a ``parse()`` function that yields a printable, pre-compiled parse tree. - Added support for rendering pre-compiled templates. - Added Python 3.3 to the list of supported versions. - Added support for `PyPy `__ (issue #125). - Added support for `Travis CI `__ (issue #124). [msabramo] - Bugfix: ``defaults.DELIMITERS`` can now be changed at runtime (issue #135). [bennoleslie] - Bugfix: exceptions raised from a property are no longer swallowed when getting a key from a context stack (issue #110). - Bugfix: lambda section values can now return non-ascii, non-unicode strings (issue #118). - Bugfix: allow ``test_pystache.py`` and ``tox`` to pass when run from a downloaded sdist (i.e. without the spec test directory). - Convert HISTORY and README files from reST to Markdown. - More robust handling of byte strings in Python 3. - Added Creative Commons license for David Phillips's logo. .. _section-6: 0.5.2 (2012-05-03) ------------------ - Added support for dot notation and version 1.1.2 of the spec (issue #99). [rbp] - Missing partials now render as empty string per latest version of spec (issue #115). - Bugfix: falsey values now coerced to strings using str(). - Bugfix: lambda return values for sections no longer pushed onto context stack (issue #113). - Bugfix: lists of lambdas for sections were not rendered (issue #114). .. _section-7: 0.5.1 (2012-04-24) ------------------ - Added support for Python 3.1 and 3.2. - Added tox support to test multiple Python versions. - Added test script entry point: pystache-test. - Added \__version_\_ package attribute. - Test harness now supports both YAML and JSON forms of Mustache spec. - Test harness no longer requires nose. .. _section-8: 0.5.0 (2012-04-03) ------------------ This version represents a major rewrite and refactoring of the code base that also adds features and fixes many bugs. All functionality and nearly all unit tests have been preserved. However, some backwards incompatible changes to the API have been made. Below is a selection of some of the changes (not exhaustive). Highlights: - Pystache now passes all tests in version 1.0.3 of the `Mustache spec `__. [pvande] - Removed View class: it is no longer necessary to subclass from View or from any other class to create a view. - Replaced Template with Renderer class: template rendering behavior can be modified via the Renderer constructor or by setting attributes on a Renderer instance. - Added TemplateSpec class: template rendering can be specified on a per-view basis by subclassing from TemplateSpec. - Introduced separation of concerns and removed circular dependencies (e.g. between Template and View classes, cf. `issue #13 `__). - Unicode now used consistently throughout the rendering process. - Expanded test coverage: nosetests now runs doctests and ~105 test cases from the Mustache spec (increasing the number of tests from 56 to ~315). - Added a rudimentary benchmarking script to gauge performance while refactoring. - Extensive documentation added (e.g. docstrings). Other changes: - Added a command-line interface. [vrde] - The main rendering class now accepts a custom partial loader (e.g. a dictionary) and a custom escape function. - Non-ascii characters in str strings are now supported while rendering. - Added string encoding, file encoding, and errors options for decoding to unicode. - Removed the output encoding option. - Removed the use of markupsafe. Bug fixes: - Context values no longer processed as template strings. [jakearchibald] - Whitespace surrounding sections is no longer altered, per the spec. [heliodor] - Zeroes now render correctly when using PyPy. [alex] - Multline comments now permitted. [fczuardi] - Extensionless template files are now supported. - Passing ``**kwargs`` to ``Template()`` no longer modifies the context. - Passing ``**kwargs`` to ``Template()`` with no context no longer raises an exception. .. _section-9: 0.4.1 (2012-03-25) ------------------ - Added support for Python 2.4. [wangtz, jvantuyl] .. _section-10: 0.4.0 (2011-01-12) ------------------ - Add support for nested contexts (within template and view) - Add support for inverted lists - Decoupled template loading .. _section-11: 0.3.1 (2010-05-07) ------------------ - Fix package .. _section-12: 0.3.0 (2010-05-03) ------------------ - View.template_path can now hold a list of path - Add {{& blah}} as an alias for {{{ blah }}} - Higher Order Sections - Inverted sections .. _section-13: 0.2.0 (2010-02-15) ------------------ - Bugfix: Methods returning False or None are not rendered - Bugfix: Don't render an empty string when a tag's value is 0. [enaeseth] - Add support for using non-callables as View attributes. [joshthecoder] - Allow using View instances as attributes. [joshthecoder] - Support for Unicode and non-ASCII-encoded bytestring output. [enaeseth] - Template file encoding awareness. [enaeseth] .. _section-14: 0.1.1 (2009-11-13) ------------------ - Ensure we're dealing with strings, always - Tests can be run by executing the test file directly .. _section-15: 0.1.0 (2009-11-12) ------------------ - First release License ======= Copyright (C) 2012 Chris Jerdonek. All rights reserved. Copyright (c) 2009 Chris Wanstrath Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. |ci| image:: https://github.com/sarnold/pystache/actions/workflows/ci.yml/badge.svg :target: https://github.com/sarnold/pystache/actions/workflows/ci.yml .. |Conda| image:: https://github.com/sarnold/pystache/actions/workflows/conda.yml/badge.svg :target: https://github.com/sarnold/pystache/actions/workflows/conda.yml .. |Wheels| image:: https://github.com/sarnold/pystache/actions/workflows/wheels.yml/badge.svg :target: https://github.com/sarnold/pystache/actions/workflows/wheels.yml .. |Release| image:: https://github.com/sarnold/pystache/actions/workflows/release.yml/badge.svg :target: https://github.com/sarnold/pystache/actions/workflows/release.yml .. |Python| image:: https://img.shields.io/badge/python-3.6+-blue.svg :target: https://www.python.org/downloads/ .. |Latest release| image:: https://img.shields.io/github/v/release/sarnold/pystache?include_prereleases :target: https://github.com/sarnold/pystache/releases/latest .. |License| image:: https://img.shields.io/github/license/sarnold/pystache :target: https://github.com/sarnold/pystache/blob/master/LICENSE .. |Maintainability| image:: https://api.codeclimate.com/v1/badges/a8fa1bf4638bfc6581b6/maintainability :target: https://codeclimate.com/github/sarnold/pystache/maintainability .. |codecov| image:: https://codecov.io/gh/sarnold/pystache/branch/master/graph/badge.svg?token=5PZNMZBI6K :target: https://codecov.io/gh/sarnold/pystache .. |image9| image:: gh/images/logo_phillips_small.png .. |image10| image:: http://i.creativecommons.org/l/by-sa/3.0/88x31.png pystache-0.6.0/test_pystache.py000066400000000000000000000010141413701016200165550ustar00rootroot00000000000000#!/usr/bin/env python # coding: utf-8 """ Runs project tests. This script is a substitute for running-- python -m pystache.commands.test It is useful in Python 2.4 because the -m flag does not accept subpackages in Python 2.4: http://docs.python.org/using/cmdline.html#cmdoption-m """ import sys from pystache.commands import test from pystache.tests.main import FROM_SOURCE_OPTION def main(sys_argv=sys.argv): sys.argv.insert(1, FROM_SOURCE_OPTION) test.main() if __name__=='__main__': main() pystache-0.6.0/tox.ini000066400000000000000000000046711413701016200146530ustar00rootroot00000000000000[tox] envlist = py{36,37,38,39}-{linux,macos,windows} skip_missing_interpreters = true isolated_build = true #skipsdist = true [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38 3.9: py39 [gh-actions:env] PLATFORM = ubuntu-18.04: linux macos-latest: macos windows-latest: windows [testenv] passenv = CI PYTHON PYTHONIOENCODING allowlist_externals = bash deps = pip>=20.0.1 nose coverage coverage_python_version commands = nosetests -sx . {posargs} bash -c './gh/fix_pkg_name.sh' [testenv:bare] # Change the working directory so that we don't import the pystache located # in the original location. deps = pip>=20.0.1 -e . changedir = {envbindir} commands = pystache-test [testenv:bench] passenv = CI PYTHON PYTHONIOENCODING deps = pip>=20.0.1 # uncomment for comparison, posargs expects a number, eg, 10000 #chevron commands_pre = pip install . commands = python pystache/tests/benchmark.py {posargs} [testenv:setup] passenv = CI PYTHON PYTHONIOENCODING deps = pyyaml twine # to run the spec tests, first init the git submodule, and then run # something like: tox -e setup . ext/spec/specs commands = python setup.py install twine check dist/* pystache-test {posargs} [testenv:lint] # oddly, this produces different results than running the same pylint # command from a shell prompt WTF? unknown cause # pylint pystache/ => 8.27/10 # tox -e lint => 8.06/10 passenv = CI PYTHON PYTHONIOENCODING deps = pip>=20.0.1 pylint commands = pylint --rcfile={toxinidir}/.pylintrc --fail-under=8.10 pystache/ [testenv:style] passenv = CI PYTHON PYTHONIOENCODING deps = pip>=20.0.1 flake8 commands = flake8 pystache/ [testenv:deploy] passenv = CI PYTHON PYTHONIOENCODING allowlist_externals = bash deps = pip>=19.0.1 wheel build twine commands = python -m build . twine check dist/* [testenv:check] passenv = CI PYTHON PYTHONIOENCODING skip_install = true allowlist_externals = bash deps = pip>=20.0.1 commands = bash -c 'export WHL_FILE=$(ls dist/*.whl); \ python -m pip install $WHL_FILE' pystache-test [testenv:docs] passenv = CI PYTHON PYTHONIOENCODING allowlist_externals = bash deps = pip>=19.0.1 wheel docutils # apt/emerge pandoc first commands = python setup.py prep bash -c 'python setup.py --long-description | rst2html.py -v --no-raw > out.html'