├── .coveragerc ├── .flake8 ├── .github ├── dependabot.yml └── workflows │ └── ci-tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGES.rst ├── CONTRIBUTORS.txt ├── COPYRIGHT.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── demo ├── __init__.py ├── helloworld.jinja2 ├── locale │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ └── messages.pot └── test_demo.py ├── docs ├── .gitignore ├── Makefile ├── api.rst ├── changes.rst ├── conf.py ├── glossary.rst └── index.rst ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── pyramid_jinja2 │ ├── __init__.py │ ├── configure.zcml │ ├── filters.py │ ├── i18n.py │ ├── scaffolds │ ├── __init__.py │ └── jinja2_starter │ │ ├── +dot+coveragerc_tmpl │ │ ├── +package+ │ │ ├── __init__.py_tmpl │ │ ├── locale │ │ │ ├── +project+.pot │ │ │ ├── de │ │ │ │ └── LC_MESSAGES │ │ │ │ │ ├── +project+.mo │ │ │ │ │ └── +project+.po │ │ │ └── fr │ │ │ │ └── LC_MESSAGES │ │ │ │ ├── +project+.mo │ │ │ │ └── +project+.po │ │ ├── resources.py │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── pyramid-16x16.png │ │ │ ├── pyramid.png │ │ │ └── theme.css │ │ ├── templates │ │ │ └── mytemplate.jinja2_tmpl │ │ ├── tests.py_tmpl │ │ └── views.py_tmpl │ │ ├── CHANGES.rst_tmpl │ │ ├── MANIFEST.in_tmpl │ │ ├── README.rst_tmpl │ │ ├── development.ini_tmpl │ │ ├── message-extraction.ini │ │ ├── pytest.ini_tmpl │ │ ├── setup.cfg_tmpl │ │ └── setup.py_tmpl │ └── settings.py ├── tests ├── __init__.py ├── babel.cfg ├── base.py ├── extensions.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ └── messages.pot ├── templates │ ├── bar │ │ └── mytemplate.jinja2 │ ├── baz1 │ │ ├── base.jinja2 │ │ ├── middle.jinja2 │ │ └── mytemplate.jinja2 │ ├── baz2 │ │ ├── base.jinja2 │ │ └── mytemplate.jinja2 │ ├── deep │ │ ├── base.jinja2 │ │ ├── forms.jinja2 │ │ ├── leaf.jinja2 │ │ └── sub │ │ │ ├── base.jinja2 │ │ │ ├── leaf.jinja2 │ │ │ └── nav.jinja2 │ ├── extends.jinja2 │ ├── extends_missing.jinja2 │ ├── extends_relbase.jinja2 │ ├── extends_spec.jinja2 │ ├── foo │ │ └── mytemplate.jinja2 │ ├── helloworld.jinja2 │ ├── i18n.jinja2 │ ├── newstyle.jinja2 │ ├── recursive │ │ ├── admin │ │ │ ├── base.html │ │ │ └── index.html │ │ └── base.html │ └── tests_and_filters.jinja2 ├── test_ext.py ├── test_filters.py ├── test_it.py └── test_settings.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | pyramid_jinja2 4 | 5 | [paths] 6 | source = 7 | pyramid_jinja2 8 | */pyramid_jinja2 9 | */site-packages/pyramid_jinja2 10 | 11 | [report] 12 | omit = 13 | */pyramid_jinja2/scaffolds/__init__.py 14 | 15 | show_missing = true 16 | precision = 2 17 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = 4 | # E203: whitespace before ':' (black fails to be PEP8 compliant) 5 | E203 6 | # E731: do not assign a lambda expression, use a def 7 | E731 8 | # W503: line break before binary operator (flake8 is not PEP8 compliant) 9 | W503 10 | # W504: line break after binary operator (flake8 is not PEP8 compliant) 11 | W504 12 | show-source = True 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every weekday 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | # Only on pushes to main or one of the release branches we build on push 5 | push: 6 | branches: 7 | - main 8 | - "[0-9].[0-9]+-branch" 9 | tags: 10 | # Build pull requests 11 | pull_request: 12 | 13 | jobs: 14 | test: 15 | strategy: 16 | matrix: 17 | py: 18 | - "3.7" 19 | - "3.8" 20 | - "3.9" 21 | - "3.10" 22 | - "3.11" 23 | - "3.12" 24 | - "pypy-3.8" 25 | os: 26 | - "ubuntu-latest" 27 | - "windows-latest" 28 | - "macos-latest" 29 | architecture: 30 | - x64 31 | - x86 32 | exclude: 33 | # Linux and macOS don't have x86 python 34 | - os: "ubuntu-latest" 35 | architecture: x86 36 | - os: "macos-latest" 37 | architecture: x86 38 | name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup python 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ matrix.py }} 46 | architecture: ${{ matrix.architecture }} 47 | - run: pip install tox 48 | - name: Running tox 49 | run: tox -e py 50 | test_old_pyramids: 51 | strategy: 52 | matrix: 53 | pyramid: 54 | - "pyramid13" 55 | - "pyramid14" 56 | - "pyramid15" 57 | - "pyramid16" 58 | - "pyramid17" 59 | - "pyramid18" 60 | - "pyramid19" 61 | - "pyramid110" 62 | - "pyramid20" 63 | - "pyramid110-jinja2legacy" 64 | - "pyramid20-jinja2legacy" 65 | name: "Python: py310-${{ matrix.pyramid }}" 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v4 69 | - name: Setup python 70 | uses: actions/setup-python@v5 71 | with: 72 | python-version: "3.10" 73 | architecture: x64 74 | - run: pip install tox 75 | - name: Running tox 76 | run: tox -e py310-${{ matrix.pyramid }} 77 | coverage: 78 | runs-on: ubuntu-latest 79 | name: Validate coverage 80 | steps: 81 | - uses: actions/checkout@v4 82 | - name: Setup python 3.10 83 | uses: actions/setup-python@v5 84 | with: 85 | python-version: "3.10" 86 | architecture: x64 87 | 88 | - run: pip install tox 89 | - run: tox -e py310,py310-jinja2legacy,coverage 90 | docs: 91 | runs-on: ubuntu-latest 92 | name: Build the documentation 93 | steps: 94 | - uses: actions/checkout@v4 95 | - name: Setup python 96 | uses: actions/setup-python@v5 97 | with: 98 | python-version: "3.10" 99 | architecture: x64 100 | - run: pip install tox 101 | - run: tox -e docs 102 | lint: 103 | runs-on: ubuntu-latest 104 | name: Lint the package 105 | steps: 106 | - uses: actions/checkout@v4 107 | - name: Setup python 108 | uses: actions/setup-python@v5 109 | with: 110 | python-version: "3.10" 111 | architecture: x64 112 | - run: pip install tox 113 | - run: tox -e lint 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bookenv/ 2 | /build 3 | .coverage 4 | .coverage.* 5 | coverage.xml 6 | dist/ 7 | /env* 8 | env*/ 9 | *.egg 10 | *.egg-info 11 | .eggs/ 12 | jyenv/ 13 | *.pyc 14 | *$py.class 15 | pypyenv/ 16 | *~ 17 | .*.swp 18 | .tox/ 19 | venv/ 20 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 2 | version: 2 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: '3.12' 7 | sphinx: 8 | configuration: docs/conf.py 9 | python: 10 | install: 11 | - method: pip 12 | path: . 13 | extra_requirements: 14 | - docs 15 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 2.10.1 (2023-02-07) 2 | =================== 3 | 4 | - Rename "master" git branch to "main" 5 | 6 | - Add Python 3.11 and 3.12 support. 7 | 8 | - Replace usage of private Pyramid APIs. 9 | 10 | 2.10 (2022-03-27) 11 | ================= 12 | 13 | - Add ``jinja2.i18n_extension`` configuration setting. 14 | See https://github.com/Pylons/pyramid_jinja2/pull/165 15 | 16 | 2.9.2 (2022-03-19) 17 | ================== 18 | 19 | - Support Jinja2 >= 3.0. 20 | 21 | - Only import ``jinja2.pass_context`` if available on Jinja2 >= 3.0, otherwise 22 | fallback to ``jinja2.contextfilter``. 23 | See https://github.com/Pylons/pyramid_jinja2/pull/164 24 | 25 | 2.9.1 (2022-03-12) 26 | ================== 27 | 28 | **This release has been yanked and is no longer available.** 29 | 30 | - Fix package metadata. No changes from 2.9. 31 | 32 | 2.9 (2022-03-12) 33 | ================ 34 | 35 | **This release has been yanked and is no longer available.** 36 | 37 | - Drop Python 3.6 support. 38 | 39 | - Add Python 3.9 and 3.10 support. 40 | 41 | - Refactor project structure to make wheel distribution smaller. 42 | 43 | - Blackify the codebase. 44 | 45 | - Use the newer ``@jinja2.pass_context`` in favor of the deprecated 46 | ``@jinja2.contextfilter``. 47 | See https://github.com/Pylons/pyramid_jinja2/pull/159 48 | 49 | 2.8 (2019-01-25) 50 | ================ 51 | 52 | - Drop Python 3.3 support. 53 | 54 | - Add Python 3.6 and 3.7 support. 55 | 56 | - Support the ``mapping`` argument in the injected ``gettext`` function. 57 | See https://github.com/Pylons/pyramid_jinja2/pull/143 58 | 59 | 2.7 (2016-11-15) 60 | ================ 61 | 62 | - Drop Python 2.6 and 3.2 support. 63 | 64 | - Add Python 3.5 support. 65 | 66 | - #123: Switch to pytest and pip for testing and installation in the 67 | documentation and scaffold. nose and coverage are still used in the core 68 | pyramid_jinja2 package [stevepiercy]. 69 | 70 | - Prefer ``resource_url`` over deprecated ``model_url``. Pyramid has changed 71 | its vocabulary, so let's reflect the changes. [Mikko Ohtamaa] 72 | 73 | - Support a dotted path to a gettext wrapper for the ``jinja2.i18n.gettext`` 74 | setting. [mmerickel] 75 | 76 | 2.6.2 (2016-01-23) 77 | ================== 78 | 79 | - Officially drop support for Python 3.2, test under Python 3.5 and pypy3 80 | [Domen Kozar] 81 | 82 | 2.6.1 (2016-01-20) 83 | ================== 84 | 85 | - Don't include .pyc in wheel file [Domen Kozar] 86 | 87 | 2.6 (2016-01-20) 88 | ================ 89 | 90 | - #116: Update scaffold to be consistent with Pyramid's default scaffolds 91 | [stevepiercy] 92 | 93 | 2.5 (2015-04-16) 94 | ================ 95 | 96 | - #106: Allow specifying a custom pyramid_jinja.i18n.GetTextWrapper [dstufft] 97 | 98 | 2.4 (2015-03-27) 99 | ================ 100 | 101 | - #105: Support ``jinja2.finalize`` configuration setting. [dstufft] 102 | 103 | - #94: Fix loading of templates with relative names on Windows [zart] 104 | 105 | - Support Python 3.4 [mmerickel] 106 | 107 | - Improve scaffold [marioidival] 108 | 109 | - #98: Avoid get_current_request where possible [housleyjk] 110 | 111 | - #99: Add resource_path filter [javex] 112 | 113 | 2.3.3 (2014-07-02) 114 | ================== 115 | 116 | - #91: Fix a recursion error while attempting to include a template with the 117 | same name as one of the parents that was already loaded. [mmerickel] 118 | 119 | 2.3.2 (2014-06-13) 120 | ================== 121 | 122 | - Fix 2.3.1 brownbag release. It had some erroneous didn't-mean-to-push 123 | changes that are now solved. Brought coverage back up to 100%. 124 | 125 | 2.3.1 (2014-06-13) 126 | ================== 127 | 128 | - Improve the template-relative searchpath logic to search more possibilities 129 | in the include-chain built up from templates including or extending 130 | other templates. The logic for when the chain was longer than just one 131 | template including another template was broken. 132 | 133 | 2.3 (2014-05-30) 134 | ================ 135 | 136 | - Require ``pyramid_jinja2`` to be included even when using 137 | ``pyramid_jinja2.renderer_factory``. It is now a thin wrapper around the 138 | default renderer and can be used to share the same settings with another 139 | file extension. [mmerickel] 140 | 141 | Backward Incompatible Changes 142 | ----------------------------- 143 | 144 | - ``pyramid_jinja2`` **must** be included into the ``Configurator`` in order 145 | to use ``pyramid_jinja2.renderer_factory`` otherwise you may see the 146 | exception:: 147 | 148 | ValueError: As of pyramid_jinja2 2.3, the use of the 149 | "pyramid_jinja2.renderer_factory" requires that pyramid_jinja2 be 150 | configured via config.include("pyramid_jinja2") or the equivalent 151 | "pyramid.includes" setting. 152 | 153 | The fix is to include ``pyramid_jinja2``:: 154 | 155 | config.include("pyramid_jinja2") 156 | 157 | 2.2 (2014-05-30) 158 | ================ 159 | 160 | - #88: Formalize template loading order and allow all lookups to fallback to 161 | the search path. A template is now always searched for relative to its 162 | parent template. If not found, the lookup will fallback to the search path. 163 | [mmerickel] 164 | 165 | - Add ``prepend`` option to ``config.add_jinja2_search_path`` to allow 166 | prepending of paths to the beginning of the search path if a path should 167 | override previously defined paths. [mmerickel] 168 | 169 | 2.1 (2014-05-16) 170 | ================ 171 | 172 | - The 2.0 series started adding the package that invoked 173 | ``config.add_jinja2_renderer`` to the template search path. This is 174 | being removed in favor of explicit search paths and will hopefully not 175 | affect many people as it has only been available for a couple weeks. The 176 | only automatic search path left is the one added by the default ``.jinja2`` 177 | renderer created when including ``pyramid_jinja2``. [mmerickel] 178 | 179 | - Adjust the ``config.include("pyramid_jinja2")`` to add any packages from 180 | ``jinja2.directories`` **before** the default search path at the base of 181 | the app. Previously there was no way to override that search path. 182 | [mmerickel] 183 | 184 | 2.0.2 (2014-05-06) 185 | ================== 186 | 187 | - The path of the child template is always considered when inheriting from 188 | a base template. Therefore when doing ``render("templates/foo.jinja2")`` 189 | and ``foo.jinja2`` has an ``{% extends "base.jinja2" %}``, the template 190 | will be searched for as ``"templates/base.jinja2"`` on the search path. 191 | Previously the path of the child template was ignored when doing the 192 | lookup for the base, causing some very subtle and unrecoverable lookup 193 | errors when the child template was found relative to the caller instead 194 | of being found on the search path. [mmerickel] 195 | 196 | - This release restores the default search path behaviors from the 1.x series 197 | that were inadvertently removed in the 2.x. The project's root package is 198 | added to the search path by default. [mmerickel] 199 | 200 | 2.0.1 (2014-04-23) 201 | ================== 202 | 203 | - #86: Fix a regression caused by the new support for extending a template 204 | relative to itself. Using ``{% extends "some_asset:spec.jinja2" %}`` was 205 | no longer working and is now fixed. [mmerickel] 206 | 207 | 208 | 2.0 (2014-04-21) 209 | ================ 210 | 211 | - Claim Python 3.4 support 212 | [mmerickel] 213 | 214 | - #75: Fix the missing piece of relative template loading by allowing a 215 | template to inherit from a template relative to itself, instead of 216 | forcing the parent to be on the search path. 217 | [mmerickel] 218 | 219 | - #73: Added a new ``config.add_jinja2_renderer`` API that can create and 220 | override multiple Jinja2 renderers, each loaded using potentially different 221 | settings and extensions. 222 | 223 | The other APIs are now keyed on the renderer extension, as each extension 224 | may have different settings. Thus ``config.add_jinja2_search_path``, 225 | ``config.add_jinja2_extension``, and ``config.get_jinja2_environment`` 226 | accept a ``name`` argument, which defaults to ``.jinja2``. 227 | 228 | This deprecates the old ``pyramid_jinja2.renderer_factory`` mechanism 229 | for adding renderers with alternate extensions. 230 | 231 | Configuration of the renderers has been updated to follow Pyramid's 232 | standard mechanisms for conflict detection. This means that if two modules 233 | both try to add a renderer for the ``.jinja2`` extension, they may raise a 234 | conflict or the modifications made by the invocation closest to the 235 | ``Configurator`` in the call-stack will win. This behavior can be affected 236 | by calling ``config.commit`` at appropriate times to force a configuration 237 | to take effect immediately. As such, configuration is deferred until 238 | commit-time, meaning that it is now possible 239 | ``config.get_jinja2_environment`` will return ``None`` because the changes 240 | have not yet been committed. 241 | [mmerickel] 242 | 243 | Backward Incompatible Changes 244 | ----------------------------- 245 | 246 | - The creation and configuration of the Jinja2 ``Environment`` is now deferred 247 | until commit-type in the Pyramid ``Configurator``. This means that 248 | ``config.get_jinja2_environment`` may return ``None``. To resolve this, 249 | invoke ``config.commit()`` before attempting to get the environment. 250 | 251 | 1.10 (2014-01-11) 252 | ================= 253 | 254 | - #77: Change semantics of ``jinja2.bytecode_caching`` setting. The new 255 | default is false (no bytecode caching) -- ``bytecode_caching`` must 256 | explicitly be set to true to enable a filesystem bytecode cache. 257 | In addition, an atexit callback to clean the cache is no longer 258 | registered (as this seemed to defeat most of the purpose of having 259 | a bytecode cache.) Finally, a more complex bytecode cache may be 260 | configured by setting ``jinja2.bytecode_caching`` directly to a 261 | ``jinja2.BytecodeCache`` instance. (This can not be done in a 262 | paste .ini file, it must be done programatically.) 263 | [dairiki] 264 | 265 | - prevent error when using `python setup.py bdist_wheel` 266 | [msabramo] 267 | 268 | 269 | 1.9 (2013-11-08) 270 | ================ 271 | 272 | - fix indentation level for Jinja2ProjectTemplate in scaffolds/__init__.py 273 | [Bruno Binet] 274 | 275 | - Remove unnecessary dependency on ``pyramid.interfaces.ITemplateRenderer`` 276 | which was deprecated in Pyramid 1.5. 277 | [mmerickel] 278 | 279 | - #68: Added `model_path_filter`, `route_path_filter` and `static_path_filter` filters 280 | [Remco] 281 | 282 | - #74: Fixed issue with route being converted as_const by jinja2 engine when using btyecode cache 283 | [Remco] 284 | 285 | 286 | 1.8 (2013-10-03) 287 | ================ 288 | 289 | - #70: Do not pin for py3.2 compatibility unless running under py3.2 290 | [dairiki] 291 | 292 | 293 | 1.7 (2013-08-07) 294 | ================ 295 | 296 | - #56: python3.3: Non-ASCII characters in changelog breaks pip installation 297 | [Domen Kozar] 298 | 299 | - #57: Remove useless warning: `DeprecationWarning: reload_templates setting 300 | is deprecated, use pyramid.reload_templates instead.` 301 | [Marc Abramowitz] 302 | 303 | 304 | 1.6 (2013-01-23) 305 | ================ 306 | 307 | - Set `jinja2.i18n.domain` default to the package name 308 | of the pyramid application. 309 | [Domen Kozar] 310 | 311 | - Add `jinja2.globals` setting to add global objects into 312 | the template context 313 | [Eugene Fominykh] 314 | 315 | - Add `jinja2.newstyle` setting to enable newstyle gettext calls 316 | [Thomas Schussler] 317 | 318 | 1.5 (2012-11-24) 319 | ================ 320 | 321 | - Add `pyramid.reload_templates` to set `jinja2.auto_reload` instead of 322 | using `reload_templates`. Deprecate the latter. 323 | [Domen Kozar] 324 | 325 | - Clear bytecode cache on atexit 326 | [Domen Kozar] 327 | 328 | - Add support for more Jinja2 options. Note support for jinja2.autoescape is 329 | limited to boolean only. 330 | 331 | * jinja2.block_start_string 332 | * jinja2.block_end_string 333 | * jinja2.variable_start_string 334 | * jinja2.variable_end_string 335 | * jinja2.comment_start_string 336 | * jinja2.comment_end_string 337 | * jinja2.line_statement_prefix 338 | * jinja2.line_comment_prefix 339 | * jinja2.trim_blocks 340 | * jinja2.newline_sequence 341 | * jinja2.optimized 342 | * jinja2.cache_size 343 | * jinja2.autoescape 344 | 345 | [Michael Ryabushkin] 346 | 347 | 1.4.2 (2012-10-17) 348 | ================== 349 | 350 | - Add `jinja2.undefined` setting to change handling of undefined types. 351 | [Robert Buchholz] 352 | 353 | - Remove redundant decoding error handling 354 | [Domen Kozar] 355 | 356 | - Configure bytecode caching by default. Introduce `jinja2.bytecode_caching` 357 | and `jinja2.bytecode_caching_directory` settings. 358 | [Domen Kozar] 359 | 360 | - Allow to add custom Jinja2 tests in `jinja2.tests` setting. 361 | [Sebastian Kalinowski] 362 | 363 | 1.4.1 (2012-09-12) 364 | ================== 365 | 366 | - Fix brown-bag release 367 | [Domen Kozar] 368 | 369 | 370 | 1.4 (2012-09-12) 371 | ================ 372 | 373 | - Correctly resolve relative search paths passed to ``add_jinja2_search_path`` 374 | and ``jinja2.directories`` 375 | [Domen Kozar] 376 | 377 | - #34: Don't recreate ``jinja2.Environment`` for ``add_jinja2_extension`` 378 | [Domen Kozar] 379 | 380 | - Drop Python 2.5 compatibility 381 | [Domen Kozar] 382 | 383 | - Addition of ``static_url`` filter. 384 | 385 | - Add ``dev`` and ``docs`` setup.py aliases (ala Pyramid). 386 | 387 | - Changed template loading relative to package calling the renderer so 388 | it works like the Chameleon template loader. 389 | 390 | 1.3 (2011-12-14) 391 | ================ 392 | 393 | - Make scaffolding compatible with Pyramid 1.3a2+. 394 | 395 | 1.2 (2011-09-27) 396 | ================ 397 | 398 | - Make tests pass on Pyramid 1.2dev. 399 | 400 | - Make compatible with Python 3.2 (requires Pyramid 1.3dev+). 401 | 402 | 1.1 (2011-07-24) 403 | ================ 404 | 405 | - Add ``get_jinja2_environment`` directive. 406 | 407 | - Add all configurator directives to documentation. 408 | 409 | 1.0 (2011-05-12) 410 | ================ 411 | 412 | - Message domain can now be specified with *jinja2.i18n.domain* for i18n 413 | 414 | - Paster template now sets up starter locale pot/po/mo files 415 | 416 | - pyramid_jinja2 now depends on Jinja2 >= 2.5.0 due to 417 | ``jinja2.Environment.install_gettext_callables`` use 418 | https://github.com/Pylons/pyramid_jinja2/pull/21 419 | 420 | - Added demo app just to visualize i18n work 421 | 422 | 0.6.2 (2011-04-06) 423 | ================== 424 | 425 | - ``jinja2.ext.i18n`` is now added by default, see ``i18n.rst`` 426 | for details 427 | 428 | - Added ``add_jinja2_extension`` directive to the Configurator 429 | 430 | - Updated jinja2.extensions parsing mechanism 431 | 432 | - Fixed docs to indicate using asset: prefix is no longer necessary 433 | 434 | 0.6.1 (2011-03-03) 435 | ================== 436 | 437 | - Asset-based loading now takes precedance and does not require 438 | "asset:" prefix 439 | 440 | - Fixed the "current" package mechanism of asset: loading so that 441 | it more accurately finds the current package 442 | 443 | - Dependency on ``pyramid_zcml`` removed. 444 | 445 | 0.6 (2011-02-15) 446 | ================ 447 | 448 | - Documentation overhauled. 449 | 450 | - Templates can now be looked up by asset spec completely bypassing 451 | the search path by specifying a prefix of ``asset:``. 452 | 453 | - Updated paster template to more closely relate to changes made 454 | to paster templmates in Pyramid core. 455 | 456 | - Add new directive ``add_jinja2_search_path`` to the configurator 457 | when ``includeme`` is used. 458 | 459 | 0.5 (2011-01-18) 460 | ================ 461 | 462 | - Add ``includeme`` function (meant to be used via ``config.include``). 463 | 464 | - Fix documentation bug related to ``paster create`` reported at 465 | https://github.com/Pylons/pyramid_jinja2/issues/12 466 | 467 | - Depend upon Pyramid 1.0a10 + (to make ZCML work). 468 | 469 | 0.4 (2010-12-16) 470 | ================ 471 | 472 | Paster Template 473 | --------------- 474 | 475 | - Changes to normalize with default templates shipping with Pyramid core: 476 | remove calls to ``config.begin()`` and ``config.end()`` from 477 | ``__init__.main``, entry point name changed to ``main``, entry 478 | ``__init__.py`` function name changed to ``main``, depend on WebError, use 479 | ``paster_plugins`` argument to setup function in setup.py, depend on 480 | Pyramid 1.0a6+ (use ``config`` rather than ``configurator``). 481 | 482 | Tests 483 | ----- 484 | 485 | - Use ``testing.setUp`` and ``testing.tearDown`` rather than constructing a 486 | Configurator (better fwd compat). 487 | 488 | Features 489 | -------- 490 | 491 | - Add ``model_url`` and ``route_url`` filter implementations (and 492 | documented). 493 | 494 | Documentation 495 | ------------- 496 | 497 | - Use Makefile which pulls in Pylons theme automagically. 498 | 499 | 0.3 (2010-11-26) 500 | ================ 501 | 502 | - Add ``jinja2.filters`` and ``jinja2.extensions`` settings (thanks to 503 | aodag). 504 | 505 | - Document all known settings. 506 | 507 | 0.2 (2010-11-06) 508 | ================ 509 | 510 | - Template autoreloading did not function, even if ``reload_templates`` was 511 | set to ``True``. 512 | 513 | 0.1 (2010-11-05) 514 | ================ 515 | 516 | - First release. *Not* backwards compatible with ``repoze.bfg.jinja2``: we 517 | use a filesystem loader (the directories to load from come from the 518 | ``jinja2.directories`` setting). No attention is paid to the current 519 | package when resolving a renderer= line. 520 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Pylons Project Contributor Agreement 2 | ==================================== 3 | 4 | The submitter agrees by adding his or her name within the section below named 5 | "Contributors" and submitting the resulting modified document to the 6 | canonical shared repository location for this software project (whether 7 | directly, as a user with "direct commit access", or via a "pull request"), he 8 | or she is signing a contract electronically. The submitter becomes a 9 | Contributor after a) he or she signs this document by adding their name 10 | beneath the "Contributors" section below, and b) the resulting document is 11 | accepted into the canonical version control repository. 12 | 13 | Treatment of Account 14 | --------------------- 15 | 16 | Contributor will not allow anyone other than the Contributor to use his or 17 | her username or source repository login to submit code to a Pylons Project 18 | source repository. Should Contributor become aware of any such use, 19 | Contributor will immediately notify Agendaless Consulting. 20 | Notification must be performed by sending an email to 21 | webmaster@agendaless.com. Until such notice is received, Contributor will be 22 | presumed to have taken all actions made through Contributor's account. If the 23 | Contributor has direct commit access, Agendaless Consulting will have 24 | complete control and discretion over capabilities assigned to Contributor's 25 | account, and may disable Contributor's account for any reason at any time. 26 | 27 | Legal Effect of Contribution 28 | ---------------------------- 29 | 30 | Upon submitting a change or new work to a Pylons Project source Repository (a 31 | "Contribution"), you agree to assign, and hereby do assign, a one-half 32 | interest of all right, title and interest in and to copyright and other 33 | intellectual property rights with respect to your new and original portions 34 | of the Contribution to Agendaless Consulting. You and Agendaless Consulting 35 | each agree that the other shall be free to exercise any and all exclusive 36 | rights in and to the Contribution, without accounting to one another, 37 | including without limitation, the right to license the Contribution to others 38 | under the Repoze Public License. This agreement shall run with title to the 39 | Contribution. Agendaless Consulting does not convey to you any right, title 40 | or interest in or to the Program or such portions of the Contribution that 41 | were taken from the Program. Your transmission of a submission to the Pylons 42 | Project source Repository and marks of identification concerning the 43 | Contribution itself constitute your intent to contribute and your assignment 44 | of the work in accordance with the provisions of this Agreement. 45 | 46 | License Terms 47 | ------------- 48 | 49 | Code committed to the Pylons Project source repository (Committed Code) must 50 | be governed by the Repoze Public License (see LICENSE.txt, aka "the RPL") 51 | or another license acceptable to Agendaless Consulting. Until 52 | Agendaless Consulting declares in writing an acceptable license other than 53 | the RPL, only the RPL shall be used. A list of exceptions is detailed within 54 | the "Licensing Exceptions" section of this document, if one exists. 55 | 56 | Representations, Warranty, and Indemnification 57 | ---------------------------------------------- 58 | 59 | Contributor represents and warrants that the Committed Code does not violate 60 | the rights of any person or entity, and that the Contributor has legal 61 | authority to enter into this Agreement and legal authority over Contributed 62 | Code. Further, Contributor indemnifies Agendaless Consulting against 63 | violations. 64 | 65 | Cryptography 66 | ------------ 67 | 68 | Contributor understands that cryptographic code may be subject to government 69 | regulations with which Agendaless Consulting and/or entities using Committed 70 | Code must comply. Any code which contains any of the items listed below must 71 | not be checked-in until Agendaless Consulting staff has been notified and has 72 | approved such contribution in writing. 73 | 74 | - Cryptographic capabilities or features 75 | 76 | - Calls to cryptographic features 77 | 78 | - User interface elements which provide context relating to cryptography 79 | 80 | - Code which may, under casual inspection, appear to be cryptographic. 81 | 82 | Notices 83 | ------- 84 | 85 | Contributor confirms that any notices required will be included in any 86 | Committed Code. 87 | 88 | Licensing Exceptions 89 | ==================== 90 | 91 | Code committed within the ``docs/`` subdirectory of the Pyramid source 92 | control repository and "docstrings" which appear in the documentation 93 | generated by runnning "make" within this directory is licensed under the 94 | Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States 95 | License (http://creativecommons.org/licenses/by-nc-sa/3.0/us/). 96 | 97 | List of Contributors 98 | ==================== 99 | 100 | The below-signed are contributors to a code repository that is part of the 101 | project named "pyramid_jinja2". Each below-signed contributor has read, 102 | understand and agrees to the terms above in the section within this document 103 | entitled "Pylons Project Contributor Agreement" as of the date beside his or 104 | her name. 105 | 106 | Contributors 107 | ------------ 108 | 109 | Chris McDonough, 2010/11/08 110 | 111 | Tres Seaver, 2010/11/09 112 | 113 | Blaise Laflamme, 2010/11/14 114 | 115 | Rocky Burt, 2011/02/13 116 | 117 | Kiall Mac Innes, 2012/05/13 118 | 119 | Domen Kožar, 2012/09/11 120 | 121 | Joao Paulo F Farias, 2012/09/11 122 | 123 | Sebastian Kalinowski, 2012/10/14 124 | 125 | Michael Ryabushkin, 2012/11/15 126 | 127 | Eugene Fominykh, 2013/01/16 128 | 129 | Thomas Schüßler, 2013/01/17 130 | 131 | Michael Merickel, 2013/10/19 132 | 133 | Remco Verhoef (nl5887), 2013/11/08 134 | 135 | Geoffrey Dairiki, 2013/11/22 136 | 137 | Steve Piercy, 2016/01/07 138 | 139 | Jonathan Vanasco, 2022/11/15 140 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Agendaless Consulting and Contributors. 2 | (http://www.agendaless.com), All Rights Reserved 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | License 2 | 3 | A copyright notice accompanies this license document that identifies 4 | the copyright holders. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | 1. Redistributions in source code must retain the accompanying 11 | copyright notice, this list of conditions, and the following 12 | disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the accompanying 15 | copyright notice, this list of conditions, and the following 16 | disclaimer in the documentation and/or other materials provided 17 | with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or 20 | promote products derived from this software without prior 21 | written permission from the copyright holders. 22 | 23 | 4. If any files are modified, you must cause the modified files to 24 | carry prominent notices stating that you changed the files and 25 | the date of any change. 26 | 27 | Disclaimer 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND 30 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 31 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 32 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 34 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 35 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 37 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 38 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 39 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 40 | SUCH DAMAGE. 41 | 42 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/pyramid_jinja2 2 | graft tests 3 | graft docs 4 | graft demo 5 | graft .github 6 | 7 | include README.rst 8 | include CHANGES.rst 9 | include LICENSE.txt 10 | include CONTRIBUTORS.txt 11 | include COPYRIGHT.txt 12 | 13 | include pyproject.toml 14 | include setup.cfg 15 | include .coveragerc 16 | include .flake8 17 | include tox.ini 18 | include .readthedocs.yaml 19 | 20 | prune docs/_build 21 | prune docs/_themes 22 | 23 | recursive-exclude * __pycache__ *.py[cod] 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Jinja2 bindings for Pyramid 2 | =========================== 3 | 4 | These are bindings for the `Jinja2 templating system `_ 5 | for the `Pyramid `_ web framework. 6 | 7 | See https://docs.pylonsproject.org/projects/pyramid-jinja2/en/latest/ for 8 | documentation or ``index.rst`` in the ``docs`` sub-directory of the source 9 | distribution. 10 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- 1 | from pyramid.config import Configurator 2 | from pyramid.i18n import TranslationStringFactory, get_locale_name, get_localizer 3 | 4 | _ = TranslationStringFactory("messages") 5 | 6 | 7 | def root_view(request): 8 | request.locale_name = "fr" 9 | localizer = get_localizer(request) 10 | return { 11 | "pyramid_translated": localizer.translate(_("Hello World")), 12 | "locale_name": get_locale_name(request), 13 | } 14 | 15 | 16 | def app(global_settings, **settings): 17 | config = Configurator(settings=settings) 18 | config.include("pyramid_jinja2") 19 | config.add_route(name="root", pattern="/") 20 | config.add_view(root_view, renderer="helloworld.jinja2") 21 | config.add_translation_dirs("demo:locale/") 22 | return config.make_wsgi_app() 23 | 24 | 25 | class Mainer(object): 26 | import wsgiref.simple_server 27 | 28 | make_server = wsgiref.simple_server.make_server 29 | 30 | def main(self): 31 | port = 8080 32 | app_config = {"DEBUG": True, "reload_templates": True} 33 | pyramid_app = app({}, **app_config) 34 | httpd = self.make_server("", port, pyramid_app) 35 | # Serve until process is killed 36 | httpd.serve_forever() 37 | 38 | 39 | main = Mainer().main 40 | 41 | if __name__ == "__main__": 42 | main() # pragma: nocover 43 | -------------------------------------------------------------------------------- /demo/helloworld.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo App 6 | 12 | 13 | 14 |
15 | Metadata 16 |
17 |
Locale Name
18 |
{{ locale_name }}
19 |
20 |
21 | 22 |
23 | I18N Demo 24 |
25 |
No Translation (english)
26 |
Hello World
27 |
Translated using Pyramid directly
28 |
{{ pyramid_translated }}
29 |
Translated using Jinja2
30 |
{% trans %}Hello World{% endtrans %}
31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /demo/locale/fr/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/demo/locale/fr/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /demo/locale/fr/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-05-12 09:14-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | msgid "Hello World" 21 | msgstr "Bonjour Monde" 22 | -------------------------------------------------------------------------------- /demo/locale/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-05-12 09:14-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | msgid "Hello World" 21 | msgstr "" 22 | -------------------------------------------------------------------------------- /demo/test_demo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pyramid.testing 4 | 5 | 6 | class DemoTests(unittest.TestCase): 7 | def test_root_view(self): 8 | from demo import root_view 9 | 10 | m = pyramid.testing.DummyRequest() 11 | root_view(m) 12 | self.assertEqual(m.locale_name, "fr") 13 | 14 | def test_app(self): 15 | from demo import app 16 | 17 | webapp = app({}) 18 | self.assertTrue(callable(webapp)) 19 | 20 | def test_main(self): 21 | from demo import Mainer 22 | 23 | class MyMainer(Mainer): 24 | def serve_forever(self): 25 | self.serving = True 26 | 27 | def make_server(self, *args, **kwargs): 28 | return Mock(args=args, kwargs=kwargs, serve_forever=self.serve_forever) 29 | 30 | mainer = MyMainer() 31 | mainer.main() 32 | self.assertTrue(getattr(mainer, "serving", False)) 33 | 34 | 35 | class Mock(object): 36 | def __init__(self, **kwargs): 37 | self.__dict__.update(kwargs) 38 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _themes/ 2 | _build/ 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " pickle to make pickle files (usable by e.g. sphinx-web)" 20 | @echo " htmlhelp to make HTML files and a HTML help project" 21 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 22 | @echo " changes to make an overview over all changed/added/deprecated items" 23 | @echo " linkcheck to check all external links for integrity" 24 | 25 | clean: 26 | -rm -rf _build/* 27 | 28 | html: 29 | mkdir -p _build/html _build/doctrees 30 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 31 | @echo 32 | @echo "Build finished. The HTML pages are in _build/html." 33 | 34 | text: 35 | mkdir -p _build/text _build/doctrees 36 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) _build/text 37 | @echo 38 | @echo "Build finished. The HTML pages are in _build/text." 39 | 40 | pickle: 41 | mkdir -p _build/pickle _build/doctrees 42 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 43 | @echo 44 | @echo "Build finished; now you can process the pickle files or run" 45 | @echo " sphinx-web _build/pickle" 46 | @echo "to start the sphinx-web server." 47 | 48 | web: pickle 49 | 50 | htmlhelp: 51 | mkdir -p _build/htmlhelp _build/doctrees 52 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 53 | @echo 54 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 55 | ".hhp project file in _build/htmlhelp." 56 | 57 | latex: 58 | mkdir -p _build/latex _build/doctrees 59 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 60 | cp _static/*.png _build/latex 61 | ./convert_images.sh 62 | cp _static/latex-warning.png _build/latex 63 | cp _static/latex-note.png _build/latex 64 | @echo 65 | @echo "Build finished; the LaTeX files are in _build/latex." 66 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 67 | "run these through (pdf)latex." 68 | 69 | changes: 70 | mkdir -p _build/changes _build/doctrees 71 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 72 | @echo 73 | @echo "The overview file is in _build/changes." 74 | 75 | linkcheck: 76 | mkdir -p _build/linkcheck _build/doctrees 77 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 78 | @echo 79 | @echo "Link check complete; look for any errors in the above output " \ 80 | "or in _build/linkcheck/output.txt." 81 | 82 | epub: 83 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) _build/epub 84 | @echo 85 | @echo "Build finished. The epub file is in _build/epub." 86 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _pyramid_jinja2_api: 2 | 3 | :mod:`pyramid_jinja2` API 4 | ------------------------- 5 | 6 | .. automodule:: pyramid_jinja2 7 | 8 | .. autofunction:: includeme 9 | 10 | .. autofunction:: add_jinja2_renderer 11 | 12 | .. autofunction:: add_jinja2_search_path 13 | 14 | .. autofunction:: add_jinja2_extension 15 | 16 | .. autofunction:: get_jinja2_environment 17 | 18 | .. autoclass:: Jinja2TemplateRenderer 19 | 20 | .. autoclass:: SmartAssetSpecLoader 21 | 22 | .. automodule:: pyramid_jinja2.i18n 23 | :members: -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. _changes: 2 | 3 | Changes 4 | ******* 5 | 6 | .. the ******-heading is used to override the ===== in CHANGES.rst 7 | 8 | .. include:: ../CHANGES.rst 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pyramid_jinja2 documentation build configuration file 2 | # 3 | # This file is execfile()d with the current directory set to its containing 4 | # dir. 5 | # 6 | # The contents of this file are pickled, so don't put values in the 7 | # namespace that aren't pickleable (module imports are okay, they're 8 | # removed automatically). 9 | # 10 | # All configuration values have a default value; values that are commented 11 | # out serve to show the default value. 12 | 13 | # If your extensions are in another directory, add it here. If the 14 | # directory is relative to the documentation root, use os.path.abspath to 15 | # make it absolute, like shown here. 16 | # sys.path.append(os.path.abspath("some/directory")) 17 | 18 | import datetime 19 | import pkg_resources 20 | import pylons_sphinx_themes 21 | 22 | # General configuration 23 | # --------------------- 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom ones. 27 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] 28 | 29 | # Add any paths that contain templates here, relative to this directory. 30 | templates_path = [".templates"] 31 | 32 | # The suffix of source filenames. 33 | source_suffix = ".rst" 34 | 35 | # The main toctree document. 36 | master_doc = "index" 37 | 38 | # General substitutions. 39 | project = "pyramid_jinja2" 40 | thisyear = datetime.datetime.now().year 41 | copyright = "2011-%s, Agendaless Consulting" % thisyear 42 | 43 | # The default replacements for |version| and |release|, also used in various 44 | # other places throughout the built documents. 45 | # 46 | # The short X.Y version. 47 | version = pkg_resources.get_distribution(project).version 48 | # The full version, including alpha/beta/rc tags. 49 | release = version 50 | 51 | # There are two options for replacing |today|: either, you set today to 52 | # some non-false value, then it is used: 53 | # today = "" 54 | # Else, today_fmt is used as the format for a strftime call. 55 | today_fmt = "%B %d, %Y" 56 | 57 | # List of documents that shouldn't be included in the build. 58 | # unused_docs = [] 59 | 60 | # List of directories, relative to source directories, that shouldn't be 61 | # searched for source files. 62 | # exclude_dirs = [] 63 | 64 | exclude_patterns = [ 65 | "_themes/README.rst", 66 | ] 67 | 68 | # The reST default role (used for this markup: `text`) to use for all 69 | # documents. 70 | # default_role = None 71 | 72 | # If true, "()" will be appended to :func: etc. cross-reference text. 73 | # add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | # add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | # show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | # pygments_style = "sphinx" 85 | 86 | 87 | # Options for HTML output 88 | # ----------------------- 89 | 90 | # Add and use Pylons theme 91 | html_theme_path = pylons_sphinx_themes.get_html_themes_path() 92 | html_theme = "pyramid" 93 | 94 | 95 | html_theme_options = {"github_url": "https://github.com/Pylons/pyramid_jinja2"} 96 | 97 | 98 | # The style sheet to use for HTML and HTML Help pages. A file of that name 99 | # must exist either in Sphinx' static/ path, or in one of the custom paths 100 | # given in html_static_path. 101 | # html_style = "repoze.css" 102 | 103 | # The name for this set of Sphinx documents. If None, it defaults to 104 | # " v documentation". 105 | # html_title = None 106 | 107 | # A shorter title for the navigation bar. Default is the same as 108 | # html_title. 109 | # html_short_title = None 110 | 111 | # The name of an image file (within the static path) to place at the top of 112 | # the sidebar. 113 | # html_logo = ".static/logo_hi.gif" 114 | 115 | # The name of an image file (within the static path) to use as favicon of 116 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 117 | # 32x32 pixels large. 118 | # html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) 121 | # here, relative to this directory. They are copied after the builtin 122 | # static files, so a file named "default.css" will overwrite the builtin 123 | # "default.css". 124 | # html_static_path = ["_static"] 125 | 126 | # If not "", a "Last updated on:" timestamp is inserted at every page 127 | # bottom, using the given strftime format. 128 | html_last_updated_fmt = "%b %d, %Y" 129 | 130 | # Do not use smart quotes. 131 | smartquotes = False 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | # Control display of sidebars and include ethical ads from RTD 135 | html_sidebars = { 136 | "**": [ 137 | "localtoc.html", 138 | "ethicalads.html", 139 | "relations.html", 140 | "sourcelink.html", 141 | "searchbox.html", 142 | ] 143 | } 144 | 145 | # Additional templates that should be rendered to pages, maps page names to 146 | # template names. 147 | # html_additional_pages = {} 148 | 149 | # If false, no module index is generated. 150 | # html_use_modindex = True 151 | 152 | # If false, no index is generated. 153 | # html_use_index = True 154 | 155 | # If true, the index is split into individual pages for each letter. 156 | # html_split_index = False 157 | 158 | # If true, the reST sources are included in the HTML build as 159 | # _sources/. 160 | # html_copy_source = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages 163 | # will contain a tag referring to it. The value of this option must 164 | # be the base URL from which the finished HTML is served. 165 | # html_use_opensearch = "" 166 | 167 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 168 | # html_file_suffix = "" 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = "atemplatedoc" 172 | 173 | # for cross referencing documentations 174 | intersphinx_mapping = { 175 | "jinja2": ("https://jinja.palletsprojects.com/en/latest/", None), 176 | "pyramid": ("https://docs.pylonsproject.org/projects/pyramid/en/latest/", None), 177 | "pyramid_zcml": ( 178 | "https://docs.pylonsproject.org/projects/pyramid_zcml/en/latest/", 179 | None, 180 | ), 181 | "pytest": ("https://docs.pytest.org/en/latest/", None), 182 | } 183 | 184 | 185 | # Options for LaTeX output 186 | # ------------------------ 187 | 188 | # The paper size ("letter" or "a4"). 189 | # latex_paper_size = "letter" 190 | 191 | # The font size ("10pt", "11pt" or "12pt"). 192 | # latex_font_size = "10pt" 193 | 194 | # Grouping the document tree into LaTeX files. List of tuples 195 | # (source start file, target name, title, 196 | # author, document class [howto/manual]). 197 | latex_documents = [ 198 | ( 199 | "index", 200 | "pyramid_jinja2.tex", 201 | "pyramid_jinja2 Documentation", 202 | "Pylons Project Developers", 203 | "manual", 204 | ), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the 208 | # top of the title page. 209 | # latex_logo = ".static/logo_hi.gif" 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are 212 | # parts, not chapters. 213 | # latex_use_parts = False 214 | 215 | # Additional stuff for the LaTeX preamble. 216 | # latex_preamble = "" 217 | 218 | # Documents to append as an appendix to all manuals. 219 | # latex_appendices = [] 220 | 221 | # If false, no module index is generated. 222 | # latex_use_modindex = True 223 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | Jinja2 10 | A `templating system written by Armin Ronacher 11 | `_. 12 | 13 | Pyramid 14 | A `web framework 15 | `_. 16 | 17 | Configurator 18 | :py:class:`pyramid.config.Configurator` 19 | 20 | pyramid_jinja2 21 | A set of bindings that make templates written for the :term:`Jinja2` 22 | templating system work under the :term:`Pyramid` web framework. 23 | 24 | Template Inheritance 25 | Allows you to build a base “skeleton” template that contains all the 26 | common elements of your site and defines blocks that child templates 27 | can override. See :ref:`Template Inheritance 28 | ` in the Jinja2 documentation. 29 | 30 | Asset Specification 31 | A string representing the path to a directory or file present in a 32 | Python module. See :ref:`Understanding Asset Specifications 33 | ` in the Pyramid documentation for 34 | more information. 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | pyramid_jinja2 3 | ============== 4 | 5 | .. _overview: 6 | 7 | Overview 8 | ======== 9 | 10 | :term:`pyramid_jinja2` is a set of bindings that make templates written for the 11 | :term:`Jinja2` templating system work under the :term:`Pyramid` web framework. 12 | 13 | 14 | .. _installation: 15 | 16 | Installation 17 | ============ 18 | 19 | Install using ``pip``, where ``$VENV`` is the path to a virtual environment. 20 | 21 | .. code-block:: bash 22 | 23 | $ $VENV/bin/pip install pyramid_jinja2 24 | 25 | 26 | .. _setup: 27 | 28 | Setup 29 | ===== 30 | 31 | .. note:: 32 | 33 | If you start a project from scratch, consider using the 34 | :ref:`project template ` which comes with a 35 | working setup and sensible defaults. 36 | 37 | There are multiple ways to make sure that ``pyramid_jinja2`` is active. 38 | All are completely equivalent: 39 | 40 | #) Use the :py:func:`~pyramid_jinja2.includeme` function via 41 | :py:meth:`~pyramid.config.Configurator.include`. 42 | 43 | .. code-block:: python 44 | 45 | config = Configurator() 46 | config.include("pyramid_jinja2") 47 | 48 | #) Add ``pyramid_jinja2`` to the list of your ``pyramid.includes`` in your 49 | :file:`.ini` settings file. 50 | 51 | .. code-block:: python 52 | 53 | pyramid.includes = 54 | pyramid_jinja2 55 | 56 | #) If you use :ref:`pyramid_zcml:index` instead of imperative configuration, 57 | ensure that some ZCML file with an analogue of the following contents is 58 | executed by your Pyramid application: 59 | 60 | .. code-block:: xml 61 | 62 | 63 | 64 | Once activated in any of these ways, the following happens: 65 | 66 | #) Files with the :file:`.jinja2` extension are considered to be 67 | :term:`Jinja2` templates and a :class:`jinja2.Environment` is registered 68 | to handle this extension. 69 | 70 | #) The :func:`pyramid_jinja2.add_jinja2_renderer` directive is added to the 71 | :term:`Configurator` instance. 72 | 73 | #) The :func:`pyramid_jinja2.add_jinja2_search_path` directive is added to 74 | the :term:`Configurator` instance. 75 | 76 | #) The :func:`pyramid_jinja2.add_jinja2_extension` directive is added to the 77 | :term:`Configurator` instance. 78 | 79 | #) The :func:`pyramid_jinja2.get_jinja2_environment` directive is added to the 80 | :term:`Configurator` instance. 81 | 82 | 83 | Preparing for distribution 84 | -------------------------- 85 | 86 | If you want to make sure your :file:`.jinja2` template files are included in 87 | your package's source distribution (e.g., when using ``python setup.py 88 | sdist``), add ``*.jinja2`` to your :file:`MANIFEST.in`: 89 | 90 | .. code-block:: text 91 | 92 | recursive-include yourapp *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.jinja2 *.js *.html *.xml 93 | 94 | 95 | Usage 96 | ===== 97 | 98 | Once ``pyramid_jinja2`` has been activated, :file:`.jinja2` templates can be 99 | used by the Pyramid rendering system. 100 | 101 | When used as the ``renderer`` argument of a view, the view must return a 102 | Python ``dict`` which will be passed into the template as the set of available 103 | variables. 104 | 105 | 106 | Template Lookup Mechanisms 107 | -------------------------- 108 | 109 | There are several ways to configure ``pyramid_jinja2`` to find your templates. 110 | 111 | 112 | Asset Specifications 113 | ~~~~~~~~~~~~~~~~~~~~ 114 | 115 | Templates may always be defined using an :term:`asset specification`. These 116 | are strings which define an absolute location of the template relative to 117 | some Python package. For example ``myapp.views:templates/home.jinja2``. These 118 | specifications are supported throughout Pyramid and provide a fool-proof way 119 | to find any supporting assets bundled with your application. 120 | 121 | Here's an example view configuration which uses an :term:`asset specification`: 122 | 123 | .. code-block:: python 124 | :linenos: 125 | 126 | @view_config(renderer="mypackage:templates/foo.jinja2") 127 | def hello_world(request): 128 | return {"a": 1} 129 | 130 | Asset specifications have some significant benefits in Pyramid, as they can be 131 | fully overridden. An add-on package can ship with code that renders using 132 | asset specifications. Later, another package can externally override the 133 | templates without having to actually modify the add-on in any way. See 134 | :ref:`pyramid:overriding_assets_section` for more information. 135 | 136 | 137 | Caller-Relative Template Lookup 138 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 139 | 140 | By default, templates are discovered relative to the caller's package. This 141 | means that if you define a view in a Python module, the templates would 142 | be found relative to the module's directory on the filesystem. 143 | 144 | Let's look at an example: 145 | 146 | .. code-block:: python 147 | :linenos: 148 | 149 | @view_config(renderer="templates/mytemplate.jinja2") 150 | def my_view(request): 151 | return {"foo": 1, "bar": 2} 152 | 153 | Imagine that the above code is in a ``myapp.admin.views`` module. The template 154 | would be relative to that module on the filesystem, as shown below: 155 | 156 | .. code-block:: text 157 | 158 | myapp 159 | |- __init__.py 160 | `- admin 161 | |- views.py 162 | `- templates 163 | |- base.jinja2 164 | `- mytemplate.jinja2 165 | 166 | Caller-relative lookup avoids naming collisions which can be common in a 167 | search path-based approach. 168 | 169 | A caller-relative template lookup is converted to a :term:`asset specification` 170 | underneath the hood. This means that it's almost always possible to override 171 | the actual template in an add-on package without having to fork the add-on 172 | itself. For example, the full asset specification for the view above would be 173 | ``myapp.admin.views:templates/mytemplate.jinja2``. This template, or the 174 | entire ``templates`` folder, may be overridden. 175 | 176 | .. code-block:: python 177 | 178 | config.override_asset( 179 | to_override="myapp.admin.views:templates/mytemplate.jinja2", 180 | override_with="yourapp:templates/sometemplate.jinja2", 181 | ) 182 | 183 | See :ref:`pyramid:overriding_assets_section` for more information. 184 | 185 | 186 | Search Path-Based Template Lookup 187 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 188 | 189 | When used outside of Pyramid, Jinja2's default lookup mechanism is a search 190 | path. To use a search path within Pyramid, simply define the 191 | ``jinja2.directories`` configuration setting, or use the 192 | :func:`~pyramid_jinja2.add_jinja2_search_path` configurator directive. 193 | 194 | Rendering :term:`Jinja2` templates with a search path is typically done as 195 | follows: 196 | 197 | .. code-block:: python 198 | 199 | @view_config(renderer="mytemplate.jinja2") 200 | def my_view(request): 201 | return {"foo": 1, "bar": 2} 202 | 203 | If ``mytemplate.jinja2`` is not found in the same directory as the module, 204 | then it will be searched for on the search path. We are now dependent on our 205 | configuration settings to tell us where the template may be located. Commonly 206 | a ``templates`` directory is created at the base of the package and the 207 | configuration file will include the following directive: 208 | 209 | .. code-block:: python 210 | 211 | jinja2.directories = mypkg:templates 212 | 213 | .. warning:: 214 | 215 | It is possible to specify a relative path to the templates folder, such 216 | as ``jinja2.directories = templates``. This folder will be found relative 217 | to the first package that includes ``pyramid_jinja2``, which will normally 218 | be the root of your application. It is always better to be explicit when 219 | in doubt. 220 | 221 | .. note:: 222 | 223 | The package that includes ``pyramid_jinja2`` will always be added 224 | to the search path (in most cases this is the top-level package in your 225 | application). This behavior may be deprecated or removed in the future. 226 | It is always better to specify your search path explicitly. 227 | 228 | 229 | Templates Including Templates 230 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 231 | 232 | :term:`Jinja2` allows :term:`template inheritance` as well as other mechanisms 233 | for templates to load each other. The lookup mechanisms supported in these 234 | cases include asset specifications, template-relative names, and normal 235 | template names found on the search path. The search path will always be 236 | consulted if a template cannot be found relative to the parent 237 | template. For example, if you had a template named ``templates/child.jinja2`` 238 | that wanted to extend ``templates/base.jinja2``, then it could use 239 | ``{% extends "base.jinja2" %}`` and locate the file relative to itself. 240 | Alternatively it could use ``{% extends "templates/base.jinja2" %}`` to find 241 | the template in a ``templates`` sub-folder rooted on the search path. The 242 | template-relative option will always override the search path. 243 | 244 | An example: 245 | 246 | .. code-block:: html+django 247 | :linenos: 248 | 249 | 250 | 251 | 252 | 253 | 254 | Hello World! 255 | 256 | 257 | 258 |
{% block content %}{% endblock %}
259 | 260 | 261 | 262 | .. code-block:: html+django 263 | :linenos: 264 | 265 | 266 | {% extends "templates/layout.jinja2" %} 267 | {% block content %} 268 |

Yes

269 |

270 | Some random paragraph. 271 |

272 | {% endblock %} 273 | 274 | For further information on :term:`Template Inheritance` in Jinja2 275 | templates please see :ref:`Template Inheritance ` 276 | in Jinja2 documentation. 277 | 278 | 279 | Adding or Overriding a Renderer 280 | ------------------------------- 281 | 282 | By default, only templates ending in the ``.jinja2`` file extension are 283 | supported. However, it is very easy to add support for alternative file 284 | extensions using the :func:`pyramid_jinja2.add_jinja2_renderer` directive. 285 | 286 | .. code-block:: python 287 | 288 | config.include("pyramid_jinja2") 289 | config.add_jinja2_renderer(".html") 290 | 291 | It would now be possible to use templates named ``foo.html`` and 292 | ``foo.jinja2``. Each renderer extension will use its own 293 | :class:`jinja2.Environment`. These alternative renderers can be extended at 294 | runtime using the ``name`` parameter to the other directives such as 295 | :func:`pyramid_jinja2.get_jinja2_environment`. 296 | 297 | .. code-block:: python 298 | 299 | config.include("pyramid_jinja2") 300 | config.add_jinja2_renderer(".html") 301 | config.add_jinja2_search_path("myapp:templates", name=".html") 302 | 303 | It is also possible to set up different renderers that use different search 304 | paths, configuration settings, and environments if necessary. This technique 305 | can come in handy when different defaults are required for rendering templates 306 | with different content types. For example, a plain text email body versus 307 | an HTML page. For this reason, :func:`pyramid_jinja2.add_jinja2_renderer` 308 | accepts an optional parameter ``settings_prefix`` which can point a renderer 309 | at a different group of settings. 310 | 311 | .. code-block:: python 312 | 313 | settings = { 314 | "jinja2.directories": "myapp:html_templates", 315 | "mail.jinja2.directories": "myapp:email_templates", 316 | } 317 | 318 | config = Configurator(settings=settings) 319 | config.include("pyramid_jinja2") 320 | config.add_jinja2_renderer(".email", settings_prefix="mail.jinja2.") 321 | 322 | Now ``foo.email`` will be rendered using the ``mail.jinja2.*`` settings. 323 | 324 | 325 | Internalization (i18n) 326 | ---------------------- 327 | 328 | When :term:`pyramid_jinja2` is included in a Pyramid application, either 329 | :ref:`jinja2.ext.i18n ` or the extension configured by 330 | ``jinja2.i18n_extension`` is automatically activated. 331 | 332 | Be sure to configure ``jinja2.i18n.domain`` according to ``setup.cfg`` domain 333 | settings. By default, ``jinja2.i18n.domain`` is set to the name of the 334 | package that included ``pyramid_jinja2``. If no package was found, it will use 335 | ``messages``. 336 | 337 | 338 | .. _settings: 339 | 340 | Settings 341 | ======== 342 | 343 | :term:`Jinja2` derives additional settings to configure its template renderer. 344 | Many of these settings are optional and only need to be set if they should be 345 | different from the default. The below values can be present in the 346 | :file:`.ini` file used to configure the Pyramid application (in the ``app`` 347 | section representing your Pyramid app) or they can be passed directly within 348 | the ``settings`` argument passed to a Pyramid Configurator. 349 | 350 | 351 | Generic Settings 352 | ---------------- 353 | 354 | These settings correspond to the ones documented in Jinja2. Set them 355 | accordingly. 356 | 357 | For reference please see: http://jinja.pocoo.org/docs/api/#high-level-api 358 | 359 | .. note:: 360 | 361 | For the boolean settings, use ``true`` or ``false``. 362 | 363 | jinja2.block_start_string 364 | 365 | jinja2.block_end_string 366 | 367 | jinja2.variable_start_string 368 | 369 | jinja2.variable_end_string 370 | 371 | jinja2.comment_start_string 372 | 373 | jinja2.comment_end_string 374 | 375 | jinja2.line_statement_prefix 376 | 377 | jinja2.line_comment_prefix 378 | 379 | jinja2.trim_blocks 380 | 381 | jinja2.newline_sequence 382 | 383 | jinja2.optimized 384 | 385 | jinja2.cache_size 386 | 387 | 388 | jinja2.autoescape 389 | ----------------- 390 | 391 | Jinja2 autoescape setting. 392 | 393 | Possible values: ``true`` or ``false``. 394 | 395 | .. warning:: 396 | 397 | By default Jinja2 sets ``autoescape`` to ``False``. 398 | 399 | pyramid_jinja2 sets it to ``True`` as it is considered a good security 400 | practice in a web setting where we want to prevent XSS attacks from 401 | rendering unsanitized user-generated content. To turn off escaping 402 | on a case-by-case basis, you may use the ``safe`` filter such as 403 | ``{{ html_blob|safe }}``. 404 | 405 | 406 | .. _setting_reload_templates: 407 | 408 | pyramid.reload_templates 409 | ------------------------ 410 | 411 | For usage see :ref:`Pyramid: Automatically Reloading Templates 412 | `. 413 | 414 | ``True`` or ``False`` represent whether Jinja2 templates should be reloaded 415 | when they change on disk. In development, it is useful to set it to ``True``. 416 | This setting sets the Jinja2 ``auto_reload`` setting. 417 | 418 | 419 | reload_templates 420 | ---------------- 421 | 422 | .. warning:: 423 | 424 | Deprecated as of version 1.5, use :ref:`setting_reload_templates` instead. 425 | 426 | 427 | .. _setting_jinja2_autoreload: 428 | 429 | jinja2.auto_reload 430 | ------------------ 431 | 432 | Use Pyramid :ref:`setting_reload_templates` setting. 433 | 434 | .. _setting_jinja2_directories: 435 | 436 | 437 | jinja2.directories 438 | ------------------ 439 | 440 | A list of directory names, or a newline-delimited string, where each line 441 | represents a directory name. These locations are where Jinja2 will search for 442 | templates. Each can optionally be an absolute resource specification (e.g., 443 | ``package:subdirectory/``). 444 | 445 | 446 | .. _setting_jinja2_input_encoding: 447 | 448 | jinja2.input_encoding 449 | --------------------- 450 | 451 | The input encoding of templates. Defaults to ``utf-8``. 452 | 453 | 454 | .. _setting_jinja2_undefined: 455 | 456 | jinja2.undefined 457 | ---------------- 458 | 459 | Changes the undefined types that are used when a variable name lookup fails. 460 | If unset, defaults to :py:class:`~jinja2.Undefined` (silent ignore). Setting 461 | it to ``strict`` will trigger :py:class:`~jinja2.StrictUndefined` behavior 462 | (which raises an error, and is recommended for development). Setting it to 463 | ``debug`` will trigger :py:class:`~jinja2.DebugUndefined`, which outputs 464 | debug information in some cases. See `Undefined Types 465 | `_. 466 | 467 | 468 | .. _setting_jinja2_extensions: 469 | 470 | jinja2.extensions 471 | ----------------- 472 | 473 | A list of extension objects, or a newline-delimited set of dotted import 474 | locations, where each line represents an extension. Either :ref:`jinja2.ext.i18n 475 | ` or the i18n extension configured using 476 | ``jinja2.i18n_extension`` is automatically activated. 477 | 478 | 479 | .. _setting_jinja2_i18n_extension: 480 | 481 | jinja2.i18n_extension 482 | --------------------- 483 | 484 | The name of the i18n extension to activate. Defaults to 485 | :ref:`jinja2.ext.i18n `. 486 | 487 | 488 | .. _setting_jinja2_i18n_domain: 489 | 490 | jinja2.i18n.domain 491 | ------------------ 492 | 493 | Pyramid domain for translations. See :term:`pyramid:Translation Domain` in the 494 | Pyramid documentation. Defaults to the name of the package that activated 495 | `pyramid_jinja2` or if that fails it will use ``messages`` as the domain. 496 | 497 | 498 | .. _setting_jinja2_i18n_gettext: 499 | 500 | jinja2.i18n.gettext 501 | ------------------- 502 | 503 | A subclass of :class:`pyramid_jinja2.i18n.GetTextWrapper` to override 504 | ``gettext`` and ``ngettext`` methods in Jinja i18n extension. The Subclass can 505 | be either a dotted name or the subclass itself. 506 | 507 | 508 | .. _setting_jinja2_filers: 509 | 510 | jinja2.filters 511 | -------------- 512 | 513 | A dictionary mapping a filter name to a filter object, or a newline-delimited 514 | string with each line in the format: 515 | 516 | .. code-block:: python 517 | 518 | name = dotted.name.to.filter 519 | 520 | representing :ref:`Jinja2 filters `. 521 | 522 | 523 | .. _setting_jinja2_globals: 524 | 525 | jinja2.globals 526 | -------------- 527 | 528 | A dictionary mapping a global name to a global template object, or a 529 | newline-delimited string with each line in the format: 530 | 531 | .. code-block:: python 532 | 533 | name = dotted.name.to.globals 534 | 535 | representing :ref:`Jinja2 globals ` 536 | 537 | 538 | .. _setting_jinja2_tests: 539 | 540 | jinja2.tests 541 | ------------ 542 | 543 | A dictionary mapping a test name to a test object, or a newline-delimited 544 | string with each line in the format: 545 | 546 | .. code-block:: python 547 | 548 | name = dotted.name.to.test 549 | 550 | representing :ref:`Jinja2 tests `. 551 | 552 | 553 | .. _setting_jinja2_byte_cache: 554 | 555 | jinja2.bytecode_caching 556 | ----------------------- 557 | 558 | If set to ``true``, a file system bytecode cache will be configured in a 559 | directory determined by :ref:`setting_jinja2_byte_cache_dir`. To configure 560 | other types of bytecode caching, ``jinja2.bytecode_caching`` may also be set 561 | directly to an instance of :class:`jinja2.BytecodeCache`. However doing so 562 | cannot be done in a paste ``.ini`` file and it must be done programmatically. 563 | By default, no bytecode cache is configured. 564 | 565 | .. versionchanged:: 1.10 566 | 567 | Previously, ``jinja2.bytecode_caching`` defaulted to ``true``. 568 | 569 | Note that configuring a filesystem bytecode cache will (not surprisingly) 570 | generate files in the cache directory. As templates are changed, some of these 571 | will become stale, pointless wastes of disk space. You are advised to consider 572 | a clean up strategy (such as a cron job) to check for and remove such files. 573 | 574 | See the :ref:`Jinja2 Documentation ` for more 575 | information on bytecode caching. 576 | 577 | .. versionchanged:: 1.10 578 | 579 | Previously, an ``atexit`` callback which called 580 | :py:meth:`jinja2.BytecodeCache.clear` was registered in an effort 581 | to delete the cache files. This is no longer done. 582 | 583 | 584 | .. _setting_jinja2_byte_cache_dir: 585 | 586 | jinja2.bytecode_caching_directory 587 | --------------------------------- 588 | 589 | Absolute path to directory to store bytecode cache files. Defaults to the 590 | system temporary directory. This is only used if ``jinja2.bytecode_caching`` is 591 | set to ``true``. 592 | 593 | 594 | .. _setting_jinja2_newstyle: 595 | 596 | jinja2.newstyle 597 | --------------- 598 | 599 | ``true`` or ``false`` to enable the use of ``newstyle`` ``gettext`` calls. 600 | Defaults to ``false``. 601 | 602 | See :ref:`jinja2:newstyle-gettext`. 603 | 604 | 605 | .. _setting_jinja2_finalize: 606 | 607 | jinja2.finalize 608 | --------------- 609 | 610 | A callable or a dotted-import string. 611 | 612 | 613 | .. _jinja2_filters: 614 | 615 | Jinja2 Filters 616 | ============== 617 | 618 | ``pyramid_jinja2`` comes with Pyramid routing specific filters. All Jinja2 619 | built-in filters are enabled in templates. Read how :ref:`jinja2:filters` work 620 | in Jinja2. 621 | 622 | 623 | Installing filters 624 | ------------------ 625 | 626 | To use these filters, configure the settings of ``jinja2.filters``: 627 | 628 | .. code-block:: ini 629 | :linenos: 630 | 631 | [app:yourapp] 632 | # ... other stuff ... 633 | jinja2.filters = 634 | model_url = pyramid_jinja2.filters:model_url_filter 635 | route_url = pyramid_jinja2.filters:route_url_filter 636 | static_url = pyramid_jinja2.filters:static_url_filter 637 | 638 | 639 | Filter reference 640 | ---------------- 641 | 642 | .. currentmodule:: pyramid_jinja2.filters 643 | .. autofunction:: resource_url_filter 644 | .. autofunction:: model_url_filter 645 | .. autofunction:: route_url_filter 646 | .. autofunction:: static_url_filter 647 | .. autofunction:: model_path_filter 648 | .. autofunction:: route_path_filter 649 | .. autofunction:: static_path_filter 650 | 651 | 652 | .. _jinja2_starter_template: 653 | 654 | Creating a Jinja2 Pyramid project 655 | ================================= 656 | 657 | After you have installed ``pyramid_jinja2``, you can invoke the following 658 | command to create a Jinja2-based Pyramid project from its included scaffold. 659 | 660 | .. code-block:: bash 661 | 662 | $ $VENV/bin/pcreate -s pyramid_jinja2_starter myproject 663 | 664 | After it's created, you can visit the ``myproject`` directory and install the 665 | project in development mode. 666 | 667 | .. code-block:: bash 668 | 669 | $ cd myproject 670 | $ $VENV/bin/pip install -e . 671 | 672 | At this point you can start the application like any other Pyramid application. 673 | 674 | .. code-block:: bash 675 | 676 | $ $VENV/bin/pserve development.ini 677 | 678 | This is a good way to see a working Pyramid application that uses Jinja2, even 679 | if you do not end up using the result. 680 | 681 | .. seealso:: See also :ref:`pyramid:project_narr`. 682 | 683 | 684 | Running tests for your application 685 | ---------------------------------- 686 | 687 | The scaffold provides a convenience for the developer to install ``pytest`` and 688 | ``pytest-cov`` as the test runner and test coverage. To run unit tests for your 689 | application, you must first install the testing dependencies. 690 | 691 | .. code-block:: bash 692 | 693 | $ $VENV/bin/pip install -e ".[testing]" 694 | 695 | Once the testing requirements are installed, then you can run the tests using 696 | the ``py.test`` command that was just installed in the ``bin`` directory of 697 | your virtual environment. The ``-q`` option means "quiet" output, and the 698 | ``--cov`` option includes test coverage. 699 | 700 | .. code-block:: bash 701 | 702 | $ $VENV/bin/py.test -q --cov 703 | 704 | The scaffold includes configuration defaults for ``py.test`` and test coverage. 705 | These configuration files are ``pytest.ini`` and ``.coveragerc``, located at 706 | the root of your package. Without these defaults, we would need to specify the 707 | path to the module on which we want to run tests and coverage. 708 | 709 | .. code-block:: bash 710 | 711 | $ $VENV/bin/py.test -q --cov=myproject myproject/tests.py 712 | 713 | .. seealso:: See py.test's documentation for :ref:`pytest:usage` or invoke 714 | ``py.test -h`` to see its full set of options. 715 | 716 | 717 | pcreate template i18n 718 | --------------------- 719 | 720 | The pcreate template automatically sets up pot/po/mo locale files for use with 721 | the generated project. 722 | 723 | The usual pattern for working with i18n in pyramid_jinja2 is as follows: 724 | 725 | .. code-block:: bash 726 | 727 | # make sure Babel is installed 728 | $ $VENV/bin/pip install Babel 729 | 730 | # extract translatable strings from *.jinja2 / *.py 731 | $ $VENV/bin/python setup.py extract_messages 732 | $ $VENV/bin/python setup.py update_catalog 733 | 734 | # Translate strings in /locale//LC_MESSAGES/.po 735 | # and re-compile *.po files 736 | $ $VENV/bin/python setup.py compile_catalog 737 | 738 | If you see the following output: 739 | 740 | .. code-block:: text 741 | 742 | running compile_catalog 743 | 1 of 1 messages (100%) translated in myproject/locale/de/LC_MESSAGES/myproject.po 744 | catalog myproject/locale/de/LC_MESSAGES/myproject.po is marked as fuzzy, skipping 745 | 1 of 1 messages (100%) translated in myproject/locale/fr/LC_MESSAGES/myproject.po 746 | catalog myproject/locale/fr/LC_MESSAGES/myproject.po is marked as fuzzy, skipping 747 | 748 | When an item is marked as fuzzy, then you should review your `.po` files to 749 | make sure translations are correct. Fuzzy is not exact matching, but matches 750 | most of a word (its root) or phrase. 751 | 752 | When you are satisfied that the translations are good, you can either remove 753 | the line marked with `#, fuzzy` immediately above its related `msgid` line 754 | (preferred) or force Babel to compile the message catalog with the `-f` flag. 755 | 756 | .. code-block:: bash 757 | 758 | $ $VENV/bin/python setup.py compile_catalog -f 759 | 760 | Assuming you have already created a project following the instructions under 761 | :ref:`jinja2_starter_template`, and started your application with ``pserve``, 762 | then you should be able to view the various translations. Simply append a GET 763 | parameter, such as http://localhost:6543/?_LOCALE_=de for German, 764 | http://localhost:6543/?_LOCALE_=fr for French, or 765 | http://localhost:6543/?_LOCALE_=en for English. The default language does not 766 | require GET parameter. 767 | 768 | The application could set the user's language preference with a cookie based on 769 | request parameters sent on the first request. Alternatively, and usually as a 770 | fallback, the application could read the web browser's `Accept-Language` header 771 | sent with each request and set the appropriate language. For example: 772 | 773 | .. code-block:: python 774 | 775 | @subscriber(NewRequest) 776 | def prepare_env(event): 777 | request = event.request 778 | # set locale depending on browser settings 779 | settings = request.registry.settings 780 | locale = settings.get("pyramid.default_locale_name", "en") 781 | available = [loc["code"] for loc in AVAILABLE_LOCALES] 782 | if request.accept_language: 783 | accepted = request.accept_language 784 | locale = accepted.best_match(available, locale) 785 | request._LOCALE_ = locale 786 | 787 | 788 | More Information 789 | ================ 790 | 791 | .. toctree:: 792 | :maxdepth: 1 793 | 794 | api.rst 795 | changes.rst 796 | glossary.rst 797 | 798 | 799 | Reporting Bugs / Development Versions 800 | ===================================== 801 | 802 | Visit https://github.com/Pylons/pyramid_jinja2 to download development or tagged 803 | versions. 804 | 805 | Visit https://github.com/Pylons/pyramid_jinja2/issues to report bugs. 806 | 807 | 808 | Indices and tables 809 | ------------------ 810 | 811 | * :ref:`glossary` 812 | * :ref:`genindex` 813 | * :ref:`modindex` 814 | * :ref:`search` 815 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 41"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | target-version = ['py37', 'py38', 'py39', 'py310'] 7 | exclude = ''' 8 | /( 9 | \.git 10 | | .tox 11 | )/ 12 | ''' 13 | 14 | [tool.isort] 15 | profile = "black" 16 | multi_line_output = 3 17 | src_paths = ["src", "tests"] 18 | skip_glob = ["docs/*"] 19 | include_trailing_comma = true 20 | force_grid_wrap = false 21 | combine_as_imports = true 22 | line_length = 88 23 | force_sort_within_sections = true 24 | default_section = "THIRDPARTY" 25 | known_first_party = "pyramid_jinja2" 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyramid_jinja2 3 | version = 2.10.1 4 | description = Jinja2 template bindings for the Pyramid web framework 5 | long_description = file: README.rst, CHANGES.rst 6 | long_description_content_type = text/x-rst 7 | keywords = web wsgi pylons pyramid jinja2 8 | license = BSD-derived (Repoze) 9 | license_files = 10 | LICENSE.txt 11 | classifiers = 12 | Development Status :: 6 - Mature 13 | Intended Audience :: Developers 14 | Programming Language :: Python 15 | Programming Language :: Python :: 3 16 | Programming Language :: Python :: 3.7 17 | Programming Language :: Python :: 3.8 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | Programming Language :: Python :: 3.12 22 | Framework :: Pyramid 23 | License :: Repoze Public License 24 | url = https://github.com/Pylons/pyramid_jinja2 25 | project_urls = 26 | Documentation = https://docs.pylonsproject.org/projects/pyramid_jinja2/en/latest/ 27 | Changelog = https://github.com/Pylons/pyramid_jinja2/blob/main/CHANGES.rst 28 | Issue Tracker = https://github.com/Pylons/pyramid_jinja2/issues 29 | 30 | author = Rocky Burt 31 | author_email = pylons-discuss@googlegroups.com 32 | maintainer = Pylons Project 33 | maintainer_email = pylons-discuss@googlegroups.com 34 | 35 | [options] 36 | package_dir= 37 | =src 38 | packages = find_namespace: 39 | include_package_data = True 40 | python_requires = >=3.7.0 41 | install_requires = 42 | jinja2>=2.5.0,!=2.11.0,!=2.11.1,!=2.11.2 43 | markupsafe 44 | pyramid>=1.3.0 # pyramid.path.DottedNameResolver 45 | zope.deprecation 46 | 47 | [options.packages.find] 48 | where = src 49 | 50 | [options.entry_points] 51 | pyramid.scaffold = 52 | pyramid_jinja2_starter = pyramid_jinja2.scaffolds:Jinja2ProjectTemplate 53 | 54 | [options.extras_require] 55 | testing = 56 | webtest 57 | coverage 58 | pytest>=5.4.2 59 | pytest-cov 60 | 61 | docs = 62 | setuptools # needed for pkg_resources in conf.py 63 | pylons-sphinx-themes >= 0.3 64 | sphinx>=1.7.5 65 | 66 | [check-manifest] 67 | ignore-bad-ideas = *.mo 68 | 69 | [tool:pytest] 70 | python_files = test_*.py 71 | testpaths = 72 | demo 73 | tests 74 | addopts = -W always --cov 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import posixpath 4 | import sys 5 | 6 | from jinja2 import Environment as _Jinja2Environment 7 | from jinja2.exceptions import TemplateNotFound 8 | from jinja2.loaders import FileSystemLoader 9 | from jinja2.utils import open_if_exists 10 | from pyramid.path import AssetResolver, DottedNameResolver 11 | from zope.deprecation import deprecated 12 | from zope.interface import Interface 13 | 14 | from .settings import ( 15 | parse_env_options_from_settings, 16 | parse_loader_options_from_settings, 17 | parse_multiline, 18 | ) 19 | 20 | ENV_CONFIG_PHASE = 0 21 | EXTRAS_CONFIG_PHASE = 1 22 | PARENT_RELATIVE_DELIM = "@@FROM_PARENT@@" 23 | 24 | 25 | class IJinja2Environment(Interface): 26 | pass 27 | 28 | 29 | class Environment(_Jinja2Environment): 30 | def join_path(self, uri, parent): 31 | if os.path.isabs(uri) or ":" in uri: 32 | # we have an asset spec or absolute path 33 | return uri 34 | 35 | # uri may be relative to the parent, shuffle it through to the loader 36 | return uri + PARENT_RELATIVE_DELIM + parent 37 | 38 | 39 | class FileInfo(object): 40 | open_if_exists = staticmethod(open_if_exists) 41 | getmtime = staticmethod(os.path.getmtime) 42 | 43 | def __init__(self, filename, encoding="utf-8"): 44 | self.filename = filename 45 | self.encoding = encoding 46 | 47 | def _delay_init(self): 48 | if "_mtime" in self.__dict__: 49 | return 50 | 51 | f = self.open_if_exists(self.filename) 52 | if f is None: 53 | raise TemplateNotFound(self.filename) 54 | self._mtime = self.getmtime(self.filename) 55 | 56 | try: 57 | data = f.read() 58 | finally: 59 | f.close() 60 | 61 | if not isinstance(data, str): 62 | data = data.decode(self.encoding) 63 | self._contents = data 64 | 65 | @property 66 | def contents(self): 67 | self._delay_init() 68 | return self._contents 69 | 70 | @property 71 | def mtime(self): 72 | self._delay_init() 73 | return self._mtime 74 | 75 | def uptodate(self): 76 | try: 77 | return os.path.getmtime(self.filename) == self.mtime 78 | except OSError: 79 | return False 80 | 81 | 82 | class _PackageFinder(object): 83 | inspect = staticmethod(inspect) 84 | 85 | def caller_package(self, excludes=()): 86 | """A list of excluded patterns, optionally containing a `.` suffix. 87 | For example, ``'pyramid.'`` would exclude exclude ``'pyramid.config'`` 88 | but not ``'pyramid'``. 89 | """ 90 | f = None 91 | for t in self.inspect.stack(): 92 | f = t[0] 93 | name = f.f_globals.get("__name__") 94 | if name: 95 | excluded = False 96 | for pattern in excludes: 97 | if pattern[-1] == "." and name.startswith(pattern): 98 | excluded = True 99 | break 100 | elif name == pattern: 101 | excluded = True 102 | break 103 | if not excluded: 104 | break 105 | 106 | if f is None: 107 | return None 108 | 109 | pname = f.f_globals.get("__name__") or "__main__" 110 | m = sys.modules[pname] 111 | f = getattr(m, "__file__", "") 112 | if ("__init__.py" in f) or ("__init__$py" in f): # empty at >>> 113 | return m 114 | 115 | pname = m.__name__.rsplit(".", 1)[0] 116 | 117 | return sys.modules[pname] 118 | 119 | 120 | _caller_package = _PackageFinder().caller_package 121 | 122 | 123 | class SmartAssetSpecLoader(FileSystemLoader): 124 | """A Jinja2 template loader that knows how to handle 125 | asset specifications. 126 | """ 127 | 128 | def __init__(self, searchpath=(), encoding="utf-8", debug=False): 129 | FileSystemLoader.__init__(self, searchpath, encoding) 130 | self.debug = debug 131 | 132 | def list_templates(self): 133 | raise TypeError("this loader cannot iterate over all templates") 134 | 135 | def _get_absolute_source(self, template): 136 | filename = AssetResolver().resolve(template).abspath() 137 | fi = FileInfo(filename, self.encoding) 138 | if os.path.isfile(fi.filename): 139 | return fi.contents, fi.filename, fi.uptodate 140 | 141 | def _relative_searchpath(self, chain): 142 | """Combine paths in the chain to construct search paths. 143 | 144 | The precedence is for the most-specific paths to be tested first, 145 | anchored at an absolute path or asset spec. From there, less-specific 146 | paths are tested. 147 | 148 | For example:: 149 | chain = [ 150 | '../forms.jinja2', 'sub/nav.jinja2', 151 | 'base.jinja2', 'myapp:templates/index.jinja2', 152 | ] 153 | searchpath = ['myapp:templates/sub/..', 'sub/..', '..', ''] 154 | """ 155 | # the initial empty string is important because not only does it allow 156 | # the stack to always contain something join, but it allows the 157 | # later for-loops to fallback to the original search path by 158 | # joining to an empty string since os.path.join('', 'foo') == 'foo' 159 | stack = [""] 160 | for path in chain: 161 | is_abspath = os.path.isabs(path) 162 | is_spec = not is_abspath and ":" in path 163 | 164 | if not is_abspath and is_spec: 165 | ppkg, ppath = path.split(":", 1) 166 | path = "{0}:{1}".format(ppkg, posixpath.dirname(ppath)) 167 | else: 168 | # this should split windows and posix paths 169 | path = os.path.dirname(path) 170 | 171 | if not path: 172 | # skip empty directories 173 | continue 174 | 175 | subpath = stack[-1] 176 | path = posixpath.join(path, subpath) 177 | stack.append(path) 178 | 179 | # do not continue further, all paths are relative to this 180 | if is_abspath or is_spec: 181 | break 182 | return list(reversed(stack)) 183 | 184 | def get_source(self, environment, template): 185 | # keep legacy asset: prefix checking that bypasses 186 | # source path checking altogether 187 | if template.startswith("asset:"): 188 | template = template[6:] 189 | 190 | # split the template into the chain of relative-imports 191 | rel_chain = template.split(PARENT_RELATIVE_DELIM) 192 | template, rel_chain = rel_chain[0], rel_chain[1:] 193 | 194 | # load the template directly if it's an absolute path or asset spec 195 | if os.path.isabs(template) or ":" in template: 196 | src = self._get_absolute_source(template) 197 | if src is not None: 198 | return src 199 | else: 200 | # fallback to the search path just incase 201 | return FileSystemLoader.get_source(self, environment, template) 202 | 203 | # try to import the template as an asset spec or absolute path 204 | # relative to its parents 205 | rel_searchpath = self._relative_searchpath(rel_chain) 206 | for parent in rel_searchpath: 207 | if os.path.isabs(parent): 208 | uri = os.path.join(parent, template) 209 | # avoid recursive includes 210 | if uri not in rel_chain: 211 | src = self._get_absolute_source(uri) 212 | if src is not None: 213 | return src 214 | # avoid doing "':' in" and then redundant "split" 215 | parts = parent.split(":", 1) 216 | if len(parts) > 1: 217 | # parent is an asset spec 218 | ppkg, ppath = parts 219 | ppath = posixpath.join(ppath, template) 220 | uri = "{0}:{1}".format(ppkg, ppath) 221 | # avoid recursive includes 222 | if uri not in rel_chain: 223 | src = self._get_absolute_source(uri) 224 | if src is not None: 225 | return src 226 | 227 | # try to load the template from the default search path 228 | for parent in rel_searchpath: 229 | try: 230 | uri = os.path.join(parent, template) 231 | # avoid recursive includes 232 | if uri not in rel_chain: 233 | return FileSystemLoader.get_source(self, environment, uri) 234 | except TemplateNotFound: 235 | pass 236 | 237 | # we're here because of an exception during the last step so extend 238 | # the message and raise an appropriate error 239 | # there should always be an exception because the rel_searchpath is 240 | # guaranteed to contain at least one element ('') 241 | searchpath = [p for p in rel_searchpath if p] + self.searchpath 242 | message = "{0}; searchpath={1}".format(template, searchpath) 243 | raise TemplateNotFound(name=template, message=message) 244 | 245 | 246 | class Jinja2TemplateRenderer(object): 247 | """Renderer for a jinja2 template""" 248 | 249 | def __init__(self, template_loader): 250 | self.template_loader = template_loader 251 | 252 | def __call__(self, value, system): 253 | try: 254 | system.update(value) 255 | except (TypeError, ValueError) as ex: 256 | raise ValueError( 257 | "renderer was passed non-dictionary " "as value: %s" % str(ex) 258 | ) 259 | template = self.template_loader() 260 | return template.render(system) 261 | 262 | 263 | class Jinja2RendererFactory(object): 264 | environment = None 265 | 266 | def __call__(self, info): 267 | name, package = info.name, info.package 268 | 269 | def template_loader(): 270 | # attempt to turn the name into a caller-relative asset spec 271 | if ":" not in name and package is not None: 272 | try: 273 | name_with_package = "%s:%s" % (package.__name__, name) 274 | return self.environment.get_template(name_with_package) 275 | except TemplateNotFound: 276 | pass 277 | 278 | return self.environment.get_template(name) 279 | 280 | return Jinja2TemplateRenderer(template_loader) 281 | 282 | 283 | def renderer_factory(info): 284 | registry = info.registry 285 | env = registry.queryUtility(IJinja2Environment, name=".jinja2") 286 | if env is None: 287 | raise ValueError( 288 | "As of pyramid_jinja2 2.3, the use of the " 289 | '"pyramid_jinja2.renderer_factory" requires that pyramid_jinja2 ' 290 | 'be configured via config.include("pyramid_jinja2") or the ' 291 | 'equivalent "pyramid.includes" setting.' 292 | ) 293 | factory = Jinja2RendererFactory() 294 | factory.environment = env 295 | return factory(info) 296 | 297 | 298 | deprecated( 299 | "renderer_factory", 300 | "The pyramid_jinja2.renderer_factory was deprecated in version 2.0 and " 301 | "will be removed in the future. You should upgrade to the newer " 302 | "config.add_jinja2_renderer() API.", 303 | ) 304 | 305 | 306 | def add_jinja2_search_path(config, searchpath, name=".jinja2", prepend=False): 307 | """ 308 | This function is added as a method of a :term:`Configurator`, and 309 | should not be called directly. Instead it should be called like so after 310 | ``pyramid_jinja2`` has been passed to ``config.include``: 311 | 312 | .. code-block:: python 313 | 314 | config.add_jinja2_search_path('anotherpackage:templates/') 315 | 316 | It will add the directory or :term:`asset specification` passed as 317 | ``searchpath`` to the current search path of the 318 | :class:`jinja2.Environment` used by the renderer identified by ``name``. 319 | 320 | By default the path is appended to the end of the search path. If 321 | ``prepend`` is set to ``True`` then the path will be inserted at the start 322 | of the search path. 323 | 324 | """ 325 | 326 | def register(): 327 | env = get_jinja2_environment(config, name) 328 | searchpaths = parse_multiline(searchpath) 329 | resolve = AssetResolver(config.package).resolve 330 | for folder in searchpaths: 331 | path = resolve(folder).abspath() 332 | if prepend: 333 | env.loader.searchpath.insert(0, path) 334 | else: 335 | env.loader.searchpath.append(path) 336 | 337 | config.action(None, register, order=EXTRAS_CONFIG_PHASE) 338 | 339 | 340 | def add_jinja2_extension(config, ext, name=".jinja2"): 341 | """ 342 | This function is added as a method of a :term:`Configurator`, and 343 | should not be called directly. Instead it should be called like so after 344 | ``pyramid_jinja2`` has been passed to ``config.include``: 345 | 346 | .. code-block:: python 347 | 348 | config.add_jinja2_extension(myext) 349 | 350 | It will add the Jinja2 extension passed as ``ext`` to the current 351 | :class:`jinja2.Environment` used by the renderer named ``name``. 352 | 353 | """ 354 | ext = config.maybe_dotted(ext) 355 | 356 | def register(): 357 | env = get_jinja2_environment(config, name) 358 | env.add_extension(ext) 359 | 360 | config.action(None, register, order=EXTRAS_CONFIG_PHASE) 361 | 362 | 363 | def get_jinja2_environment(config, name=".jinja2"): 364 | """ 365 | This function is added as a method of a :term:`Configurator`, and 366 | should not be called directly. Instead it should be called like so after 367 | ``pyramid_jinja2`` has been passed to ``config.include``: 368 | 369 | .. code-block:: python 370 | 371 | config.get_jinja2_environment() 372 | 373 | It will return the configured ``jinja2.Environment`` for the 374 | renderer named ``name``. The environment is created as an :term:`action` 375 | which is deferred to allow users to override the configuration. In order 376 | to get back the configured environment, you must either force a commit 377 | via ``config.commit`` or schedule an action which can setup the 378 | environment after it has been created: 379 | 380 | .. code-block:: python 381 | 382 | def setup_jinja2_env(): 383 | env = config.get_jinja2_environment() 384 | # ... 385 | config.action(None, setup_jinja2_env, order=999) 386 | 387 | """ 388 | registry = config.registry 389 | return registry.queryUtility(IJinja2Environment, name=name) 390 | 391 | 392 | def create_environment_from_options(env_opts, loader_opts): 393 | loader = SmartAssetSpecLoader(**loader_opts) 394 | 395 | newstyle = env_opts.pop("newstyle", False) 396 | gettext = env_opts.pop("gettext", None) 397 | filters = env_opts.pop("filters", {}) 398 | tests = env_opts.pop("tests", {}) 399 | globals = env_opts.pop("globals", {}) 400 | 401 | env = Environment(loader=loader, **env_opts) 402 | 403 | env.install_gettext_callables(gettext.gettext, gettext.ngettext, newstyle=newstyle) 404 | 405 | env.filters.update(filters) 406 | env.tests.update(tests) 407 | env.globals.update(globals) 408 | 409 | return env 410 | 411 | 412 | def add_jinja2_renderer(config, name, settings_prefix="jinja2.", package=None): 413 | """ 414 | This function is added as a method of a :term:`Configurator`, and 415 | should not be called directly. Instead it should be called like so after 416 | ``pyramid_jinja2`` has been passed to ``config.include``: 417 | 418 | .. code-block:: python 419 | 420 | config.add_jinja2_renderer('.html', settings_prefix='jinja2.') 421 | 422 | It will register a new renderer, loaded from settings at the specified 423 | ``settings_prefix`` prefix. This renderer will be active for files using 424 | the specified extension ``name``. 425 | 426 | """ 427 | renderer_factory = Jinja2RendererFactory() 428 | config.add_renderer(name, renderer_factory) 429 | 430 | package = package or config.package 431 | resolver = DottedNameResolver(package=package) 432 | 433 | def register(): 434 | registry = config.registry 435 | settings = config.get_settings() 436 | 437 | loader_opts = parse_loader_options_from_settings( 438 | settings, 439 | settings_prefix, 440 | resolver.maybe_resolve, 441 | package, 442 | ) 443 | env_opts = parse_env_options_from_settings( 444 | settings, 445 | settings_prefix, 446 | resolver.maybe_resolve, 447 | package, 448 | ) 449 | env = create_environment_from_options(env_opts, loader_opts) 450 | renderer_factory.environment = env 451 | 452 | registry.registerUtility(env, IJinja2Environment, name=name) 453 | 454 | config.action(("jinja2-renderer", name), register, order=ENV_CONFIG_PHASE) 455 | 456 | 457 | def includeme(config): 458 | """Set up standard configurator registrations. Use via: 459 | 460 | .. code-block:: python 461 | 462 | config = Configurator() 463 | config.include('pyramid_jinja2') 464 | 465 | Once this function has been invoked, the ``.jinja2`` renderer is 466 | available for use in Pyramid and these new directives are available as 467 | methods of the configurator: 468 | 469 | - ``add_jinja2_renderer``: Add a new Jinja2 renderer, with a different 470 | file extension and/or settings. 471 | 472 | - ``add_jinja2_search_path``: Add a new location to the search path 473 | for the specified renderer. 474 | 475 | - ``add_jinja2_extension``: Add a list of extensions to the Jinja2 476 | environment used by the specified renderer. 477 | 478 | - ``get_jinja2_environment``: Return the :class:`jinja2.Environment` 479 | used by the specified renderer. 480 | 481 | """ 482 | config.add_directive("add_jinja2_renderer", add_jinja2_renderer) 483 | config.add_directive("add_jinja2_search_path", add_jinja2_search_path) 484 | config.add_directive("add_jinja2_extension", add_jinja2_extension) 485 | config.add_directive("get_jinja2_environment", get_jinja2_environment) 486 | 487 | package = _caller_package(("pyramid", "pyramid.", "pyramid_jinja2")) 488 | config.add_jinja2_renderer(".jinja2", package=package) 489 | 490 | # always insert default search path relative to package 491 | default_search_path = "%s:" % (package.__name__,) 492 | config.add_jinja2_search_path(default_search_path, name=".jinja2") 493 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/configure.zcml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/filters.py: -------------------------------------------------------------------------------- 1 | from pyramid.threadlocal import get_current_request 2 | from pyramid.url import resource_url, route_path, route_url, static_path, static_url 3 | 4 | try: 5 | from jinja2 import pass_context 6 | except ImportError: 7 | # jinja2 < 3.0 fallback 8 | from jinja2 import contextfilter as pass_context 9 | 10 | __all__ = [ 11 | "resource_url_filter", 12 | "model_url_filter", 13 | "route_url_filter", 14 | "route_path_filter", 15 | "static_url_filter", 16 | "static_path_filter", 17 | ] 18 | 19 | 20 | @pass_context 21 | def resource_url_filter(ctx, model, *elements, **kw): 22 | """A filter from ``model`` to a string representing the absolute URL. 23 | This filter calls :py:func:`pyramid.url.resource_url`. 24 | 25 | Example:: 26 | 27 | 28 | See my object 29 | 30 | 31 | You can also specify optional view name attached at the end of a path:: 32 | 33 | 34 | Edit my object 35 | 36 | 37 | """ 38 | request = ctx.get("request") or get_current_request() 39 | return resource_url(model, request, *elements, **kw) 40 | 41 | 42 | @pass_context 43 | def model_url_filter(ctx, model, *elements, **kw): 44 | """A filter from ``model`` to a string representing the absolute URL. 45 | This filter calls :py:func:`pyramid.url.resource_url`. 46 | 47 | .. note :: 48 | 49 | This is being deprecated. 50 | See :py:func:`pyramid_jinja2.filters.resource_url` 51 | 52 | """ 53 | request = ctx.get("request") or get_current_request() 54 | return resource_url(model, request, *elements, **kw) 55 | 56 | 57 | @pass_context 58 | def model_path_filter(ctx, model, *elements, **kw): 59 | """A filter from ``model`` to a string representing the relative URL. 60 | This filter calls :py:meth:`pyramid.request.Request.resource_path`. 61 | """ 62 | request = ctx.get("request") or get_current_request() 63 | return request.resource_path(model, *elements, **kw) 64 | 65 | 66 | @pass_context 67 | def route_url_filter(ctx, route_name, *elements, **kw): 68 | """A filter from ``route_name`` to a string representing the absolute URL. 69 | This filter calls :py:func:`pyramid.url.route_url`. 70 | 71 | Example:: 72 | 73 | 74 | Sign in 75 | 76 | 77 | """ 78 | request = ctx.get("request") or get_current_request() 79 | return route_url(route_name, request, *elements, **kw) 80 | 81 | 82 | @pass_context 83 | def route_path_filter(ctx, route_name, *elements, **kw): 84 | """A filter from ``route_name`` to a string representing the relative URL. 85 | This filter calls :py:func:`pyramid.url.route_path`. 86 | """ 87 | request = ctx.get("request") or get_current_request() 88 | return route_path(route_name, request, *elements, **kw) 89 | 90 | 91 | @pass_context 92 | def static_url_filter(ctx, path, **kw): 93 | """A filter from ``path`` to a string representing the absolute URL. 94 | This filter calls :py:func:`pyramid.url.static_url`. 95 | 96 | Example:: 97 | 98 | 99 | 100 | """ 101 | request = ctx.get("request") or get_current_request() 102 | return static_url(path, request, **kw) 103 | 104 | 105 | @pass_context 106 | def static_path_filter(ctx, path, **kw): 107 | """A filter from ``path`` to a string representing the relative URL. 108 | This filter calls :py:func:`pyramid.url.static_path`. 109 | """ 110 | request = ctx.get("request") or get_current_request() 111 | return static_path(path, request, **kw) 112 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/i18n.py: -------------------------------------------------------------------------------- 1 | from pyramid import i18n 2 | from pyramid.threadlocal import get_current_request 3 | 4 | 5 | class GetTextWrapper(object): 6 | """Implements `gettext` and `ngettext` functions for 7 | :meth:`jinja2.Environment.install_gettext_translations` 8 | """ 9 | 10 | def __init__(self, domain): 11 | self.domain = domain 12 | 13 | @property 14 | def localizer(self): 15 | request = get_current_request() 16 | try: 17 | return request.localizer 18 | except AttributeError: # pragma: nocover (pyramid < 1.5) 19 | return i18n.get_localizer(request) 20 | 21 | def gettext(self, message, mapping=None): 22 | """Implements jinja.ext.i18n `gettext` function""" 23 | return self.localizer.translate(message, domain=self.domain, mapping=mapping) 24 | 25 | def ngettext(self, singular, plural, n): 26 | """Implements jinja.ext.i18n `ngettext` function""" 27 | return self.localizer.pluralize(singular, plural, n, domain=self.domain) 28 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/__init__.py: -------------------------------------------------------------------------------- 1 | try: # pragma: no cover (pyramid 1.0.X) 2 | # "pyramid.paster.paste_script_template_renderer" doesn't exist past 1.0.X 3 | from pyramid.paster import PyramidTemplate, paste_script_template_renderer 4 | except ImportError: # pragma: no cover 5 | try: # pragma: no cover (pyramid 1.1.X, 1.2.X) 6 | # trying to import "paste_script_template_renderer" fails on 1.3.X 7 | from pyramid.scaffolds import PyramidTemplate, paste_script_template_renderer 8 | except ImportError: # pragma: no cover (pyramid >=1.3a2) 9 | paste_script_template_renderer = None 10 | from pyramid.scaffolds import PyramidTemplate 11 | 12 | 13 | class Jinja2ProjectTemplate(PyramidTemplate): 14 | _template_dir = "jinja2_starter" 15 | summary = "Pyramid Jinja2 starter project" 16 | template_renderer = staticmethod(paste_script_template_renderer) 17 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+dot+coveragerc_tmpl: -------------------------------------------------------------------------------- 1 | [run] 2 | source = {{package}} 3 | omit = {{package}}/test* 4 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/__init__.py_tmpl: -------------------------------------------------------------------------------- 1 | from pyramid.config import Configurator 2 | from {{package}}.resources import get_root 3 | 4 | 5 | def main(global_config, **settings): 6 | """ This function returns a WSGI application. 7 | 8 | It is usually called by the PasteDeploy framework during 9 | ``paster serve``. 10 | """ 11 | settings = dict(settings) 12 | settings.setdefault('jinja2.i18n.domain', '{{project}}') 13 | 14 | config = Configurator(root_factory=get_root, settings=settings) 15 | config.add_translation_dirs('locale/') 16 | config.include('pyramid_jinja2') 17 | 18 | config.add_static_view('static', 'static') 19 | config.add_view('{{package}}.views.my_view', 20 | context='{{package}}.resources.MyResource', 21 | renderer="templates/mytemplate.jinja2") 22 | 23 | return config.make_wsgi_app() 24 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/+project+.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-05-12 09:14-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | msgid "Welcome to" 21 | msgstr "" 22 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/de/LC_MESSAGES/+project+.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/de/LC_MESSAGES/+project+.mo -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/de/LC_MESSAGES/+project+.po: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-05-12 09:14-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | msgid "Welcome to" 21 | msgstr "Willkommen zum" 22 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/fr/LC_MESSAGES/+project+.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/fr/LC_MESSAGES/+project+.mo -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/locale/fr/LC_MESSAGES/+project+.po: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-05-12 09:14-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | msgid "Welcome to" 21 | msgstr "Bienvenue au" 22 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/resources.py: -------------------------------------------------------------------------------- 1 | class MyResource(object): 2 | pass 3 | 4 | 5 | root = MyResource() 6 | 7 | 8 | def get_root(request): 9 | return root 10 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/favicon.ico -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/pyramid-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/pyramid-16x16.png -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/pyramid.png -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/static/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | body { 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-weight: 300; 5 | color: #ffffff; 6 | background: #bc2131; 7 | } 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | font-weight: 300; 16 | } 17 | p { 18 | font-weight: 300; 19 | } 20 | .font-normal { 21 | font-weight: 400; 22 | } 23 | .font-semi-bold { 24 | font-weight: 600; 25 | } 26 | .font-bold { 27 | font-weight: 700; 28 | } 29 | .starter-template { 30 | margin-top: 250px; 31 | } 32 | .starter-template .content { 33 | margin-left: 10px; 34 | } 35 | .starter-template .content h1 { 36 | margin-top: 10px; 37 | font-size: 60px; 38 | } 39 | .starter-template .content h1 .smaller { 40 | font-size: 40px; 41 | color: #f2b7bd; 42 | } 43 | .starter-template .content .lead { 44 | font-size: 25px; 45 | color: #f2b7bd; 46 | } 47 | .starter-template .content .lead .font-normal { 48 | color: #ffffff; 49 | } 50 | .starter-template .links { 51 | float: right; 52 | right: 0; 53 | margin-top: 125px; 54 | } 55 | .starter-template .links ul { 56 | display: block; 57 | padding: 0; 58 | margin: 0; 59 | } 60 | .starter-template .links ul li { 61 | list-style: none; 62 | display: inline; 63 | margin: 0 10px; 64 | } 65 | .starter-template .links ul li:first-child { 66 | margin-left: 0; 67 | } 68 | .starter-template .links ul li:last-child { 69 | margin-right: 0; 70 | } 71 | .starter-template .links ul li.current-version { 72 | color: #f2b7bd; 73 | font-weight: 400; 74 | } 75 | .starter-template .links ul li a { 76 | color: #ffffff; 77 | } 78 | .starter-template .links ul li a:hover { 79 | text-decoration: underline; 80 | } 81 | .starter-template .links ul li .icon-muted { 82 | color: #eb8b95; 83 | margin-right: 5px; 84 | } 85 | .starter-template .links ul li:hover .icon-muted { 86 | color: #ffffff; 87 | } 88 | .starter-template .copyright { 89 | margin-top: 10px; 90 | font-size: 0.9em; 91 | color: #f2b7bd; 92 | text-transform: lowercase; 93 | float: right; 94 | right: 0; 95 | } 96 | @media (max-width: 1199px) { 97 | .starter-template .content h1 { 98 | font-size: 45px; 99 | } 100 | .starter-template .content h1 .smaller { 101 | font-size: 30px; 102 | } 103 | .starter-template .content .lead { 104 | font-size: 20px; 105 | } 106 | } 107 | @media (max-width: 991px) { 108 | .starter-template { 109 | margin-top: 0; 110 | } 111 | .starter-template .logo { 112 | margin: 40px auto; 113 | } 114 | .starter-template .content { 115 | margin-left: 0; 116 | text-align: center; 117 | } 118 | .starter-template .content h1 { 119 | margin-bottom: 20px; 120 | } 121 | .starter-template .links { 122 | float: none; 123 | text-align: center; 124 | margin-top: 60px; 125 | } 126 | .starter-template .copyright { 127 | float: none; 128 | text-align: center; 129 | } 130 | } 131 | @media (max-width: 767px) { 132 | .starter-template .content h1 .smaller { 133 | font-size: 25px; 134 | display: block; 135 | } 136 | .starter-template .content .lead { 137 | font-size: 16px; 138 | } 139 | .starter-template .links { 140 | margin-top: 40px; 141 | } 142 | .starter-template .links ul li { 143 | display: block; 144 | margin: 0; 145 | } 146 | .starter-template .links ul li .icon-muted { 147 | display: none; 148 | } 149 | .starter-template .copyright { 150 | margin-top: 20px; 151 | } 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/templates/mytemplate.jinja2_tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Starter Scaffold for Pyramid Jinja2 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |

37 | Pyramid 38 | Jinja2 scaffold 39 |

40 |

41 | {% trans %}Welcome to{% endtrans %} \{\{ project \}\}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

42 |
43 |
44 |
45 |
46 | 55 |
56 |
57 | 60 |
61 |
62 |
63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/tests.py_tmpl: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyramid import testing 3 | from pyramid.i18n import TranslationStringFactory 4 | 5 | _ = TranslationStringFactory('{{package}}') 6 | 7 | 8 | class ViewTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | testing.setUp() 12 | 13 | def tearDown(self): 14 | testing.tearDown() 15 | 16 | def test_my_view(self): 17 | from {{package}}.views import my_view 18 | request = testing.DummyRequest() 19 | response = my_view(request) 20 | self.assertEqual(response['project'], '{{project}}') 21 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/+package+/views.py_tmpl: -------------------------------------------------------------------------------- 1 | from pyramid.i18n import TranslationStringFactory 2 | 3 | _ = TranslationStringFactory('{{project}}') 4 | 5 | 6 | def my_view(request): 7 | return {'project': '{{project}}'} 8 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/CHANGES.rst_tmpl: -------------------------------------------------------------------------------- 1 | 0.0 2 | --- 3 | 4 | - Initial version 5 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/MANIFEST.in_tmpl: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst 2 | recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.jinja2 *.js *.html *.xml 3 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/README.rst_tmpl: -------------------------------------------------------------------------------- 1 | {{project}} README 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/development.ini_tmpl: -------------------------------------------------------------------------------- 1 | ### 2 | # app configuration 3 | # http://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html 4 | ### 5 | 6 | [app:main] 7 | use = egg:{{project}} 8 | 9 | pyramid.reload_templates = true 10 | pyramid.debug_authorization = false 11 | pyramid.debug_notfound = false 12 | pyramid.debug_routematch = false 13 | pyramid.debug_templates = true 14 | pyramid.default_locale_name = en 15 | pyramid.includes = 16 | pyramid_debugtoolbar 17 | 18 | # By default, the toolbar only appears for clients from IP addresses 19 | # '127.0.0.1' and '::1'. 20 | # debugtoolbar.hosts = 127.0.0.1 ::1 21 | 22 | ### 23 | # wsgi server configuration 24 | ### 25 | 26 | [server:main] 27 | use = egg:waitress#main 28 | host = 127.0.0.1 29 | port = 6543 30 | 31 | ### 32 | # logging configuration 33 | # http://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html 34 | ### 35 | 36 | [loggers] 37 | keys = root, {{package_logger}} 38 | 39 | [handlers] 40 | keys = console 41 | 42 | [formatters] 43 | keys = generic 44 | 45 | [logger_root] 46 | level = INFO 47 | handlers = console 48 | 49 | [logger_{{package_logger}}] 50 | level = DEBUG 51 | handlers = 52 | qualname = {{package}} 53 | 54 | [handler_console] 55 | class = StreamHandler 56 | args = (sys.stderr,) 57 | level = NOTSET 58 | formatter = generic 59 | 60 | [formatter_generic] 61 | format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s 62 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/message-extraction.ini: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **.jinja2] 3 | encoding = utf-8 4 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/pytest.ini_tmpl: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = {{package}} 3 | python_files = *.py 4 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/setup.cfg_tmpl: -------------------------------------------------------------------------------- 1 | [compile_catalog] 2 | directory = {{package}}/locale 3 | domain = {{project}} 4 | statistics = true 5 | 6 | [extract_messages] 7 | add_comments = TRANSLATORS: 8 | output_file = {{package}}/locale/{{project}}.pot 9 | width = 80 10 | mapping_file = message-extraction.ini 11 | 12 | [init_catalog] 13 | domain = {{project}} 14 | input_file = {{package}}/locale/{{project}}.pot 15 | output_dir = {{package}}/locale 16 | 17 | [update_catalog] 18 | domain = {{project}} 19 | input_file = {{package}}/locale/{{project}}.pot 20 | output_dir = {{package}}/locale 21 | previous = true 22 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/scaffolds/jinja2_starter/setup.py_tmpl: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | with open(os.path.join(here, 'README.rst')) as f: 7 | README = f.read() 8 | with open(os.path.join(here, 'CHANGES.rst')) as f: 9 | CHANGES = f.read() 10 | 11 | requires = [ 12 | 'pyramid', 13 | 'pyramid_jinja2', 14 | 'pyramid_debugtoolbar', 15 | 'waitress', 16 | ] 17 | 18 | tests_require = [ 19 | 'WebTest >= 1.3.1', # py3 compat 20 | 'pytest', # includes virtualenv 21 | 'pytest-cov', 22 | ] 23 | 24 | setup(name='{{project}}', 25 | version='0.0', 26 | description='{{project}}', 27 | long_description=README + '\n\n' + CHANGES, 28 | classifiers=[ 29 | "Programming Language :: Python", 30 | "Framework :: Pyramid", 31 | "Topic :: Internet :: WWW/HTTP", 32 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 33 | ], 34 | author='', 35 | author_email='', 36 | url='', 37 | keywords='web pyramid pylons', 38 | packages=find_packages(), 39 | include_package_data=True, 40 | zip_safe=False, 41 | extras_require={ 42 | 'testing': tests_require, 43 | }, 44 | install_requires=requires, 45 | entry_points="""\ 46 | [paste.app_factory] 47 | main = {{package}}:main 48 | """, 49 | ) 50 | -------------------------------------------------------------------------------- /src/pyramid_jinja2/settings.py: -------------------------------------------------------------------------------- 1 | from jinja2 import ( 2 | BytecodeCache, 3 | DebugUndefined, 4 | FileSystemBytecodeCache, 5 | StrictUndefined, 6 | Undefined, 7 | ) 8 | from pyramid.path import AssetResolver 9 | from pyramid.settings import asbool 10 | 11 | from .i18n import GetTextWrapper 12 | 13 | _JINJA2_ENVIRONMENT_DEFAULTS = { 14 | "autoescape": True, 15 | } 16 | 17 | 18 | def splitlines(s): 19 | return filter(None, [x.strip() for x in s.splitlines()]) 20 | 21 | 22 | def parse_named_assetspecs(input, maybe_dotted): 23 | """ 24 | Parse a dictionary of asset specs. 25 | Parses config values from .ini file and returns a dictionary with 26 | imported objects 27 | """ 28 | # input must be a string or dict 29 | result = {} 30 | if isinstance(input, str): 31 | for f in splitlines(input): 32 | name, impl = f.split("=", 1) 33 | result[name.strip()] = maybe_dotted(impl.strip()) 34 | else: 35 | for name, impl in input.items(): 36 | result[name] = maybe_dotted(impl) 37 | return result 38 | 39 | 40 | def parse_multiline(extensions): 41 | if isinstance(extensions, str): 42 | extensions = list(splitlines(extensions)) 43 | return extensions 44 | 45 | 46 | def parse_undefined(undefined): 47 | if undefined == "strict": 48 | return StrictUndefined 49 | if undefined == "debug": 50 | return DebugUndefined 51 | return Undefined 52 | 53 | 54 | def parse_loader_options_from_settings(settings, prefix, maybe_dotted, package): 55 | """Parse options for use with the SmartAssetSpecLoader.""" 56 | package = package or "__main__" 57 | 58 | def sget(name, default=None): 59 | return settings.get(prefix + name, default) 60 | 61 | debug = sget("debug_templates", None) 62 | if debug is None: 63 | # bw-compat prior to checking debug_templates for specific prefix 64 | debug = settings.get("debug_templates", None) 65 | debug = asbool(debug) 66 | 67 | input_encoding = sget("input_encoding", "utf-8") 68 | 69 | # get jinja2 directories 70 | resolve = AssetResolver(package).resolve 71 | directories = parse_multiline(sget("directories") or "") 72 | directories = [resolve(d).abspath() for d in directories] 73 | 74 | return dict( 75 | debug=debug, 76 | encoding=input_encoding, 77 | searchpath=directories, 78 | ) 79 | 80 | 81 | def parse_env_options_from_settings( 82 | settings, 83 | prefix, 84 | maybe_dotted, 85 | package, 86 | defaults=None, 87 | ): 88 | """Parse options for use with the Jinja2 Environment.""" 89 | 90 | def sget(name, default=None): 91 | return settings.get(prefix + name, default) 92 | 93 | if defaults is None: 94 | defaults = _JINJA2_ENVIRONMENT_DEFAULTS 95 | 96 | opts = {} 97 | 98 | reload_templates = sget("reload_templates") 99 | if reload_templates is None: 100 | reload_templates = settings.get("pyramid.reload_templates") 101 | opts["auto_reload"] = asbool(reload_templates) 102 | 103 | # set string settings 104 | for key_name in ( 105 | "block_start_string", 106 | "block_end_string", 107 | "variable_start_string", 108 | "variable_end_string", 109 | "comment_start_string", 110 | "comment_end_string", 111 | "line_statement_prefix", 112 | "line_comment_prefix", 113 | "newline_sequence", 114 | ): 115 | value = sget(key_name, defaults.get(key_name)) 116 | if value is not None: 117 | opts[key_name] = value 118 | 119 | # boolean settings 120 | for key_name in ("autoescape", "trim_blocks", "optimized", "lstrip_blocks"): 121 | value = sget(key_name, defaults.get(key_name)) 122 | if value is not None: 123 | opts[key_name] = asbool(value) 124 | 125 | # integer settings 126 | for key_name in ("cache_size",): 127 | value = sget(key_name, defaults.get(key_name)) 128 | if value is not None: 129 | opts[key_name] = int(value) 130 | 131 | opts["undefined"] = parse_undefined(sget("undefined", "")) 132 | 133 | # get supplementary jinja2 settings 134 | domain = sget("i18n.domain", package and package.__name__ or "messages") 135 | gettext_wrapper = maybe_dotted(sget("i18n.gettext", GetTextWrapper)) 136 | opts["gettext"] = gettext_wrapper(domain=domain) 137 | 138 | # get jinja2 extensions 139 | extensions = parse_multiline(sget("extensions", "")) 140 | i18n_extension = sget("i18n_extension", "jinja2.ext.i18n") 141 | if i18n_extension not in extensions: 142 | extensions.append(i18n_extension) 143 | opts["extensions"] = extensions 144 | 145 | # get jinja2 bytecode caching settings and set up bytecaching 146 | bytecode_caching = sget("bytecode_caching", False) 147 | if isinstance(bytecode_caching, BytecodeCache): 148 | opts["bytecode_cache"] = bytecode_caching 149 | elif asbool(bytecode_caching): 150 | bytecode_caching_directory = sget("bytecode_caching_directory", None) 151 | opts["bytecode_cache"] = FileSystemBytecodeCache(bytecode_caching_directory) 152 | 153 | # should newstyle gettext calls be enabled? 154 | opts["newstyle"] = asbool(sget("newstyle", False)) 155 | 156 | # Do we have a finalize function? 157 | finalize = sget("finalize") 158 | if finalize is not None: 159 | opts["finalize"] = maybe_dotted(finalize) 160 | 161 | # add custom jinja2 filters 162 | opts["filters"] = parse_named_assetspecs(sget("filters", ""), maybe_dotted) 163 | 164 | # add custom jinja2 tests 165 | opts["tests"] = parse_named_assetspecs(sget("tests", ""), maybe_dotted) 166 | 167 | # add custom jinja2 functions 168 | opts["globals"] = parse_named_assetspecs(sget("globals", ""), maybe_dotted) 169 | 170 | return opts 171 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # tests package 2 | -------------------------------------------------------------------------------- /tests/babel.cfg: -------------------------------------------------------------------------------- 1 | [jinja2: **.jinja2] 2 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 3 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | from pyramid import testing 2 | 3 | 4 | class Base(object): 5 | def setUp(self): 6 | self.request = testing.DummyRequest() 7 | self.config = testing.setUp(request=self.request) 8 | self.request.registry = self.config.registry 9 | import os 10 | 11 | here = os.path.abspath(os.path.dirname(__file__)) 12 | self.templates_dir = os.path.join(here, "templates") 13 | 14 | def tearDown(self): 15 | testing.tearDown() 16 | del self.config 17 | 18 | 19 | class Mock(object): 20 | def __init__(self, **kwargs): 21 | self.__dict__.update(kwargs) 22 | -------------------------------------------------------------------------------- /tests/extensions.py: -------------------------------------------------------------------------------- 1 | from jinja2 import nodes 2 | from jinja2.ext import Extension 3 | 4 | 5 | class TestExtension(Extension): 6 | tags = {"test_ext"} 7 | 8 | def parse(self, parser): 9 | return nodes.Const("This is test extension") # pragma: nocover 10 | -------------------------------------------------------------------------------- /tests/locale/en/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/pyramid_jinja2/b5953e11ca3b80c1a210066cd475838557d191cb/tests/locale/en/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tests/locale/en/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-04-06 16:16-0430\n" 12 | "PO-Revision-Date: 2011-04-06 16:17-0530\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: en \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 0.9.6\n" 20 | 21 | #: templates/i18n.jinja2:2 22 | msgid "some translated text here" 23 | msgstr "yay it worked!" 24 | 25 | #: templates/i18n.jinja2:3 26 | msgid "some translated text with ${var} here" 27 | msgstr "yay it works with ${var} too!" 28 | 29 | -------------------------------------------------------------------------------- /tests/locale/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2011 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2011. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2011-04-06 16:16-0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | 20 | #: templates/i18n.jinja2:2 21 | msgid "some translated text here" 22 | msgstr "" 23 | 24 | #: templates/i18n.jinja2:3 25 | msgid "some translated text with ${var} here" 26 | msgstr "" 27 | 28 | -------------------------------------------------------------------------------- /tests/templates/bar/mytemplate.jinja2: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /tests/templates/baz1/base.jinja2: -------------------------------------------------------------------------------- 1 | baz1 2 | {% block baz1_body %} 3 | {% endblock baz1_body %} 4 | -------------------------------------------------------------------------------- /tests/templates/baz1/middle.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | -------------------------------------------------------------------------------- /tests/templates/baz1/mytemplate.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "middle.jinja2" %} 2 | {% block baz1_body %}baz1 body{% endblock baz1_body %} 3 | -------------------------------------------------------------------------------- /tests/templates/baz2/base.jinja2: -------------------------------------------------------------------------------- 1 | baz2 2 | {% block baz2_body %} 3 | baz1 foo 4 | {% endblock baz2_body %} 5 | -------------------------------------------------------------------------------- /tests/templates/baz2/mytemplate.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% block baz2_body %}baz2 body{% endblock baz2_body %} 3 | -------------------------------------------------------------------------------- /tests/templates/deep/base.jinja2: -------------------------------------------------------------------------------- 1 | deep-base {% block content %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/deep/forms.jinja2: -------------------------------------------------------------------------------- 1 | deep-forms 2 | -------------------------------------------------------------------------------- /tests/templates/deep/leaf.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% include "sub/nav.jinja2" %} 3 | {% block content %}deep-leaf{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/deep/sub/base.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "../base.jinja2" %} 2 | {% block content %}sub-base {% block subcontent %}{% endblock %}{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/templates/deep/sub/leaf.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "base.jinja2" %} 2 | {% block subcontent %}sub-leaf{% endblock%} 3 | -------------------------------------------------------------------------------- /tests/templates/deep/sub/nav.jinja2: -------------------------------------------------------------------------------- 1 | sub-nav 2 | 3 | {% include "../forms.jinja2" %} 4 | -------------------------------------------------------------------------------- /tests/templates/extends.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "helloworld.jinja2" %} 2 | {% block content %}Yo!{% endblock %} 3 | 4 | -------------------------------------------------------------------------------- /tests/templates/extends_missing.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "missing/tmpl.jinja2" %} 2 | {% block content %}Yo!{% endblock %} 3 | 4 | -------------------------------------------------------------------------------- /tests/templates/extends_relbase.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "templates/helloworld.jinja2" %} 2 | {% block content %}Yo!{% endblock %} 3 | 4 | -------------------------------------------------------------------------------- /tests/templates/extends_spec.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "tests:templates/helloworld.jinja2" %} 2 | {% block content %}Yo!{% endblock %} 3 | 4 | -------------------------------------------------------------------------------- /tests/templates/foo/mytemplate.jinja2: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /tests/templates/helloworld.jinja2: -------------------------------------------------------------------------------- 1 | {% set a, b = "foo", "föö" %} 2 | Hello {{ b }}{% block content %}{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/templates/i18n.jinja2: -------------------------------------------------------------------------------- 1 | some untranslated text here 2 | {% trans %}some translated text here{% endtrans %} 3 | {{ gettext("some translated text with ${var} here", mapping={"var": var}) }} 4 | -------------------------------------------------------------------------------- /tests/templates/newstyle.jinja2: -------------------------------------------------------------------------------- 1 | {{ gettext("my hovercraft is full of %(what)s!", what=what) }} 2 | -------------------------------------------------------------------------------- /tests/templates/recursive/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /tests/templates/recursive/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | -------------------------------------------------------------------------------- /tests/templates/recursive/base.html: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /tests/templates/tests_and_filters.jinja2: -------------------------------------------------------------------------------- 1 | {{ "some text"|my_filter }} {% if "other text" is my_test %}is not False{% endif %} {{ my_global(1) }} 2 | -------------------------------------------------------------------------------- /tests/test_ext.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyramid.path import DottedNameResolver 4 | 5 | from .base import Base 6 | 7 | 8 | class TestExtensions(Base, unittest.TestCase): 9 | def test_custom_extension(self): 10 | from pyramid_jinja2 import create_environment_from_options 11 | from pyramid_jinja2.settings import parse_env_options_from_settings 12 | 13 | options = { 14 | "extensions": "tests.extensions.TestExtension", 15 | } 16 | settings = parse_env_options_from_settings(options, "", maybe_dotted, None) 17 | env = create_environment_from_options(settings, {}) 18 | ext = env.extensions["tests.extensions.TestExtension"] 19 | from . import extensions 20 | 21 | self.assertEqual(ext.__class__, extensions.TestExtension) 22 | 23 | def test_i18n(self): 24 | from pyramid_jinja2 import create_environment_from_options 25 | from pyramid_jinja2.settings import parse_env_options_from_settings 26 | 27 | settings = parse_env_options_from_settings({}, "", maybe_dotted, None) 28 | env = create_environment_from_options(settings, {}) 29 | 30 | self.assertTrue(hasattr(env, "install_gettext_translations")) 31 | 32 | self.config.add_translation_dirs("tests:locale/") 33 | self.request.locale_name = "en" 34 | template = env.get_template("tests:templates/i18n.jinja2") 35 | context = {"var": "variables"} 36 | self.assertEqual( 37 | template.render(**context), 38 | "some untranslated text here\nyay it worked!\n" 39 | "yay it works with variables too!", 40 | ) 41 | 42 | 43 | resolver = DottedNameResolver() 44 | maybe_dotted = resolver.maybe_resolve 45 | 46 | 47 | class GetTextWrapperTests(unittest.TestCase): 48 | def test_it(self): 49 | from pyramid_jinja2.i18n import GetTextWrapper 50 | 51 | class MyGetTextWrapper(GetTextWrapper): 52 | class localizer: 53 | @staticmethod 54 | def translate(s, domain, mapping): 55 | return s 56 | 57 | @staticmethod 58 | def pluralize(s1, s2, n, domain): 59 | return s2 60 | 61 | def __init__(self): 62 | GetTextWrapper.__init__(self, "messages") 63 | 64 | self.assertEqual(MyGetTextWrapper().gettext("foo"), "foo") 65 | self.assertEqual(MyGetTextWrapper().ngettext("foo", "foos", 3), "foos") 66 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyramid import testing 4 | 5 | 6 | class DummyRoot(object): 7 | __name__ = __parent__ = None 8 | 9 | 10 | class DummyModel(object): 11 | __name__ = "dummy" 12 | __parent__ = DummyRoot() 13 | 14 | 15 | class Base(object): 16 | def setUp(self): 17 | self.request = testing.DummyRequest() 18 | self.config = testing.setUp(request=self.request) 19 | self.request.registry = self.config.registry 20 | 21 | from pyramid_jinja2 import Environment 22 | 23 | self.environment = Environment() 24 | 25 | self._addFilters() 26 | 27 | def tearDown(self): 28 | testing.tearDown() 29 | 30 | def _addFilters(self): 31 | pass # pragma: nocover 32 | 33 | def _callFUT(self, context, tmpl): 34 | tmpl = self.environment.from_string(tmpl) 35 | return tmpl.render(**context) 36 | 37 | 38 | class Test_model_url_filter(Base, unittest.TestCase): 39 | def _addFilters(self): 40 | from pyramid_jinja2.filters import model_url_filter 41 | 42 | self.environment.filters["model_url"] = model_url_filter 43 | 44 | def test_filter(self): 45 | model = DummyModel() 46 | rendered = self._callFUT({"model": model}, "{{ model|model_url }}") 47 | self.assertEqual(rendered, "http://example.com/dummy/") 48 | 49 | def test_filter_with_elements(self): 50 | model = DummyModel() 51 | rendered = self._callFUT({"model": model}, "{{ model|model_url('edit') }}") 52 | self.assertEqual(rendered, "http://example.com/dummy/edit") 53 | 54 | 55 | class Test_resource_url_filter(Base, unittest.TestCase): 56 | def _addFilters(self): 57 | from pyramid_jinja2.filters import resource_url_filter 58 | 59 | self.environment.filters["resource_url"] = resource_url_filter 60 | 61 | def test_filter(self): 62 | model = DummyModel() 63 | rendered = self._callFUT({"model": model}, "{{ model|resource_url }}") 64 | self.assertEqual(rendered, "http://example.com/dummy/") 65 | 66 | def test_filter_with_elements(self): 67 | model = DummyModel() 68 | rendered = self._callFUT({"model": model}, "{{ model|resource_url('edit') }}") 69 | self.assertEqual(rendered, "http://example.com/dummy/edit") 70 | 71 | 72 | class Test_model__filter(Base, unittest.TestCase): 73 | def _addFilters(self): 74 | from pyramid_jinja2.filters import model_path_filter 75 | 76 | self.environment.filters["model_path"] = model_path_filter 77 | 78 | def test_filter(self): 79 | model = DummyModel() 80 | rendered = self._callFUT({"model": model}, "{{ model|model_path }}") 81 | self.assertEqual(rendered, "/dummy/") 82 | 83 | def test_filter_with_elements(self): 84 | model = DummyModel() 85 | rendered = self._callFUT({"model": model}, "{{ model|model_path('edit') }}") 86 | self.assertEqual(rendered, "/dummy/edit") 87 | 88 | 89 | class Test_route_url_filter(Base, unittest.TestCase): 90 | def _addFilters(self): 91 | from pyramid_jinja2.filters import route_url_filter 92 | 93 | self.environment.filters["route_url"] = route_url_filter 94 | 95 | self.config.add_route("dummy_route1", "/dummy/") 96 | self.config.add_route("dummy_route2", "/dummy/:name/") 97 | 98 | def test_filter(self): 99 | rendered = self._callFUT({}, "{{ 'dummy_route1'|route_url }}") 100 | self.assertEqual(rendered, "http://example.com/dummy/") 101 | 102 | def test_filter_with_arguments(self): 103 | rendered = self._callFUT({}, "{{ 'dummy_route2'|route_url('x', name='test') }}") 104 | self.assertEqual(rendered, "http://example.com/dummy/test/x") 105 | 106 | 107 | class Test_route_path_filter(Base, unittest.TestCase): 108 | def _addFilters(self): 109 | from pyramid_jinja2.filters import route_path_filter 110 | 111 | self.environment.filters["route_path"] = route_path_filter 112 | 113 | self.config.add_route("dummy_route1", "/dummy/") 114 | self.config.add_route("dummy_route2", "/dummy/:name/") 115 | 116 | def test_filter(self): 117 | rendered = self._callFUT({}, "{{ 'dummy_route1'|route_path }}") 118 | self.assertEqual(rendered, "/dummy/") 119 | 120 | def test_filter_with_arguments(self): 121 | rendered = self._callFUT( 122 | {}, "{{ 'dummy_route2'|route_path('x', name='test') }}" 123 | ) 124 | self.assertEqual(rendered, "/dummy/test/x") 125 | 126 | 127 | class Test_static_url_filter(Base, unittest.TestCase): 128 | def _addFilters(self): 129 | from pyramid_jinja2.filters import static_url_filter 130 | 131 | self.environment.filters["static_url"] = static_url_filter 132 | 133 | self.config.add_static_view("myfiles", "dummy1:static") 134 | self.config.add_static_view("otherfiles/{owner}", "dummy2:files") 135 | 136 | def test_filter(self): 137 | rendered = self._callFUT( 138 | {}, "{{ 'dummy1:static/the/quick/brown/fox.svg'|static_url }}" 139 | ) 140 | self.assertEqual(rendered, "http://example.com/myfiles/the/quick/brown/fox.svg") 141 | 142 | def test_filter_with_arguments(self): 143 | rendered = self._callFUT( 144 | {}, "{{ 'dummy2:files/report.txt'|static_url(owner='foo') }}" 145 | ) 146 | self.assertEqual(rendered, "http://example.com/otherfiles/foo/report.txt") 147 | 148 | 149 | class Test_static_path_filter(Base, unittest.TestCase): 150 | def _addFilters(self): 151 | from pyramid_jinja2.filters import static_path_filter 152 | 153 | self.environment.filters["static_path"] = static_path_filter 154 | 155 | self.config.add_static_view("myfiles", "dummy1:static") 156 | self.config.add_static_view("otherfiles/{owner}", "dummy2:files") 157 | 158 | def test_filter(self): 159 | rendered = self._callFUT( 160 | {}, "{{ 'dummy1:static/the/quick/brown/fox.svg'|static_path }}" 161 | ) 162 | self.assertEqual(rendered, "/myfiles/the/quick/brown/fox.svg") 163 | 164 | def test_filter_with_arguments(self): 165 | rendered = self._callFUT( 166 | {}, "{{ 'dummy2:files/report.txt'|static_path(owner='foo') }}" 167 | ) 168 | self.assertEqual(rendered, "/otherfiles/foo/report.txt") 169 | 170 | 171 | class Test_filters_not_caching(Base, unittest.TestCase): 172 | def _addFilters(self): 173 | from pyramid_jinja2.filters import route_url_filter 174 | 175 | self.environment.filters["route_url"] = route_url_filter 176 | 177 | self.config.add_route("dummy_route1", "/dummy/") 178 | self.config.add_route("dummy_route2", "/dummy/:name/") 179 | 180 | def test_filter(self): 181 | self.request.application_url = "http://example.com" 182 | self.request.host = "example.com:80" 183 | rendered = self._callFUT({}, "{{ 'dummy_route1'|route_url }}") 184 | self.assertEqual(rendered, "http://example.com/dummy/") 185 | 186 | self.request.application_url = "http://sub.example.com" 187 | self.request.host = "sub.example.com:80" 188 | rendered = self._callFUT({}, "{{ 'dummy_route1'|route_url }}") 189 | self.assertEqual(rendered, "http://sub.example.com/dummy/") 190 | 191 | def test_filter_with_arguments(self): 192 | self.request.application_url = "http://example.com" 193 | self.request.host = "example.com:80" 194 | rendered = self._callFUT({}, "{{ 'dummy_route2'|route_url('x', name='test') }}") 195 | self.assertEqual(rendered, "http://example.com/dummy/test/x") 196 | 197 | self.request.application_url = "http://sub.example.com" 198 | self.request.host = "sub.example.com:80" 199 | rendered = self._callFUT({}, "{{ 'dummy_route2'|route_url('x', name='test') }}") 200 | self.assertEqual(rendered, "http://sub.example.com/dummy/test/x") 201 | -------------------------------------------------------------------------------- /tests/test_it.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import unittest 3 | 4 | from pyramid import testing 5 | 6 | from .base import Base, Mock 7 | 8 | 9 | def dummy_filter(value): 10 | return "hoge" # pragma: nocover 11 | 12 | 13 | class Test_renderer_factory(Base, unittest.TestCase): 14 | def setUp(self): 15 | Base.setUp(self) 16 | import warnings 17 | 18 | self.warnings = warnings.catch_warnings() 19 | self.warnings.__enter__() 20 | warnings.simplefilter("ignore", DeprecationWarning) 21 | 22 | def tearDown(self): 23 | self.warnings.__exit__(None, None, None) 24 | Base.tearDown(self) 25 | 26 | def _callFUT(self, info): 27 | from pyramid_jinja2 import renderer_factory 28 | 29 | return renderer_factory(info) 30 | 31 | def test_require_default_renderer(self): 32 | info = DummyRendererInfo( 33 | { 34 | "name": "helloworld.jinja2", 35 | "package": None, 36 | "registry": self.config.registry, 37 | } 38 | ) 39 | self.assertRaises(ValueError, lambda: self._callFUT(info)) 40 | 41 | def test_no_directories(self): 42 | from jinja2.exceptions import TemplateNotFound 43 | 44 | self.config.include("pyramid_jinja2") 45 | info = DummyRendererInfo( 46 | { 47 | "name": "helloworld.jinja2", 48 | "package": None, 49 | "registry": self.config.registry, 50 | } 51 | ) 52 | renderer = self._callFUT(info) 53 | self.assertRaises(TemplateNotFound, lambda: renderer({}, {"system": 1})) 54 | 55 | def test_no_environment(self): 56 | self.config.registry.settings.update({"jinja2.directories": self.templates_dir}) 57 | self.config.include("pyramid_jinja2") 58 | info = DummyRendererInfo( 59 | { 60 | "name": "helloworld.jinja2", 61 | "package": None, 62 | "registry": self.config.registry, 63 | } 64 | ) 65 | renderer = self._callFUT(info) 66 | environ = self.config.get_jinja2_environment() 67 | self.assertEqual(environ.loader.searchpath[0], self.templates_dir) 68 | self.assertTrue(renderer.template_loader is not None) 69 | 70 | def test_composite_directories_path(self): 71 | twice = self.templates_dir + "\n" + self.templates_dir 72 | self.config.registry.settings["jinja2.directories"] = twice 73 | self.config.include("pyramid_jinja2") 74 | info = DummyRendererInfo( 75 | { 76 | "name": "helloworld.jinja2", 77 | "package": None, 78 | "registry": self.config.registry, 79 | } 80 | ) 81 | self._callFUT(info) 82 | environ = self.config.get_jinja2_environment() 83 | self.assertEqual(environ.loader.searchpath[:2], [self.templates_dir] * 2) 84 | 85 | def test_with_environ(self): 86 | from pyramid_jinja2 import IJinja2Environment 87 | 88 | environ = DummyEnviron() 89 | self.config.registry.registerUtility( 90 | environ, IJinja2Environment, name=".jinja2" 91 | ) 92 | info = DummyRendererInfo( 93 | { 94 | "name": "helloworld.jinja2", 95 | "package": None, 96 | "registry": self.config.registry, 97 | } 98 | ) 99 | renderer = self._callFUT(info) 100 | self.assertTrue(renderer.template_loader) 101 | 102 | def test_with_filters_object(self): 103 | self.config.registry.settings.update( 104 | { 105 | "jinja2.directories": self.templates_dir, 106 | "jinja2.filters": {"dummy": dummy_filter}, 107 | } 108 | ) 109 | self.config.include("pyramid_jinja2") 110 | info = DummyRendererInfo( 111 | { 112 | "name": "helloworld.jinja2", 113 | "package": None, 114 | "registry": self.config.registry, 115 | } 116 | ) 117 | self._callFUT(info) 118 | environ = self.config.get_jinja2_environment() 119 | self.assertEqual(environ.filters["dummy"], dummy_filter) 120 | 121 | def test_with_filters_string(self): 122 | m = "tests.test_it" 123 | self.config.registry.settings.update( 124 | { 125 | "jinja2.directories": self.templates_dir, 126 | "jinja2.filters": "dummy=%s:dummy_filter" % m, 127 | } 128 | ) 129 | self.config.include("pyramid_jinja2") 130 | info = DummyRendererInfo( 131 | { 132 | "name": "helloworld.jinja2", 133 | "package": None, 134 | "registry": self.config.registry, 135 | } 136 | ) 137 | self._callFUT(info) 138 | environ = self.config.get_jinja2_environment() 139 | self.assertEqual(environ.filters["dummy"], dummy_filter) 140 | 141 | 142 | class TestJinja2TemplateRenderer(Base, unittest.TestCase): 143 | def _getTargetClass(self): 144 | from pyramid_jinja2 import Jinja2TemplateRenderer 145 | 146 | return Jinja2TemplateRenderer 147 | 148 | def _makeOne(self, *arg, **kw): 149 | klass = self._getTargetClass() 150 | return klass(*arg, **kw) 151 | 152 | def test_call(self): 153 | template = DummyTemplate() 154 | instance = self._makeOne(lambda: template) 155 | result = instance({}, {"system": 1}) 156 | self.assertTrue(isinstance(result, str)) 157 | self.assertEqual(result, "result") 158 | 159 | def test_call_with_system_context(self): 160 | template = DummyTemplate() 161 | instance = self._makeOne(lambda: template) 162 | result = instance({}, {"context": 1}) 163 | self.assertTrue(isinstance(result, str)) 164 | self.assertEqual(result, "result") 165 | 166 | def test_call_with_nondict_value(self): 167 | template = DummyTemplate() 168 | instance = self._makeOne(lambda: template) 169 | self.assertRaises(ValueError, instance, None, {"context": 1}) 170 | 171 | 172 | class SearchPathTests(object): 173 | def test_relative_tmpl_helloworld(self): 174 | from pyramid.renderers import render 175 | 176 | result = render("templates/helloworld.jinja2", {}) 177 | self.assertEqual(result, "\nHello föö") 178 | 179 | def test_relative_tmpl_extends(self): 180 | from pyramid.renderers import render 181 | 182 | result = render("templates/extends.jinja2", {}) 183 | self.assertEqual(result, "\nHello fööYo!") 184 | 185 | def test_relative_tmpl_extends_spec(self): 186 | from pyramid.renderers import render 187 | 188 | result = render("templates/extends_spec.jinja2", {"a": 1}) 189 | self.assertEqual(result, "\nHello fööYo!") 190 | 191 | def test_asset_tmpl_helloworld(self): 192 | from pyramid.renderers import render 193 | 194 | result = render("tests:templates/helloworld.jinja2", {"a": 1}) 195 | self.assertEqual(result, "\nHello föö") 196 | 197 | def test_asset_tmpl_extends(self): 198 | from pyramid.renderers import render 199 | 200 | result = render("tests:templates/extends.jinja2", {"a": 1}) 201 | self.assertEqual(result, "\nHello fööYo!") 202 | 203 | def test_asset_tmpl_extends_spec(self): 204 | from pyramid.renderers import render 205 | 206 | result = render("tests:templates/extends_spec.jinja2", {"a": 1}) 207 | self.assertEqual(result, "\nHello fööYo!") 208 | 209 | def test_asset_tmpl_deep_sub_leaf(self): 210 | from pyramid.renderers import render 211 | 212 | result = render("tests:templates/deep/sub/leaf.jinja2", {}) 213 | self.assertEqual(result, "deep-base sub-base sub-leaf") 214 | 215 | def test_asset_tmpl_deep_leaf(self): 216 | from pyramid.renderers import render 217 | 218 | result = render("tests:templates/deep/leaf.jinja2", {}) 219 | self.assertEqual(result, "sub-nav\n\ndeep-formsdeep-base deep-leaf") 220 | 221 | def test_abs_tmpl_extends(self): 222 | import os.path 223 | 224 | from pyramid.renderers import render 225 | 226 | here = os.path.abspath(os.path.dirname(__file__)) 227 | result = render(os.path.join(here, "templates", "extends.jinja2"), {"a": 1}) 228 | self.assertEqual(result, "\nHello fööYo!") 229 | 230 | def test_abs_tmpl_extends_missing(self): 231 | import os.path 232 | 233 | from jinja2 import TemplateNotFound 234 | from pyramid.renderers import render 235 | 236 | here = os.path.abspath(os.path.dirname(__file__)) 237 | templates_dir = os.path.join(here, "templates") 238 | self.assertRaises( 239 | TemplateNotFound, 240 | lambda: render(os.path.join(templates_dir, "/extends_missing.jinja2"), {}), 241 | ) 242 | 243 | 244 | class TestIntegrationWithSearchPath(SearchPathTests, unittest.TestCase): 245 | def setUp(self): 246 | config = testing.setUp() 247 | config.add_settings({"jinja2.directories": "tests:templates"}) 248 | config.include("pyramid_jinja2") 249 | self.config = config 250 | 251 | def tearDown(self): 252 | testing.tearDown() 253 | 254 | def test_tmpl_helloworld(self): 255 | from pyramid.renderers import render 256 | 257 | result = render("helloworld.jinja2", {"a": 1}) 258 | self.assertEqual(result, "\nHello föö") 259 | 260 | def test_tmpl_extends(self): 261 | from pyramid.renderers import render 262 | 263 | result = render("extends.jinja2", {"a": 1}) 264 | self.assertEqual(result, "\nHello fööYo!") 265 | 266 | def test_tmpl_extends_spec(self): 267 | from pyramid.renderers import render 268 | 269 | result = render("extends_spec.jinja2", {"a": 1}) 270 | self.assertEqual(result, "\nHello fööYo!") 271 | 272 | def test_tmpl_extends_relbase(self): 273 | from pyramid.renderers import render 274 | 275 | # this should pass as it will fallback to the new search path 276 | # and find it from there 277 | self.config.add_jinja2_search_path("tests:") 278 | result = render("extends_relbase.jinja2", {"a": 1}) 279 | self.assertEqual(result, "\nHello fööYo!") 280 | 281 | def test_caller_relative_tmpl_extends_relbase(self): 282 | from pyramid.renderers import render 283 | 284 | # this should pass as it will fallback to the new search path 285 | # and find it from there 286 | self.config.add_jinja2_search_path("tests:") 287 | result = render("templates/extends_relbase.jinja2", {"a": 1}) 288 | self.assertEqual(result, "\nHello fööYo!") 289 | 290 | def test_recursive_tmpl(self): 291 | from pyramid.renderers import render 292 | 293 | self.config.add_jinja2_renderer(".html") 294 | self.config.add_jinja2_search_path("tests:templates/recursive", name=".html") 295 | result = render("admin/index.html", {}) 296 | self.assertEqual(result, "foo") 297 | 298 | 299 | class TestIntegrationDefaultSearchPath(SearchPathTests, unittest.TestCase): 300 | def setUp(self): 301 | config = testing.setUp() 302 | config.include("pyramid_jinja2") 303 | 304 | def tearDown(self): 305 | testing.tearDown() 306 | 307 | 308 | class TestIntegrationReloading(unittest.TestCase): 309 | def setUp(self): 310 | config = testing.setUp() 311 | config.add_settings( 312 | { 313 | "pyramid.reload_templates": "true", 314 | } 315 | ) 316 | config.include("pyramid_jinja2") 317 | self.config = config 318 | 319 | def tearDown(self): 320 | testing.tearDown() 321 | 322 | def test_render_reload_templates(self): 323 | import os 324 | import tempfile 325 | import time 326 | 327 | from webtest import TestApp 328 | 329 | fd, path = tempfile.mkstemp(".jinja2") 330 | try: 331 | with open(path, "wb") as fp: 332 | fp.write(b"foo") 333 | 334 | self.config.add_view(lambda r: {}, renderer=path) 335 | app = TestApp(self.config.make_wsgi_app()) 336 | 337 | result = app.get("/").body 338 | self.assertEqual(result, b"foo") 339 | 340 | # need mtime to change and most systems have 1-second resolution 341 | time.sleep(1) 342 | with open(path, "wb") as fp: 343 | fp.write(b"bar") 344 | 345 | result = app.get("/").body 346 | self.assertEqual(result, b"bar") 347 | finally: 348 | os.close(fd) 349 | os.unlink(path) 350 | 351 | 352 | class Test_filters_and_tests(Base, unittest.TestCase): 353 | def _set_up_environ(self): 354 | self.config.include("pyramid_jinja2") 355 | return self.config.get_jinja2_environment() 356 | 357 | def _assert_has_test(self, test_name, test_obj): 358 | environ = self._set_up_environ() 359 | self.assertTrue(test_name in environ.tests) 360 | self.assertEqual(environ.tests[test_name], test_obj) 361 | 362 | def _assert_has_filter(self, filter_name, filter_obj): 363 | environ = self._set_up_environ() 364 | self.assertTrue(filter_name in environ.filters) 365 | self.assertEqual(environ.filters[filter_name], filter_obj) 366 | 367 | def _assert_has_global(self, global_name, global_obj): 368 | environ = self._set_up_environ() 369 | self.assertTrue(global_name in environ.globals) 370 | self.assertEqual(environ.globals[global_name], global_obj) 371 | 372 | def test_set_single_filter(self): 373 | filters = "my_filter = tests.test_it.my_test_func" 374 | self.config.registry.settings["jinja2.filters"] = filters 375 | self._assert_has_filter("my_filter", my_test_func) 376 | 377 | def test_set_single_test(self): 378 | filters = "my_test = tests.test_it.my_test_func" 379 | self.config.registry.settings["jinja2.tests"] = filters 380 | self._assert_has_test("my_test", my_test_func) 381 | 382 | def test_set_single_global(self): 383 | filters = "my_test = tests.test_it.my_test_func" 384 | self.config.registry.settings["jinja2.globals"] = filters 385 | self._assert_has_global("my_test", my_test_func) 386 | 387 | def test_set_multi_filters(self): 388 | self.config.registry.settings["jinja2.filters"] = ( 389 | "my_filter1 = tests.test_it.my_test_func\n" 390 | "my_filter2 = tests.test_it.my_test_func\n" 391 | "my_filter3 = tests.test_it.my_test_func" 392 | ) 393 | self._assert_has_filter("my_filter1", my_test_func) 394 | self._assert_has_filter("my_filter2", my_test_func) 395 | self._assert_has_filter("my_filter3", my_test_func) 396 | 397 | def test_set_multi_tests(self): 398 | self.config.registry.settings["jinja2.tests"] = ( 399 | "my_test1 = tests.test_it.my_test_func\n" 400 | "my_test2 = tests.test_it.my_test_func\n" 401 | "my_test3 = tests.test_it.my_test_func" 402 | ) 403 | self._assert_has_test("my_test1", my_test_func) 404 | self._assert_has_test("my_test2", my_test_func) 405 | self._assert_has_test("my_test3", my_test_func) 406 | 407 | def test_set_multi_globals(self): 408 | self.config.registry.settings["jinja2.globals"] = ( 409 | "my_global1 = tests.test_it.my_test_func\n" 410 | "my_global2 = tests.test_it.my_test_func\n" 411 | "my_global3 = tests.test_it.my_test_func" 412 | ) 413 | self._assert_has_global("my_global1", my_test_func) 414 | self._assert_has_global("my_global2", my_test_func) 415 | self._assert_has_global("my_global3", my_test_func) 416 | 417 | def test_filter_and_test_and_global_works_in_render(self): 418 | from pyramid.renderers import render 419 | 420 | config = testing.setUp() 421 | config.include("pyramid_jinja2") 422 | config.add_settings( 423 | { 424 | "jinja2.directories": "tests:templates", 425 | "jinja2.tests": "my_test = tests.test_it.my_test_func", 426 | "jinja2.filters": "my_filter = tests.test_it.my_test_func", 427 | "jinja2.globals": "my_global = tests.test_it.my_test_func", 428 | } 429 | ) 430 | config.add_jinja2_renderer(".jinja2") 431 | result = render("tests_and_filters.jinja2", {}) 432 | # my_test_func returns "True" - it will be rendered as True when used 433 | # as filter and will pass in tests 434 | self.assertEqual(result, "True is not False True") 435 | testing.tearDown() 436 | 437 | 438 | class Test_includeme(unittest.TestCase): 439 | def test_it(self): 440 | from pyramid.interfaces import IRendererFactory 441 | 442 | from pyramid_jinja2 import Jinja2RendererFactory, includeme 443 | 444 | config = testing.setUp() 445 | config.registry.settings["jinja2.directories"] = "/foobar" 446 | includeme(config) 447 | utility = config.registry.getUtility(IRendererFactory, name=".jinja2") 448 | self.assertTrue(isinstance(utility, Jinja2RendererFactory)) 449 | 450 | 451 | class Test_add_jinja2_searchpath(unittest.TestCase): 452 | def test_it_relative_to_package(self): 453 | import os 454 | 455 | from pyramid_jinja2 import includeme 456 | import tests 457 | 458 | config = testing.setUp() 459 | # hack because pyramid pre 1.6 doesn't configure testing configurator 460 | # with the correct package name 461 | config.package = tests 462 | config.package_name = "tests" 463 | config.add_settings({"jinja2.directories": "foobar"}) 464 | includeme(config) 465 | env = config.get_jinja2_environment() 466 | self.assertEqual(len(env.loader.searchpath), 2) 467 | self.assertEqual( 468 | [x.split(os.sep)[-3:] for x in env.loader.searchpath][0], 469 | ["pyramid_jinja2", "tests", "foobar"], 470 | ) 471 | self.assertEqual( 472 | [x.split(os.sep)[-2:] for x in env.loader.searchpath][1], 473 | ["pyramid_jinja2", "tests"], 474 | ) 475 | 476 | config.add_jinja2_search_path("grrr", prepend=True) 477 | self.assertEqual(len(env.loader.searchpath), 3) 478 | self.assertEqual( 479 | [x.split(os.sep)[-3:] for x in env.loader.searchpath][0], 480 | ["pyramid_jinja2", "tests", "grrr"], 481 | ) 482 | self.assertEqual( 483 | [x.split(os.sep)[-3:] for x in env.loader.searchpath][1], 484 | ["pyramid_jinja2", "tests", "foobar"], 485 | ) 486 | self.assertEqual( 487 | [x.split(os.sep)[-2:] for x in env.loader.searchpath][2], 488 | ["pyramid_jinja2", "tests"], 489 | ) 490 | 491 | 492 | class Test_get_jinja2_environment(unittest.TestCase): 493 | def test_it(self): 494 | from pyramid_jinja2 import Environment, includeme 495 | 496 | config = testing.setUp() 497 | includeme(config) 498 | self.assertEqual(config.get_jinja2_environment().__class__, Environment) 499 | 500 | 501 | class Test_bytecode_caching(unittest.TestCase): 502 | def test_default(self): 503 | from pyramid_jinja2 import includeme 504 | 505 | config = testing.setUp() 506 | config.registry.settings = {} 507 | includeme(config) 508 | env = config.get_jinja2_environment() 509 | self.assertTrue(env.bytecode_cache is None) 510 | self.assertFalse(env.auto_reload) 511 | 512 | def test_default_bccache(self): 513 | import jinja2.bccache 514 | 515 | from pyramid_jinja2 import includeme 516 | 517 | config = testing.setUp() 518 | config.registry.settings = {"jinja2.bytecode_caching": "true"} 519 | includeme(config) 520 | env = config.get_jinja2_environment() 521 | self.assertTrue( 522 | isinstance(env.bytecode_cache, jinja2.bccache.FileSystemBytecodeCache) 523 | ) 524 | self.assertTrue(env.bytecode_cache.directory) 525 | self.assertFalse(env.auto_reload) 526 | 527 | def test_directory(self): 528 | import tempfile 529 | 530 | from pyramid_jinja2 import includeme 531 | 532 | tmpdir = tempfile.mkdtemp() 533 | config = testing.setUp() 534 | config.registry.settings["jinja2.bytecode_caching"] = "1" 535 | config.registry.settings["jinja2.bytecode_caching_directory"] = tmpdir 536 | includeme(config) 537 | env = config.get_jinja2_environment() 538 | self.assertEqual(env.bytecode_cache.directory, tmpdir) 539 | # TODO: test tmpdir is deleted when interpreter exits 540 | 541 | def test_bccache_instance(self): 542 | import jinja2.bccache 543 | 544 | from pyramid_jinja2 import includeme 545 | 546 | mycache = jinja2.bccache.MemcachedBytecodeCache(DummyMemcachedClient()) 547 | config = testing.setUp() 548 | config.registry.settings = {"jinja2.bytecode_caching": mycache} 549 | includeme(config) 550 | env = config.get_jinja2_environment() 551 | self.assertTrue(env.bytecode_cache is mycache) 552 | self.assertFalse(env.auto_reload) 553 | 554 | def test_pyramid_reload_templates(self): 555 | from pyramid_jinja2 import includeme 556 | 557 | config = testing.setUp() 558 | config.registry.settings = {} 559 | config.registry.settings["pyramid.reload_templates"] = "true" 560 | includeme(config) 561 | env = config.get_jinja2_environment() 562 | self.assertTrue(env.auto_reload) 563 | 564 | 565 | class TestSmartAssetSpecLoader(unittest.TestCase): 566 | def _makeOne(self, **kw): 567 | from pyramid_jinja2 import SmartAssetSpecLoader 568 | 569 | return SmartAssetSpecLoader(**kw) 570 | 571 | def test_list_templates(self): 572 | loader = self._makeOne() 573 | self.assertRaises(TypeError, loader.list_templates) 574 | 575 | def test_get_source_invalid_spec(self): 576 | from jinja2.exceptions import TemplateNotFound 577 | 578 | loader = self._makeOne() 579 | self.assertRaises( 580 | TemplateNotFound, loader.get_source, None, "asset:foobar.jinja2" 581 | ) 582 | 583 | def test_get_source_spec(self): 584 | loader = self._makeOne() 585 | asset = "tests:templates/helloworld.jinja2" 586 | self.assertNotEqual(loader.get_source(None, asset), None) 587 | 588 | def test_get_source_legacy_spec(self): 589 | loader = self._makeOne() 590 | # make sure legacy prefixed asset spec based loading works 591 | asset = "asset:tests:templates/helloworld.jinja2" 592 | self.assertNotEqual(loader.get_source(None, asset), None) 593 | 594 | def test_get_source_from_path(self): 595 | import os.path 596 | 597 | here = os.path.abspath(os.path.dirname(__file__)) 598 | loader = self._makeOne(searchpath=[here]) 599 | asset = "templates/helloworld.jinja2" 600 | self.assertNotEqual(loader.get_source(None, asset), None) 601 | 602 | 603 | class TestFileInfo(unittest.TestCase): 604 | def test_mtime(self): 605 | from pyramid.asset import abspath_from_asset_spec 606 | 607 | from pyramid_jinja2 import FileInfo 608 | 609 | filename = abspath_from_asset_spec("templates/helloworld.jinja2", "tests") 610 | 611 | fi = FileInfo(filename) 612 | assert "_mtime" not in fi.__dict__ 613 | assert fi.mtime is not None 614 | assert fi.mtime == fi._mtime 615 | 616 | def test_uptodate(self): 617 | from pyramid_jinja2 import FileInfo 618 | 619 | fi = FileInfo("foobar") 620 | assert fi.uptodate() is False 621 | 622 | def test_notfound(self): 623 | from jinja2 import TemplateNotFound 624 | 625 | from pyramid_jinja2 import FileInfo 626 | 627 | fi = FileInfo("foobar") 628 | self.assertRaises(TemplateNotFound, lambda: fi._delay_init()) 629 | 630 | def test_delay_init(self): 631 | from pyramid_jinja2 import FileInfo 632 | 633 | class MyFileInfo(FileInfo): 634 | filename = "foo.jinja2" 635 | 636 | def __init__(self, data): 637 | self.data = data 638 | FileInfo.__init__(self, self.filename) 639 | 640 | def open_if_exists(self, fname): 641 | return StringIO(self.data) 642 | 643 | def getmtime(self, fname): 644 | return 1 645 | 646 | mi = MyFileInfo("nothing good here, move along") 647 | mi._delay_init() 648 | self.assertEqual(mi._contents, mi.data) 649 | 650 | 651 | class TestJinja2SearchPathIntegration(unittest.TestCase): 652 | def test_it(self): 653 | import os 654 | 655 | from pyramid.config import Configurator 656 | from webtest import TestApp 657 | 658 | from pyramid_jinja2 import includeme 659 | 660 | here = os.path.abspath(os.path.dirname(__file__)) 661 | templates_dir = os.path.join(here, "templates") 662 | 663 | def myview(request): 664 | return {} 665 | 666 | config1 = Configurator( 667 | settings={"jinja2.directories": os.path.join(templates_dir, "foo")} 668 | ) 669 | includeme(config1) 670 | config1.add_view(view=myview, renderer="mytemplate.jinja2") 671 | config2 = Configurator( 672 | settings={"jinja2.directories": os.path.join(templates_dir, "bar")} 673 | ) 674 | includeme(config2) 675 | config2.add_view(view=myview, renderer="mytemplate.jinja2") 676 | self.assertNotEqual(config1.registry.settings, config2.registry.settings) 677 | 678 | app1 = config1.make_wsgi_app() 679 | testapp = TestApp(app1) 680 | self.assertEqual(testapp.get("/").body, b"foo") 681 | 682 | app2 = config2.make_wsgi_app() 683 | testapp = TestApp(app2) 684 | self.assertEqual(testapp.get("/").body, b"bar") 685 | 686 | def test_it_relative_to_template(self): 687 | from pyramid.config import Configurator 688 | from webtest import TestApp 689 | 690 | from pyramid_jinja2 import includeme 691 | 692 | def myview(request): 693 | return {} 694 | 695 | config = Configurator(settings={"jinja2.directories": "templates"}) 696 | includeme(config) 697 | config.add_view(view=myview, name="baz1", renderer="baz1/mytemplate.jinja2") 698 | config.add_view(view=myview, name="baz2", renderer="baz2/mytemplate.jinja2") 699 | 700 | app1 = config.make_wsgi_app() 701 | testapp = TestApp(app1) 702 | self.assertEqual(testapp.get("/baz1").body, b"baz1\nbaz1 body") 703 | self.assertEqual(testapp.get("/baz2").body, b"baz2\nbaz2 body") 704 | 705 | 706 | class TestPackageFinder(unittest.TestCase): 707 | def test_caller_package(self): 708 | from pyramid_jinja2 import _PackageFinder 709 | 710 | pf = _PackageFinder() 711 | 712 | class MockInspect(object): 713 | def __init__(self, items=()): 714 | self.items = items 715 | 716 | def stack(self): 717 | return self.items 718 | 719 | pf.inspect = MockInspect() 720 | self.assertEqual(pf.caller_package(), None) 721 | 722 | import xml # noqa F401 723 | 724 | pf.inspect.items = [(Mock(f_globals={"__name__": "xml"}),)] 725 | 726 | 727 | class TestNewstyle(unittest.TestCase): 728 | def test_it(self): 729 | import os 730 | 731 | from pyramid.config import Configurator 732 | from webtest import TestApp 733 | 734 | from pyramid_jinja2 import includeme 735 | 736 | here = os.path.abspath(os.path.dirname(__file__)) 737 | templates_dir = os.path.join(here, "templates") 738 | 739 | def myview(request): 740 | return {"what": "eels"} 741 | 742 | config = Configurator( 743 | settings={"jinja2.directories": templates_dir, "jinja2.newstyle": True} 744 | ) 745 | includeme(config) 746 | config.add_view(view=myview, renderer="newstyle.jinja2") 747 | 748 | app = config.make_wsgi_app() 749 | testapp = TestApp(app) 750 | self.assertEqual( 751 | testapp.get("/").body.decode("utf-8"), "my hovercraft is full of eels!" 752 | ) 753 | 754 | 755 | class Test_add_jinja2_extension(Base, unittest.TestCase): 756 | def test_it(self): 757 | self.config.include("pyramid_jinja2") 758 | env_before = self.config.get_jinja2_environment() 759 | 760 | class MockExt(object): 761 | identifier = "foobar" 762 | 763 | def __init__(self, x): 764 | self.x = x 765 | 766 | self.config.add_jinja2_extension(MockExt) 767 | 768 | env_after = self.config.get_jinja2_environment() 769 | self.assertTrue("foobar" in env_after.extensions) 770 | self.assertTrue(env_before is env_after) 771 | 772 | def test_alternate_renderer_extension(self): 773 | self.config.include("pyramid_jinja2") 774 | self.config.add_jinja2_renderer(".html") 775 | env_before = self.config.get_jinja2_environment(".html") 776 | 777 | class MockExt(object): 778 | identifier = "foobar" 779 | 780 | def __init__(self, x): 781 | self.x = x 782 | 783 | self.config.add_jinja2_extension(MockExt, ".html") 784 | 785 | env_after = self.config.get_jinja2_environment(".html") 786 | default_env = self.config.get_jinja2_environment() 787 | 788 | self.assertTrue("foobar" in env_after.extensions) 789 | self.assertTrue("foobar" not in default_env.extensions) 790 | self.assertTrue(env_before is env_after) 791 | 792 | 793 | def my_test_func(*args, **kwargs): 794 | """Used as a fake filter/test function""" 795 | return True 796 | 797 | 798 | class DummyMemcachedClient(dict): 799 | """A memcached client acceptable to jinja2.MemcachedBytecodeCache.""" 800 | 801 | def set(self, key, value, timeout): 802 | self[key] = value # pragma: no cover 803 | 804 | 805 | class DummyEnviron(dict): 806 | def get_template(self, path): # pragma: no cover 807 | return path 808 | 809 | 810 | class DummyTemplate(object): 811 | def render(self, system): 812 | return b"result".decode("utf-8") 813 | 814 | 815 | class DummyRendererInfo(object): 816 | def __init__(self, kw): 817 | self.__dict__.update(kw) 818 | if "registry" in self.__dict__: 819 | self.settings = self.registry.settings 820 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import unittest 3 | 4 | 5 | class Test_parse_named_assetspecs(unittest.TestCase): 6 | def _callFUT(self, *args, **kwargs): 7 | from pyramid_jinja2.settings import parse_named_assetspecs 8 | 9 | return parse_named_assetspecs(*args, **kwargs) 10 | 11 | def test_it_with_strings(self): 12 | from pyramid.path import DottedNameResolver 13 | 14 | import pyramid_jinja2 15 | import tests 16 | 17 | resolver = DottedNameResolver() 18 | result = self._callFUT( 19 | """ 20 | foo = pyramid_jinja2 21 | bar = tests 22 | """, 23 | resolver.maybe_resolve, 24 | ) 25 | self.assertEqual(result["foo"], pyramid_jinja2) 26 | self.assertEqual(result["bar"], tests) 27 | 28 | def test_it_with_dict(self): 29 | from pyramid.path import DottedNameResolver 30 | 31 | import pyramid_jinja2 32 | import tests 33 | 34 | resolver = DottedNameResolver() 35 | result = self._callFUT( 36 | { 37 | "foo": "pyramid_jinja2", 38 | "bar": tests, 39 | }, 40 | resolver.maybe_resolve, 41 | ) 42 | self.assertEqual(result["foo"], pyramid_jinja2) 43 | self.assertEqual(result["bar"], tests) 44 | 45 | 46 | class Test_parse_loader_options_from_settings(unittest.TestCase): 47 | def _callFUT(self, *args, **kwargs): 48 | from pyramid_jinja2.settings import parse_loader_options_from_settings 49 | 50 | return parse_loader_options_from_settings(*args, **kwargs) 51 | 52 | def test_defaults(self): 53 | options = self._callFUT({}, "p.", None, None) 54 | self.assertEqual(options["debug"], False) 55 | self.assertEqual(options["encoding"], "utf-8") 56 | self.assertEqual(len(options["searchpath"]), 0) 57 | 58 | def test_options(self): 59 | options = self._callFUT( 60 | { 61 | "debug_templates": "false", 62 | "p.debug_templates": "true", 63 | "p.input_encoding": "ascii", 64 | "p.directories": "tests:templates", 65 | }, 66 | "p.", 67 | None, 68 | None, 69 | ) 70 | self.assertEqual(options["debug"], True) 71 | self.assertEqual(options["encoding"], "ascii") 72 | self.assertEqual(len(options["searchpath"]), 1) 73 | self.assertTrue( 74 | options["searchpath"][0].endswith( 75 | os.path.join("pyramid_jinja2", "tests", "templates") 76 | ) 77 | ) 78 | 79 | def test_options_with_spec(self): 80 | options = self._callFUT({"p.directories": "pyramid_jinja2:"}, "p.", None, None) 81 | self.assertEqual(len(options["searchpath"]), 1) 82 | self.assertTrue(options["searchpath"][0].endswith("pyramid_jinja2")) 83 | 84 | def test_options_with_abspath(self): 85 | import os.path 86 | 87 | here = os.path.dirname(os.path.abspath(__file__)) 88 | options = self._callFUT({"p.directories": here}, "p.", None, None) 89 | self.assertEqual(len(options["searchpath"]), 1) 90 | self.assertEqual(options["searchpath"][0], here) 91 | 92 | def test_options_with_relpath(self): 93 | import os 94 | 95 | import pyramid_jinja2 96 | 97 | options = self._callFUT({"p.directories": "foo"}, "p.", None, pyramid_jinja2) 98 | self.assertEqual(len(options["searchpath"]), 1) 99 | self.assertEqual( 100 | options["searchpath"][0].split(os.sep)[-2:], ["pyramid_jinja2", "foo"] 101 | ) 102 | 103 | def test_debug_fallback(self): 104 | options = self._callFUT( 105 | { 106 | "debug_templates": "true", 107 | }, 108 | "p.", 109 | None, 110 | None, 111 | ) 112 | self.assertEqual(options["debug"], True) 113 | 114 | 115 | class Test_parse_env_options_from_settings(unittest.TestCase): 116 | def _callFUT(self, settings, prefix=""): 117 | from pyramid.path import DottedNameResolver 118 | 119 | import pyramid_jinja2 120 | from pyramid_jinja2.settings import parse_env_options_from_settings 121 | 122 | resolver = DottedNameResolver() 123 | return parse_env_options_from_settings( 124 | settings, 125 | prefix, 126 | resolver.maybe_resolve, 127 | pyramid_jinja2, 128 | ) 129 | 130 | def test_most_settings(self): 131 | from pyramid_jinja2.i18n import GetTextWrapper 132 | 133 | settings = { 134 | "j2.block_start_string": "<<<", 135 | "j2.block_end_string": ">>>", 136 | "j2.variable_start_string": "<|<", 137 | "j2.variable_end_string": ">|>", 138 | "j2.comment_start_string": "<+<", 139 | "j2.comment_end_string": ">+>", 140 | "j2.line_statement_prefix": ">.>", 141 | "j2.line_comment_prefix": "^.^", 142 | "j2.trim_blocks": "true", 143 | "j2.newline_sequence": "\r", 144 | "j2.optimized": "true", 145 | "j2.autoescape": "false", 146 | "j2.cache_size": "300", 147 | } 148 | opts = self._callFUT(settings, "j2.") 149 | # test 150 | self.assertEqual(opts["block_start_string"], "<<<") 151 | self.assertEqual(opts["block_end_string"], ">>>") 152 | self.assertEqual(opts["variable_start_string"], "<|<") 153 | self.assertEqual(opts["variable_end_string"], ">|>") 154 | self.assertEqual(opts["comment_start_string"], "<+<") 155 | self.assertEqual(opts["comment_end_string"], ">+>") 156 | self.assertEqual(opts["line_statement_prefix"], ">.>") 157 | self.assertEqual(opts["line_comment_prefix"], "^.^") 158 | self.assertEqual(opts["trim_blocks"], True) 159 | self.assertEqual(opts["newline_sequence"], "\r") 160 | self.assertEqual(opts["optimized"], True) 161 | self.assertEqual(opts["autoescape"], False) 162 | self.assertEqual(opts["cache_size"], 300) 163 | self.assertEqual(opts["gettext"].domain, "pyramid_jinja2") 164 | self.assertFalse("finalize" in opts) 165 | self.assertTrue(isinstance(opts["gettext"], GetTextWrapper)) 166 | 167 | def test_finalize(self): 168 | settings = { 169 | "j2.finalize": "tests.test_settings._fake_finalize", 170 | } 171 | opts = self._callFUT(settings, "j2.") 172 | self.assertTrue(opts["finalize"] is _fake_finalize) 173 | 174 | def test_override_gettext(self): 175 | class FakeGettextWrapper(object): 176 | def __init__(self, domain): 177 | self.domain = domain 178 | 179 | settings = { 180 | "j2.i18n.gettext": FakeGettextWrapper, 181 | "j2.i18n.domain": "testdomain", 182 | } 183 | opts = self._callFUT(settings, "j2.") 184 | self.assertTrue(isinstance(opts["gettext"], FakeGettextWrapper)) 185 | self.assertEqual(opts["gettext"].domain, "testdomain") 186 | 187 | def test_strict_undefined(self): 188 | from jinja2 import StrictUndefined 189 | 190 | settings = {"j2.undefined": "strict"} 191 | opts = self._callFUT(settings, "j2.") 192 | self.assertEqual(opts["undefined"], StrictUndefined) 193 | 194 | def test_debug_undefined(self): 195 | from jinja2 import DebugUndefined 196 | 197 | settings = {"j2.undefined": "debug"} 198 | opts = self._callFUT(settings, "j2.") 199 | self.assertEqual(opts["undefined"], DebugUndefined) 200 | 201 | def test_default_undefined(self): 202 | from jinja2 import Undefined 203 | 204 | settings = {"j2.undefined": ""} 205 | opts = self._callFUT(settings, "j2.") 206 | self.assertEqual(opts["undefined"], Undefined) 207 | 208 | def test_default_extensions(self): 209 | opts = self._callFUT({}, "j2.") 210 | self.assertEqual(opts["extensions"], ["jinja2.ext.i18n"]) 211 | 212 | def test_override_i18n_extension(self): 213 | settings = { 214 | "j2.i18n_extension": "test.TestI18NExtension", 215 | } 216 | opts = self._callFUT(settings, "j2.") 217 | self.assertEqual(opts["extensions"], ["test.TestI18NExtension"]) 218 | 219 | 220 | # This is just a fake top level name that we can pass into maybe_dotted that 221 | # will resolve. 222 | _fake_finalize = object() 223 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | lint, 4 | py37,py38,py39,py310,py311,py312,pypy3, 5 | py39-pyramid{13,14,15,16,17,18,110,20}, 6 | py39-jinja2legacy, 7 | coverage, 8 | docs 9 | 10 | [testenv] 11 | commands = 12 | python --version 13 | pytest {posargs:} 14 | deps = 15 | pyramid13: pyramid <= 1.3.99 16 | pyramid14: pyramid <= 1.4.99 17 | pyramid15: pyramid <= 1.5.99 18 | pyramid16: pyramid <= 1.6.99 19 | pyramid17: pyramid <= 1.7.99 20 | pyramid18: pyramid <= 1.8.99 21 | pyramid19: pyramid <= 1.9.99 22 | pyramid110: pyramid <= 1.10.99 23 | pyramid20: pyramid <= 2.0.99 24 | jinja2legacy: jinja2 < 3.0 25 | jinja2legacy: markupsafe < 2.0 26 | extras = 27 | testing 28 | setenv = 29 | COVERAGE_FILE=.coverage.{envname} 30 | 31 | [testenv:coverage] 32 | commands = 33 | coverage combine 34 | coverage xml 35 | coverage report --fail-under=100 36 | deps = 37 | coverage 38 | setenv = 39 | COVERAGE_FILE=.coverage 40 | 41 | [testenv:docs] 42 | allowlist_externals = make 43 | commands = 44 | make -C docs html epub BUILDDIR={envdir} 45 | extras = 46 | docs 47 | 48 | 49 | [testenv:lint] 50 | skip_install = True 51 | commands = 52 | isort --check-only --df src/pyramid_jinja2 tests demo 53 | black --check --diff src/pyramid_jinja2 tests demo 54 | flake8 src/pyramid_jinja2/ tests demo 55 | check-manifest 56 | # build sdist/wheel 57 | python -m build . 58 | twine check dist/* 59 | deps = 60 | black 61 | build 62 | check-manifest 63 | flake8 64 | flake8-bugbear 65 | isort 66 | readme_renderer 67 | twine 68 | 69 | [testenv:format] 70 | skip_install = true 71 | commands = 72 | isort src/pyramid_jinja2 tests demo 73 | black src/pyramid_jinja2 tests demo 74 | deps = 75 | black 76 | isort 77 | 78 | [testenv:build] 79 | skip_install = true 80 | commands = 81 | # clean up build/ and dist/ folders 82 | python -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' 83 | # Make sure we aren't forgetting anything 84 | check-manifest 85 | # build sdist/wheel 86 | python -m build . 87 | # Verify all is well 88 | twine check dist/* 89 | 90 | deps = 91 | build 92 | check-manifest 93 | readme_renderer 94 | twine 95 | --------------------------------------------------------------------------------