From 65adae8035a442c9d6bace4ad41aafdfe2efa552 Mon Sep 17 00:00:00 2001 From: luoyaoming Date: Tue, 7 May 2024 15:42:49 +0800 Subject: [PATCH] Import Upstream version 3.1.2 --- CHANGES.rst | 60 ++++++++ PKG-INFO | 4 +- debian/changelog | 17 --- debian/control | 69 --------- debian/copyright | 194 -------------------------- debian/jinja.vim | 135 ------------------ debian/jinja.yaml | 4 - debian/patches/series | 1 - debian/python-jinja2-doc.docs | 1 - debian/python-jinja2-doc.examples | 1 - debian/python-jinja2-doc.links | 5 - debian/python-jinja2.pyremove | 2 - debian/python3-jinja2.bcep | 1 - debian/python3-jinja2.dirs | 2 - debian/python3-jinja2.install | 2 - debian/rules | 29 ---- debian/source/format | 1 - debian/watch | 3 - docs/api.rst | 33 ++--- docs/faq.rst | 200 +++++++------------------- docs/integration.rst | 19 +++ docs/intro.rst | 2 +- docs/sandbox.rst | 125 ++++++++++------- docs/switching.rst | 201 +++++++++++---------------- docs/templates.rst | 92 ++++++++---- docs/tricks.rst | 6 +- requirements/dev.txt | 122 +++------------- requirements/docs.txt | 38 +++-- requirements/tests.txt | 15 +- requirements/typing.txt | 11 +- setup.cfg | 7 +- setup.py | 1 - src/Jinja2.egg-info/PKG-INFO | 4 +- src/Jinja2.egg-info/SOURCES.txt | 2 +- src/Jinja2.egg-info/entry_points.txt | 3 +- src/jinja2/__init__.py | 10 +- src/jinja2/_identifier.py | 2 +- src/jinja2/async_utils.py | 11 +- src/jinja2/bccache.py | 54 ++++++- src/jinja2/compiler.py | 12 +- src/jinja2/debug.py | 72 +--------- src/jinja2/environment.py | 58 ++++---- src/jinja2/ext.py | 62 +++------ src/jinja2/filters.py | 144 ++++++++++--------- src/jinja2/idtracking.py | 2 +- src/jinja2/lexer.py | 35 +++-- src/jinja2/loaders.py | 19 ++- src/jinja2/nativetypes.py | 12 +- src/jinja2/parser.py | 36 ++--- src/jinja2/runtime.py | 51 ------- src/jinja2/sandbox.py | 2 +- src/jinja2/utils.py | 99 ------------- src/jinja2/visitor.py | 6 +- tests/test_async.py | 60 ++++---- tests/test_async_filters.py | 20 +++ tests/test_debug.py | 2 +- tests/test_ext.py | 14 +- tests/test_features.py | 14 -- tests/test_filters.py | 30 ++++ tests/test_loader.py | 9 ++ tests/test_nativetypes.py | 7 + tests/test_pickle.py | 6 + tests/test_regression.py | 17 --- tox.ini | 2 +- 64 files changed, 803 insertions(+), 1477 deletions(-) delete mode 100644 debian/changelog delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100644 debian/jinja.vim delete mode 100644 debian/jinja.yaml delete mode 100644 debian/patches/series delete mode 100644 debian/python-jinja2-doc.docs delete mode 100644 debian/python-jinja2-doc.examples delete mode 100644 debian/python-jinja2-doc.links delete mode 100644 debian/python-jinja2.pyremove delete mode 100644 debian/python3-jinja2.bcep delete mode 100644 debian/python3-jinja2.dirs delete mode 100644 debian/python3-jinja2.install delete mode 100755 debian/rules delete mode 100644 debian/source/format delete mode 100644 debian/watch delete mode 100644 tests/test_features.py create mode 100644 tests/test_pickle.py diff --git a/CHANGES.rst b/CHANGES.rst index 9c58019..fede06e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,65 @@ .. currentmodule:: jinja2 +Version 3.1.2 +------------- + +Released 2022-04-28 + +- Add parameters to ``Environment.overlay`` to match ``__init__``. + :issue:`1645` +- Handle race condition in ``FileSystemBytecodeCache``. :issue:`1654` + + +Version 3.1.1 +------------- + +Released 2022-03-25 + +- The template filename on Windows uses the primary path separator. + :issue:`1637` + + +Version 3.1.0 +------------- + +Released 2022-03-24 + +- Drop support for Python 3.6. :pr:`1534` +- Remove previously deprecated code. :pr:`1544` + + - ``WithExtension`` and ``AutoEscapeExtension`` are built-in now. + - ``contextfilter`` and ``contextfunction`` are replaced by + ``pass_context``. ``evalcontextfilter`` and + ``evalcontextfunction`` are replaced by ``pass_eval_context``. + ``environmentfilter`` and ``environmentfunction`` are replaced + by ``pass_environment``. + - ``Markup`` and ``escape`` should be imported from MarkupSafe. + - Compiled templates from very old Jinja versions may need to be + recompiled. + - Legacy resolve mode for ``Context`` subclasses is no longer + supported. Override ``resolve_or_missing`` instead of + ``resolve``. + - ``unicode_urlencode`` is renamed to ``url_quote``. + +- Add support for native types in macros. :issue:`1510` +- The ``{% trans %}`` tag can use ``pgettext`` and ``npgettext`` by + passing a context string as the first token in the tag, like + ``{% trans "title" %}``. :issue:`1430` +- Update valid identifier characters from Python 3.6 to 3.7. + :pr:`1571` +- Filters and tests decorated with ``@async_variant`` are pickleable. + :pr:`1612` +- Add ``items`` filter. :issue:`1561` +- Subscriptions (``[0]``, etc.) can be used after filters, tests, and + calls when the environment is in async mode. :issue:`1573` +- The ``groupby`` filter is case-insensitive by default, matching + other comparison filters. Added the ``case_sensitive`` parameter to + control this. :issue:`1463` +- Windows drive-relative path segments in template names will not + result in ``FileSystemLoader`` and ``PackageLoader`` loading from + drive-relative paths. :pr:`1621` + + Version 3.0.3 ------------- diff --git a/PKG-INFO b/PKG-INFO index d71c2d3..7b92c21 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: Jinja2 -Version: 3.0.3 +Version: 3.1.2 Summary: A very fast and expressive template engine. Home-page: https://palletsprojects.com/p/jinja/ Author: Armin Ronacher @@ -24,7 +24,7 @@ Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Text Processing :: Markup :: HTML -Requires-Python: >=3.6 +Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: i18n License-File: LICENSE.rst diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index c8a4714..0000000 --- a/debian/changelog +++ /dev/null @@ -1,17 +0,0 @@ -jinja2 (3.0.3-ok3) yangtze; urgency=medium - - * Add python3-sphinxcontrib-log-cabinet to Build-Depends. - - -- sufang Fri, 17 Mar 2023 14:19:01 +0800 - -jinja2 (3.0.3-ok2) yangtze; urgency=medium - - * Update version info. - - -- sufang Fri, 17 Mar 2023 11:05:53 +0800 - -jinja2 (3.0.3-ok1) yangtze; urgency=medium - - * Build for openkylin. - - -- sufang Tue, 15 Nov 2022 09:30:53 +0800 diff --git a/debian/control b/debian/control deleted file mode 100644 index 9704e3c..0000000 --- a/debian/control +++ /dev/null @@ -1,69 +0,0 @@ -Source: jinja2 -Section: python -Priority: optional -Maintainer: OpenKylin Developers -Build-Depends: - debhelper-compat (= 12), - dh-python, - python3-all, - python3-babel, - python3-markupsafe (>= 2.0), - python3-pallets-sphinx-themes, - python3-pygments, - python3-setuptools, - python3-sphinx, - python3-sphinx-issues, - python3-sphinxcontrib-log-cabinet -Standards-Version: 4.5.0 -Homepage: http://jinja.pocoo.org/ -Vcs-Git: https://gitee.com/openkylin/jinja2.git -Vcs-Browser: https://gitee.com/openkylin/jinja2 - -Package: python-jinja2-doc -Section: doc -Architecture: all -Depends: - ${misc:Depends}, - ${sphinxdoc:Depends}, -Recommends: - python3-jinja2, -Multi-Arch: foreign -Description: documentation for the Jinja2 Python library - Jinja2 is a small but fast and easy to use stand-alone template engine - . - This package contains the documentation for Jinja2 in HTML and - reStructuredText formats. - -Package: python3-jinja2 -Architecture: all -Depends: - python3-markupsafe (>= 2.0), - ${misc:Depends}, - ${python3:Depends}, -Recommends: - python3-babel, - python3-pkg-resources, -Suggests: - python-jinja2-doc, -Breaks: - python-jinja2 (<< 2.11.1-1), -Replaces: - python-jinja2 (<< 2.11.1-1), -Description: small but fast and easy to use stand-alone template engine - Jinja2 is a template engine written in pure Python. It provides a Django - inspired non-XML syntax but supports inline expressions and an optional - sandboxed environment. - . - The key-features are: - * Configurable syntax. If you are generating LaTeX or other formats with - Jinja2 you can change the delimiters to something that integrates better - into the LaTeX markup. - * Fast. While performance is not the primarily target of Jinja2 it’s - surprisingly fast. The overhead compared to regular Python code was reduced - to the very minimum. - * Easy to debug. Jinja2 integrates directly into the Python traceback system - which allows you to debug Jinja2 templates with regular Python debugging - helpers. - * Secure. It’s possible to evaluate untrusted template code if the optional - sandbox is enabled. This allows Jinja2 to be used as templating language - for applications where users may modify the template design. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 10ed0af..0000000 --- a/debian/copyright +++ /dev/null @@ -1,194 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: jinja2 -Source: -# -# Please double check copyright with the licensecheck(1) command. - -Files: CHANGES.rst - MANIFEST.in - PKG-INFO - README.rst - docs/Makefile - docs/_static/jinja-logo-sidebar.png - docs/_static/jinja-logo.png - docs/api.rst - docs/changes.rst - docs/conf.py - docs/examples/cache_extension.py - docs/examples/inline_gettext_extension.py - docs/faq.rst - docs/index.rst - docs/integration.rst - docs/intro.rst - docs/license.rst - docs/make.bat - docs/nativetypes.rst - docs/sandbox.rst - docs/switching.rst - docs/templates.rst - docs/tricks.rst - examples/basic/cycle.py - examples/basic/debugger.py - examples/basic/inheritance.py - examples/basic/templates/broken.html - examples/basic/templates/subbroken.html - examples/basic/test.py - examples/basic/test_filter_and_linestatements.py - examples/basic/test_loop_filter.py - examples/basic/translate.py - requirements/dev.txt - requirements/docs.txt - requirements/tests.txt - requirements/typing.txt - setup.cfg - setup.py - src/Jinja2.egg-info/PKG-INFO - src/Jinja2.egg-info/SOURCES.txt - src/Jinja2.egg-info/dependency_links.txt - src/Jinja2.egg-info/entry_points.txt - src/Jinja2.egg-info/requires.txt - src/Jinja2.egg-info/top_level.txt - src/jinja2/__init__.py - src/jinja2/_identifier.py - src/jinja2/async_utils.py - src/jinja2/bccache.py - src/jinja2/compiler.py - src/jinja2/constants.py - src/jinja2/debug.py - src/jinja2/defaults.py - src/jinja2/environment.py - src/jinja2/exceptions.py - src/jinja2/ext.py - src/jinja2/filters.py - src/jinja2/idtracking.py - src/jinja2/lexer.py - src/jinja2/loaders.py - src/jinja2/meta.py - src/jinja2/nativetypes.py - src/jinja2/nodes.py - src/jinja2/optimizer.py - src/jinja2/parser.py - src/jinja2/py.typed - src/jinja2/runtime.py - src/jinja2/sandbox.py - src/jinja2/tests.py - src/jinja2/utils.py - src/jinja2/visitor.py - tests/conftest.py - tests/res/__init__.py - tests/res/package.zip - tests/res/templates/broken.html - tests/res/templates/foo/test.html - tests/res/templates/mojibake.txt - tests/res/templates/syntaxerror.html - tests/res/templates/test.html - tests/res/templates2/foo - tests/test_api.py - tests/test_async.py - tests/test_async_filters.py - tests/test_bytecode_cache.py - tests/test_compile.py - tests/test_core_tags.py - tests/test_debug.py - tests/test_ext.py - tests/test_features.py - tests/test_filters.py - tests/test_idtracking.py - tests/test_imports.py - tests/test_inheritance.py - tests/test_lexnparse.py - tests/test_loader.py - tests/test_nativetypes.py - tests/test_nodes.py - tests/test_regression.py - tests/test_runtime.py - tests/test_security.py - tests/test_tests.py - tests/test_utils.py - tox.ini -Copyright: __NO_COPYRIGHT_NOR_LICENSE__ -License: __NO_COPYRIGHT_NOR_LICENSE__ - -Files: docs/extensions.rst -Copyright: __NO_COPYRIGHT__ in: docs/extensions.rst -License: __UNKNOWN__ - - String formatting is used even if no placeholders are used, which - makes all strings use a consistent format. Remember to escape any - raw percent signs as ``%%``, such as ``100%%``. - - The translated string is marked safe, formatting performs escaping - as needed. Mark a parameter as ``|safe`` if it has already been - escaped. - . - Expression Statement - -Files: LICENSE.rst -Copyright: 2007 Pallets -License: BSD-3-Clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - . - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - . - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - . - 3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - . - On Debian systems, the complete text of the BSD 3-clause "New" or "Revised" - License can be found in `/usr/share/common-licenses/BSD'. - -#---------------------------------------------------------------------------- -# xml and html files (skipped): -# artwork/jinjalogo.svg - -#---------------------------------------------------------------------------- -# Files marked as NO_LICENSE_TEXT_FOUND may be covered by the following -# license/copyright files. - -#---------------------------------------------------------------------------- -# License file: LICENSE.rst - Copyright 2007 Pallets - . - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - . - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - . - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - . - 3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/jinja.vim b/debian/jinja.vim deleted file mode 100644 index 2fd5838..0000000 --- a/debian/jinja.vim +++ /dev/null @@ -1,135 +0,0 @@ -" Vim syntax file -" Language: Jinja template -" Maintainer: Armin Ronacher -" Last Change: 2008 May 9 -" Version: 1.1 -" -" Known Bugs: -" because of odd limitations dicts and the modulo operator -" appear wrong in the template. -" -" Changes: -" -" 2008 May 9: Added support for Jinja2 changes (new keyword rules) - -" .vimrc variable to disable html highlighting -if !exists('g:jinja_syntax_html') - let g:jinja_syntax_html=1 -endif - -" For version 5.x: Clear all syntax items -" For version 6.x: Quit when a syntax file was already loaded -if !exists("main_syntax") - if version < 600 - syntax clear - elseif exists("b:current_syntax") - finish -endif - let main_syntax = 'jinja' -endif - -" Pull in the HTML syntax. -if g:jinja_syntax_html - if version < 600 - so :p:h/html.vim - else - runtime! syntax/html.vim - unlet b:current_syntax - endif -endif - -syntax case match - -" Jinja template built-in tags and parameters (without filter, macro, is and raw, they -" have special threatment) -syn keyword jinjaStatement containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained and if else in not or recursive as import - -syn keyword jinjaStatement containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained is filter skipwhite nextgroup=jinjaFilter -syn keyword jinjaStatement containedin=jinjaTagBlock contained macro skipwhite nextgroup=jinjaFunction -syn keyword jinjaStatement containedin=jinjaTagBlock contained block skipwhite nextgroup=jinjaBlockName - -" Variable Names -syn match jinjaVariable containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained /[a-zA-Z_][a-zA-Z0-9_]*/ -syn keyword jinjaSpecial containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained false true none False True None loop super caller varargs kwargs - -" Filters -syn match jinjaOperator "|" containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained skipwhite nextgroup=jinjaFilter -syn match jinjaFilter contained /[a-zA-Z_][a-zA-Z0-9_]*/ -syn match jinjaFunction contained /[a-zA-Z_][a-zA-Z0-9_]*/ -syn match jinjaBlockName contained /[a-zA-Z_][a-zA-Z0-9_]*/ - -" Jinja template constants -syn region jinjaString containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained start=/"/ skip=/\(\\\)\@\)*\\"/ end=/"/ -syn region jinjaString containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained start=/'/ skip=/\(\\\)\@\)*\\'/ end=/'/ -syn match jinjaNumber containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained /[0-9]\+\(\.[0-9]\+\)\?/ - -" Operators -syn match jinjaOperator containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained /[+\-*\/<>=!,:]/ -syn match jinjaPunctuation containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained /[()\[\]]/ -syn match jinjaOperator containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained /\./ nextgroup=jinjaAttribute -syn match jinjaAttribute contained /[a-zA-Z_][a-zA-Z0-9_]*/ - -" Jinja template tag and variable blocks -syn region jinjaNested matchgroup=jinjaOperator start="(" end=")" transparent display containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained -syn region jinjaNested matchgroup=jinjaOperator start="\[" end="\]" transparent display containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained -syn region jinjaNested matchgroup=jinjaOperator start="{" end="}" transparent display containedin=jinjaVarBlock,jinjaTagBlock,jinjaNested contained -syn region jinjaTagBlock matchgroup=jinjaTagDelim start=/{%-\?/ end=/-\?%}/ containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaRaw,jinjaString,jinjaNested,jinjaComment - -syn region jinjaVarBlock matchgroup=jinjaVarDelim start=/{{-\?/ end=/-\?}}/ containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaRaw,jinjaString,jinjaNested,jinjaComment - -" Jinja template 'raw' tag -syn region jinjaRaw matchgroup=jinjaRawDelim start="{%\s*raw\s*%}" end="{%\s*endraw\s*%}" containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaString,jinjaComment - -" Jinja comments -syn region jinjaComment matchgroup=jinjaCommentDelim start="{#" end="#}" containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaString,jinjaComment - -" Block start keywords. A bit tricker. We only highlight at the start of a -" tag block and only if the name is not followed by a comma or equals sign -" which usually means that we have to deal with an assignment. -syn match jinjaStatement containedin=jinjaTagBlock contained /\({%-\?\s*\)\@<=\<[a-zA-Z_][a-zA-Z0-9_]*\>\(\s*[,=]\)\@!/ - -" and context modifiers -syn match jinjaStatement containedin=jinjaTagBlock contained /\/ - - -" Define the default highlighting. -" For version 5.7 and earlier: only when not done already -" For version 5.8 and later: only when an item doesn't have highlighting yet -if version >= 508 || !exists("did_jinja_syn_inits") - if version < 508 - let did_jinja_syn_inits = 1 - command -nargs=+ HiLink hi link - else - command -nargs=+ HiLink hi def link - endif - - HiLink jinjaPunctuation jinjaOperator - HiLink jinjaAttribute jinjaVariable - HiLink jinjaFunction jinjaFilter - - HiLink jinjaTagDelim jinjaTagBlock - HiLink jinjaVarDelim jinjaVarBlock - HiLink jinjaCommentDelim jinjaComment - HiLink jinjaRawDelim jinja - - HiLink jinjaSpecial Special - HiLink jinjaOperator Normal - HiLink jinjaRaw Normal - HiLink jinjaTagBlock PreProc - HiLink jinjaVarBlock PreProc - HiLink jinjaStatement Statement - HiLink jinjaFilter Function - HiLink jinjaBlockName Function - HiLink jinjaVariable Identifier - HiLink jinjaString Constant - HiLink jinjaNumber Constant - HiLink jinjaComment Comment - - delcommand HiLink -endif - -let b:current_syntax = "jinja" - -if main_syntax == 'jinja' - unlet main_syntax -endif diff --git a/debian/jinja.yaml b/debian/jinja.yaml deleted file mode 100644 index b225372..0000000 --- a/debian/jinja.yaml +++ /dev/null @@ -1,4 +0,0 @@ -addon: jinja -description: "allow syntax highlighting for Jinja templates" -files: - - syntax/jinja.vim diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index 4a97dfa..0000000 --- a/debian/patches/series +++ /dev/null @@ -1 +0,0 @@ -# You must remove unused comment lines for the released package. diff --git a/debian/python-jinja2-doc.docs b/debian/python-jinja2-doc.docs deleted file mode 100644 index 4ecc793..0000000 --- a/debian/python-jinja2-doc.docs +++ /dev/null @@ -1 +0,0 @@ -docs/_build/html diff --git a/debian/python-jinja2-doc.examples b/debian/python-jinja2-doc.examples deleted file mode 100644 index e39721e..0000000 --- a/debian/python-jinja2-doc.examples +++ /dev/null @@ -1 +0,0 @@ -examples/* diff --git a/debian/python-jinja2-doc.links b/debian/python-jinja2-doc.links deleted file mode 100644 index 99f63cb..0000000 --- a/debian/python-jinja2-doc.links +++ /dev/null @@ -1,5 +0,0 @@ -/usr/share/doc/python-jinja2-doc/examples /usr/share/doc/python3-jinja2/examples -/usr/share/doc/python-jinja2-doc/html /usr/share/doc/python-jinja2/html -/usr/share/doc/python-jinja2-doc/html /usr/share/doc/python3-jinja2/html -/usr/share/doc/python-jinja2-doc/html/_sources /usr/share/doc/python-jinja2/rst -/usr/share/doc/python-jinja2-doc/html/_sources /usr/share/doc/python3-jinja2/rst diff --git a/debian/python-jinja2.pyremove b/debian/python-jinja2.pyremove deleted file mode 100644 index 0d51d15..0000000 --- a/debian/python-jinja2.pyremove +++ /dev/null @@ -1,2 +0,0 @@ -jinja2/asyncfilters.py -jinja2/asyncsupport.py diff --git a/debian/python3-jinja2.bcep b/debian/python3-jinja2.bcep deleted file mode 100644 index 39c7f78..0000000 --- a/debian/python3-jinja2.bcep +++ /dev/null @@ -1 +0,0 @@ -re|-3.6|/usr/lib/python3/dist-packages/jinja2|.*/async(support|filters).py diff --git a/debian/python3-jinja2.dirs b/debian/python3-jinja2.dirs deleted file mode 100644 index 0874de0..0000000 --- a/debian/python3-jinja2.dirs +++ /dev/null @@ -1,2 +0,0 @@ -/usr/share/vim/addons/syntax/ -/usr/share/vim/registry diff --git a/debian/python3-jinja2.install b/debian/python3-jinja2.install deleted file mode 100644 index 77355bc..0000000 --- a/debian/python3-jinja2.install +++ /dev/null @@ -1,2 +0,0 @@ -debian/jinja.yaml /usr/share/vim/registry -debian/jinja.vim /usr/share/vim/addons/syntax/ diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 4e4552c..0000000 --- a/debian/rules +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/make -f - -#export DH_VERBOSE=1 - -export PYBUILD_NAME=jinja2 -export SETUPTOOLS_USE_DISTUTILS=stdlib - -%: - dh $@ --with python3,sphinxdoc --buildsystem=pybuild - -override_dh_auto_clean: - make -C docs clean - dh_auto_clean - -override_dh_auto_build-indep: - PYTHONPATH=$(CURDIR)/src:$(CURDIR)/docs make -C docs html - -override_dh_installdocs: - # install into -doc package and keep the symlink - dh_installdocs -p python-jinja2-doc --doc-main-package python-jinja2-doc - dh_installdocs --remaining - -override_dh_installexamples: - # install into -doc package and keep the symlink - dh_installexamples -p python-jinja2-doc --doc-main-package python-jinja2-doc - dh_installexamples --remaining - -override_dh_installchangelogs: - dh_installchangelogs CHANGES.rst diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 89ae9db..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/debian/watch b/debian/watch deleted file mode 100644 index a3aa165..0000000 --- a/debian/watch +++ /dev/null @@ -1,3 +0,0 @@ -version=3 -opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -https://pypi.debian.net/Jinja2/Jinja2-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff --git a/docs/api.rst b/docs/api.rst index 9c6f3a1..b2db537 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -273,7 +273,7 @@ modified identifier syntax. Filters and tests may contain dots to group filters and tests by topic. For example it's perfectly valid to add a function into the filter dict and call it `to.str`. The regular expression for filter and test identifiers is -``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*```. +``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*``. Undefined Types @@ -410,16 +410,19 @@ The Context .. automethod:: jinja2.runtime.Context.call(callable, \*args, \**kwargs) -.. admonition:: Implementation +The context is immutable, it prevents modifications, and if it is +modified somehow despite that those changes may not show up. For +performance, Jinja does not use the context as data storage for, only as +a primary data source. Variables that the template does not define are +looked up in the context, but variables the template does define are +stored locally. - Context is immutable for the same reason Python's frame locals are - immutable inside functions. Both Jinja and Python are not using the - context / frame locals as data storage for variables but only as primary - data source. +Instead of modifying the context directly, a function should return +a value that can be assigned to a variable within the template itself. - When a template accesses a variable the template does not define, Jinja - looks up the variable in the context, after that the variable is treated - as if it was defined in the template. +.. code-block:: jinja + + {% set comments = get_latest_comments() %} .. _loaders: @@ -597,18 +600,6 @@ functions to a Jinja environment. .. autofunction:: jinja2.pass_environment -.. autofunction:: jinja2.contextfilter - -.. autofunction:: jinja2.evalcontextfilter - -.. autofunction:: jinja2.environmentfilter - -.. autofunction:: jinja2.contextfunction - -.. autofunction:: jinja2.evalcontextfunction - -.. autofunction:: jinja2.environmentfunction - .. autofunction:: jinja2.clear_caches .. autofunction:: jinja2.is_undefined diff --git a/docs/faq.rst b/docs/faq.rst index dd78217..493dc38 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,175 +1,75 @@ Frequently Asked Questions ========================== -This page answers some of the often asked questions about Jinja. - -.. highlight:: html+jinja Why is it called Jinja? ----------------------- -The name Jinja was chosen because it's the name of a Japanese temple and -temple and template share a similar pronunciation. It is not named after -the city in Uganda. +"Jinja" is a Japanese `Shinto shrine`_, or temple, and temple and +template share a similar English pronunciation. It is not named after +the `city in Uganda`_. -How fast is it? ---------------- +.. _Shinto shrine: https://en.wikipedia.org/wiki/Shinto_shrine +.. _city in Uganda: https://en.wikipedia.org/wiki/Jinja%2C_Uganda -We really hate benchmarks especially since they don't reflect much. The -performance of a template depends on many factors and you would have to -benchmark different engines in different situations. The benchmarks from the -testsuite show that Jinja has a similar performance to `Mako`_ and is between -10 and 20 times faster than Django's template engine or Genshi. These numbers -should be taken with tons of salt as the benchmarks that took these numbers -only test a few performance related situations such as looping. Generally -speaking the performance of a template engine doesn't matter much as the -usual bottleneck in a web application is either the database or the application -code. -.. _Mako: https://www.makotemplates.org/ +How fast is Jinja? +------------------ -How Compatible is Jinja with Django? ------------------------------------- +Jinja is relatively fast among template engines because it compiles and +caches template code to Python code, so that the template does not need +to be parsed and interpreted each time. Rendering a template becomes as +close to executing a Python function as possible. -The default syntax of Jinja matches Django syntax in many ways. However -this similarity doesn't mean that you can use a Django template unmodified -in Jinja. For example filter arguments use a function call syntax rather -than a colon to separate filter name and arguments. Additionally the -extension interface in Jinja is fundamentally different from the Django one -which means that your custom tags won't work any longer. +Jinja also makes extensive use of caching. Templates are cached by name +after loading, so future uses of the template avoid loading. The +template loading itself uses a bytecode cache to avoid repeated +compiling. The caches can be external to persist across restarts. +Templates can also be precompiled and loaded as fast Python imports. -Generally speaking you will use much less custom extensions as the Jinja -template system allows you to use a certain subset of Python expressions -which can replace most Django extensions. For example instead of using -something like this:: +We dislike benchmarks because they don't reflect real use. Performance +depends on many factors. Different engines have different default +configurations and tradeoffs that make it unclear how to set up a useful +comparison. Often, database access, API calls, and data processing have +a much larger effect on performance than the template engine. - {% load comments %} - {% get_latest_comments 10 as latest_comments %} - {% for comment in latest_comments %} - ... - {% endfor %} -You will most likely provide an object with attributes to retrieve -comments from the database:: - - {% for comment in models.comments.latest(10) %} - ... - {% endfor %} - -Or directly provide the model for quick testing:: - - {% for comment in Comment.objects.order_by('-pub_date')[:10] %} - ... - {% endfor %} - -Please keep in mind that even though you may put such things into templates -it still isn't a good idea. Queries should go into the view code and not -the template! - -Isn't it a terrible idea to put Logic into Templates? ------------------------------------------------------ +Isn't it a bad idea to put logic in templates? +---------------------------------------------- Without a doubt you should try to remove as much logic from templates as -possible. But templates without any logic mean that you have to do all -the processing in the code which is boring and stupid. A template engine -that does that is shipped with Python and called `string.Template`. Comes -without loops and if conditions and is by far the fastest template engine -you can get for Python. +possible. With less logic, the template is easier to understand, has +fewer potential side effects, and is faster to compile and render. But a +template without any logic means processing must be done in code before +rendering. A template engine that does that is shipped with Python, +called :class:`string.Template`, and while it's definitely fast it's not +convenient. -So some amount of logic is required in templates to keep everyone happy. -And Jinja leaves it pretty much to you how much logic you want to put into -templates. There are some restrictions in what you can do and what not. +Jinja's features such as blocks, statements, filters, and function calls +make it much easier to write expressive templates, with very few +restrictions. Jinja doesn't allow arbitrary Python code in templates, or +every feature available in the Python language. This keeps the engine +easier to maintain, and keeps templates more readable. -Jinja neither allows you to put arbitrary Python code into templates nor -does it allow all Python expressions. The operators are limited to the -most common ones and more advanced expressions such as list comprehensions -and generator expressions are not supported. This keeps the template engine -easier to maintain and templates more readable. +Some amount of logic is required in templates to keep everyone happy. +Too much logic in the template can make it complex to reason about and +maintain. It's up to you to decide how your application will work and +balance how much logic you want to put in the template. -Why is Autoescaping not the Default? ------------------------------------- -There are multiple reasons why automatic escaping is not the default mode -and also not the recommended one. While automatic escaping of variables -means that you will less likely have an XSS problem it also causes a huge -amount of extra processing in the template engine which can cause serious -performance problems. As Python doesn't provide a way to mark strings as -unsafe Jinja has to hack around that limitation by providing a custom -string class (the :class:`Markup` string) that safely interacts with safe -and unsafe strings. - -With explicit escaping however the template engine doesn't have to perform -any safety checks on variables. Also a human knows not to escape integers -or strings that may never contain characters one has to escape or already -HTML markup. For example when iterating over a list over a table of -integers and floats for a table of statistics the template designer can -omit the escaping because he knows that integers or floats don't contain -any unsafe parameters. - -Additionally Jinja is a general purpose template engine and not only used -for HTML/XML generation. For example you may generate LaTeX, emails, -CSS, JavaScript, or configuration files. - -Why is the Context immutable? ------------------------------ - -When writing a :func:`pass_context` function, you may have noticed that -the context tries to stop you from modifying it. If you have managed to -modify the context by using an internal context API you may have noticed -that changes in the context don't seem to be visible in the template. -The reason for this is that Jinja uses the context only as primary data -source for template variables for performance reasons. - -If you want to modify the context write a function that returns a variable -instead that one can assign to a variable by using set:: - - {% set comments = get_latest_comments() %} - -My tracebacks look weird. What's happening? -------------------------------------------- - -Jinja can rewrite tracebacks so they show the template lines numbers and -source rather than the underlying compiled code, but this requires -special Python support. CPython <3.7 requires ``ctypes``, and PyPy -requires transparent proxy support. - -If you are using Google App Engine, ``ctypes`` is not available. You can -make it available in development, but not in production. - -.. code-block:: python - - import os - if os.environ.get('SERVER_SOFTWARE', '').startswith('Dev'): - from google.appengine.tools.devappserver2.python import sandbox - sandbox._WHITE_LIST_C_MODULES += ['_ctypes', 'gestalt'] - -Credit for this snippet goes to `Thomas Johansson -`_ - -My Macros are overridden by something +Why is HTML escaping not the default? ------------------------------------- -In some situations the Jinja scoping appears arbitrary: +Jinja provides a feature that can be enabled to escape HTML syntax in +rendered templates. However, it is disabled by default. -layout.tmpl: +Jinja is a general purpose template engine, it is not only used for HTML +documents. You can generate plain text, LaTeX, emails, CSS, JavaScript, +configuration files, etc. HTML escaping wouldn't make sense for any of +these document types. -.. sourcecode:: jinja - - {% macro foo() %}LAYOUT{% endmacro %} - {% block body %}{% endblock %} - -child.tmpl: - -.. sourcecode:: jinja - - {% extends 'layout.tmpl' %} - {% macro foo() %}CHILD{% endmacro %} - {% block body %}{{ foo() }}{% endblock %} - -This will print ``LAYOUT`` in Jinja. This is a side effect of having -the parent template evaluated after the child one. This allows child -templates passing information to the parent template. To avoid this -issue rename the macro or variable in the parent template to have an -uncommon prefix. - -.. _Jinja 1: https://pypi.org/project/Jinja/ +While automatic escaping means that you are less likely have an XSS +problem, it also requires significant extra processing during compiling +and rendering, which can reduce performance. Jinja uses MarkupSafe for +escaping, which provides optimized C code for speed, but it still +introduces overhead to track escaping across methods and formatting. diff --git a/docs/integration.rst b/docs/integration.rst index 633e80d..b945fb3 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -1,6 +1,25 @@ Integration =========== + +Flask +----- + +The `Flask`_ web application framework, also maintained by Pallets, uses +Jinja templates by default. Flask sets up a Jinja environment and +template loader for you, and provides functions to easily render +templates from view functions. + +.. _Flask: https://flask.palletsprojects.com + + +Django +------ + +Django supports using Jinja as its template engine, see +https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines. + + .. _babel-integration: Babel diff --git a/docs/intro.rst b/docs/intro.rst index 9eeaa05..fd6f84f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -30,7 +30,7 @@ Installation ------------ We recommend using the latest version of Python. Jinja supports Python -3.6 and newer. We also recommend using a `virtual environment`_ in order +3.7 and newer. We also recommend using a `virtual environment`_ in order to isolate your project dependencies from other projects and the system. .. _virtual environment: https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments diff --git a/docs/sandbox.rst b/docs/sandbox.rst index 1222d02..fc9c31f 100644 --- a/docs/sandbox.rst +++ b/docs/sandbox.rst @@ -1,18 +1,56 @@ Sandbox ======= -The Jinja sandbox can be used to evaluate untrusted code. Access to unsafe -attributes and methods is prohibited. +The Jinja sandbox can be used to render untrusted templates. Access to +attributes, method calls, operators, mutating data structures, and +string formatting can be intercepted and prohibited. -Assuming `env` is a :class:`SandboxedEnvironment` in the default configuration -the following piece of code shows how it works: +.. code-block:: pycon + + >>> from jinja2.sandbox import SandboxedEnvironment + >>> env = SandboxedEnvironment() + >>> func = lambda: "Hello, Sandbox!" + >>> env.from_string("{{ func() }}").render(func=func) + 'Hello, Sandbox!' + >>> env.from_string("{{ func.__code__.co_code }}").render(func=func) + Traceback (most recent call last): + ... + SecurityError: access to attribute '__code__' of 'function' object is unsafe. + +A sandboxed environment can be useful, for example, to allow users of an +internal reporting system to create custom emails. You would document +what data is available in the templates, then the user would write a +template using that information. Your code would generate the report +data and pass it to the user's sandboxed template to render. + + +Security Considerations +----------------------- + +The sandbox alone is not a solution for perfect security. Keep these +things in mind when using the sandbox. + +Templates can still raise errors when compiled or rendered. Your code +should attempt to catch errors instead of crashing. + +It is possible to construct a relatively small template that renders to +a very large amount of output, which could correspond to a high use of +CPU or memory. You should run your application with limits on resources +such as CPU and memory to mitigate this. + +Jinja only renders text, it does not understand, for example, JavaScript +code. Depending on how the rendered template will be used, you may need +to do other postprocessing to restrict the output. + +Pass only the data that is relevant to the template. Avoid passing +global data, or objects with methods that have side effects. By default +the sandbox prevents private and internal attribute access. You can +override :meth:`~SandboxedEnvironment.is_safe_attribute` to further +restrict attributes access. Decorate methods with :func:`unsafe` to +prevent calling them from templates when passing objects as data. Use +:class:`ImmutableSandboxedEnvironment` to prevent modifying lists and +dictionaries. ->>> env.from_string("{{ func.func_code }}").render(func=lambda:None) -u'' ->>> env.from_string("{{ func.func_code.do_something }}").render(func=lambda:None) -Traceback (most recent call last): - ... -SecurityError: access to attribute 'func_code' of 'function' object is unsafe. API --- @@ -34,61 +72,40 @@ API .. autofunction:: modifies_known_mutable -.. admonition:: Note - - The Jinja sandbox alone is no solution for perfect security. Especially - for web applications you have to keep in mind that users may create - templates with arbitrary HTML in so it's crucial to ensure that (if you - are running multiple users on the same server) they can't harm each other - via JavaScript insertions and much more. - - Also the sandbox is only as good as the configuration. We strongly - recommend only passing non-shared resources to the template and use - some sort of whitelisting for attributes. - - Also keep in mind that templates may raise runtime or compile time errors, - so make sure to catch them. Operator Intercepting --------------------- -.. versionadded:: 2.6 +For performance, Jinja outputs operators directly when compiling. This +means it's not possible to intercept operator behavior by overriding +:meth:`SandboxEnvironment.call ` by default, because +operator special methods are handled by the Python interpreter, and +might not correspond with exactly one method depending on the operator's +use. -For maximum performance Jinja will let operators call directly the type -specific callback methods. This means that it's not possible to have this -intercepted by overriding :meth:`Environment.call`. Furthermore a -conversion from operator to special method is not always directly possible -due to how operators work. For instance for divisions more than one -special method exist. +The sandbox can instruct the compiler to output a function to intercept +certain operators instead. Override +:attr:`SandboxedEnvironment.intercepted_binops` and +:attr:`SandboxedEnvironment.intercepted_unops` with the operator symbols +you want to intercept. The compiler will replace the symbols with calls +to :meth:`SandboxedEnvironment.call_binop` and +:meth:`SandboxedEnvironment.call_unop` instead. The default +implementation of those methods will use +:attr:`SandboxedEnvironment.binop_table` and +:attr:`SandboxedEnvironment.unop_table` to translate operator symbols +into :mod:`operator` functions. -With Jinja 2.6 there is now support for explicit operator intercepting. -This can be used to customize specific operators as necessary. In order -to intercept an operator one has to override the -:attr:`SandboxedEnvironment.intercepted_binops` attribute. Once the -operator that needs to be intercepted is added to that set Jinja will -generate bytecode that calls the :meth:`SandboxedEnvironment.call_binop` -function. For unary operators the `unary` attributes and methods have to -be used instead. +For example, the power (``**``) operator can be disabled: -The default implementation of :attr:`SandboxedEnvironment.call_binop` -will use the :attr:`SandboxedEnvironment.binop_table` to translate -operator symbols into callbacks performing the default operator behavior. - -This example shows how the power (``**``) operator can be disabled in -Jinja:: +.. code-block:: python from jinja2.sandbox import SandboxedEnvironment - class MyEnvironment(SandboxedEnvironment): - intercepted_binops = frozenset(['**']) + intercepted_binops = frozenset(["**"]) def call_binop(self, context, operator, left, right): - if operator == '**': - return self.undefined('the power operator is unavailable') - return SandboxedEnvironment.call_binop(self, context, - operator, left, right) + if operator == "**": + return self.undefined("The power (**) operator is unavailable.") -Make sure to always call into the super method, even if you are not -intercepting the call. Jinja might internally call the method to -evaluate expressions. + return super().call_binop(self, context, operator, left, right) diff --git a/docs/switching.rst b/docs/switching.rst index b9ff954..caa35c3 100644 --- a/docs/switching.rst +++ b/docs/switching.rst @@ -1,141 +1,73 @@ -Switching from other Template Engines +Switching From Other Template Engines ===================================== -.. highlight:: html+jinja - -If you have used a different template engine in the past and want to switch -to Jinja here is a small guide that shows the basic syntactic and semantic -changes between some common, similar text template engines for Python. - -Jinja 1 -------- - -Jinja 2 is mostly compatible with Jinja 1 in terms of API usage and template -syntax. The differences between Jinja 1 and 2 are explained in the following -list. - -API -~~~ - -Loaders - Jinja 2 uses a different loader API. Because the internal representation - of templates changed there is no longer support for external caching - systems such as memcached. The memory consumed by templates is comparable - with regular Python modules now and external caching doesn't give any - advantage. If you have used a custom loader in the past have a look at - the new :ref:`loader API `. - -Loading templates from strings - In the past it was possible to generate templates from a string with the - default environment configuration by using `jinja.from_string`. Jinja 2 - provides a :class:`Template` class that can be used to do the same, but - with optional additional configuration. - -Automatic unicode conversion - Jinja 1 performed automatic conversion of bytes in a given encoding - into unicode objects. This conversion is no longer implemented as it - was inconsistent as most libraries are using the regular Python - ASCII bytes to Unicode conversion. An application powered by Jinja 2 - *has to* use unicode internally everywhere or make sure that Jinja 2 - only gets unicode strings passed. - -i18n - Jinja 1 used custom translators for internationalization. i18n is now - available as Jinja 2 extension and uses a simpler, more gettext friendly - interface and has support for babel. For more details see - :ref:`i18n-extension`. - -Internal methods - Jinja 1 exposed a few internal methods on the environment object such - as `call_function`, `get_attribute` and others. While they were marked - as being an internal method it was possible to override them. Jinja 2 - doesn't have equivalent methods. - -Sandbox - Jinja 1 was running sandbox mode by default. Few applications actually - used that feature so it became optional in Jinja 2. For more details - about the sandboxed execution see :class:`SandboxedEnvironment`. - -Context - Jinja 1 had a stacked context as storage for variables passed to the - environment. In Jinja 2 a similar object exists but it doesn't allow - modifications nor is it a singleton. As inheritance is dynamic now - multiple context objects may exist during template evaluation. - -Filters and Tests - Filters and tests are regular functions now. It's no longer necessary - and allowed to use factory functions. - - -Templates -~~~~~~~~~ - -Jinja 2 has mostly the same syntax as Jinja 1. What's different is that -macros require parentheses around the argument list now. - -Additionally Jinja 2 allows dynamic inheritance now and dynamic includes. -The old helper function `rendertemplate` is gone now, `include` can be used -instead. Includes no longer import macros and variable assignments, for -that the new `import` tag is used. This concept is explained in the -:ref:`import` documentation. - -Another small change happened in the `for`-tag. The special loop variable -doesn't have a `parent` attribute, instead you have to alias the loop -yourself. See :ref:`accessing-the-parent-loop` for more details. +This is a brief guide on some of the differences between Jinja syntax +and other template languages. See :doc:`/templates` for a comprehensive +guide to Jinja syntax and features. Django ------ If you have previously worked with Django templates, you should find -Jinja very familiar. In fact, most of the syntax elements look and -work the same. +Jinja very familiar. Many of the syntax elements look and work the same. +However, Jinja provides some more syntax elements, and some work a bit +differently. -However, Jinja provides some more syntax elements covered in the -documentation and some work a bit different. +This section covers the template changes. The API, including extension +support, is fundamentally different so it won't be covered here. + +Django supports using Jinja as its template engine, see +https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines. -This section covers the template changes. As the API is fundamentally -different we won't cover it here. Method Calls ~~~~~~~~~~~~ -In Django method calls work implicitly, while Jinja requires the explicit -Python syntax. Thus this Django code:: +In Django, methods are called implicitly, without parentheses. + +.. code-block:: django {% for page in user.get_created_pages %} ... {% endfor %} -...looks like this in Jinja:: +In Jinja, using parentheses is required for calls, like in Python. This +allows you to pass variables to the method, which is not possible +in Django. This syntax is also used for calling macros. + +.. code-block:: jinja {% for page in user.get_created_pages() %} ... {% endfor %} -This allows you to pass variables to the method, which is not possible in -Django. This syntax is also used for macros. Filter Arguments ~~~~~~~~~~~~~~~~ -Jinja provides more than one argument for filters. Also the syntax for -argument passing is different. A template that looks like this in Django:: +In Django, one literal value can be passed to a filter after a colon. + +.. code-block:: django {{ items|join:", " }} -looks like this in Jinja:: +In Jinja, filters can take any number of positional and keyword +arguments in parentheses, like function calls. Arguments can also be +variables instead of literal values. - {{ items|join(', ') }} +.. code-block:: jinja + + {{ items|join(", ") }} -It is a bit more verbose, but it allows different types of arguments - -including variables - and more than one of them. Tests ~~~~~ -In addition to filters there also are tests you can perform using the is -operator. Here are some examples:: +In addition to filters, Jinja also has "tests" used with the ``is`` +operator. This operator is not the same as the Python operator. + +.. code-block:: jinja {% if user.user_id is odd %} {{ user.username|e }} is odd @@ -146,11 +78,10 @@ operator. Here are some examples:: Loops ~~~~~ -For loops work very similarly to Django, but notably the Jinja special -variable for the loop context is called `loop`, not `forloop` as in Django. +In Django, the special variable for the loop context is called +``forloop``, and the ``empty`` is used for no loop items. -In addition, the Django `empty` argument is called `else` in Jinja. For -example, the Django template:: +.. code-block:: django {% for item in items %} {{ item }} @@ -158,52 +89,74 @@ example, the Django template:: No items! {% endfor %} -...looks like this in Jinja:: +In Jinja, the special variable for the loop context is called ``loop``, +and the ``else`` block is used for no loop items. + +.. code-block:: jinja {% for item in items %} - {{ item }} + {{ loop.index}}. {{ item }} {% else %} No items! {% endfor %} + Cycle ~~~~~ -The ``{% cycle %}`` tag does not exist in Jinja; however, you can achieve the -same output by using the `cycle` method on the loop context special variable. +In Django, the ``{% cycle %}`` can be used in a for loop to alternate +between values per loop. -The following Django template:: +.. code-block:: django {% for user in users %}
  • {{ user }}
  • {% endfor %} -...looks like this in Jinja:: +In Jinja, the ``loop`` context has a ``cycle`` method. + +.. code-block:: jinja {% for user in users %}
  • {{ user }}
  • {% endfor %} -There is no equivalent of ``{% cycle ... as variable %}``. +A cycler can also be assigned to a variable and used outside or across +loops with the ``cycle()`` global function. Mako ---- -.. highlight:: html+mako +You can configure Jinja to look more like Mako: -If you have used Mako so far and want to switch to Jinja you can configure -Jinja to look more like Mako: +.. code-block:: python -.. sourcecode:: python + env = Environment( + block_start_string="<%", + block_end_string="%>", + variable_start_string="${", + variable_end_string="}", + comment_start_string="<%doc>", + commend_end_string="", + line_statement_prefix="%", + line_comment_prefix="##", + ) - env = Environment('<%', '%>', '${', '}', '<%doc>', '', '%', '##') +With an environment configured like that, Jinja should be able to +interpret a small subset of Mako templates without any changes. -With an environment configured like that, Jinja should be able to interpret -a small subset of Mako templates. Jinja does not support embedded Python -code, so you would have to move that out of the template. The syntax for defs -(which are called macros in Jinja) and template inheritance is different too. -The following Mako template:: +Jinja does not support embedded Python code, so you would have to move +that out of the template. You could either process the data with the +same code before rendering, or add a global function or filter to the +Jinja environment. + +The syntax for defs (which are called macros in Jinja) and template +inheritance is different too. + +The following Mako template: + +.. code-block:: mako <%inherit file="layout.html" /> <%def name="title()">Page Title @@ -213,7 +166,9 @@ The following Mako template:: % endfor -Looks like this in Jinja with the above configuration:: +Looks like this in Jinja with the above configuration: + +.. code-block:: jinja <% extends "layout.html" %> <% block title %>Page Title<% endblock %> diff --git a/docs/templates.rst b/docs/templates.rst index 89958b8..7a64750 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -938,6 +938,23 @@ are available on a macro object: If a macro name starts with an underscore, it's not exported and can't be imported. +Due to how scopes work in Jinja, a macro in a child template does not +override a macro in a parent template. The following will output +"LAYOUT", not "CHILD". + +.. code-block:: jinja + :caption: ``layout.txt`` + + {% macro foo() %}LAYOUT{% endmacro %} + {% block body %}{% endblock %} + +.. code-block:: jinja + :caption: ``child.txt`` + + {% extends 'layout.txt' %} + {% macro foo() %}CHILD{% endmacro %} + {% block body %}{{ foo() }}{% endblock %} + .. _call: @@ -1113,42 +1130,45 @@ at the same time. They are documented in detail in the Include ~~~~~~~ -The `include` tag is useful to include a template and return the -rendered contents of that file into the current namespace:: +The ``include`` tag renders another template and outputs the result into +the current template. + +.. code-block:: jinja {% include 'header.html' %} - Body + Body goes here. {% include 'footer.html' %} -Included templates have access to the variables of the active context by -default. For more details about context behavior of imports and includes, -see :ref:`import-visibility`. +The included template has access to context of the current template by +default. Use ``without context`` to use a separate context instead. +``with context`` is also valid, but is the default behavior. See +:ref:`import-visibility`. -From Jinja 2.2 onwards, you can mark an include with ``ignore missing``; in -which case Jinja will ignore the statement if the template to be included -does not exist. When combined with ``with`` or ``without context``, it must -be placed *before* the context visibility statement. Here are some valid -examples:: +The included template can ``extend`` another template and override +blocks in that template. However, the current template cannot override +any blocks that the included template outputs. +Use ``ignore missing`` to ignore the statement if the template does not +exist. It must be placed *before* a context visibility statement. + +.. code-block:: jinja + + {% include "sidebar.html" without context %} {% include "sidebar.html" ignore missing %} {% include "sidebar.html" ignore missing with context %} {% include "sidebar.html" ignore missing without context %} -.. versionadded:: 2.2 +If a list of templates is given, each will be tried in order until one +is not missing. This can be used with ``ignore missing`` to ignore if +none of the templates exist. -You can also provide a list of templates that are checked for existence -before inclusion. The first template that exists will be included. If -`ignore missing` is given, it will fall back to rendering nothing if -none of the templates exist, otherwise it will raise an exception. - -Example:: +.. code-block:: jinja {% include ['page_detailed.html', 'page.html'] %} {% include ['special_sidebar.html', 'sidebar.html'] ignore missing %} -.. versionchanged:: 2.4 - If a template object was passed to the template context, you can - include that object using `include`. +A variable, with either a template name or template object, can also be +passed to the statment. .. _import: @@ -1732,11 +1752,35 @@ to disable it for a block. .. versionadded:: 2.10 The ``trimmed`` and ``notrimmed`` modifiers have been added. +If the translation depends on the context that the message appears in, +the ``pgettext`` and ``npgettext`` functions take a ``context`` string +as the first argument, which is used to select the appropriate +translation. To specify a context with the ``{% trans %}`` tag, provide +a string as the first token after ``trans``. + +.. code-block:: jinja + + {% trans "fruit" %}apple{% endtrans %} + {% trans "fruit" trimmed count -%} + 1 apple + {%- pluralize -%} + {{ count }} apples + {%- endtrans %} + +.. versionadded:: 3.1 + A context can be passed to the ``trans`` tag to use ``pgettext`` and + ``npgettext``. + It's possible to translate strings in expressions with these functions: -- ``gettext``: translate a single string -- ``ngettext``: translate a pluralizable string -- ``_``: alias for ``gettext`` +- ``_(message)``: Alias for ``gettext``. +- ``gettext(message)``: Translate a message. +- ``ngettext(singluar, plural, n)``: Translate a singular or plural + message based on a count variable. +- ``pgettext(context, message)``: Like ``gettext()``, but picks the + translation based on the context string. +- ``npgettext(context, singular, plural, n)``: Like ``npgettext()``, + but picks the translation based on the context string. You can print a translated string like this: diff --git a/docs/tricks.rst b/docs/tricks.rst index 762a69c..b58c5bb 100644 --- a/docs/tricks.rst +++ b/docs/tricks.rst @@ -26,7 +26,7 @@ basic skeleton is added to the file so that if it's indeed rendered with `standalone` set to `True` a very basic HTML skeleton is added:: {% if not standalone %}{% extends 'default.html' %}{% endif -%} - + {% block title %}The Page Title{% endblock %} {% block body %} @@ -74,8 +74,8 @@ sense to define a default for that variable:: ... ... diff --git a/requirements/dev.txt b/requirements/dev.txt index 392f7a4..c9bcd60 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,134 +1,58 @@ +# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile-multi # To update, run: # -# pip-compile requirements/dev.in +# pip-compile-multi # -alabaster==0.7.12 - # via sphinx -attrs==21.2.0 - # via pytest -babel==2.9.1 - # via sphinx -backports.entry-points-selectable==1.1.0 - # via virtualenv -certifi==2021.10.8 - # via requests +-r docs.txt +-r tests.txt +-r typing.txt cfgv==3.3.1 # via pre-commit -charset-normalizer==2.0.7 - # via requests -click==8.0.3 - # via pip-tools -distlib==0.3.3 +click==8.1.2 + # via + # pip-compile-multi + # pip-tools +distlib==0.3.4 # via virtualenv -docutils==0.17.1 - # via sphinx -filelock==3.3.2 +filelock==3.6.0 # via # tox # virtualenv -identify==2.3.3 +identify==2.5.0 # via pre-commit -idna==3.3 - # via requests -imagesize==1.2.0 - # via sphinx -iniconfig==1.1.1 - # via pytest -jinja2==3.0.2 - # via sphinx -markupsafe==2.0.1 - # via jinja2 -mypy==0.910 - # via -r requirements/typing.in -mypy-extensions==0.4.3 - # via mypy nodeenv==1.6.0 # via pre-commit -packaging==21.2 - # via - # pallets-sphinx-themes - # pytest - # sphinx - # tox -pallets-sphinx-themes==2.0.1 - # via -r requirements/docs.in pep517==0.12.0 # via pip-tools -pip-tools==6.4.0 +pip-compile-multi==2.4.5 # via -r requirements/dev.in -platformdirs==2.4.0 +pip-tools==6.6.0 + # via pip-compile-multi +platformdirs==2.5.2 # via virtualenv -pluggy==1.0.0 - # via - # pytest - # tox -pre-commit==2.15.0 +pre-commit==2.18.1 # via -r requirements/dev.in -py==1.11.0 - # via - # pytest - # tox -pygments==2.10.0 - # via sphinx -pyparsing==2.4.7 - # via packaging -pytest==6.2.5 - # via -r requirements/tests.in -pytz==2021.3 - # via babel pyyaml==6.0 # via pre-commit -requests==2.26.0 - # via sphinx six==1.16.0 # via # tox # virtualenv -snowballstemmer==2.1.0 - # via sphinx -sphinx==4.2.0 - # via - # -r requirements/docs.in - # pallets-sphinx-themes - # sphinx-issues - # sphinxcontrib-log-cabinet -sphinx-issues==1.2.0 - # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.2 - # via sphinx -sphinxcontrib-devhelp==1.0.2 - # via sphinx -sphinxcontrib-htmlhelp==2.0.0 - # via sphinx -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-log-cabinet==1.0.1 - # via -r requirements/docs.in -sphinxcontrib-qthelp==1.0.3 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 - # via sphinx toml==0.10.2 # via - # mypy # pre-commit - # pytest # tox -tomli==1.2.2 - # via pep517 -tox==3.24.4 +toposort==1.7 + # via pip-compile-multi +tox==3.25.0 # via -r requirements/dev.in -typing-extensions==3.10.0.2 - # via mypy -urllib3==1.26.7 - # via requests -virtualenv==20.10.0 +virtualenv==20.14.1 # via # pre-commit # tox -wheel==0.37.0 +wheel==0.37.1 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/docs.txt b/requirements/docs.txt index bd47a92..88f6279 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,50 +1,51 @@ +# SHA1:45c590f97fe95b8bdc755eef796e91adf5fbe4ea # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile-multi # To update, run: # -# pip-compile requirements/docs.in +# pip-compile-multi # alabaster==0.7.12 # via sphinx -babel==2.9.1 +babel==2.10.1 # via sphinx certifi==2021.10.8 # via requests -charset-normalizer==2.0.7 +charset-normalizer==2.0.12 # via requests docutils==0.17.1 # via sphinx idna==3.3 # via requests -imagesize==1.2.0 +imagesize==1.3.0 # via sphinx -jinja2==3.0.2 +jinja2==3.1.1 # via sphinx -markupsafe==2.0.1 +markupsafe==2.1.1 # via jinja2 -packaging==21.2 +packaging==21.3 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.0.1 +pallets-sphinx-themes==2.0.2 # via -r requirements/docs.in -pygments==2.10.0 +pygments==2.12.0 # via sphinx -pyparsing==2.4.7 +pyparsing==3.0.8 # via packaging -pytz==2021.3 +pytz==2022.1 # via babel -requests==2.26.0 +requests==2.27.1 # via sphinx -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 # via sphinx -sphinx==4.2.0 +sphinx==4.5.0 # via # -r requirements/docs.in # pallets-sphinx-themes # sphinx-issues # sphinxcontrib-log-cabinet -sphinx-issues==1.2.0 +sphinx-issues==3.0.1 # via -r requirements/docs.in sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -60,8 +61,5 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.7 +urllib3==1.26.9 # via requests - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements/tests.txt b/requirements/tests.txt index 02dd8e8..4cd3fe9 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,22 +1,23 @@ +# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile-multi # To update, run: # -# pip-compile requirements/tests.in +# pip-compile-multi # -attrs==21.2.0 +attrs==21.4.0 # via pytest iniconfig==1.1.1 # via pytest -packaging==21.2 +packaging==21.3 # via pytest pluggy==1.0.0 # via pytest py==1.11.0 # via pytest -pyparsing==2.4.7 +pyparsing==3.0.8 # via packaging -pytest==6.2.5 +pytest==7.1.2 # via -r requirements/tests.in -toml==0.10.2 +tomli==2.0.1 # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index 7a3e613..2d97fef 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -1,14 +1,15 @@ +# SHA1:7983aaa01d64547827c20395d77e248c41b2572f # -# This file is autogenerated by pip-compile with python 3.10 +# This file is autogenerated by pip-compile-multi # To update, run: # -# pip-compile requirements/typing.in +# pip-compile-multi # -mypy==0.910 +mypy==0.950 # via -r requirements/typing.in mypy-extensions==0.4.3 # via mypy -toml==0.10.2 +tomli==2.0.1 # via mypy -typing-extensions==3.10.0.2 +typing-extensions==4.2.0 # via mypy diff --git a/setup.cfg b/setup.cfg index 1b72fa4..6c86169 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,8 +32,8 @@ classifiers = [options] packages = find: package_dir = = src -include_package_data = true -python_requires = >= 3.6 +include_package_data = True +python_requires = >= 3.7 [options.packages.find] where = src @@ -72,7 +72,8 @@ per-file-ignores = [mypy] files = src/jinja2 -python_version = 3.6 +python_version = 3.7 +show_error_codes = True disallow_subclassing_any = True disallow_untyped_calls = True disallow_untyped_defs = True diff --git a/setup.py b/setup.py index 8bd74f3..79d0708 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python from setuptools import setup # Metadata goes in setup.cfg. These are here for GitHub's dependency graph. diff --git a/src/Jinja2.egg-info/PKG-INFO b/src/Jinja2.egg-info/PKG-INFO index d71c2d3..7b92c21 100644 --- a/src/Jinja2.egg-info/PKG-INFO +++ b/src/Jinja2.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: Jinja2 -Version: 3.0.3 +Version: 3.1.2 Summary: A very fast and expressive template engine. Home-page: https://palletsprojects.com/p/jinja/ Author: Armin Ronacher @@ -24,7 +24,7 @@ Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Text Processing :: Markup :: HTML -Requires-Python: >=3.6 +Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: i18n License-File: LICENSE.rst diff --git a/src/Jinja2.egg-info/SOURCES.txt b/src/Jinja2.egg-info/SOURCES.txt index 75286f1..7f7a21f 100644 --- a/src/Jinja2.egg-info/SOURCES.txt +++ b/src/Jinja2.egg-info/SOURCES.txt @@ -80,7 +80,6 @@ tests/test_compile.py tests/test_core_tags.py tests/test_debug.py tests/test_ext.py -tests/test_features.py tests/test_filters.py tests/test_idtracking.py tests/test_imports.py @@ -89,6 +88,7 @@ tests/test_lexnparse.py tests/test_loader.py tests/test_nativetypes.py tests/test_nodes.py +tests/test_pickle.py tests/test_regression.py tests/test_runtime.py tests/test_security.py diff --git a/src/Jinja2.egg-info/entry_points.txt b/src/Jinja2.egg-info/entry_points.txt index 3619483..7b9666c 100644 --- a/src/Jinja2.egg-info/entry_points.txt +++ b/src/Jinja2.egg-info/entry_points.txt @@ -1,3 +1,2 @@ [babel.extractors] -jinja2 = jinja2.ext:babel_extract [i18n] - +jinja2 = jinja2.ext:babel_extract[i18n] diff --git a/src/jinja2/__init__.py b/src/jinja2/__init__.py index 9dcd901..e323926 100644 --- a/src/jinja2/__init__.py +++ b/src/jinja2/__init__.py @@ -14,9 +14,6 @@ from .exceptions import TemplateRuntimeError as TemplateRuntimeError from .exceptions import TemplatesNotFound as TemplatesNotFound from .exceptions import TemplateSyntaxError as TemplateSyntaxError from .exceptions import UndefinedError as UndefinedError -from .filters import contextfilter -from .filters import environmentfilter -from .filters import evalcontextfilter from .loaders import BaseLoader as BaseLoader from .loaders import ChoiceLoader as ChoiceLoader from .loaders import DictLoader as DictLoader @@ -31,15 +28,10 @@ from .runtime import make_logging_undefined as make_logging_undefined from .runtime import StrictUndefined as StrictUndefined from .runtime import Undefined as Undefined from .utils import clear_caches as clear_caches -from .utils import contextfunction -from .utils import environmentfunction -from .utils import escape -from .utils import evalcontextfunction from .utils import is_undefined as is_undefined -from .utils import Markup from .utils import pass_context as pass_context from .utils import pass_environment as pass_environment from .utils import pass_eval_context as pass_eval_context from .utils import select_autoescape as select_autoescape -__version__ = "3.0.3" +__version__ = "3.1.2" diff --git a/src/jinja2/_identifier.py b/src/jinja2/_identifier.py index 224d544..928c150 100644 --- a/src/jinja2/_identifier.py +++ b/src/jinja2/_identifier.py @@ -2,5 +2,5 @@ import re # generated by scripts/generate_identifier_pattern.py pattern = re.compile( - r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 + r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 ) diff --git a/src/jinja2/async_utils.py b/src/jinja2/async_utils.py index 35e6cb1..1a4f389 100644 --- a/src/jinja2/async_utils.py +++ b/src/jinja2/async_utils.py @@ -1,5 +1,6 @@ import inspect import typing as t +from functools import WRAPPER_ASSIGNMENTS from functools import wraps from .utils import _PassArg @@ -23,7 +24,15 @@ def async_variant(normal_func): # type: ignore def is_async(args: t.Any) -> bool: return t.cast(bool, args[0].environment.is_async) - @wraps(normal_func) + # Take the doc and annotations from the sync function, but the + # name from the async function. Pallets-Sphinx-Themes + # build_function_directive expects __wrapped__ to point to the + # sync function. + async_func_attrs = ("__module__", "__name__", "__qualname__") + normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs)) + + @wraps(normal_func, assigned=normal_func_attrs) + @wraps(async_func, assigned=async_func_attrs, updated=()) def wrapper(*args, **kwargs): # type: ignore b = is_async(args) diff --git a/src/jinja2/bccache.py b/src/jinja2/bccache.py index 3bb61b7..d0ddf56 100644 --- a/src/jinja2/bccache.py +++ b/src/jinja2/bccache.py @@ -79,7 +79,7 @@ class Bucket: self.reset() return - def write_bytecode(self, f: t.BinaryIO) -> None: + def write_bytecode(self, f: t.IO[bytes]) -> None: """Dump the bytecode into the file or file like object passed.""" if self.code is None: raise TypeError("can't write empty bucket") @@ -262,13 +262,55 @@ class FileSystemBytecodeCache(BytecodeCache): def load_bytecode(self, bucket: Bucket) -> None: filename = self._get_cache_filename(bucket) - if os.path.exists(filename): - with open(filename, "rb") as f: - bucket.load_bytecode(f) + # Don't test for existence before opening the file, since the + # file could disappear after the test before the open. + try: + f = open(filename, "rb") + except (FileNotFoundError, IsADirectoryError, PermissionError): + # PermissionError can occur on Windows when an operation is + # in progress, such as calling clear(). + return + + with f: + bucket.load_bytecode(f) def dump_bytecode(self, bucket: Bucket) -> None: - with open(self._get_cache_filename(bucket), "wb") as f: - bucket.write_bytecode(f) + # Write to a temporary file, then rename to the real name after + # writing. This avoids another process reading the file before + # it is fully written. + name = self._get_cache_filename(bucket) + f = tempfile.NamedTemporaryFile( + mode="wb", + dir=os.path.dirname(name), + prefix=os.path.basename(name), + suffix=".tmp", + delete=False, + ) + + def remove_silent() -> None: + try: + os.remove(f.name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + pass + + try: + with f: + bucket.write_bytecode(f) + except BaseException: + remove_silent() + raise + + try: + os.replace(f.name, name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + remove_silent() + except BaseException: + remove_silent() + raise def clear(self) -> None: # imported lazily here because google app-engine doesn't support diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 52fd5b8..3458095 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -218,7 +218,7 @@ class Frame: def copy(self) -> "Frame": """Create a copy of the current one.""" - rv = t.cast(Frame, object.__new__(self.__class__)) + rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) rv.symbols = self.symbols.copy() return rv @@ -724,6 +724,7 @@ class CodeGenerator(NodeVisitor): """ self.writeline("resolve = context.resolve_or_missing") self.writeline("undefined = environment.undefined") + self.writeline("concat = environment.concat") # always use the standard Undefined class for the implicit else of # conditional expressions self.writeline("cond_expr_undefined = Undefined") @@ -835,7 +836,6 @@ class CodeGenerator(NodeVisitor): else: exported_names = sorted(exported) - self.writeline("from __future__ import generator_stop") # Python < 3.7 self.writeline("from jinja2.runtime import " + ", ".join(exported_names)) # if we want a deferred initialization we cannot move the @@ -1755,7 +1755,7 @@ class CodeGenerator(NodeVisitor): self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool ) -> t.Iterator[None]: if self.environment.is_async: - self.write("await auto_await(") + self.write("(await auto_await(") if is_filter: self.write(f"{self.filters[node.name]}(") @@ -1790,7 +1790,7 @@ class CodeGenerator(NodeVisitor): self.write(")") if self.environment.is_async: - self.write(")") + self.write("))") @optimizeconst def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None: @@ -1842,7 +1842,7 @@ class CodeGenerator(NodeVisitor): self, node: nodes.Call, frame: Frame, forward_caller: bool = False ) -> None: if self.environment.is_async: - self.write("await auto_await(") + self.write("(await auto_await(") if self.environment.sandboxed: self.write("environment.call(context, ") else: @@ -1858,7 +1858,7 @@ class CodeGenerator(NodeVisitor): self.signature(node, frame, extra_kwargs) self.write(")") if self.environment.is_async: - self.write(")") + self.write("))") def visit_Keyword(self, node: nodes.Keyword, frame: Frame) -> None: self.write(node.key + "=") diff --git a/src/jinja2/debug.py b/src/jinja2/debug.py index 805866b..7ed7e92 100644 --- a/src/jinja2/debug.py +++ b/src/jinja2/debug.py @@ -1,4 +1,3 @@ -import platform import sys import typing as t from types import CodeType @@ -68,7 +67,8 @@ def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException: # Assign tb_next in reverse to avoid circular references. for tb in reversed(stack): - tb_next = tb_set_next(tb, tb_next) + tb.tb_next = tb_next + tb_next = tb return exc_value.with_traceback(tb_next) @@ -189,71 +189,3 @@ def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any data[name] = value return data - - -if sys.version_info >= (3, 7): - # tb_next is directly assignable as of Python 3.7 - def tb_set_next( - tb: TracebackType, tb_next: t.Optional[TracebackType] - ) -> TracebackType: - tb.tb_next = tb_next - return tb - - -elif platform.python_implementation() == "PyPy": - # PyPy might have special support, and won't work with ctypes. - try: - import tputil # type: ignore - except ImportError: - # Without tproxy support, use the original traceback. - def tb_set_next( - tb: TracebackType, tb_next: t.Optional[TracebackType] - ) -> TracebackType: - return tb - - else: - # With tproxy support, create a proxy around the traceback that - # returns the new tb_next. - def tb_set_next( - tb: TracebackType, tb_next: t.Optional[TracebackType] - ) -> TracebackType: - def controller(op): # type: ignore - if op.opname == "__getattribute__" and op.args[0] == "tb_next": - return tb_next - - return op.delegate() - - return tputil.make_proxy(controller, obj=tb) # type: ignore - - -else: - # Use ctypes to assign tb_next at the C level since it's read-only - # from Python. - import ctypes - - class _CTraceback(ctypes.Structure): - _fields_ = [ - # Extra PyObject slots when compiled with Py_TRACE_REFS. - ("PyObject_HEAD", ctypes.c_byte * object().__sizeof__()), - # Only care about tb_next as an object, not a traceback. - ("tb_next", ctypes.py_object), - ] - - def tb_set_next( - tb: TracebackType, tb_next: t.Optional[TracebackType] - ) -> TracebackType: - c_tb = _CTraceback.from_address(id(tb)) - - # Clear out the old tb_next. - if tb.tb_next is not None: - c_tb_next = ctypes.py_object(tb.tb_next) - c_tb.tb_next = ctypes.py_object() - ctypes.pythonapi.Py_DecRef(c_tb_next) - - # Assign the new tb_next. - if tb_next is not None: - c_tb_next = ctypes.py_object(tb_next) - ctypes.pythonapi.Py_IncRef(c_tb_next) - c_tb.tb_next = c_tb_next - - return tb diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py index a231d9c..ea04e8b 100644 --- a/src/jinja2/environment.py +++ b/src/jinja2/environment.py @@ -2,7 +2,6 @@ options. """ import os -import sys import typing import typing as t import weakref @@ -282,6 +281,8 @@ class Environment: #: :class:`~jinja2.compiler.CodeGenerator` for more information. code_generator_class: t.Type["CodeGenerator"] = CodeGenerator + concat = "".join + #: the context class that is used for templates. See #: :class:`~jinja2.runtime.Context` for more information. context_class: t.Type[Context] = Context @@ -392,6 +393,8 @@ class Environment: line_comment_prefix: t.Optional[str] = missing, trim_blocks: bool = missing, lstrip_blocks: bool = missing, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing, + keep_trailing_newline: bool = missing, extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing, optimized: bool = missing, undefined: t.Type[Undefined] = missing, @@ -401,6 +404,7 @@ class Environment: cache_size: int = missing, auto_reload: bool = missing, bytecode_cache: t.Optional["BytecodeCache"] = missing, + enable_async: bool = False, ) -> "Environment": """Create a new overlay environment that shares all the data with the current environment except for cache and the overridden attributes. @@ -412,9 +416,13 @@ class Environment: up completely. Not all attributes are truly linked, some are just copied over so modifications on the original environment may not shine through. + + .. versionchanged:: 3.1.2 + Added the ``newline_sequence``,, ``keep_trailing_newline``, + and ``enable_async`` parameters to match ``__init__``. """ args = dict(locals()) - del args["self"], args["cache_size"], args["extensions"] + del args["self"], args["cache_size"], args["extensions"], args["enable_async"] rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) @@ -436,6 +444,9 @@ class Environment: if extensions is not missing: rv.extensions.update(load_extensions(rv, extensions)) + if enable_async is not missing: + rv.is_async = enable_async + return _environment_config_check(rv) @property @@ -938,7 +949,7 @@ class Environment: @internalcode def _load_template( - self, name: str, globals: t.Optional[t.Mapping[str, t.Any]] + self, name: str, globals: t.Optional[t.MutableMapping[str, t.Any]] ) -> "Template": if self.loader is None: raise TypeError("no loader for this environment specified") @@ -966,13 +977,15 @@ class Environment: self, name: t.Union[str, "Template"], parent: t.Optional[str] = None, - globals: t.Optional[t.Mapping[str, t.Any]] = None, + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, ) -> "Template": """Load a template by name with :attr:`loader` and return a :class:`Template`. If the template does not exist a :exc:`TemplateNotFound` exception is raised. - :param name: Name of the template to load. + :param name: Name of the template to load. When loading + templates from the filesystem, "/" is used as the path + separator, even on Windows. :param parent: The name of the parent template importing this template. :meth:`join_path` can be used to implement name transformations with this. @@ -1001,7 +1014,7 @@ class Environment: self, names: t.Iterable[t.Union[str, "Template"]], parent: t.Optional[str] = None, - globals: t.Optional[t.Mapping[str, t.Any]] = None, + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, ) -> "Template": """Like :meth:`get_template`, but tries loading multiple names. If none of the names can be loaded a :exc:`TemplatesNotFound` @@ -1057,7 +1070,7 @@ class Environment: str, "Template", t.List[t.Union[str, "Template"]] ], parent: t.Optional[str] = None, - globals: t.Optional[t.Mapping[str, t.Any]] = None, + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, ) -> "Template": """Use :meth:`select_template` if an iterable of template names is given, or :meth:`get_template` if one name is given. @@ -1073,7 +1086,7 @@ class Environment: def from_string( self, source: t.Union[str, nodes.Template], - globals: t.Optional[t.Mapping[str, t.Any]] = None, + globals: t.Optional[t.MutableMapping[str, t.Any]] = None, template_class: t.Optional[t.Type["Template"]] = None, ) -> "Template": """Load a template from a source string without using @@ -1092,7 +1105,7 @@ class Environment: return cls.from_code(self, self.compile(source), gs, None) def make_globals( - self, d: t.Optional[t.Mapping[str, t.Any]] + self, d: t.Optional[t.MutableMapping[str, t.Any]] ) -> t.MutableMapping[str, t.Any]: """Make the globals map for a template. Any given template globals overlay the environment :attr:`globals`. @@ -1268,14 +1281,11 @@ class Template: close = False - if sys.version_info < (3, 7): - loop = asyncio.get_event_loop() - else: - try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - close = True + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + close = True try: return loop.run_until_complete(self.render_async(*args, **kwargs)) @@ -1286,7 +1296,7 @@ class Template: ctx = self.new_context(dict(*args, **kwargs)) try: - return concat(self.root_render_func(ctx)) # type: ignore + return self.environment.concat(self.root_render_func(ctx)) # type: ignore except Exception: self.environment.handle_exception() @@ -1307,7 +1317,9 @@ class Template: ctx = self.new_context(dict(*args, **kwargs)) try: - return concat([n async for n in self.root_render_func(ctx)]) # type: ignore + return self.environment.concat( # type: ignore + [n async for n in self.root_render_func(ctx)] # type: ignore + ) except Exception: return self.environment.handle_exception() @@ -1331,13 +1343,7 @@ class Template: async def to_list() -> t.List[str]: return [x async for x in self.generate_async(*args, **kwargs)] - if sys.version_info < (3, 7): - loop = asyncio.get_event_loop() - out = loop.run_until_complete(to_list()) - else: - out = asyncio.run(to_list()) - - yield from out + yield from asyncio.run(to_list()) return ctx = self.new_context(dict(*args, **kwargs)) diff --git a/src/jinja2/ext.py b/src/jinja2/ext.py index 3e98293..d555054 100644 --- a/src/jinja2/ext.py +++ b/src/jinja2/ext.py @@ -2,7 +2,6 @@ import pprint import re import typing as t -import warnings from markupsafe import Markup @@ -91,7 +90,7 @@ class Extension: def bind(self, environment: Environment) -> "Extension": """Create a copy of this extension bound to another environment.""" - rv = t.cast(Extension, object.__new__(self.__class__)) + rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) rv.environment = environment return rv @@ -355,13 +354,19 @@ class InternationalizationExtension(Extension): def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]: """Parse a translatable tag.""" lineno = next(parser.stream).lineno - num_called_num = False + + context = None + context_token = parser.stream.next_if("string") + + if context_token is not None: + context = context_token.value # find all the variables referenced. Additionally a variable can be # defined in the body of the trans block too, but this is checked at # a later state. plural_expr: t.Optional[nodes.Expr] = None plural_expr_assignment: t.Optional[nodes.Assign] = None + num_called_num = False variables: t.Dict[str, nodes.Expr] = {} trimmed = None while parser.stream.current.type != "block_end": @@ -456,6 +461,7 @@ class InternationalizationExtension(Extension): node = self._make_node( singular, plural, + context, variables, plural_expr, bool(referenced), @@ -511,6 +517,7 @@ class InternationalizationExtension(Extension): self, singular: str, plural: t.Optional[str], + context: t.Optional[str], variables: t.Dict[str, nodes.Expr], plural_expr: t.Optional[nodes.Expr], vars_referenced: bool, @@ -527,21 +534,18 @@ class InternationalizationExtension(Extension): if plural: plural = plural.replace("%%", "%") - # singular only: - if plural_expr is None: - gettext = nodes.Name("gettext", "load") - node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None) + func_name = "gettext" + func_args: t.List[nodes.Expr] = [nodes.Const(singular)] - # singular and plural - else: - ngettext = nodes.Name("ngettext", "load") - node = nodes.Call( - ngettext, - [nodes.Const(singular), nodes.Const(plural), plural_expr], - [], - None, - None, - ) + if context is not None: + func_args.insert(0, nodes.Const(context)) + func_name = f"p{func_name}" + + if plural_expr is not None: + func_name = f"n{func_name}" + func_args.extend((nodes.Const(plural), plural_expr)) + + node = nodes.Call(nodes.Name(func_name, "load"), func_args, [], None, None) # in case newstyle gettext is used, the method is powerful # enough to handle the variable expansion and autoescape @@ -597,28 +601,6 @@ class LoopControlExtension(Extension): return nodes.Continue(lineno=token.lineno) -class WithExtension(Extension): - def __init__(self, environment: Environment) -> None: - super().__init__(environment) - warnings.warn( - "The 'with' extension is deprecated and will be removed in" - " Jinja 3.1. This is built in now.", - DeprecationWarning, - stacklevel=3, - ) - - -class AutoEscapeExtension(Extension): - def __init__(self, environment: Environment) -> None: - super().__init__(environment) - warnings.warn( - "The 'autoescape' extension is deprecated and will be" - " removed in Jinja 3.1. This is built in now.", - DeprecationWarning, - stacklevel=3, - ) - - class DebugExtension(Extension): """A ``{% debug %}`` tag that dumps the available variables, filters, and tests. @@ -874,6 +856,4 @@ def babel_extract( i18n = InternationalizationExtension do = ExprStmtExtension loopcontrols = LoopControlExtension -with_ = WithExtension -autoescape = AutoEscapeExtension debug = DebugExtension diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py index ffb98bf..ed07c4c 100644 --- a/src/jinja2/filters.py +++ b/src/jinja2/filters.py @@ -4,7 +4,6 @@ import random import re import typing import typing as t -import warnings from collections import abc from itertools import chain from itertools import groupby @@ -44,58 +43,6 @@ K = t.TypeVar("K") V = t.TypeVar("V") -def contextfilter(f: F) -> F: - """Pass the context as the first argument to the decorated function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use :func:`~jinja2.pass_context` - instead. - """ - warnings.warn( - "'contextfilter' is renamed to 'pass_context', the old name" - " will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_context(f) - - -def evalcontextfilter(f: F) -> F: - """Pass the eval context as the first argument to the decorated - function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use - :func:`~jinja2.pass_eval_context` instead. - - .. versionadded:: 2.4 - """ - warnings.warn( - "'evalcontextfilter' is renamed to 'pass_eval_context', the old" - " name will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_eval_context(f) - - -def environmentfilter(f: F) -> F: - """Pass the environment as the first argument to the decorated - function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use - :func:`~jinja2.pass_environment` instead. - """ - warnings.warn( - "'environmentfilter' is renamed to 'pass_environment', the old" - " name will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_environment(f) - - def ignore_case(value: V) -> V: """For use as a postprocessor for :func:`make_attrgetter`. Converts strings to lowercase and returns other types as-is.""" @@ -271,6 +218,36 @@ def do_lower(s: str) -> str: return soft_str(s).lower() +def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K, V]]: + """Return an iterator over the ``(key, value)`` items of a mapping. + + ``x|items`` is the same as ``x.items()``, except if ``x`` is + undefined an empty iterator is returned. + + This filter is useful if you expect the template to be rendered with + an implementation of Jinja in another programming language that does + not have a ``.items()`` method on its mapping type. + + .. code-block:: html+jinja + +
    + {% for key, value in my_dict|items %} +
    {{ key }} +
    {{ value }} + {% endfor %} +
    + + .. versionadded:: 3.1 + """ + if isinstance(value, Undefined): + return + + if not isinstance(value, abc.Mapping): + raise TypeError("Can only get item pairs from a mapping.") + + yield from value.items() + + @pass_eval_context def do_xmlattr( eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True @@ -415,7 +392,7 @@ def do_sort( .. sourcecode:: jinja - {% for user users|sort(attribute="age,name") %} + {% for user in users|sort(attribute="age,name") %} ... {% endfor %} @@ -1164,7 +1141,7 @@ def do_round( return round(value, precision) func = getattr(math, method) - return t.cast(float, func(value * (10 ** precision)) / (10 ** precision)) + return t.cast(float, func(value * (10**precision)) / (10**precision)) class _GroupTuple(t.NamedTuple): @@ -1186,7 +1163,8 @@ def sync_do_groupby( value: "t.Iterable[V]", attribute: t.Union[str, int], default: t.Optional[t.Any] = None, -) -> "t.List[t.Tuple[t.Any, t.List[V]]]": + case_sensitive: bool = False, +) -> "t.List[_GroupTuple]": """Group a sequence of objects by an attribute using Python's :func:`itertools.groupby`. The attribute can use dot notation for nested access, like ``"address.city"``. Unlike Python's ``groupby``, @@ -1226,18 +1204,42 @@ def sync_do_groupby(
  • {{ city }}: {{ items|map(attribute="name")|join(", ") }}
  • {% endfor %} + Like the :func:`~jinja-filters.sort` filter, sorting and grouping is + case-insensitive by default. The ``key`` for each group will have + the case of the first item in that group of values. For example, if + a list of users has cities ``["CA", "NY", "ca"]``, the "CA" group + will have two values. This can be disabled by passing + ``case_sensitive=True``. + + .. versionchanged:: 3.1 + Added the ``case_sensitive`` parameter. Sorting and grouping is + case-insensitive by default, matching other filters that do + comparisons. + .. versionchanged:: 3.0 Added the ``default`` parameter. .. versionchanged:: 2.6 The attribute supports dot notation for nested access. """ - expr = make_attrgetter(environment, attribute, default=default) - return [ + expr = make_attrgetter( + environment, + attribute, + postprocess=ignore_case if not case_sensitive else None, + default=default, + ) + out = [ _GroupTuple(key, list(values)) for key, values in groupby(sorted(value, key=expr), expr) ] + if not case_sensitive: + # Return the real key from the first value instead of the lowercase key. + output_expr = make_attrgetter(environment, attribute, default=default) + out = [_GroupTuple(output_expr(values[0]), values) for _, values in out] + + return out + @async_variant(sync_do_groupby) # type: ignore async def do_groupby( @@ -1245,13 +1247,26 @@ async def do_groupby( value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", attribute: t.Union[str, int], default: t.Optional[t.Any] = None, -) -> "t.List[t.Tuple[t.Any, t.List[V]]]": - expr = make_attrgetter(environment, attribute, default=default) - return [ + case_sensitive: bool = False, +) -> "t.List[_GroupTuple]": + expr = make_attrgetter( + environment, + attribute, + postprocess=ignore_case if not case_sensitive else None, + default=default, + ) + out = [ _GroupTuple(key, await auto_to_list(values)) for key, values in groupby(sorted(await auto_to_list(value), key=expr), expr) ] + if not case_sensitive: + # Return the real key from the first value instead of the lowercase key. + output_expr = make_attrgetter(environment, attribute, default=default) + out = [_GroupTuple(output_expr(values[0]), values) for _, values in out] + + return out + @pass_environment def sync_do_sum( @@ -1271,13 +1286,13 @@ def sync_do_sum( Total: {{ items|sum(attribute='price') }} .. versionchanged:: 2.6 - The `attribute` parameter was added to allow suming up over - attributes. Also the `start` parameter was moved on to the right. + The ``attribute`` parameter was added to allow summing up over + attributes. Also the ``start`` parameter was moved on to the right. """ if attribute is not None: iterable = map(make_attrgetter(environment, attribute), iterable) - return sum(iterable, start) + return sum(iterable, start) # type: ignore[no-any-return, call-overload] @async_variant(sync_do_sum) # type: ignore @@ -1792,6 +1807,7 @@ FILTERS = { "length": len, "list": do_list, "lower": do_lower, + "items": do_items, "map": do_map, "min": do_min, "max": do_max, diff --git a/src/jinja2/idtracking.py b/src/jinja2/idtracking.py index 38c525e..995ebaa 100644 --- a/src/jinja2/idtracking.py +++ b/src/jinja2/idtracking.py @@ -84,7 +84,7 @@ class Symbols: return rv def copy(self) -> "Symbols": - rv = t.cast(Symbols, object.__new__(self.__class__)) + rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) rv.refs = self.refs.copy() rv.loads = self.loads.copy() diff --git a/src/jinja2/lexer.py b/src/jinja2/lexer.py index c25ab0f..aff7e9f 100644 --- a/src/jinja2/lexer.py +++ b/src/jinja2/lexer.py @@ -507,19 +507,17 @@ class Lexer: # block suffix if trimming is enabled block_suffix_re = "\\n?" if environment.trim_blocks else "" - # If lstrip is enabled, it should not be applied if there is any - # non-whitespace between the newline and block. - self.lstrip_unless_re = c(r"[^ \t]") if environment.lstrip_blocks else None + self.lstrip_blocks = environment.lstrip_blocks self.newline_sequence = environment.newline_sequence self.keep_trailing_newline = environment.keep_trailing_newline root_raw_re = ( - fr"(?P{block_start_re}(\-|\+|)\s*raw\s*" - fr"(?:\-{block_end_re}\s*|{block_end_re}))" + rf"(?P{block_start_re}(\-|\+|)\s*raw\s*" + rf"(?:\-{block_end_re}\s*|{block_end_re}))" ) root_parts_re = "|".join( - [root_raw_re] + [fr"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules] + [root_raw_re] + [rf"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules] ) # global lexing rules @@ -527,7 +525,7 @@ class Lexer: "root": [ # directives _Rule( - c(fr"(.*?)(?:{root_parts_re})"), + c(rf"(.*?)(?:{root_parts_re})"), OptionalLStrip(TOKEN_DATA, "#bygroup"), # type: ignore "#bygroup", ), @@ -538,8 +536,8 @@ class Lexer: TOKEN_COMMENT_BEGIN: [ _Rule( c( - fr"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*" - fr"|{comment_end_re}{block_suffix_re}))" + rf"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*" + rf"|{comment_end_re}{block_suffix_re}))" ), (TOKEN_COMMENT, TOKEN_COMMENT_END), "#pop", @@ -550,8 +548,8 @@ class Lexer: TOKEN_BLOCK_BEGIN: [ _Rule( c( - fr"(?:\+{block_end_re}|\-{block_end_re}\s*" - fr"|{block_end_re}{block_suffix_re})" + rf"(?:\+{block_end_re}|\-{block_end_re}\s*" + rf"|{block_end_re}{block_suffix_re})" ), TOKEN_BLOCK_END, "#pop", @@ -561,7 +559,7 @@ class Lexer: # variables TOKEN_VARIABLE_BEGIN: [ _Rule( - c(fr"\-{variable_end_re}\s*|{variable_end_re}"), + c(rf"\-{variable_end_re}\s*|{variable_end_re}"), TOKEN_VARIABLE_END, "#pop", ) @@ -571,9 +569,9 @@ class Lexer: TOKEN_RAW_BEGIN: [ _Rule( c( - fr"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*" - fr"(?:\+{block_end_re}|\-{block_end_re}\s*" - fr"|{block_end_re}{block_suffix_re}))" + rf"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*" + rf"(?:\+{block_end_re}|\-{block_end_re}\s*" + rf"|{block_end_re}{block_suffix_re}))" ), OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END), # type: ignore "#pop", @@ -697,7 +695,6 @@ class Lexer: statetokens = self.rules[stack[-1]] source_length = len(source) balancing_stack: t.List[str] = [] - lstrip_unless_re = self.lstrip_unless_re newlines_stripped = 0 line_starting = True @@ -723,7 +720,7 @@ class Lexer: # tuples support more options if isinstance(tokens, tuple): - groups = m.groups() + groups: t.Sequence[str] = m.groups() if isinstance(tokens, OptionalLStrip): # Rule supports lstrip. Match will look like @@ -743,7 +740,7 @@ class Lexer: # Not marked for preserving whitespace. strip_sign != "+" # lstrip is enabled. - and lstrip_unless_re is not None + and self.lstrip_blocks # Not a variable expression. and not m.groupdict().get(TOKEN_VARIABLE_BEGIN) ): @@ -753,7 +750,7 @@ class Lexer: if l_pos > 0 or line_starting: # If there's only whitespace between the newline and the # tag, strip it. - if not lstrip_unless_re.search(text, l_pos): + if whitespace_re.fullmatch(text, l_pos): groups = [text[:l_pos], *groups[1:]] for idx, token in enumerate(tokens): diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py index 4fac3a0..d2f9809 100644 --- a/src/jinja2/loaders.py +++ b/src/jinja2/loaders.py @@ -3,6 +3,7 @@ sources. """ import importlib.util import os +import posixpath import sys import typing as t import weakref @@ -193,7 +194,9 @@ class FileSystemLoader(BaseLoader): ) -> t.Tuple[str, str, t.Callable[[], bool]]: pieces = split_template_path(template) for searchpath in self.searchpath: - filename = os.path.join(searchpath, *pieces) + # Use posixpath even on Windows to avoid "drive:" or UNC + # segments breaking out of the search directory. + filename = posixpath.join(searchpath, *pieces) f = open_if_exists(filename) if f is None: continue @@ -210,7 +213,8 @@ class FileSystemLoader(BaseLoader): except OSError: return False - return contents, filename, uptodate + # Use normpath to convert Windows altsep to sep. + return contents, os.path.normpath(filename), uptodate raise TemplateNotFound(template) def list_templates(self) -> t.List[str]: @@ -296,7 +300,7 @@ class PackageLoader(BaseLoader): if isinstance(loader, zipimport.zipimporter): self._archive = loader.archive pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore - template_root = os.path.join(pkgdir, package_path) + template_root = os.path.join(pkgdir, package_path).rstrip(os.path.sep) else: roots: t.List[str] = [] @@ -326,7 +330,12 @@ class PackageLoader(BaseLoader): def get_source( self, environment: "Environment", template: str ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]: - p = os.path.join(self._template_root, *split_template_path(template)) + # Use posixpath even on Windows to avoid "drive:" or UNC + # segments breaking out of the search directory. Use normpath to + # convert Windows altsep to sep. + p = os.path.normpath( + posixpath.join(self._template_root, *split_template_path(template)) + ) up_to_date: t.Optional[t.Callable[[], bool]] if self._archive is None: @@ -603,7 +612,7 @@ class ModuleLoader(BaseLoader): if not isinstance(path, abc.Iterable) or isinstance(path, str): path = [path] - mod.__path__ = [os.fspath(p) for p in path] # type: ignore + mod.__path__ = [os.fspath(p) for p in path] sys.modules[package_name] = weakref.proxy( mod, lambda x: sys.modules.pop(package_name, None) diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py index 20597d5..ac08610 100644 --- a/src/jinja2/nativetypes.py +++ b/src/jinja2/nativetypes.py @@ -3,6 +3,7 @@ from ast import literal_eval from ast import parse from itertools import chain from itertools import islice +from types import GeneratorType from . import nodes from .compiler import CodeGenerator @@ -31,7 +32,9 @@ def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]: if not isinstance(raw, str): return raw else: - raw = "".join([str(v) for v in chain(head, values)]) + if isinstance(values, GeneratorType): + values = chain(head, values) + raw = "".join([str(v) for v in values]) try: return literal_eval( @@ -86,6 +89,7 @@ class NativeEnvironment(Environment): """An environment that renders templates to native Python types.""" code_generator_class = NativeCodeGenerator + concat = staticmethod(native_concat) # type: ignore class NativeTemplate(Template): @@ -101,7 +105,9 @@ class NativeTemplate(Template): ctx = self.new_context(dict(*args, **kwargs)) try: - return native_concat(self.root_render_func(ctx)) # type: ignore + return self.environment_class.concat( # type: ignore + self.root_render_func(ctx) # type: ignore + ) except Exception: return self.environment.handle_exception() @@ -114,7 +120,7 @@ class NativeTemplate(Template): ctx = self.new_context(dict(*args, **kwargs)) try: - return native_concat( + return self.environment_class.concat( # type: ignore [n async for n in self.root_render_func(ctx)] # type: ignore ) except Exception: diff --git a/src/jinja2/parser.py b/src/jinja2/parser.py index 7ad73fc..cefce2d 100644 --- a/src/jinja2/parser.py +++ b/src/jinja2/parser.py @@ -160,7 +160,7 @@ class Parser: self._last_identifier += 1 rv = object.__new__(nodes.InternalName) nodes.Node.__init__(rv, f"fi{self._last_identifier}", lineno=lineno) - return rv # type: ignore + return rv def parse_statement(self) -> t.Union[nodes.Node, t.List[nodes.Node]]: """Parse a single statement.""" @@ -364,14 +364,10 @@ class Parser: node.names = [] def parse_context() -> bool: - if ( - self.stream.current.value - in { - "with", - "without", - } - and self.stream.look().test("name:context") - ): + if self.stream.current.value in { + "with", + "without", + } and self.stream.look().test("name:context"): node.with_context = next(self.stream).value == "with" self.stream.skip() return True @@ -957,19 +953,15 @@ class Parser: kwargs = [] if self.stream.current.type == "lparen": args, kwargs, dyn_args, dyn_kwargs = self.parse_call_args() - elif ( - self.stream.current.type - in { - "name", - "string", - "integer", - "float", - "lparen", - "lbracket", - "lbrace", - } - and not self.stream.current.test_any("name:else", "name:or", "name:and") - ): + elif self.stream.current.type in { + "name", + "string", + "integer", + "float", + "lparen", + "lbracket", + "lbrace", + } and not self.stream.current.test_any("name:else", "name:or", "name:and"): if self.stream.current.test("name:is"): self.fail("You cannot chain multiple tests with is") arg_node = self.parse_primary() diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py index 2346cf2..985842b 100644 --- a/src/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -49,7 +49,6 @@ exported = [ "Markup", "TemplateRuntimeError", "missing", - "concat", "escape", "markup_join", "str_join", @@ -89,18 +88,6 @@ def str_join(seq: t.Iterable[t.Any]) -> str: return concat(map(str, seq)) -def unicode_join(seq: t.Iterable[t.Any]) -> str: - import warnings - - warnings.warn( - "This template must be recompiled with at least Jinja 3.0, or" - " it will fail in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return str_join(seq) - - def new_context( environment: "Environment", template_name: t.Optional[str], @@ -173,27 +160,6 @@ class Context: :class:`Undefined` object for missing variables. """ - _legacy_resolve_mode: t.ClassVar[bool] = False - - def __init_subclass__(cls) -> None: - if "resolve_or_missing" in cls.__dict__: - # If the subclass overrides resolve_or_missing it opts in to - # modern mode no matter what. - cls._legacy_resolve_mode = False - elif "resolve" in cls.__dict__ or cls._legacy_resolve_mode: - # If the subclass overrides resolve, or if its base is - # already in legacy mode, warn about legacy behavior. - import warnings - - warnings.warn( - "Overriding 'resolve' is deprecated and will not have" - " the expected behavior in Jinja 3.1. Override" - " 'resolve_or_missing' instead ", - DeprecationWarning, - stacklevel=2, - ) - cls._legacy_resolve_mode = True - def __init__( self, environment: "Environment", @@ -251,15 +217,6 @@ class Context: :param key: The variable name to look up. """ - if self._legacy_resolve_mode: - if key in self.vars: - return self.vars[key] - - if key in self.parent: - return self.parent[key] - - return self.environment.undefined(name=key) - rv = self.resolve_or_missing(key) if rv is missing: @@ -277,14 +234,6 @@ class Context: :param key: The variable name to look up. """ - if self._legacy_resolve_mode: - rv = self.resolve(key) - - if isinstance(rv, Undefined): - return missing - - return rv - if key in self.vars: return self.vars[key] diff --git a/src/jinja2/sandbox.py b/src/jinja2/sandbox.py index 4294884..06d7414 100644 --- a/src/jinja2/sandbox.py +++ b/src/jinja2/sandbox.py @@ -409,7 +409,7 @@ class ImmutableSandboxedEnvironment(SandboxedEnvironment): class SandboxedFormatter(Formatter): def __init__(self, env: Environment, **kwargs: t.Any) -> None: self._env = env - super().__init__(**kwargs) # type: ignore + super().__init__(**kwargs) def get_field( self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any] diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py index 567185f..9b5f5a5 100644 --- a/src/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -3,7 +3,6 @@ import json import os import re import typing as t -import warnings from collections import abc from collections import deque from random import choice @@ -84,74 +83,9 @@ class _PassArg(enum.Enum): if hasattr(obj, "jinja_pass_arg"): return obj.jinja_pass_arg # type: ignore - for prefix in "context", "eval_context", "environment": - squashed = prefix.replace("_", "") - - for name in f"{squashed}function", f"{squashed}filter": - if getattr(obj, name, False) is True: - warnings.warn( - f"{name!r} is deprecated and will stop working" - f" in Jinja 3.1. Use 'pass_{prefix}' instead.", - DeprecationWarning, - stacklevel=2, - ) - return cls[prefix] - return None -def contextfunction(f: F) -> F: - """Pass the context as the first argument to the decorated function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use :func:`~jinja2.pass_context` - instead. - """ - warnings.warn( - "'contextfunction' is renamed to 'pass_context', the old name" - " will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_context(f) - - -def evalcontextfunction(f: F) -> F: - """Pass the eval context as the first argument to the decorated - function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use - :func:`~jinja2.pass_eval_context` instead. - - .. versionadded:: 2.4 - """ - warnings.warn( - "'evalcontextfunction' is renamed to 'pass_eval_context', the" - " old name will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_eval_context(f) - - -def environmentfunction(f: F) -> F: - """Pass the environment as the first argument to the decorated - function. - - .. deprecated:: 3.0 - Will be removed in Jinja 3.1. Use - :func:`~jinja2.pass_environment` instead. - """ - warnings.warn( - "'environmentfunction' is renamed to 'pass_environment', the" - " old name will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return pass_environment(f) - - def internalcode(f: F) -> F: """Marks the function as internally used""" internal_code.add(f.__code__) @@ -483,18 +417,6 @@ def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str: return rv -def unicode_urlencode(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str: - import warnings - - warnings.warn( - "'unicode_urlencode' has been renamed to 'url_quote'. The old" - " name will be removed in Jinja 3.1.", - DeprecationWarning, - stacklevel=2, - ) - return url_quote(obj, charset=charset, for_qs=for_qs) - - @abc.MutableMapping.register class LRUCache: """A simple LRU Cache implementation.""" @@ -831,24 +753,3 @@ class Namespace: def __repr__(self) -> str: return f"" - - -class Markup(markupsafe.Markup): - def __new__(cls, base="", encoding=None, errors="strict"): # type: ignore - warnings.warn( - "'jinja2.Markup' is deprecated and will be removed in Jinja" - " 3.1. Import 'markupsafe.Markup' instead.", - DeprecationWarning, - stacklevel=2, - ) - return super().__new__(cls, base, encoding, errors) - - -def escape(s: t.Any) -> str: - warnings.warn( - "'jinja2.escape' is deprecated and will be removed in Jinja" - " 3.1. Import 'markupsafe.escape' instead.", - DeprecationWarning, - stacklevel=2, - ) - return markupsafe.escape(s) diff --git a/src/jinja2/visitor.py b/src/jinja2/visitor.py index b150e57..17c6aab 100644 --- a/src/jinja2/visitor.py +++ b/src/jinja2/visitor.py @@ -30,7 +30,7 @@ class NodeVisitor: exists for this node. In that case the generic visit function is used instead. """ - return getattr(self, f"visit_{type(node).__name__}", None) # type: ignore + return getattr(self, f"visit_{type(node).__name__}", None) def visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any: """Visit a node.""" @@ -43,8 +43,8 @@ class NodeVisitor: def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any: """Called if no explicit visitor function exists for a node.""" - for node in node.iter_child_nodes(): - self.visit(node, *args, **kwargs) + for child_node in node.iter_child_nodes(): + self.visit(child_node, *args, **kwargs) class NodeTransformer(NodeVisitor): diff --git a/tests/test_async.py b/tests/test_async.py index 375a7ba..c9ba70c 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,5 +1,4 @@ import asyncio -import sys import pytest @@ -14,19 +13,6 @@ from jinja2.exceptions import UndefinedError from jinja2.nativetypes import NativeEnvironment -if sys.version_info < (3, 7): - - def run(coro): - loop = asyncio.get_event_loop() - return loop.run_until_complete(coro) - - -else: - - def run(coro): - return asyncio.run(coro) - - def test_basic_async(): t = Template( "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True @@ -35,7 +21,7 @@ def test_basic_async(): async def func(): return await t.render_async() - rv = run(func()) + rv = asyncio.run(func()) assert rv == "[1][2][3]" @@ -51,7 +37,7 @@ def test_await_on_calls(): async def func(): return await t.render_async(async_func=async_func, normal_func=normal_func) - rv = run(func()) + rv = asyncio.run(func()) assert rv == "65" @@ -65,7 +51,6 @@ def test_await_on_calls_normal_render(): return 23 rv = t.render(async_func=async_func, normal_func=normal_func) - assert rv == "65" @@ -81,7 +66,7 @@ def test_await_and_macros(): async def func(): return await t.render_async(async_func=async_func) - rv = run(func()) + rv = asyncio.run(func()) assert rv == "[42][42]" @@ -95,7 +80,7 @@ def test_async_blocks(): async def func(): return await t.render_async() - rv = run(func()) + rv = asyncio.run(func()) assert rv == "" @@ -172,19 +157,18 @@ class TestAsyncImports: test_env_async.from_string('{% from "foo" import bar, with with context %}') def test_exports(self, test_env_async): - m = run( - test_env_async.from_string( - """ + coro = test_env_async.from_string( + """ {% macro toplevel() %}...{% endmacro %} {% macro __private() %}...{% endmacro %} {% set variable = 42 %} {% for item in [1] %} {% macro notthere() %}{% endmacro %} {% endfor %} - """ - )._get_default_module_async() - ) - assert run(m.toplevel()) == "..." + """ + )._get_default_module_async() + m = asyncio.run(coro) + assert asyncio.run(m.toplevel()) == "..." assert not hasattr(m, "__missing") assert m.variable == 42 assert not hasattr(m, "notthere") @@ -621,7 +605,7 @@ def test_namespace_awaitable(test_env_async): actual = await t.render_async() assert actual == "Bar" - run(_test()) + asyncio.run(_test()) def test_chainable_undefined_aiter(): @@ -634,7 +618,7 @@ def test_chainable_undefined_aiter(): rv = await t.render_async(a={}) assert rv == "" - run(_test()) + asyncio.run(_test()) @pytest.fixture @@ -648,7 +632,7 @@ def test_native_async(async_native_env): rv = await t.render_async(x=23) assert rv == 23 - run(_test()) + asyncio.run(_test()) def test_native_list_async(async_native_env): @@ -657,4 +641,20 @@ def test_native_list_async(async_native_env): rv = await t.render_async(x=list(range(3))) assert rv == [0, 1, 2] - run(_test()) + asyncio.run(_test()) + + +def test_getitem_after_filter(): + env = Environment(enable_async=True) + env.filters["add_each"] = lambda v, x: [i + x for i in v] + t = env.from_string("{{ (a|add_each(2))[1:] }}") + out = t.render(a=range(3)) + assert out == "[3, 4]" + + +def test_getitem_after_call(): + env = Environment(enable_async=True) + env.globals["add_each"] = lambda v, x: [i + x for i in v] + t = env.from_string("{{ add_each(a, 2)[1:] }}") + out = t.render(a=range(3)) + assert out == "[3, 4]" diff --git a/tests/test_async_filters.py b/tests/test_async_filters.py index 5d4f332..f5b2627 100644 --- a/tests/test_async_filters.py +++ b/tests/test_async_filters.py @@ -57,6 +57,26 @@ def test_groupby(env_async, items): ] +@pytest.mark.parametrize( + ("case_sensitive", "expect"), + [ + (False, "a: 1, 3\nb: 2\n"), + (True, "A: 3\na: 1\nb: 2\n"), + ], +) +def test_groupby_case(env_async, case_sensitive, expect): + tmpl = env_async.from_string( + "{% for k, vs in data|groupby('k', case_sensitive=cs) %}" + "{{ k }}: {{ vs|join(', ', attribute='v') }}\n" + "{% endfor %}" + ) + out = tmpl.render( + data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}], + cs=case_sensitive, + ) + assert out == expect + + @mark_dualiter("items", lambda: [("a", 1), ("a", 2), ("b", 1)]) def test_groupby_tuple_index(env_async, items): tmpl = env_async.from_string( diff --git a/tests/test_debug.py b/tests/test_debug.py index 1cb931c..bc11f40 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -25,7 +25,7 @@ class TestDebug: m = re.search(expected_tb.strip(), "".join(tb)) assert ( m is not None - ), "Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{expected_tb}" + ), f"Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{expected_tb}" def test_runtime_error(self, fs_env): def test(): diff --git a/tests/test_ext.py b/tests/test_ext.py index b54e905..2e842e0 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -43,6 +43,9 @@ newstyle_i18n_templates = { "pgettext.html": '{{ pgettext("fruit", "Apple") }}', "npgettext.html": '{{ npgettext("fruit", "%(num)s apple", "%(num)s apples",' " apples) }}", + "pgettext_block": "{% trans 'fruit' num=apples %}Apple{% endtrans %}", + "npgettext_block": "{% trans 'fruit' num=apples %}{{ num }} apple" + "{% pluralize %}{{ num }} apples{% endtrans %}", "transvars1.html": "{% trans %}User: {{ num }}{% endtrans %}", "transvars2.html": "{% trans num=count %}User: {{ num }}{% endtrans %}", "transvars3.html": "{% trans count=num %}User: {{ count }}{% endtrans %}", @@ -593,11 +596,20 @@ class TestNewstyleInternationalization: tmpl = newstyle_i18n_env.get_template("pgettext.html") assert tmpl.render(LANGUAGE="de") == "Apple" - def test_context_newstyle_plural(self): + def test_context_plural(self): tmpl = newstyle_i18n_env.get_template("npgettext.html") assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple" assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples" + def test_context_block(self): + tmpl = newstyle_i18n_env.get_template("pgettext_block") + assert tmpl.render(LANGUAGE="de") == "Apple" + + def test_context_plural_block(self): + tmpl = newstyle_i18n_env.get_template("npgettext_block") + assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple" + assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples" + class TestAutoEscape: def test_scoped_setting(self): diff --git a/tests/test_features.py b/tests/test_features.py deleted file mode 100644 index 4f36458..0000000 --- a/tests/test_features.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from jinja2 import Template - - -# Python < 3.7 -def test_generator_stop(): - class X: - def __getattr__(self, name): - raise StopIteration() - - t = Template("a{{ bad.bar() }}b") - with pytest.raises(RuntimeError): - t.render(bad=X()) diff --git a/tests/test_filters.py b/tests/test_filters.py index 2195157..73f0f0b 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -251,6 +251,17 @@ class TestFilter: out = tmpl.render() assert out == "foo" + def test_items(self, env): + d = {i: c for i, c in enumerate("abc")} + tmpl = env.from_string("""{{ d|items|list }}""") + out = tmpl.render(d=d) + assert out == "[(0, 'a'), (1, 'b'), (2, 'c')]" + + def test_items_undefined(self, env): + tmpl = env.from_string("""{{ d|items|list }}""") + out = tmpl.render() + assert out == "[]" + def test_pprint(self, env): from pprint import pformat @@ -608,6 +619,25 @@ class TestFilter: ) assert out == "NY: emma, john\nWA: smith\n" + @pytest.mark.parametrize( + ("case_sensitive", "expect"), + [ + (False, "a: 1, 3\nb: 2\n"), + (True, "A: 3\na: 1\nb: 2\n"), + ], + ) + def test_groupby_case(self, env, case_sensitive, expect): + tmpl = env.from_string( + "{% for k, vs in data|groupby('k', case_sensitive=cs) %}" + "{{ k }}: {{ vs|join(', ', attribute='v') }}\n" + "{% endfor %}" + ) + out = tmpl.render( + data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}], + cs=case_sensitive, + ) + assert out == expect + def test_filtertag(self, env): tmpl = env.from_string( "{% filter upper|replace('FOO', 'foo') %}foobar{% endfilter %}" diff --git a/tests/test_loader.py b/tests/test_loader.py index b300c8f..04c921d 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -171,6 +171,15 @@ class TestFileSystemLoader: t = e.get_template("mojibake.txt") assert t.render() == expect + def test_filename_normpath(self): + """Nested template names should only contain ``os.sep`` in the + loaded filename. + """ + loader = loaders.FileSystemLoader(self.searchpath) + e = Environment(loader=loader) + t = e.get_template("foo/test.html") + assert t.filename == str(self.searchpath / "foo" / "test.html") + class TestModuleLoader: archive = None diff --git a/tests/test_nativetypes.py b/tests/test_nativetypes.py index 9bae938..8c85252 100644 --- a/tests/test_nativetypes.py +++ b/tests/test_nativetypes.py @@ -153,3 +153,10 @@ def test_leading_spaces(env): t = env.from_string(" {{ True }}") result = t.render() assert result == " True" + + +def test_macro(env): + t = env.from_string("{%- macro x() -%}{{- [1,2] -}}{%- endmacro -%}{{- x()[1] -}}") + result = t.render() + assert result == 2 + assert isinstance(result, int) diff --git a/tests/test_pickle.py b/tests/test_pickle.py new file mode 100644 index 0000000..b0f6bcf --- /dev/null +++ b/tests/test_pickle.py @@ -0,0 +1,6 @@ +import pickle + + +def test_environment(env): + env = pickle.loads(pickle.dumps(env)) + assert env.from_string("x={{ x }}").render(x=42) == "x=42" diff --git a/tests/test_regression.py b/tests/test_regression.py index 7e23369..46e492b 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -591,23 +591,6 @@ class TestBug: env = MyEnvironment(loader=loader) assert env.get_template("test").render(foobar="test") == "test" - def test_legacy_custom_context(self, env): - from jinja2.runtime import Context, missing - - with pytest.deprecated_call(): - - class MyContext(Context): - def resolve(self, name): - if name == "foo": - return 42 - return super().resolve(name) - - x = MyContext(env, parent={"bar": 23}, name="foo", blocks={}) - assert x._legacy_resolve_mode - assert x.resolve_or_missing("foo") == 42 - assert x.resolve_or_missing("bar") == 23 - assert x.resolve_or_missing("baz") is missing - def test_recursive_loop_bug(self, env): tmpl = env.from_string( "{%- for value in values recursive %}1{% else %}0{% endfor -%}" diff --git a/tox.ini b/tox.ini index f14c957..056ca0d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{311,310,39,38,37,36,py37} + py3{11,10,9,8,7},pypy3{8,7} style typing docs