diff options
115 files changed, 10253 insertions, 10337 deletions
diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 48eb150..680a15c 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -4,32 +4,34 @@ trigger: variables: vmImage: ubuntu-latest - python.version: 3.7 - TOXENV: py,coverage-ci - hasTestResults: true + python.version: '3.8' + TOXENV: py + hasTestResults: 'true' strategy: matrix: - Python 3.7 Linux: + Python 3.8 Linux: vmImage: ubuntu-latest - Python 3.7 Windows: + Python 3.8 Windows: vmImage: windows-latest - Python 3.7 Mac: + Python 3.8 Mac: vmImage: macos-latest PyPy 3 Linux: python.version: pypy3 + Python 3.7 Linux: + python.version: '3.7' Python 3.6 Linux: - python.version: 3.6 + python.version: '3.6' Python 3.5 Linux: - python.version: 3.5 + python.version: '3.5' Python 2.7 Linux: - python.version: 2.7 - Python 2.7 Windows: - python.version: 2.7 - vmImage: windows-latest + python.version: '2.7' Docs: - TOXENV: docs-html - hasTestResults: false + TOXENV: docs + hasTestResults: 'false' + Style: + TOXENV: style + hasTestResults: 'false' pool: vmImage: $[ variables.vmImage ] @@ -43,19 +45,5 @@ steps: - script: pip --disable-pip-version-check install -U tox displayName: Install tox - - script: tox -s false -- --junit-xml=test-results.xml + - script: tox displayName: Run tox - - - task: PublishTestResults@2 - inputs: - testResultsFiles: test-results.xml - testRunTitle: $(Agent.JobName) - condition: eq(variables['hasTestResults'], 'true') - displayName: Publish test results - - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: coverage.xml - condition: eq(variables['hasTestResults'], 'true') - displayName: Publish coverage results @@ -10,8 +10,11 @@ dist/ .tox/ .cache/ .idea/ +env/ venv/ venv-*/ .coverage .coverage.* htmlcov +.pytest_cache/ +/.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3a341a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/asottile/reorder_python_imports + rev: v1.9.0 + hooks: + - id: reorder-python-imports + args: ["--application-directories", "src"] + - repo: https://github.com/ambv/black + rev: 19.10b0 + hooks: + - id: black + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/CHANGES.rst b/CHANGES.rst index f3ad466..511b22b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,160 @@ .. currentmodule:: jinja2 +Version 2.11.3 +-------------- + +Released 2021-01-31 + +- Improve the speed of the ``urlize`` filter by reducing regex + backtracking. Email matching requires a word character at the start + of the domain part, and only word characters in the TLD. :pr:`1343` + + +Version 2.11.2 +-------------- + +Released 2020-04-13 + +- Fix a bug that caused callable objects with ``__getattr__``, like + :class:`~unittest.mock.Mock` to be treated as a + :func:`contextfunction`. :issue:`1145` +- Update ``wordcount`` filter to trigger :class:`Undefined` methods + by wrapping the input in :func:`soft_unicode`. :pr:`1160` +- Fix a hang when displaying tracebacks on Python 32-bit. + :issue:`1162` +- Showing an undefined error for an object that raises + ``AttributeError`` on access doesn't cause a recursion error. + :issue:`1177` +- Revert changes to :class:`~loaders.PackageLoader` from 2.10 which + removed the dependency on setuptools and pkg_resources, and added + limited support for namespace packages. The changes caused issues + when using Pytest. Due to the difficulty in supporting Python 2 and + :pep:`451` simultaneously, the changes are reverted until 3.0. + :pr:`1182` +- Fix line numbers in error messages when newlines are stripped. + :pr:`1178` +- The special ``namespace()`` assignment object in templates works in + async environments. :issue:`1180` +- Fix whitespace being removed before tags in the middle of lines when + ``lstrip_blocks`` is enabled. :issue:`1138` +- :class:`~nativetypes.NativeEnvironment` doesn't evaluate + intermediate strings during rendering. This prevents early + evaluation which could change the value of an expression. + :issue:`1186` + + +Version 2.11.1 +-------------- + +Released 2020-01-30 + +- Fix a bug that prevented looking up a key after an attribute + (``{{ data.items[1:] }}``) in an async template. :issue:`1141` + + +Version 2.11.0 +-------------- + +Released 2020-01-27 + +- Drop support for Python 2.6, 3.3, and 3.4. This will be the last + version to support Python 2.7 and 3.5. +- Added a new ``ChainableUndefined`` class to support getitem and + getattr on an undefined object. :issue:`977` +- Allow ``{%+`` syntax (with NOP behavior) when ``lstrip_blocks`` is + disabled. :issue:`748` +- Added a ``default`` parameter for the ``map`` filter. :issue:`557` +- Exclude environment globals from + :func:`meta.find_undeclared_variables`. :issue:`931` +- Float literals can be written with scientific notation, like + 2.56e-3. :issue:`912`, :pr:`922` +- Int and float literals can be written with the '_' separator for + legibility, like 12_345. :pr:`923` +- Fix a bug causing deadlocks in ``LRUCache.setdefault``. :pr:`1000` +- The ``trim`` filter takes an optional string of characters to trim. + :pr:`828` +- A new ``jinja2.ext.debug`` extension adds a ``{% debug %}`` tag to + quickly dump the current context and available filters and tests. + :issue:`174`, :pr:`798, 983` +- Lexing templates with large amounts of whitespace is much faster. + :issue:`857`, :pr:`858` +- Parentheses around comparisons are preserved, so + ``{{ 2 * (3 < 5) }}`` outputs "2" instead of "False". + :issue:`755`, :pr:`938` +- Add new ``boolean``, ``false``, ``true``, ``integer`` and ``float`` + tests. :pr:`824` +- The environment's ``finalize`` function is only applied to the + output of expressions (constant or not), not static template data. + :issue:`63` +- When providing multiple paths to ``FileSystemLoader``, a template + can have the same name as a directory. :issue:`821` +- Always return :class:`Undefined` when omitting the ``else`` clause + in a ``{{ 'foo' if bar }}`` expression, regardless of the + environment's ``undefined`` class. Omitting the ``else`` clause is a + valid shortcut and should not raise an error when using + :class:`StrictUndefined`. :issue:`710`, :pr:`1079` +- Fix behavior of ``loop`` control variables such as ``length`` and + ``revindex0`` when looping over a generator. :issue:`459, 751, 794`, + :pr:`993` +- Async support is only loaded the first time an environment enables + it, in order to avoid a slow initial import. :issue:`765` +- In async environments, the ``|map`` filter will await the filter + call if needed. :pr:`913` +- In for loops that access ``loop`` attributes, the iterator is not + advanced ahead of the current iteration unless ``length``, + ``revindex``, ``nextitem``, or ``last`` are accessed. This makes it + less likely to break ``groupby`` results. :issue:`555`, :pr:`1101` +- In async environments, the ``loop`` attributes ``length`` and + ``revindex`` work for async iterators. :pr:`1101` +- In async environments, values from attribute/property access will + be awaited if needed. :pr:`1101` +- :class:`~loader.PackageLoader` doesn't depend on setuptools or + pkg_resources. :issue:`970` +- ``PackageLoader`` has limited support for :pep:`420` namespace + packages. :issue:`1097` +- Support :class:`os.PathLike` objects in + :class:`~loader.FileSystemLoader` and :class:`~loader.ModuleLoader`. + :issue:`870` +- :class:`~nativetypes.NativeTemplate` correctly handles quotes + between expressions. ``"'{{ a }}', '{{ b }}'"`` renders as the tuple + ``('1', '2')`` rather than the string ``'1, 2'``. :issue:`1020` +- Creating a :class:`~nativetypes.NativeTemplate` directly creates a + :class:`~nativetypes.NativeEnvironment` instead of a default + :class:`Environment`. :issue:`1091` +- After calling ``LRUCache.copy()``, the copy's queue methods point to + the correct queue. :issue:`843` +- Compiling templates always writes UTF-8 instead of defaulting to the + system encoding. :issue:`889` +- ``|wordwrap`` filter treats existing newlines as separate paragraphs + to be wrapped individually, rather than creating short intermediate + lines. :issue:`175` +- Add ``break_on_hyphens`` parameter to ``|wordwrap`` filter. + :issue:`550` +- Cython compiled functions decorated as context functions will be + passed the context. :pr:`1108` +- When chained comparisons of constants are evaluated at compile time, + the result follows Python's behavior of returning ``False`` if any + comparison returns ``False``, rather than only the last one. + :issue:`1102` +- Tracebacks for exceptions in templates show the correct line numbers + and source for Python >= 3.7. :issue:`1104` +- Tracebacks for template syntax errors in Python 3 no longer show + internal compiler frames. :issue:`763` +- Add a ``DerivedContextReference`` node that can be used by + extensions to get the current context and local variables such as + ``loop``. :issue:`860` +- Constant folding during compilation is applied to some node types + that were previously overlooked. :issue:`733` +- ``TemplateSyntaxError.source`` is not empty when raised from an + included template. :issue:`457` +- Passing an ``Undefined`` value to ``get_template`` (such as through + ``extends``, ``import``, or ``include``), raises an + ``UndefinedError`` consistently. ``select_template`` will show the + undefined message in the list of attempts rather than the empty + string. :issue:`1037` +- ``TemplateSyntaxError`` can be pickled. :pr:`1117` + + Version 2.10.3 -------------- @@ -201,7 +356,7 @@ Released 2017-01-07, codename Derivation - Change the logic for macro autoescaping to be based on the runtime autoescaping information at call time instead of macro define time. - Ported a modified version of the ``tojson`` filter from Flask to - Jinja2 and hooked it up with the new policy framework. + Jinja and hooked it up with the new policy framework. - Block sets are now marked ``safe`` by default. - On Python 2 the asciification of ASCII strings can now be disabled with the ``compiler.ascii_str`` policy. @@ -247,9 +402,9 @@ Released 2015-07-26, codename Replacement - Changed cache keys to use absolute file names if available instead of load names. - Fixed loop length calculation for some iterators. -- Changed how Jinja2 enforces strings to be native strings in Python 2 +- Changed how Jinja enforces strings to be native strings in Python 2 to work when people break their default encoding. -- Added :func:`make_logging_undefined` which returns an undefined +- Added ``make_logging_undefined`` which returns an undefined object that logs failures into a logger. - If unmarshalling of cached data fails the template will be reloaded now. @@ -265,8 +420,7 @@ Released 2015-07-26, codename Replacement - Add ability to use custom subclasses of ``jinja2.compiler.CodeGenerator`` and ``jinja2.runtime.Context`` by adding two new attributes to the environment - (``code_generator_class`` and ``context_class``) (pull request - ``:issue:`404```). + (``code_generator_class`` and ``context_class``). :pr:`404` - Added support for context/environment/evalctx decorator functions on the finalize callback of the environment. - Escape query strings for urlencode properly. Previously slashes were @@ -368,12 +522,12 @@ Released 2011-07-24, codename Convolution Previously an import suddenly "disappeared" in a scoped block. - Automatically detect newer Python interpreter versions before loading code from bytecode caches to prevent segfaults on invalid - opcodes. The segfault in earlier Jinja2 versions here was not a - Jinja2 bug but a limitation in the underlying Python interpreter. If - you notice Jinja2 segfaulting in earlier versions after an upgrade + opcodes. The segfault in earlier Jinja versions here was not a + Jinja bug but a limitation in the underlying Python interpreter. If + you notice Jinja segfaulting in earlier versions after an upgrade of the Python interpreter you don't have to upgrade, it's enough to flush the bytecode cache. This just no longer makes this necessary, - Jinja2 will automatically detect these cases now. + Jinja will automatically detect these cases now. - The sum filter can now sum up values by attribute. This is a backwards incompatible change. The argument to the filter previously was the optional starting index which defaults to zero. This now @@ -441,7 +595,7 @@ Released 2010-08-17 than the pluralize count will no longer raise a :exc:`KeyError`. - Removed builtin markup class and switched to markupsafe. For backwards compatibility the pure Python implementation still exists - but is pulled from markupsafe by the Jinja2 developers. The debug + but is pulled from markupsafe by the Jinja developers. The debug support went into a separate feature called "debugsupport" and is disabled by default because it is only relevant for Python 2.4 - Fixed an issue with unary operators having the wrong precedence. @@ -457,7 +611,6 @@ Released 2010-05-29, codename Incoherence - Fixed a bug for getattribute constant folding. - Support for newstyle gettext translations which result in a nicer in-template user interface and more consistent catalogs. - (:ref:`newstyle-gettext`) - It's now possible to register extensions after an environment was created. @@ -479,13 +632,13 @@ Released 2010-04-13, codename Correlation through a template object if it was passed to it. This makes it possible to import or extend from a template object that was passed to the template. -- Added a :class:`ModuleLoader` that can load templates from +- Added a ``ModuleLoader`` that can load templates from precompiled sources. The environment now features a method to compile the templates from a configured loader into a zip file or folder. - The _speedups C extension now supports Python 3. - Added support for autoescaping toggling sections and support for - evaluation contexts (:ref:`eval-context`). + evaluation contexts. - Extensions have a priority now. @@ -524,7 +677,7 @@ Version 2.2.1 Released 2009-09-14 -- Fixes some smaller problems for Jinja2 on Jython. +- Fixes some smaller problems for Jinja on Jython. Version 2.2 @@ -587,7 +740,7 @@ Released 2008-11-23, codename Yasuzō - Added ``sort`` filter that works like ``dictsort`` but for arbitrary sequences. - Fixed a bug with empty statements in macros. -- Implemented a bytecode cache system. (:ref:`bytecode-cache`) +- Implemented a bytecode cache system. - The template context is now weakref-able - Inclusions and imports "with context" forward all variables now, not only the initial context. @@ -607,13 +760,11 @@ Released 2008-07-17, codename Jinjavitus from slightly. It's now possible to give attributes or items a higher priority by either using dot-notation lookup or the bracket syntax. This also changed the AST slightly. ``Subscript`` is gone - and was replaced with :class:`~jinja2.nodes.Getitem` and - :class:`~jinja2.nodes.Getattr`. For more information see :ref:`the - implementation details <notes-on-subscriptions>`. + and was replaced with ``Getitem`` and ``Getattr``. - Added support for preprocessing and token stream filtering for extensions. This would allow extensions to allow simplified gettext calls in template data and something similar. -- Added :meth:`jinja2.environment.TemplateStream.dump`. +- Added ``TemplateStream.dump``. - Added missing support for implicit string literal concatenation. ``{{ "foo" "bar" }}`` is equivalent to ``{{ "foobar" }}`` - ``else`` is optional for conditional expressions. If not given it @@ -630,4 +781,4 @@ Version 2.0rc1 Released 2008-06-09 -- First release of Jinja2 +- First release of Jinja 2. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f4ba197 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at report@palletsprojects.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/MANIFEST.in b/MANIFEST.in index 8cae0c7..909102a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,9 @@ -include CHANGES.rst tox.ini +include CHANGES.rst +include tox.ini graft artwork graft docs prune docs/_build graft examples graft ext graft tests +global-exclude *.pyc diff --git a/docs/api.rst b/docs/api.rst index 7faf08a..9d901d8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,16 +2,18 @@ API === .. module:: jinja2 - :synopsis: public Jinja2 API + :noindex: + :synopsis: public Jinja API -This document describes the API to Jinja2 and not the template language. It -will be most useful as reference to those implementing the template interface -to the application and not those who are creating Jinja2 templates. +This document describes the API to Jinja and not the template language +(for that, see :doc:`/templates`). It will be most useful as reference +to those implementing the template interface to the application and not +those who are creating Jinja templates. Basics ------ -Jinja2 uses a central object called the template :class:`Environment`. +Jinja uses a central object called the template :class:`Environment`. Instances of this class are used to store the configuration and global objects, and are used to load templates from the file system or other locations. Even if you are creating templates from strings by using the constructor of @@ -23,7 +25,7 @@ initialization and use that to load templates. In some cases however, it's useful to have multiple environments side by side, if different configurations are in use. -The simplest way to configure Jinja2 to load templates for your application +The simplest way to configure Jinja to load templates for your application looks roughly like this:: from jinja2 import Environment, PackageLoader, select_autoescape @@ -36,8 +38,8 @@ This will create a template environment with the default settings and a loader that looks up the templates in the `templates` folder inside the `yourapplication` python package. Different loaders are available and you can also write your own if you want to load templates from a -database or other resources. This also enables :ref:`autoescaping` for -HTML and XML files. +database or other resources. This also enables autoescaping for HTML and +XML files. To load a template from this environment you just have to call the :meth:`get_template` method which then returns the loaded :class:`Template`:: @@ -52,11 +54,17 @@ Using a template loader rather than passing strings to :class:`Template` or :meth:`Environment.from_string` has multiple advantages. Besides being a lot easier to use it also enables template inheritance. +.. admonition:: Notes on Autoescaping + + In future versions of Jinja we might enable autoescaping by default + for security reasons. As such you are encouraged to explicitly + configure autoescaping now instead of relying on the default. + Unicode ------- -Jinja2 is using Unicode internally which means that you have to pass Unicode +Jinja is using Unicode internally which means that you have to pass Unicode objects to the render function or bytestrings that only consist of ASCII characters. Additionally newlines are normalized to one end of line sequence which is per default UNIX style (``\n``). @@ -81,24 +89,24 @@ second line of the Python module using the Unicode literal:: We recommend utf-8 as Encoding for Python modules and templates as it's possible to represent every Unicode character in utf-8 and because it's -backwards compatible to ASCII. For Jinja2 the default encoding of templates +backwards compatible to ASCII. For Jinja the default encoding of templates is assumed to be utf-8. -It is not possible to use Jinja2 to process non-Unicode data. The reason -for this is that Jinja2 uses Unicode already on the language level. For -example Jinja2 treats the non-breaking space as valid whitespace inside +It is not possible to use Jinja to process non-Unicode data. The reason +for this is that Jinja uses Unicode already on the language level. For +example Jinja treats the non-breaking space as valid whitespace inside expressions which requires knowledge of the encoding or operating on an Unicode string. For more details about Unicode in Python have a look at the excellent `Unicode documentation`_. -Another important thing is how Jinja2 is handling string literals in +Another important thing is how Jinja is handling string literals in templates. A naive implementation would be using Unicode strings for all string literals but it turned out in the past that this is problematic as some libraries are typechecking against `str` explicitly. For example `datetime.strftime` does not accept Unicode arguments. To not break it -completely Jinja2 is returning `str` for strings that fit into ASCII and +completely Jinja is returning `str` for strings that fit into ASCII and for everything else `unicode`: >>> m = Template(u"{% set a, b = 'foo', 'föö' %}").module @@ -114,8 +122,8 @@ High Level API -------------- The high-level API is the API you will use in the application to load and -render Jinja2 templates. The :ref:`low-level-api` on the other side is only -useful if you want to dig deeper into Jinja2 or :ref:`develop extensions +render Jinja templates. The :ref:`low-level-api` on the other side is only +useful if you want to dig deeper into Jinja or :ref:`develop extensions <jinja-extensions>`. .. autoclass:: Environment([options]) @@ -248,37 +256,44 @@ useful if you want to dig deeper into Jinja2 or :ref:`develop extensions :members: disable_buffering, enable_buffering, dump -.. _autoescaping: - Autoescaping ------------ -Autoescaping is a feature to mitigate injection attacks when rendering -HTML and XML documents. When autoescaping is enabled, Jinja will use -`MarkupSafe`_ to escape unsafe characters in the output of expressions, -unless the output is marked safe. For example, if a comment contained -``<script>alert("hello")</script>``, the tags would be rendered with -escapes like ``<script>``. In a user's browser, the comment would -display as text, rather than being interpreted as a script. - -Because Jinja can be used to render any type of document for many types -of applications, not just HTML with untrusted input, autoescaping is not -enabled by default. You should configure a sensible default based on -your use case when creating the environment. The -:func:`~jinja2.select_autoescape` function can be used to enable -autoescaping for HTML templates while disabling it in text templates. +.. versionchanged:: 2.4 + +Jinja now comes with autoescaping support. As of Jinja 2.9 the +autoescape extension is removed and built-in. However autoescaping is +not yet enabled by default though this will most likely change in the +future. It's recommended to configure a sensible default for +autoescaping. This makes it possible to enable and disable autoescaping +on a per-template basis (HTML versus text for instance). .. autofunction:: jinja2.select_autoescape -You can also pass ``autoescape=True`` to enable it unconditionally, or -pass your own function that takes the name of the template and returns -whether it should be enabled. When writing a function, make sure to -accept ``None`` as a name, as that will be passed for string templates. +Here a recommended setup that enables autoescaping for templates ending +in ``'.html'``, ``'.htm'`` and ``'.xml'`` and disabling it by default +for all other extensions. You can use the :func:`~jinja2.select_autoescape` +function for this:: -Inside a template the behaviour can be temporarily changed by using -the ``autoescape`` block, see :ref:`autoescape-overrides`. + from jinja2 import Environment, PackageLoader, select_autoescape + env = Environment(autoescape=select_autoescape(['html', 'htm', 'xml']), + loader=PackageLoader('mypackage')) -.. _MarkupSafe: https://markupsafe.palletsprojects.com/ +The :func:`~jinja.select_autoescape` function returns a function that +works roughly like this:: + + def autoescape(template_name): + if template_name is None: + return False + if template_name.endswith(('.html', '.htm', '.xml')) + +When implementing a guessing autoescape function, make sure you also +accept `None` as valid template name. This will be passed when generating +templates from strings. You should always configure autoescaping as +defaults in the future might change. + +Inside the templates the behaviour can be temporarily changed by using +the `autoescape` block (see :ref:`autoescape-overrides`). .. _identifier-naming: @@ -286,10 +301,8 @@ the ``autoescape`` block, see :ref:`autoescape-overrides`. Notes on Identifiers -------------------- -Jinja2 uses the regular Python 2.x naming rules. Valid identifiers have to -match ``[a-zA-Z_][a-zA-Z0-9_]*``. As a matter of fact non ASCII characters -are currently not allowed. This limitation will probably go away as soon as -unicode identifiers are fully specified for Python 3. +Jinja uses Python naming rules. Valid identifiers can be any combination +of Unicode characters accepted by Python. Filters and tests are looked up in separate namespaces and have slightly modified identifier syntax. Filters and tests may contain dots to group @@ -309,7 +322,7 @@ unable to look up a name or access an attribute one of those objects is created and returned. Some operations on undefined values are then allowed, others fail. -The closest to regular Python behavior is the `StrictUndefined` which +The closest to regular Python behavior is the :class:`StrictUndefined` which disallows all operations beside testing if it's an undefined object. .. autoclass:: jinja2.Undefined() @@ -340,6 +353,8 @@ disallows all operations beside testing if it's an undefined object. :attr:`_undefined_exception` with an error message generated from the undefined hints stored on the undefined object. +.. autoclass:: jinja2.ChainableUndefined() + .. autoclass:: jinja2.DebugUndefined() .. autoclass:: jinja2.StrictUndefined() @@ -367,7 +382,7 @@ Undefined objects are created by calling :attr:`undefined`. To disallow a method, just override it and raise :attr:`~Undefined._undefined_exception`. Because this is a very common - idom in undefined objects there is the helper method + idiom in undefined objects there is the helper method :meth:`~Undefined._fail_with_undefined_error` that does the error raising automatically. Here a class that works like the regular :class:`Undefined` but chokes on iteration:: @@ -429,11 +444,11 @@ The Context .. admonition:: Implementation Context is immutable for the same reason Python's frame locals are - immutable inside functions. Both Jinja2 and Python are not using the + 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. - When a template accesses a variable the template does not define, Jinja2 + 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. @@ -453,7 +468,7 @@ own loader, subclass :class:`BaseLoader` and override `get_source`. .. autoclass:: jinja2.BaseLoader :members: get_source, load -Here a list of the builtin loaders Jinja2 provides: +Here a list of the builtin loaders Jinja provides: .. autoclass:: jinja2.FileSystemLoader @@ -515,37 +530,38 @@ Builtin bytecode caches: Async Support ------------- -Starting with version 2.9, Jinja2 also supports the Python `async` and -`await` constructs. As far as template designers go this feature is -entirely opaque to them however as a developer you should be aware of how -it's implemented as it influences what type of APIs you can safely expose -to the template environment. - -First you need to be aware that by default async support is disabled as -enabling it will generate different template code behind the scenes which -passes everything through the asyncio event loop. This is important to -understand because it has some impact to what you are doing: - -* template rendering will require an event loop to be set for the - current thread (``asyncio.get_event_loop`` needs to return one) -* all template generation code internally runs async generators which - means that you will pay a performance penalty even if the non sync - methods are used! -* The sync methods are based on async methods if the async mode is - enabled which means that `render` for instance will internally invoke - `render_async` and run it as part of the current event loop until the - execution finished. +.. versionadded:: 2.9 + +Jinja supports the Python ``async`` and ``await`` syntax. For the +template designer, this support (when enabled) is entirely transparent, +templates continue to look exactly the same. However, developers should +be aware of the implementation as it affects what types of APIs you can +use. + +By default, async support is disabled. Enabling it will cause the +environment to compile different code behind the scenes in order to +handle async and sync code in an asyncio event loop. This has the +following implications: + +- Template rendering requires an event loop to be available to the + current thread. :func:`asyncio.get_event_loop` must return an event + loop. +- The compiled code uses ``await`` for functions and attributes, and + uses ``async for`` loops. In order to support using both async and + sync functions in this context, a small wrapper is placed around + all calls and access, which add overhead compared to purely async + code. +- Sync methods and filters become wrappers around their corresponding + async implementations where needed. For example, ``render`` invokes + ``async_render``, and ``|map`` supports async iterables. Awaitable objects can be returned from functions in templates and any -function call in a template will automatically await the result. This -means that you can let provide a method that asynchronously loads data -from a database if you so desire and from the template designer's point of -view this is just another function they can call. This means that the -``await`` you would normally issue in Python is implied. However this -only applies to function calls. If an attribute for instance would be an -avaitable object then this would not result in the expected behavior. +function call in a template will automatically await the result. The +``await`` you would normally add in Python is implied. For example, you +can provide a method that asynchronously loads data from a database, and +from the template designer's point of view it can be called like any +other function. -Likewise iterations with a `for` loop support async iterators. .. _policies: @@ -562,7 +578,7 @@ Example:: env.policies['urlize.rel'] = 'nofollow noopener' ``compiler.ascii_str``: - This boolean controls on Python 2 if Jinja2 should store ASCII only + This boolean controls on Python 2 if Jinja should store ASCII only literals as bytestring instead of unicode strings. This used to be always enabled for Jinja versions below 2.9 and now can be changed. Traditionally it was done this way since some APIs in Python 2 failed @@ -610,7 +626,7 @@ Utilities --------- These helper functions and classes are useful if you add custom filters or -functions to a Jinja2 environment. +functions to a Jinja environment. .. autofunction:: jinja2.environmentfilter @@ -624,15 +640,29 @@ functions to a Jinja2 environment. .. autofunction:: jinja2.evalcontextfunction +.. function:: escape(s) + + Convert the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in string `s` + to HTML-safe sequences. Use this if you need to display text that might + contain such characters in HTML. This function will not escaped objects + that do have an HTML representation such as already escaped data. + + The return value is a :class:`Markup` string. + .. autofunction:: jinja2.clear_caches .. autofunction:: jinja2.is_undefined -.. autofunction:: markupsafe.escape - -.. autoclass:: markupsafe.Markup +.. autoclass:: jinja2.Markup([string]) :members: escape, unescape, striptags +.. admonition:: Note + + The Jinja :class:`Markup` class is compatible with at least Pylons and + Genshi. It's expected that more template engines and framework will pick + up the `__html__` concept soon. + + Exceptions ---------- @@ -711,10 +741,9 @@ paragraphs and marks the return value as safe HTML string if autoescaping is enabled:: import re - from jinja2 import evalcontextfilter - from markupsafe import Markup, escape + from jinja2 import evalcontextfilter, Markup, escape - _paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}') + _paragraph_re = re.compile(r'(?:\r\n|\r(?!\n)|\n){2,}') @evalcontextfilter def nl2br(eval_ctx, value): @@ -886,9 +915,9 @@ don't recommend using any of those. .. admonition:: Note - The low-level API is fragile. Future Jinja2 versions will try not to - change it in a backwards incompatible way but modifications in the Jinja2 - core may shine through. For example if Jinja2 introduces a new AST node + The low-level API is fragile. Future Jinja versions will try not to + change it in a backwards incompatible way but modifications in the Jinja + core may shine through. For example if Jinja introduces a new AST node in later versions that may be returned by :meth:`~Environment.parse`. The Meta API diff --git a/docs/cache_extension.py b/docs/examples/cache_extension.py index 992b595..387cd46 100644 --- a/docs/cache_extension.py +++ b/docs/examples/cache_extension.py @@ -4,16 +4,13 @@ from jinja2.ext import Extension class FragmentCacheExtension(Extension): # a set of names that trigger the extension. - tags = {'cache'} + tags = {"cache"} def __init__(self, environment): super(FragmentCacheExtension, self).__init__(environment) # add the defaults to the environment - environment.extend( - fragment_cache_prefix='', - fragment_cache=None - ) + environment.extend(fragment_cache_prefix="", fragment_cache=None) def parse(self, parser): # the first token is the token that started the tag. In our case @@ -27,19 +24,20 @@ class FragmentCacheExtension(Extension): # if there is a comma, the user provided a timeout. If not use # None as second parameter. - if parser.stream.skip_if('comma'): + if parser.stream.skip_if("comma"): args.append(parser.parse_expression()) else: args.append(nodes.Const(None)) # now we parse the body of the cache block up to `endcache` and # drop the needle (which would always be `endcache` in that case) - body = parser.parse_statements(['name:endcache'], drop_needle=True) + body = parser.parse_statements(["name:endcache"], drop_needle=True) # now return a `CallBlock` node that calls our _cache_support # helper method on this extension. - return nodes.CallBlock(self.call_method('_cache_support', args), - [], [], body).set_lineno(lineno) + return nodes.CallBlock( + self.call_method("_cache_support", args), [], [], body + ).set_lineno(lineno) def _cache_support(self, name, timeout, caller): """Helper callback.""" diff --git a/docs/examples/inline_gettext_extension.py b/docs/examples/inline_gettext_extension.py new file mode 100644 index 0000000..47bc9cc --- /dev/null +++ b/docs/examples/inline_gettext_extension.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +import re + +from jinja2.exceptions import TemplateSyntaxError +from jinja2.ext import Extension +from jinja2.lexer import count_newlines +from jinja2.lexer import Token + + +_outside_re = re.compile(r"\\?(gettext|_)\(") +_inside_re = re.compile(r"\\?[()]") + + +class InlineGettext(Extension): + """This extension implements support for inline gettext blocks:: + + <h1>_(Welcome)</h1> + <p>_(This is a paragraph)</p> + + Requires the i18n extension to be loaded and configured. + """ + + def filter_stream(self, stream): + paren_stack = 0 + + for token in stream: + if token.type != "data": + yield token + continue + + pos = 0 + lineno = token.lineno + + while 1: + if not paren_stack: + match = _outside_re.search(token.value, pos) + else: + match = _inside_re.search(token.value, pos) + if match is None: + break + new_pos = match.start() + if new_pos > pos: + preval = token.value[pos:new_pos] + yield Token(lineno, "data", preval) + lineno += count_newlines(preval) + gtok = match.group() + if gtok[0] == "\\": + yield Token(lineno, "data", gtok[1:]) + elif not paren_stack: + yield Token(lineno, "block_begin", None) + yield Token(lineno, "name", "trans") + yield Token(lineno, "block_end", None) + paren_stack = 1 + else: + if gtok == "(" or paren_stack > 1: + yield Token(lineno, "data", gtok) + paren_stack += gtok == ")" and -1 or 1 + if not paren_stack: + yield Token(lineno, "block_begin", None) + yield Token(lineno, "name", "endtrans") + yield Token(lineno, "block_end", None) + pos = match.end() + + if pos < len(token.value): + yield Token(lineno, "data", token.value[pos:]) + + if paren_stack: + raise TemplateSyntaxError( + "unclosed gettext expression", + token.lineno, + stream.name, + stream.filename, + ) diff --git a/docs/extensions.rst b/docs/extensions.rst index fde2ae4..7abed65 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -3,7 +3,7 @@ Extensions ========== -Jinja2 supports extensions that can add extra filters, tests, globals or even +Jinja supports extensions that can add extra filters, tests, globals or even extend the parser. The main motivation of extensions is to move often used code into a reusable class like adding support for internationalization. @@ -11,11 +11,11 @@ code into a reusable class like adding support for internationalization. Adding Extensions ----------------- -Extensions are added to the Jinja2 environment at creation time. Once the +Extensions are added to the Jinja environment at creation time. Once the environment is created additional extensions cannot be added. To add an extension pass a list of extension classes or import paths to the -`extensions` parameter of the :class:`Environment` constructor. The following -example creates a Jinja2 environment with the i18n extension loaded:: +``extensions`` parameter of the :class:`~jinja2.Environment` constructor. The following +example creates a Jinja environment with the i18n extension loaded:: jinja_env = Environment(extensions=['jinja2.ext.i18n']) @@ -25,16 +25,15 @@ example creates a Jinja2 environment with the i18n extension loaded:: i18n Extension -------------- -**Import name:** `jinja2.ext.i18n` +**Import name:** ``jinja2.ext.i18n`` -The i18n extension can be used in combination with `gettext`_ or `babel`_. If -the i18n extension is enabled Jinja2 provides a `trans` statement that marks -the wrapped string as translatable and calls `gettext`. +The i18n extension can be used in combination with `gettext`_ or +`Babel`_. When it's enabled, Jinja provides a ``trans`` statement that +marks a block as translatable and calls ``gettext``. -After enabling, dummy `_` function that forwards calls to `gettext` is added -to the environment globals. An internationalized application then has to -provide a `gettext` function and optionally an `ngettext` function into the -namespace, either globally or for each rendering. +After enabling, an application has to provide ``gettext`` and +``ngettext`` functions, either globally or when rendering. A ``_()`` +function is added as an alias to the ``gettext`` function. Environment Methods ~~~~~~~~~~~~~~~~~~~ @@ -44,36 +43,37 @@ additional methods: .. method:: jinja2.Environment.install_gettext_translations(translations, newstyle=False) - Installs a translation globally for that environment. The translations - object provided must implement at least `ugettext` and `ungettext`. - The `gettext.NullTranslations` and `gettext.GNUTranslations` classes - as well as `Babel`_\s `Translations` class are supported. + Installs a translation globally for the environment. The + ``translations`` object must implement ``gettext`` and ``ngettext`` + (or ``ugettext`` and ``ungettext`` for Python 2). + :class:`gettext.NullTranslations`, :class:`gettext.GNUTranslations`, + and `Babel`_\s ``Translations`` are supported. - .. versionchanged:: 2.5 newstyle gettext added + .. versionchanged:: 2.5 Added new-style gettext support. .. method:: jinja2.Environment.install_null_translations(newstyle=False) - Install dummy gettext functions. This is useful if you want to prepare - the application for internationalization but don't want to implement the - full internationalization system yet. + Install no-op gettext functions. This is useful if you want to + prepare the application for internationalization but don't want to + implement the full system yet. - .. versionchanged:: 2.5 newstyle gettext added + .. versionchanged:: 2.5 Added new-style gettext support. .. method:: jinja2.Environment.install_gettext_callables(gettext, ngettext, newstyle=False) - Installs the given `gettext` and `ngettext` callables into the - environment as globals. They are supposed to behave exactly like the - standard library's :func:`gettext.ugettext` and - :func:`gettext.ungettext` functions. + Install the given ``gettext`` and ``ngettext`` callables into the + environment. They should behave exactly like + :func:`gettext.gettext` and :func:`gettext.ngettext` (or + ``ugettext`` and ``ungettext`` for Python 2). - If `newstyle` is activated, the callables are wrapped to work like + If ``newstyle`` is activated, the callables are wrapped to work like newstyle callables. See :ref:`newstyle-gettext` for more information. - .. versionadded:: 2.5 + .. versionadded:: 2.5 Added new-style gettext support. .. method:: jinja2.Environment.uninstall_gettext_translations() - Uninstall the translations again. + Uninstall the environment's globally installed translation. .. method:: jinja2.Environment.extract_translations(source) @@ -82,95 +82,109 @@ additional methods: For every string found this function yields a ``(lineno, function, message)`` tuple, where: - * `lineno` is the number of the line on which the string was found, - * `function` is the name of the `gettext` function used (if the - string was extracted from embedded Python code), and - * `message` is the string itself (a `unicode` object, or a tuple - of `unicode` objects for functions with multiple string arguments). + - ``lineno`` is the number of the line on which the string was + found. + - ``function`` is the name of the ``gettext`` function used (if + the string was extracted from embedded Python code). + - ``message`` is the string itself (``unicode`` on Python 2), or a + tuple of strings for functions with multiple arguments. - If `Babel`_ is installed, :ref:`the babel integration <babel-integration>` - can be used to extract strings for babel. + If `Babel`_ is installed, see :ref:`babel-integration` to extract + the strings. -For a web application that is available in multiple languages but gives all -the users the same language (for example a multilingual forum software -installed for a French community) may load the translations once and add the -translation methods to the environment at environment generation time:: +For a web application that is available in multiple languages but gives +all the users the same language (for example, multilingual forum +software installed for a French community), the translation may be +installed when the environment is created. + +.. code-block:: python translations = get_gettext_translations() - env = Environment(extensions=['jinja2.ext.i18n']) + env = Environment(extensions=["jinja2.ext.i18n"]) env.install_gettext_translations(translations) -The `get_gettext_translations` function would return the translator for the -current configuration. (For example by using `gettext.find`) +The ``get_gettext_translations`` function would return the translator +for the current configuration, for example by using ``gettext.find``. -The usage of the `i18n` extension for template designers is covered as part -:ref:`of the template documentation <i18n-in-templates>`. +The usage of the ``i18n`` extension for template designers is covered in +:ref:`the template documentation <i18n-in-templates>`. .. _gettext: https://docs.python.org/3/library/gettext.html .. _Babel: http://babel.pocoo.org/ -.. _newstyle-gettext: Whitespace Trimming ~~~~~~~~~~~~~~~~~~~ .. versionadded:: 2.10 -Linebreaks and surrounding whitespace can be automatically trimmed by enabling -the ``ext.i18n.trimmed`` :ref:`policy <ext-i18n-trimmed>`. +Within ``{% trans %}`` blocks, it can be useful to trim line breaks and +whitespace so that the block of text looks like a simple string with +single spaces in the translation file. + +Linebreaks and surrounding whitespace can be automatically trimmed by +enabling the ``ext.i18n.trimmed`` :ref:`policy <ext-i18n-trimmed>`. + +.. _newstyle-gettext: -Newstyle Gettext -~~~~~~~~~~~~~~~~ +New Style Gettext +~~~~~~~~~~~~~~~~~ .. versionadded:: 2.5 -Starting with version 2.5 you can use newstyle gettext calls. These are -inspired by trac's internal gettext functions and are fully supported by -the babel extraction tool. They might not work as expected by other -extraction tools in case you are not using Babel's. +New style gettext calls are less to type, less error prone, and support +autoescaping better. -What's the big difference between standard and newstyle gettext calls? In -general they are less to type and less error prone. Also if they are used -in an :ref:`autoescaping` environment they better support automatic escaping. -Here are some common differences between old and new calls: +You can use "new style" gettext calls by setting +``env.newstyle_gettext = True`` or passing ``newstyle=True`` to +``env.install_translations``. They are fully supported by the Babel +extraction tool, but might not work as expected with other extraction +tools. -standard gettext: +With standard ``gettext`` calls, string formatting is a separate step +done with the ``|format`` filter. This requires duplicating work for +``ngettext`` calls. -.. sourcecode:: html+jinja +.. sourcecode:: jinja - {{ gettext('Hello World!') }} - {{ gettext('Hello %(name)s!')|format(name='World') }} - {{ ngettext('%(num)d apple', '%(num)d apples', apples|count)|format( - num=apples|count - )}} + {{ gettext("Hello, World!") }} + {{ gettext("Hello, %(name)s!")|format(name=name) }} + {{ ngettext( + "%(num)d apple", "%(num)d apples", apples|count + )|format(num=apples|count) }} -newstyle gettext looks like this instead: +New style ``gettext`` make formatting part of the call, and behind the +scenes enforce more consistency. -.. sourcecode:: html+jinja +.. sourcecode:: jinja - {{ gettext('Hello World!') }} - {{ gettext('Hello %(name)s!', name='World') }} - {{ ngettext('%(num)d apple', '%(num)d apples', apples|count) }} + {{ gettext("Hello, World!") }} + {{ gettext("Hello, %(name)s!", name=name) }} + {{ ngettext("%(num)d apple", "%(num)d apples", apples|count) }} -The advantages of newstyle gettext are that you have less to type and that -named placeholders become mandatory. The latter sounds like a -disadvantage but solves a lot of troubles translators are often facing -when they are unable to switch the positions of two placeholder. With -newstyle gettext, all format strings look the same. +The advantages of newstyle gettext are: + +- There's no separate formatting step, you don't have to remember to + use the ``|format`` filter. +- Only named placeholders are allowed. This solves a common problem + translators face because positional placeholders can't switch + positions meaningfully. Named placeholders always carry semantic + information about what value goes where. +- 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. -Furthermore with newstyle gettext, string formatting is also used if no -placeholders are used which makes all strings behave exactly the same. -Last but not least are newstyle gettext calls able to properly mark -strings for autoescaping which solves lots of escaping related issues many -templates are experiencing over time when using autoescaping. Expression Statement -------------------- -**Import name:** `jinja2.ext.do` +**Import name:** ``jinja2.ext.do`` -The "do" aka expression-statement extension adds a simple `do` tag to the +The "do" aka expression-statement extension adds a simple ``do`` tag to the template engine that works like a variable expression but ignores the return value. @@ -179,10 +193,10 @@ return value. Loop Controls ------------- -**Import name:** `jinja2.ext.loopcontrols` +**Import name:** ``jinja2.ext.loopcontrols`` -This extension adds support for `break` and `continue` in loops. After -enabling, Jinja2 provides those two keywords which work exactly like in +This extension adds support for ``break`` and ``continue`` in loops. After +enabling, Jinja provides those two keywords which work exactly like in Python. .. _with-extension: @@ -190,23 +204,35 @@ Python. With Statement -------------- -**Import name:** `jinja2.ext.with_` +**Import name:** ``jinja2.ext.with_`` .. versionchanged:: 2.9 -This extension is now built-in and no longer does anything. + This extension is now built-in and no longer does anything. .. _autoescape-extension: Autoescape Extension -------------------- -**Import name:** `jinja2.ext.autoescape` +**Import name:** ``jinja2.ext.autoescape`` .. versionchanged:: 2.9 -This extension was removed and is now built-in. Enabling the extension -no longer does anything. + This extension was removed and is now built-in. Enabling the + extension no longer does anything. + + +.. _debug-extension: + +Debug Extension +--------------- + +**Import name:** ``jinja2.ext.debug`` + +Adds a ``{% debug %}`` tag to dump the current context as well as the +available filters and tests. This is useful to see what's available to +use in the template without setting up a debugger. .. _writing-extensions: @@ -216,25 +242,29 @@ Writing Extensions .. module:: jinja2.ext -By writing extensions you can add custom tags to Jinja2. This is a non-trivial +By writing extensions you can add custom tags to Jinja. This is a non-trivial task and usually not needed as the default tags and expressions cover all common use cases. The i18n extension is a good example of why extensions are useful. Another one would be fragment caching. When writing extensions you have to keep in mind that you are working with the -Jinja2 template compiler which does not validate the node tree you are passing +Jinja template compiler which does not validate the node tree you are passing to it. If the AST is malformed you will get all kinds of compiler or runtime errors that are horrible to debug. Always make sure you are using the nodes you create correctly. The API documentation below shows which nodes exist and how to use them. -Example Extension -~~~~~~~~~~~~~~~~~ -The following example implements a `cache` tag for Jinja2 by using the +Example Extensions +------------------ + +Cache +~~~~~ + +The following example implements a ``cache`` tag for Jinja by using the `cachelib`_ library: -.. literalinclude:: cache_extension.py +.. literalinclude:: examples/cache_extension.py :language: python And here is how you use it in an environment:: @@ -258,8 +288,30 @@ following example caches a sidebar for 300 seconds: .. _cachelib: https://github.com/pallets/cachelib + +Inline ``gettext`` +~~~~~~~~~~~~~~~~~~ + +The following example demonstrates using :meth:`Extension.filter_stream` +to parse calls to the ``_()`` gettext function inline with static data +without needing Jinja blocks. + +.. code-block:: html + + <h1>_(Welcome)</h1> + <p>_(This is a paragraph)</p> + +It requires the i18n extension to be loaded and configured. + +.. literalinclude:: examples/inline_gettext_extension.py + :language: python + + Extension API -~~~~~~~~~~~~~ +------------- + +Extension +~~~~~~~~~ Extensions always have to extend the :class:`jinja2.ext.Extension` class: @@ -276,8 +328,9 @@ Extensions always have to extend the :class:`jinja2.ext.Extension` class: If the extension implements custom tags this is a set of tag names the extension is listening for. -Parser API -~~~~~~~~~~ + +Parser +~~~~~~ The parser passed to :meth:`Extension.parse` provides ways to parse expressions of different types. The following methods may be used by @@ -292,7 +345,7 @@ extensions: The filename of the template the parser processes. This is **not** the load name of the template. For the load name see :attr:`name`. For templates that were not loaded form the file system this is - `None`. + ``None``. .. attribute:: name @@ -319,7 +372,7 @@ extensions: .. attribute:: type The type of the token. This string is interned so you may compare - it with arbitrary strings using the `is` operator. + it with arbitrary strings using the ``is`` operator. .. attribute:: value @@ -330,6 +383,7 @@ characters in strings: .. autofunction:: jinja2.lexer.count_newlines + AST ~~~ @@ -339,7 +393,7 @@ code objects. Extensions that provide custom statements can return nodes to execute custom Python code. The list below describes all nodes that are currently available. The AST may -change between Jinja2 versions but will stay backwards compatible. +change between Jinja versions but will stay backwards compatible. For more information have a look at the repr of :meth:`jinja2.Environment.parse`. diff --git a/docs/faq.rst b/docs/faq.rst index 73d4659..3d38e10 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -18,7 +18,7 @@ How fast is it? 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 Jinja2 has a similar performance to `Mako`_ and is between +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 @@ -28,12 +28,12 @@ code. .. _Mako: https://www.makotemplates.org/ -How Compatible is Jinja2 with Django? -------------------------------------- +How Compatible is Jinja with Django? +------------------------------------ -The default syntax of Jinja2 matches Django syntax in many ways. However +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 Jinja2. For example filter arguments use a function call syntax rather +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. @@ -80,7 +80,7 @@ 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. -Jinja2 neither allows you to put arbitrary Python code into templates nor +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 @@ -106,7 +106,7 @@ 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 Jinja2 is a general purpose template engine and not only used +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. @@ -130,7 +130,7 @@ My tracebacks look weird. What's happening? If the debugsupport module is not compiled and you are using a Python installation without ctypes (Python 2.4 without ctypes, Jython or Google's -AppEngine) Jinja2 is unable to provide correct debugging information and +AppEngine) Jinja is unable to provide correct debugging information and the traceback may be incomplete. There is currently no good workaround for Jython or the AppEngine as ctypes is unavailable there and it's not possible to use the debugsupport extension. @@ -147,10 +147,10 @@ work in production environments:: Credit for this snippet goes to `Thomas Johansson <https://stackoverflow.com/questions/3086091/debug-jinja2-in-google-app-engine/3694434#3694434>`_ -Why is there no Python 2.3/2.4/2.5/3.1/3.2 support? ---------------------------------------------------- +Why is there no Python 2.3/2.4/2.5/2.6/3.1/3.2/3.3 support? +----------------------------------------------------------- -Python 2.3 is missing a lot of features that are used heavily in Jinja2. This +Python 2.3 is missing a lot of features that are used heavily in Jinja. This decision was made as with the upcoming Python 2.6 and 3.0 versions it becomes harder to maintain the code for older Python versions. If you really need Python 2.3 support you either have to use Jinja 1 or other templating @@ -160,7 +160,11 @@ Python 2.4/2.5/3.1/3.2 support was removed when we switched to supporting Python 2 and 3 by the same sourcecode (without using 2to3). It was required to drop support because only Python 2.6/2.7 and >=3.3 support byte and unicode literals in a way compatible to each other version. If you really need support -for older Python 2 (or 3) versions, you can just use Jinja2 2.6. +for older Python 2 (or 3) versions, you can just use Jinja 2.6. + +Python 2.6/3.3 support was dropped because it got dropped in various upstream +projects (such as wheel or pytest), which would make it difficult to continue +supporting it. Jinja 2.10 was the last version supporting Python 2.6/3.3. My Macros are overridden by something ------------------------------------- @@ -182,7 +186,7 @@ child.tmpl: {% macro foo() %}CHILD{% endmacro %} {% block body %}{{ foo() }}{% endblock %} -This will print ``LAYOUT`` in Jinja2. This is a side effect of having +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 diff --git a/docs/integration.rst b/docs/integration.rst index e84e579..2cfad55 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -1,7 +1,7 @@ Integration =========== -Jinja2 provides some code for integration into other tools such as frameworks, +Jinja provides some code for integration into other tools such as frameworks, the `Babel`_ library or your favourite editor for fancy code highlighting. This is a brief description of whats included. @@ -19,7 +19,7 @@ support is implemented as part of the :ref:`i18n-extension` extension. Gettext messages extracted from both `trans` tags and code expressions. -To extract gettext messages from templates, the project needs a Jinja2 section +To extract gettext messages from templates, the project needs a Jinja section in its Babel extraction method `mapping file`_: .. sourcecode:: ini @@ -59,7 +59,7 @@ With `Pylons`_ 0.9.7 onwards it's incredible easy to integrate Jinja into a Pylons powered application. The template engine is configured in `config/environment.py`. The configuration -for Jinja2 looks something like that:: +for Jinja looks something like that:: from jinja2 import Environment, PackageLoader config['pylons.app_globals'].jinja_env = Environment( @@ -81,21 +81,26 @@ snippet and add it into your `config/environment.py`:: TextMate -------- -There is a `bundle for TextMate`_ that supports syntax highlighting for Jinja1 and Jinja2 for text based -templates as well as HTML. It also contains a few often used snippets. +There is a `bundle for TextMate`_ that supports syntax highlighting for Jinja 1 +and Jinja 2 for text based templates as well as HTML. It also contains a few +often used snippets. .. _bundle for TextMate: https://github.com/mitsuhiko/jinja2-tmbundle Vim --- -A syntax plugin for `Vim`_ exists in the Vim-scripts directory as well as the -`ext` folder at the root of the Jinja2 project. `The script -<https://www.vim.org/scripts/script.php?script_id=1856>`_ supports Jinja1 and -Jinja2. Once installed two file types are available `jinja` and `htmljinja`. -The first one for text based templates, the latter for HTML templates. +A syntax plugin for `Vim`_ is available `from the jinja repository +<https://github.com/pallets/jinja/blob/master/ext/Vim/jinja.vim>`_. The script +supports Jinja 1 and Jinja 2. Once installed, two file types are available +(``jinja`` and ``htmljinja``). The first one is for text-based templates and the +second is for HTML templates. For HTML documents, the plugin attempts to +automatically detect Jinja syntax inside of existing HTML documents. -Copy the files into your `syntax` folder. +If you are using a plugin manager like `Pathogen`_, see the `vim-jinja +<https://github.com/mitsuhiko/vim-jinja>`_ repository for installing in the +``bundle/`` directory. .. _Babel: http://babel.pocoo.org/ .. _Vim: https://www.vim.org/ +.. _Pathogen: https://github.com/tpope/vim-pathogen diff --git a/docs/intro.rst b/docs/intro.rst index 90636f5..c20c5e9 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -1,20 +1,21 @@ Introduction ============ -This is the documentation for the Jinja2 general purpose templating language. -Jinja2 is a library for Python that is designed to be flexible, fast and secure. +This is the documentation for the Jinja general purpose templating language. +Jinja is a library for Python that is designed to be flexible, fast and secure. If you have any exposure to other text-based template languages, such as Smarty or -Django, you should feel right at home with Jinja2. It's both designer and +Django, you should feel right at home with Jinja. It's both designer and developer friendly by sticking to Python's principles and adding functionality useful for templating environments. Prerequisites ------------- -Jinja2 works with Python 2.6.x, 2.7.x and >= 3.3. If you are using Python -3.2 you can use an older release of Jinja2 (2.6) as support for Python 3.2 -was dropped in Jinja2 version 2.7. +Jinja works with Python 2.7.x and >= 3.5. If you are using Python +3.2 you can use an older release of Jinja (2.6) as support for Python 3.2 +was dropped in Jinja version 2.7. The last release which supported Python 2.6 +and 3.3 was Jinja 2.10. If you wish to use the :class:`~jinja2.PackageLoader` class, you will also need `setuptools`_ or `distribute`_ installed at runtime. @@ -22,31 +23,11 @@ need `setuptools`_ or `distribute`_ installed at runtime. Installation ------------ -You have multiple ways to install Jinja2. If you are unsure what to do, go -with the Python egg or tarball. +You can install the most recent Jinja version using `pip`_:: -As a Python egg (via `easy_install`) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can install the most recent Jinja2 version using `easy_install`_ or `pip`_:: - - easy_install Jinja2 pip install Jinja2 -This will install a Jinja2 egg in your Python installation's site-packages -directory. - -From the tarball release -~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Download the most recent tarball from the `download page`_ -2. Unpack the tarball -3. ``python setup.py install`` - -Note that you either have to have `setuptools` or `distribute` installed; -the latter is preferred. - -This will install Jinja2 into your Python installation's site-packages directory. +This will install Jinja in your Python installation's site-packages directory. Installing the development version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -60,10 +41,8 @@ As an alternative to steps 4 you can also do ``python setup.py develop`` which will install the package via `distribute` in development mode. This also has the advantage that the C extensions are compiled. -.. _download page: https://pypi.org/project/Jinja2/ .. _distribute: https://pypi.org/project/distribute/ .. _setuptools: https://pypi.org/project/setuptools/ -.. _easy_install: https://setuptools.readthedocs.io/en/latest/easy_install.html .. _pip: https://pypi.org/project/pip/ .. _git: https://git-scm.com/ @@ -71,16 +50,15 @@ has the advantage that the C extensions are compiled. MarkupSafe Dependency ~~~~~~~~~~~~~~~~~~~~~ -As of version 2.7 Jinja2 depends on the `MarkupSafe`_ module. If you -install Jinja2 via `pip` or `easy_install` it will be installed -automatically for you. +As of version 2.7 Jinja depends on the `MarkupSafe`_ module. If you install +Jinja via ``pip`` it will be installed automatically for you. .. _MarkupSafe: https://markupsafe.palletsprojects.com/ Basic API Usage --------------- -This section gives you a brief introduction to the Python API for Jinja2 +This section gives you a brief introduction to the Python API for Jinja templates. The most basic way to create a template and render it is through @@ -99,22 +77,6 @@ called with a dict or keyword arguments expands the template. The dict or keywords arguments passed to the template are the so-called "context" of the template. -What you can see here is that Jinja2 is using unicode internally and the +What you can see here is that Jinja is using unicode internally and the return value is an unicode string. So make sure that your application is indeed using unicode internally. - - -Experimental Python 3 Support ------------------------------ - -Jinja 2.7 brings experimental support for Python >=3.3. It means that all -unittests pass on the new version, but there might still be small bugs in -there and behavior might be inconsistent. If you notice any bugs, please -provide feedback in the `Jinja bug tracker`_. - -Also please keep in mind that the documentation is written with Python 2 -in mind, so you will have to adapt the shown code examples to Python 3 syntax -for yourself. - - -.. _Jinja bug tracker: https://github.com/pallets/jinja/issues diff --git a/docs/sandbox.rst b/docs/sandbox.rst index 71ccc0d..1222d02 100644 --- a/docs/sandbox.rst +++ b/docs/sandbox.rst @@ -1,7 +1,7 @@ Sandbox ======= -The Jinja2 sandbox can be used to evaluate untrusted code. Access to unsafe +The Jinja sandbox can be used to evaluate untrusted code. Access to unsafe attributes and methods is prohibited. Assuming `env` is a :class:`SandboxedEnvironment` in the default configuration @@ -36,7 +36,7 @@ API .. admonition:: Note - The Jinja2 sandbox alone is no solution for perfect security. Especially + 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 @@ -54,7 +54,7 @@ Operator Intercepting .. versionadded:: 2.6 -For maximum performance Jinja2 will let operators call directly the type +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 @@ -65,7 +65,7 @@ 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 Jinja2 will +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. @@ -75,7 +75,7 @@ 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 -Jinja2:: +Jinja:: from jinja2.sandbox import SandboxedEnvironment @@ -90,5 +90,5 @@ Jinja2:: operator, left, right) Make sure to always call into the super method, even if you are not -intercepting the call. Jinja2 might internally call the method to +intercepting the call. Jinja might internally call the method to evaluate expressions. diff --git a/docs/switching.rst b/docs/switching.rst index 01a7d0d..8225b2e 100644 --- a/docs/switching.rst +++ b/docs/switching.rst @@ -4,21 +4,21 @@ Switching from other Template Engines .. highlight:: html+jinja If you have used a different template engine in the past and want to switch -to Jinja2 here is a small guide that shows the basic syntactic and semantic +to Jinja here is a small guide that shows the basic syntactic and semantic changes between some common, similar text template engines for Python. -Jinja1 ------- +Jinja 1 +------- -Jinja2 is mostly compatible with Jinja1 in terms of API usage and template -syntax. The differences between Jinja1 and 2 are explained in the following +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 - Jinja2 uses a different loader API. Because the internal representation + 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 @@ -27,38 +27,38 @@ Loaders 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`. Jinja2 + 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 - Jinja1 performed automatic conversion of bytestrings in a given encoding + Jinja 1 performed automatic conversion of bytestrings 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 - bytestring to Unicode conversion. An application powered by Jinja2 - *has to* use unicode internally everywhere or make sure that Jinja2 only + bytestring 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 - Jinja1 used custom translators for internationalization. i18n is now - available as Jinja2 extension and uses a simpler, more gettext friendly + 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 - Jinja1 exposed a few internal methods on the environment object such + 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. Jinja2 + as being an internal method it was possible to override them. Jinja 2 doesn't have equivalent methods. Sandbox - Jinja1 was running sandbox mode by default. Few applications actually - used that feature so it became optional in Jinja2. For more details + 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 - Jinja1 had a stacked context as storage for variables passed to the - environment. In Jinja2 a similar object exists but it doesn't allow + 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. @@ -70,10 +70,10 @@ Filters and Tests Templates ~~~~~~~~~ -Jinja2 has mostly the same syntax as Jinja1. What's different is that +Jinja 2 has mostly the same syntax as Jinja 1. What's different is that macros require parentheses around the argument list now. -Additionally Jinja2 allows dynamic inheritance now and dynamic includes. +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 @@ -88,10 +88,10 @@ Django ------ If you have previously worked with Django templates, you should find -Jinja2 very familiar. In fact, most of the syntax elements look and +Jinja very familiar. In fact, most of the syntax elements look and work the same. -However, Jinja2 provides some more syntax elements covered in the +However, Jinja provides some more syntax elements covered in the documentation and some work a bit different. This section covers the template changes. As the API is fundamentally @@ -119,12 +119,12 @@ Django. This syntax is also used for macros. Filter Arguments ~~~~~~~~~~~~~~~~ -Jinja2 provides more than one argument for filters. Also the syntax for +Jinja provides more than one argument for filters. Also the syntax for argument passing is different. A template that looks like this in Django:: {{ items|join:", " }} -looks like this in Jinja2:: +looks like this in Jinja:: {{ items|join(', ') }} @@ -146,10 +146,10 @@ operator. Here are some examples:: Loops ~~~~~ -For loops work very similarly to Django, but notably the Jinja2 special +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 addition, the Django `empty` argument is called `else` in Jinja2. For +In addition, the Django `empty` argument is called `else` in Jinja. For example, the Django template:: {% for item in items %} @@ -158,7 +158,7 @@ example, the Django template:: No items! {% endfor %} -...looks like this in Jinja2:: +...looks like this in Jinja:: {% for item in items %} {{ item }} @@ -169,7 +169,7 @@ example, the Django template:: Cycle ~~~~~ -The ``{% cycle %}`` tag does not exist in Jinja2; however, you can achieve the +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. The following Django template:: @@ -178,7 +178,7 @@ The following Django template:: <li class="{% cycle 'odd' 'even' %}">{{ user }}</li> {% endfor %} -...looks like this in Jinja2:: +...looks like this in Jinja:: {% for user in users %} <li class="{{ loop.cycle('odd', 'even') }}">{{ user }}</li> @@ -192,17 +192,17 @@ Mako .. highlight:: html+mako -If you have used Mako so far and want to switch to Jinja2 you can configure -Jinja2 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: .. sourcecode:: python env = Environment('<%', '%>', '${', '}', '<%doc>', '</%doc>', '%', '##') -With an environment configured like that, Jinja2 should be able to interpret -a small subset of Mako templates. Jinja2 does not support embedded Python +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 Jinja2) and template inheritance is different too. +(which are called macros in Jinja) and template inheritance is different too. The following Mako template:: <%inherit file="layout.html" /> @@ -213,7 +213,7 @@ The following Mako template:: % endfor </ul> -Looks like this in Jinja2 with the above configuration:: +Looks like this in Jinja with the above configuration:: <% extends "layout.html" %> <% block title %>Page Title<% endblock %> diff --git a/docs/templates.rst b/docs/templates.rst index fd3e39f..89c2a50 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -57,6 +57,20 @@ configured as follows: * ``# ... ##`` for :ref:`Line Statements <line-statements>` +Template File Extension +~~~~~~~~~~~~~~~~~~~~~~~ + +As stated above, any file can be loaded as a template, regardless of +file extension. Adding a ``.jinja`` extension, like ``user.html.jinja`` +may make it easier for some IDEs or editor plugins, but is not required. +Autoescaping, introduced later, can be applied based on file extension, +so you'll need to take the extra suffix into account in that case. + +Another good heuristic for identifying templates is that they are in a +``templates`` folder, regardless of extension. This is a common layout +for projects. + + .. _variables: Variables @@ -91,7 +105,7 @@ printed or iterated over, and to fail for every other operation. .. admonition:: Implementation - For the sake of convenience, ``foo.bar`` in Jinja2 does the following + For the sake of convenience, ``foo.bar`` in Jinja does the following things on the Python layer: - check for an attribute called `bar` on `foo` @@ -231,7 +245,7 @@ a list of numbers from ``1`` to ``9``, the output would be ``123456789``. If :ref:`line-statements` are enabled, they strip leading whitespace automatically up to the beginning of the line. -By default, Jinja2 also removes trailing newlines. To keep single +By default, Jinja also removes trailing newlines. To keep single trailing newlines, configure Jinja to `keep_trailing_newline`. .. admonition:: Note @@ -271,6 +285,11 @@ include example Jinja syntax in a template, you can use this snippet:: </ul> {% endraw %} +.. admonition:: Note + + Minus sign at the end of ``{% raw -%}`` tag cleans all the spaces and newlines + preceding the first character of your raw data. + .. _line-statements: @@ -365,6 +384,10 @@ In this example, the ``{% block %}`` tags define four blocks that child template can fill in. All the `block` tag does is tell the template engine that a child template may override those placeholders in the template. +``block`` tags can be inside other blocks such as ``if``, but they will +always be executed regardless of if the ``if`` block is actually +rendered. + Child Template ~~~~~~~~~~~~~~ @@ -422,7 +445,7 @@ If you want to print a block multiple times, you can, however, use the special Super Blocks ~~~~~~~~~~~~ -It's possible to render the contents of the parent block by calling `super`. +It's possible to render the contents of the parent block by calling ``super()``. This gives back the results of the parent block:: {% block sidebar %} @@ -432,10 +455,45 @@ This gives back the results of the parent block:: {% endblock %} +Nesting extends +~~~~~~~~~~~~~~~ + +In the case of multiple levels of ``{% extends %}``, +``super`` references may be chained (as in ``super.super()``) +to skip levels in the inheritance tree. + +For example:: + + # parent.tmpl + body: {% block body %}Hi from parent.{% endblock %} + + # child.tmpl + {% extends "parent.tmpl" %} + {% block body %}Hi from child. {{ super() }}{% endblock %} + + # grandchild1.tmpl + {% extends "child.tmpl" %} + {% block body %}Hi from grandchild1.{% endblock %} + + # grandchild2.tmpl + {% extends "child.tmpl" %} + {% block body %}Hi from grandchild2. {{ super.super() }} {% endblock %} + + +Rendering ``child.tmpl`` will give +``body: Hi from child. Hi from parent.`` + +Rendering ``grandchild1.tmpl`` will give +``body: Hi from grandchild1.`` + +Rendering ``grandchild2.tmpl`` will give +``body: Hi from grandchild2. Hi from parent.`` + + Named Block End-Tags ~~~~~~~~~~~~~~~~~~~~ -Jinja2 allows you to put the name of the block after the end tag for better +Jinja allows you to put the name of the block after the end tag for better readability:: {% block sidebar %} @@ -527,23 +585,26 @@ When automatic escaping is enabled, everything is escaped by default except for values explicitly marked as safe. Variables and expressions can be marked as safe either in: -a. the context dictionary by the application with :class:`markupsafe.Markup`, or -b. the template, with the `|safe` filter +a. The context dictionary by the application with + :class:`markupsafe.Markup` +b. The template, with the ``|safe`` filter. -The main problem with this approach is that Python itself doesn't have the -concept of tainted values; so whether a value is safe or unsafe can get lost. +If a string that you marked safe is passed through other Python code +that doesn't understand that mark, it may get lost. Be aware of when +your data is marked safe and how it is processed before arriving at the +template. -If a value is not marked safe, auto-escaping will take place; which means that -you could end up with double-escaped contents. Double-escaping is easy to -avoid, however: just rely on the tools Jinja2 provides and *don't use builtin -Python constructs such as str.format or the string modulo operator (%)*. +If a value has been escaped but is not marked safe, auto-escaping will +still take place and result in double-escaped characters. If you know +you have data that is already safe but not marked, be sure to wrap it in +``Markup`` or use the ``|safe`` filter. -Jinja2 functions (macros, `super`, `self.BLOCKNAME`) always return template +Jinja functions (macros, `super`, `self.BLOCKNAME`) always return template data that is marked as safe. String literals in templates with automatic escaping are considered unsafe because native Python strings (``str``, ``unicode``, ``basestring``) are not -:class:`markupsafe.Markup` strings with an ``__html__`` attribute. +`MarkupSafe.Markup` strings with an ``__html__`` attribute. .. _list-of-control-structures: @@ -686,7 +747,7 @@ writing `{% set outer_loop = loop %}` after the loop that we want to use recursively. Then, we can call it using `{{ outer_loop(...) }}` Please note that assignments in loops will be cleared at the end of the -iteration and cannot outlive the loop scope. Older versions of Jinja2 had +iteration and cannot outlive the loop scope. Older versions of Jinja had a bug where in some circumstances it appeared that assignments would work. This is not supported. See :ref:`assignments` for more information about how to deal with this. @@ -863,7 +924,7 @@ Here's an example of how a call block can be used with arguments:: Filters ~~~~~~~ -Filter sections allow you to apply regular Jinja2 filters on a block of +Filter sections allow you to apply regular Jinja filters on a block of template data. Just wrap the code in the special `filter` section:: {% filter upper %} @@ -922,7 +983,7 @@ Assignments use the `set` tag and can have multiple targets:: {% endfor %} Found item having something: {{ ns.found }} - Note hat the ``obj.attr`` notation in the `set` tag is only allowed for + Note that the ``obj.attr`` notation in the `set` tag is only allowed for namespace objects; attempting to assign an attribute on any other object will raise an exception. @@ -986,7 +1047,7 @@ at the same time. They are documented in detail in the Include ~~~~~~~ -The `include` statement is useful to include a template and return the +The `include` tag is useful to include a template and return the rendered contents of that file into the current namespace:: {% include 'header.html' %} @@ -1028,7 +1089,7 @@ Example:: Import ~~~~~~ -Jinja2 supports putting often used code into macros. These macros can go into +Jinja supports putting often used code into macros. These macros can go into different templates and get imported from there. This works similarly to the import statements in Python. It's important to know that imports are cached and imported templates don't have access to the current template variables, @@ -1131,19 +1192,24 @@ Literals The simplest form of expressions are literals. Literals are representations for Python objects such as strings and numbers. The following literals exist: -"Hello World": +``"Hello World"`` Everything between two double or single quotes is a string. They are useful whenever you need a string in the template (e.g. as arguments to function calls and filters, or just to extend or include a template). -42 / 42.23: - Integers and floating point numbers are created by just writing the - number down. If a dot is present, the number is a float, otherwise an - integer. Keep in mind that, in Python, ``42`` and ``42.0`` - are different (``int`` and ``float``, respectively). +``42`` / ``123_456`` + Integers are whole numbers without a decimal part. The '_' character + can be used to separate groups for legibility. + +``42.23`` / ``42.1e2`` / ``123_456.789`` + Floating point numbers can be written using a '.' as a decimal mark. + They can also be written in scientific notation with an upper or + lower case 'e' to indicate the exponent part. The '_' character can + be used to separate groups for legibility, but cannot be used in the + exponent part. -['list', 'of', 'objects']: +``['list', 'of', 'objects']`` Everything between two brackets is a list. Lists are useful for storing sequential data to be iterated over. For example, you can easily create a list of links using lists and tuples for (and with) a for loop:: @@ -1155,20 +1221,20 @@ for Python objects such as strings and numbers. The following literals exist: {% endfor %} </ul> -('tuple', 'of', 'values'): +``('tuple', 'of', 'values')`` Tuples are like lists that cannot be modified ("immutable"). If a tuple only has one item, it must be followed by a comma (``('1-tuple',)``). Tuples are usually used to represent items of two or more elements. See the list example above for more details. -{'dict': 'of', 'key': 'and', 'value': 'pairs'}: +``{'dict': 'of', 'key': 'and', 'value': 'pairs'}`` A dict in Python is a structure that combines keys and values. Keys must be unique and always have exactly one value. Dicts are rarely used in templates; they are useful in some rare cases such as the :func:`xmlattr` filter. -true / false: - true is always true and false is always false. +``true`` / ``false`` + ``true`` is always true and ``false`` is always false. .. admonition:: Note @@ -1186,73 +1252,73 @@ Math Jinja allows you to calculate with values. This is rarely useful in templates but exists for completeness' sake. The following operators are supported: -\+ +``+`` Adds two objects together. Usually the objects are numbers, but if both are strings or lists, you can concatenate them this way. This, however, is not the preferred way to concatenate strings! For string concatenation, have a look-see at the ``~`` operator. ``{{ 1 + 1 }}`` is ``2``. -\- +``-`` Subtract the second number from the first one. ``{{ 3 - 2 }}`` is ``1``. -/ +``/`` Divide two numbers. The return value will be a floating point number. ``{{ 1 / 2 }}`` is ``{{ 0.5 }}``. -// +``//`` Divide two numbers and return the truncated integer result. ``{{ 20 // 7 }}`` is ``2``. -% +``%`` Calculate the remainder of an integer division. ``{{ 11 % 7 }}`` is ``4``. -\* +``*`` Multiply the left operand with the right one. ``{{ 2 * 2 }}`` would return ``4``. This can also be used to repeat a string multiple times. ``{{ '=' * 80 }}`` would print a bar of 80 equal signs. -\** +``**`` Raise the left operand to the power of the right operand. ``{{ 2**3 }}`` would return ``8``. Comparisons ~~~~~~~~~~~ -== +``==`` Compares two objects for equality. -!= +``!=`` Compares two objects for inequality. -> - `true` if the left hand side is greater than the right hand side. +``>`` + ``true`` if the left hand side is greater than the right hand side. ->= - `true` if the left hand side is greater or equal to the right hand side. +``>=`` + ``true`` if the left hand side is greater or equal to the right hand side. -< - `true` if the left hand side is lower than the right hand side. +``<`` + ``true`` if the left hand side is lower than the right hand side. -<= - `true` if the left hand side is lower or equal to the right hand side. +``<=`` + ``true`` if the left hand side is lower or equal to the right hand side. Logic ~~~~~ -For `if` statements, `for` filtering, and `if` expressions, it can be useful to +For ``if`` statements, ``for`` filtering, and ``if`` expressions, it can be useful to combine multiple expressions: -and +``and`` Return true if the left and the right operand are true. -or +``or`` Return true if the left or the right operand are true. -not +``not`` negate a statement (see below). -(expr) - group an expression. +``(expr)`` + Parentheses group an expression. .. admonition:: Note @@ -1268,30 +1334,30 @@ Other Operators The following operators are very useful but don't fit into any of the other two categories: -in +``in`` Perform a sequence / mapping containment test. Returns true if the left operand is contained in the right. ``{{ 1 in [1, 2, 3] }}`` would, for example, return true. -is +``is`` Performs a :ref:`test <tests>`. -\| +``|`` Applies a :ref:`filter <filters>`. -~ +``~`` Converts all operands into strings and concatenates them. ``{{ "Hello " ~ name ~ "!" }}`` would return (assuming `name` is set to ``'John'``) ``Hello John!``. -() +``()`` Call a callable: ``{{ post.render() }}``. Inside of the parentheses you can use positional arguments and keyword arguments like in Python: ``{{ post.render(user, full=true) }}``. -. / [] +``.`` / ``[]`` Get an attribute of an object. (See :ref:`variables`) @@ -1310,7 +1376,8 @@ The general syntax is ``<do something> if <something is true> else <do something else>``. The `else` part is optional. If not provided, the else block implicitly -evaluates into an undefined object: +evaluates into an :class:`Undefined` object (regardless of what ``undefined`` +in the environment is set to): .. sourcecode:: jinja @@ -1330,13 +1397,27 @@ Here is an example that uses methods defined on strings (where ``page.title`` is {{ page.title.capitalize() }} -This also works for methods on user-defined types. -For example, if variable ``f`` of type ``Foo`` has a method ``bar`` defined on it, -you can do the following: +This works for methods on user-defined types. For example, if variable +``f`` of type ``Foo`` has a method ``bar`` defined on it, you can do the +following: .. code-block:: text - {{ f.bar() }} + {{ f.bar(value) }} + +Operator methods also work as expected. For example, ``%`` implements +printf-style for strings: + +.. code-block:: text + + {{ "Hello, %s!" % name }} + +Although you should prefer the ``.format`` method for that case (which +is a bit contrived in the context of rendering a template): + +.. code-block:: text + + {{ "Hello, {}!".format(name) }} .. _builtin-filters: @@ -1399,41 +1480,44 @@ The following functions are available in the global scope by default: .. class:: cycler(\*items) - The cycler allows you to cycle among values similar to how `loop.cycle` - works. Unlike `loop.cycle`, you can use this cycler outside of - loops or over multiple loops. + Cycle through values by yielding them one at a time, then restarting + once the end is reached. - This can be very useful if you want to show a list of folders and - files with the folders on top but both in the same list with alternating - row colors. + Similar to ``loop.cycle``, but can be used outside loops or across + multiple loops. For example, render a list of folders and files in a + list, alternating giving them "odd" and "even" classes. - The following example shows how `cycler` can be used:: + .. code-block:: html+jinja - {% set row_class = cycler('odd', 'even') %} + {% set row_class = cycler("odd", "even") %} <ul class="browser"> {% for folder in folders %} - <li class="folder {{ row_class.next() }}">{{ folder|e }}</li> + <li class="folder {{ row_class.next() }}">{{ folder }} {% endfor %} - {% for filename in files %} - <li class="file {{ row_class.next() }}">{{ filename|e }}</li> + {% for file in files %} + <li class="file {{ row_class.next() }}">{{ file }} {% endfor %} </ul> - A cycler has the following attributes and methods: + :param items: Each positional argument will be yielded in the order + given for each cycle. - .. method:: reset() + .. versionadded:: 2.1 - Resets the cycle to the first item. + .. method:: current + :property: - .. method:: next() + Return the current item. Equivalent to the item that will be + returned next time :meth:`next` is called. - Goes one item ahead and returns the then-current item. + .. method:: next() - .. attribute:: current + Return the current item, then advance :attr:`current` to the + next item. - Returns the current item. + .. method:: reset() - .. versionadded:: 2.1 + Resets the current item to the first item. .. class:: joiner(sep=', ') @@ -1482,39 +1566,49 @@ The following functions are available in the global scope by default: Extensions ---------- -The following sections cover the built-in Jinja2 extensions that may be +The following sections cover the built-in Jinja extensions that may be enabled by an application. An application could also provide further extensions not covered by this documentation; in which case there should be a separate document explaining said :ref:`extensions <jinja-extensions>`. + .. _i18n-in-templates: i18n ~~~~ -If the i18n extension is enabled, it's possible to mark parts in the template -as translatable. To mark a section as translatable, you can use `trans`:: +If the :ref:`i18n-extension` is enabled, it's possible to mark text in +the template as translatable. To mark a section as translatable, use a +``trans`` block: + +.. code-block:: jinja + + {% trans %}Hello, {{ user }}!{% endtrans %} + +Inside the block, no statements are allowed, only text and simple +variable tags. + +Variable tags can only be a name, not attribute access, filters, or +other expressions. To use an expression, bind it to a name in the +``trans`` tag for use in the block. - <p>{% trans %}Hello {{ user }}!{% endtrans %}</p> +.. code-block:: jinja -To translate a template expression --- say, using template filters, or by just -accessing an attribute of an object --- you need to bind the expression to a -name for use within the translation block:: + {% trans user=user.username %}Hello, {{ user }}!{% endtrans %} - <p>{% trans user=user.username %}Hello {{ user }}!{% endtrans %}</p> +To bind more than one expression, separate each with a comma (``,``). -If you need to bind more than one expression inside a `trans` tag, separate -the pieces with a comma (``,``):: +.. code-block:: jinja {% trans book_title=book.title, author=author.name %} This is {{ book_title }} by {{ author }} {% endtrans %} -Inside trans tags no statements are allowed, only variable tags are. +To pluralize, specify both the singular and plural forms separated by +the ``pluralize`` tag. -To pluralize, specify both the singular and plural forms with the `pluralize` -tag, which appears between `trans` and `endtrans`:: +.. code-block:: jinja {% trans count=list|length %} There is {{ count }} {{ name }} object. @@ -1522,60 +1616,70 @@ tag, which appears between `trans` and `endtrans`:: There are {{ count }} {{ name }} objects. {% endtrans %} -By default, the first variable in a block is used to determine the correct -singular or plural form. If that doesn't work out, you can specify the name -which should be used for pluralizing by adding it as parameter to `pluralize`:: +By default, the first variable in a block is used to determine whether +to use singular or plural form. If that isn't correct, specify the +variable used for pluralizing as a parameter to ``pluralize``. + +.. code-block:: jinja {% trans ..., user_count=users|length %}... {% pluralize user_count %}...{% endtrans %} -When translating longer blocks of text, whitespace and linebreaks result in -rather ugly and error-prone translation strings. To avoid this, a trans block -can be marked as trimmed which will replace all linebreaks and the whitespace -surrounding them with a single space and remove leading/trailing whitespace:: +When translating blocks of text, whitespace and linebreaks result in +hard to read and error-prone translation strings. To avoid this, a trans +block can be marked as trimmed, which will replace all linebreaks and +the whitespace surrounding them with a single space and remove leading +and trailing whitespace. + +.. code-block:: jinja {% trans trimmed book_title=book.title %} This is {{ book_title }}. You should read it! {% endtrans %} -If trimming is enabled globally, the `notrimmed` modifier can be used to -disable it for a `trans` block. +This results in ``This is %(book_title)s. You should read it!`` in the +translation file. + +If trimming is enabled globally, the ``notrimmed`` modifier can be used +to disable it for a block. .. versionadded:: 2.10 - The `trimmed` and `notrimmed` modifiers have been added. + The ``trimmed`` and ``notrimmed`` modifiers have been added. -It's also possible to translate strings in expressions. For that purpose, -three functions exist: +It's possible to translate strings in expressions with these functions: -- `gettext`: translate a single string -- `ngettext`: translate a pluralizable string -- `_`: alias for `gettext` +- ``gettext``: translate a single string +- ``ngettext``: translate a pluralizable string +- ``_``: alias for ``gettext`` -For example, you can easily print a translated string like this:: +You can print a translated string like this: - {{ _('Hello World!') }} +.. code-block:: jinja -To use placeholders, use the `format` filter:: + {{ _("Hello, World!") }} - {{ _('Hello %(user)s!')|format(user=user.username) }} +To use placeholders, use the ``format`` filter. -For multiple placeholders, always use keyword arguments to `format`, -as other languages may not use the words in the same order. +.. code-block:: jinja -.. versionchanged:: 2.5 + {{ _("Hello, %(user)s!")|format(user=user.username) }} -If newstyle gettext calls are activated (:ref:`newstyle-gettext`), using -placeholders is a lot easier: +Always use keyword arguments to ``format``, as other languages may not +use the words in the same order. -.. sourcecode:: html+jinja +If :ref:`newstyle-gettext` calls are activated, using placeholders is +easier. Formatting is part of the ``gettext`` call instead of using the +``format`` filter. + +.. sourcecode:: jinja {{ gettext('Hello World!') }} {{ gettext('Hello %(name)s!', name='World') }} {{ ngettext('%(num)d apple', '%(num)d apples', apples|count) }} -Note that the `ngettext` function's format string automatically receives -the count as a `num` parameter in addition to the regular parameters. +The ``ngettext`` function's format string automatically receives the +count as a ``num`` parameter in addition to the given parameters. Expression Statement @@ -1613,6 +1717,29 @@ Note that ``loop.index`` starts with 1, and ``loop.index0`` starts with 0 (See: :ref:`for-loop`). +Debug Statement +~~~~~~~~~~~~~~~ + +If the :ref:`debug-extension` is enabled, a ``{% debug %}`` tag will be +available to dump the current context as well as the available filters +and tests. This is useful to see what's available to use in the template +without setting up a debugger. + +.. code-block:: html+jinja + + <pre>{% debug %}</pre> + +.. code-block:: text + + {'context': {'cycler': <class 'jinja2.utils.Cycler'>, + ..., + 'namespace': <class 'jinja2.utils.Namespace'>}, + 'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd', + ..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'], + 'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined', + ..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']} + + With Statement ~~~~~~~~~~~~~~ @@ -1647,7 +1774,7 @@ behavior of referencing one variable to another had some unintended consequences. In particular one variable could refer to another defined in the same with block's opening statement. This caused issues with the cleaned up scoping behavior and has since been improved. In particular -in newer Jinja2 versions the following code always refers to the variable +in newer Jinja versions the following code always refers to the variable `a` from outside the `with` block:: {% with a={}, b=a.attribute %}...{% endwith %} @@ -1672,8 +1799,8 @@ Autoescape Overrides .. versionadded:: 2.4 -If you want you can activate and deactivate :ref:`autoescaping` from within -a template. +If you want you can activate and deactivate the autoescaping from within +the templates. Example:: @@ -1685,7 +1812,7 @@ Example:: Autoescaping is inactive within this block {% endautoescape %} -After an ``endautoescape`` the behavior is reverted to what it was before. +After an `endautoescape` the behavior is reverted to what it was before. .. admonition:: Extension diff --git a/docs/tricks.rst b/docs/tricks.rst index 4d33e22..78ac408 100644 --- a/docs/tricks.rst +++ b/docs/tricks.rst @@ -3,7 +3,7 @@ Tips and Tricks .. highlight:: html+jinja -This part of the documentation shows some tips and tricks for Jinja2 +This part of the documentation shows some tips and tricks for Jinja templates. @@ -12,7 +12,7 @@ templates. Null-Master Fallback -------------------- -Jinja2 supports dynamic inheritance and does not distinguish between parent +Jinja supports dynamic inheritance and does not distinguish between parent and child template as long as no `extends` tag is visited. While this leads to the surprising behavior that everything before the first `extends` tag including whitespace is printed out instead of being ignored, it can be used diff --git a/examples/basic/cycle.py b/examples/basic/cycle.py index 73dd632..25dcb0b 100644 --- a/examples/basic/cycle.py +++ b/examples/basic/cycle.py @@ -1,13 +1,18 @@ -from jinja2 import Environment - - -env = Environment(line_statement_prefix="#", variable_start_string="${", variable_end_string="}") +from __future__ import print_function +from jinja2 import Environment -print env.from_string("""\ +env = Environment( + line_statement_prefix="#", variable_start_string="${", variable_end_string="}" +) +print( + env.from_string( + """\ <ul> # for item in range(10) <li class="${loop.cycle('odd', 'even')}">${item}</li> # endfor </ul>\ -""").render() +""" + ).render() +) diff --git a/examples/basic/debugger.py b/examples/basic/debugger.py index 4291ff7..d3c1a60 100644 --- a/examples/basic/debugger.py +++ b/examples/basic/debugger.py @@ -1,7 +1,8 @@ +from __future__ import print_function + from jinja2 import Environment from jinja2.loaders import FileSystemLoader -env = Environment(loader=FileSystemLoader('templates')) - -tmpl = env.get_template('broken.html') -print tmpl.render(seq=[3, 2, 4, 5, 3, 2, 0, 2, 1]) +env = Environment(loader=FileSystemLoader("templates")) +tmpl = env.get_template("broken.html") +print(tmpl.render(seq=[3, 2, 4, 5, 3, 2, 0, 2, 1])) diff --git a/examples/basic/inheritance.py b/examples/basic/inheritance.py index aa687c8..4a881bf 100644 --- a/examples/basic/inheritance.py +++ b/examples/basic/inheritance.py @@ -1,12 +1,15 @@ +from __future__ import print_function + from jinja2 import Environment from jinja2.loaders import DictLoader - -env = Environment(loader=DictLoader({ -'a': '''[A[{% block body %}{% endblock %}]]''', -'b': '''{% extends 'a' %}{% block body %}[B]{% endblock %}''', -'c': '''{% extends 'b' %}{% block body %}###{{ super() }}###{% endblock %}''' -})) - - -print env.get_template('c').render() +env = Environment( + loader=DictLoader( + { + "a": "[A[{% block body %}{% endblock %}]]", + "b": "{% extends 'a' %}{% block body %}[B]{% endblock %}", + "c": "{% extends 'b' %}{% block body %}###{{ super() }}###{% endblock %}", + } + ) +) +print(env.get_template("c").render()) diff --git a/examples/basic/test.py b/examples/basic/test.py index 8f7dde7..80b9d1f 100644 --- a/examples/basic/test.py +++ b/examples/basic/test.py @@ -1,8 +1,12 @@ +from __future__ import print_function + from jinja2 import Environment from jinja2.loaders import DictLoader -env = Environment(loader=DictLoader({ -'child.html': u'''\ +env = Environment( + loader=DictLoader( + { + "child.html": u"""\ {% extends master_layout or 'master.html' %} {% include helpers = 'helpers.html' %} {% macro get_the_answer() %}42{% endmacro %} @@ -11,17 +15,17 @@ env = Environment(loader=DictLoader({ {{ get_the_answer() }} {{ helpers.conspirate() }} {% endblock %} -''', -'master.html': u'''\ +""", + "master.html": u"""\ <!doctype html> <title>{{ title }}</title> {% block body %}{% endblock %} -''', -'helpers.html': u'''\ +""", + "helpers.html": u"""\ {% macro conspirate() %}23{% endmacro %} -''' -})) - - +""", + } + ) +) tmpl = env.get_template("child.html") print(tmpl.render()) diff --git a/examples/basic/test_filter_and_linestatements.py b/examples/basic/test_filter_and_linestatements.py index 79e0a4b..673b67e 100644 --- a/examples/basic/test_filter_and_linestatements.py +++ b/examples/basic/test_filter_and_linestatements.py @@ -1,8 +1,12 @@ -from jinja2 import Environment +from __future__ import print_function +from jinja2 import Environment -env = Environment(line_statement_prefix='%', variable_start_string="${", variable_end_string="}") -tmpl = env.from_string("""\ +env = Environment( + line_statement_prefix="%", variable_start_string="${", variable_end_string="}" +) +tmpl = env.from_string( + """\ % macro foo() ${caller(42)} % endmacro @@ -20,6 +24,6 @@ tmpl = env.from_string("""\ - ${item} % endfor % endfilter -""") - +""" +) print(tmpl.render(seq=range(10))) diff --git a/examples/basic/test_loop_filter.py b/examples/basic/test_loop_filter.py index 0469d04..39be08d 100644 --- a/examples/basic/test_loop_filter.py +++ b/examples/basic/test_loop_filter.py @@ -1,12 +1,15 @@ +from __future__ import print_function + from jinja2 import Environment -tmpl = Environment().from_string("""\ +tmpl = Environment().from_string( + """\ <ul> {%- for item in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] if item % 2 == 0 %} <li>{{ loop.index }} / {{ loop.length }}: {{ item }}</li> {%- endfor %} </ul> if condition: {{ 1 if foo else 0 }} -""") - +""" +) print(tmpl.render(foo=True)) diff --git a/examples/basic/translate.py b/examples/basic/translate.py index 1fb8ee6..71547f4 100644 --- a/examples/basic/translate.py +++ b/examples/basic/translate.py @@ -1,14 +1,20 @@ +from __future__ import print_function + from jinja2 import Environment -env = Environment(extensions=['jinja2.ext.i18n']) -env.globals['gettext'] = { - 'Hello %(user)s!': 'Hallo %(user)s!' -}.__getitem__ -env.globals['ngettext'] = lambda s, p, n: { - '%(count)s user': '%(count)d Benutzer', - '%(count)s users': '%(count)d Benutzer' +env = Environment(extensions=["jinja2.ext.i18n"]) +env.globals["gettext"] = {"Hello %(user)s!": "Hallo %(user)s!"}.__getitem__ +env.globals["ngettext"] = lambda s, p, n: { + "%(count)s user": "%(count)d Benutzer", + "%(count)s users": "%(count)d Benutzer", }[n == 1 and s or p] -print env.from_string("""\ +print( + env.from_string( + """\ {% trans %}Hello {{ user }}!{% endtrans %} -{% trans count=users|count %}{{ count }} user{% pluralize %}{{ count }} users{% endtrans %} -""").render(user="someone", users=[1, 2, 3]) +{% trans count=users|count -%} +{{ count }} user{% pluralize %}{{ count }} users +{% endtrans %} +""" + ).render(user="someone", users=[1, 2, 3]) +) diff --git a/examples/bench.py b/examples/bench.py deleted file mode 100644 index c648dc6..0000000 --- a/examples/bench.py +++ /dev/null @@ -1,433 +0,0 @@ -"""\ - This benchmark compares some python templating engines with Jinja 2 so - that we get a picture of how fast Jinja 2 is for a semi real world - template. If a template engine is not installed the test is skipped.\ -""" -import sys -import cgi -from timeit import Timer -from jinja2 import Environment as JinjaEnvironment - -context = { - 'page_title': 'mitsuhiko\'s benchmark', - 'table': [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10) for x in range(1000)] -} - -jinja_template = JinjaEnvironment( - line_statement_prefix='%', - variable_start_string="${", - variable_end_string="}" -).from_string("""\ -<!doctype html> -<html> - <head> - <title>${page_title|e}</title> - </head> - <body> - <div class="header"> - <h1>${page_title|e}</h1> - </div> - <ul class="navigation"> - % for href, caption in [ - ('index.html', 'Index'), - ('downloads.html', 'Downloads'), - ('products.html', 'Products') - ] - <li><a href="${href|e}">${caption|e}</a></li> - % endfor - </ul> - <div class="table"> - <table> - % for row in table - <tr> - % for cell in row - <td>${cell}</td> - % endfor - </tr> - % endfor - </table> - </div> - </body> -</html>\ -""") - -def test_jinja(): - jinja_template.render(context) - -try: - from tornado.template import Template -except ImportError: - test_tornado = None -else: - tornado_template = Template("""\ -<!doctype html> -<html> - <head> - <title>{{ page_title }}</title> - </head> - <body> - <div class="header"> - <h1>{{ page_title }}</h1> - </div> - <ul class="navigation"> - {% for href, caption in [ \ - ('index.html', 'Index'), \ - ('downloads.html', 'Downloads'), \ - ('products.html', 'Products') \ - ] %} - <li><a href="{{ href }}">{{ caption }}</a></li> - {% end %} - </ul> - <div class="table"> - <table> - {% for row in table %} - <tr> - {% for cell in row %} - <td>{{ cell }}</td> - {% end %} - </tr> - {% end %} - </table> - </div> - </body> -</html>\ -""") - - def test_tornado(): - tornado_template.generate(**context) - -try: - from django.conf import settings - settings.configure() - from django.template import Template as DjangoTemplate, Context as DjangoContext -except ImportError: - test_django = None -else: - django_template = DjangoTemplate("""\ -<!doctype html> -<html> - <head> - <title>{{ page_title }}</title> - </head> - <body> - <div class="header"> - <h1>{{ page_title }}</h1> - </div> - <ul class="navigation"> - {% for href, caption in navigation %} - <li><a href="{{ href }}">{{ caption }}</a></li> - {% endfor %} - </ul> - <div class="table"> - <table> - {% for row in table %} - <tr> - {% for cell in row %} - <td>{{ cell }}</td> - {% endfor %} - </tr> - {% endfor %} - </table> - </div> - </body> -</html>\ -""") - - def test_django(): - c = DjangoContext(context) - c['navigation'] = [('index.html', 'Index'), ('downloads.html', 'Downloads'), - ('products.html', 'Products')] - django_template.render(c) - -try: - from mako.template import Template as MakoTemplate -except ImportError: - test_mako = None -else: - mako_template = MakoTemplate("""\ -<!doctype html> -<html> - <head> - <title>${page_title|h}</title> - </head> - <body> - <div class="header"> - <h1>${page_title|h}</h1> - </div> - <ul class="navigation"> - % for href, caption in [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]: - <li><a href="${href|h}">${caption|h}</a></li> - % endfor - </ul> - <div class="table"> - <table> - % for row in table: - <tr> - % for cell in row: - <td>${cell}</td> - % endfor - </tr> - % endfor - </table> - </div> - </body> -</html>\ -""") - - def test_mako(): - mako_template.render(**context) - -try: - from genshi.template import MarkupTemplate as GenshiTemplate -except ImportError: - test_genshi = None -else: - genshi_template = GenshiTemplate("""\ -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/"> - <head> - <title>${page_title}</title> - </head> - <body> - <div class="header"> - <h1>${page_title}</h1> - </div> - <ul class="navigation"> - <li py:for="href, caption in [ - ('index.html', 'Index'), - ('downloads.html', 'Downloads'), - ('products.html', 'Products')]"><a href="${href}">${caption}</a></li> - </ul> - <div class="table"> - <table> - <tr py:for="row in table"> - <td py:for="cell in row">${cell}</td> - </tr> - </table> - </div> - </body> -</html>\ -""") - - def test_genshi(): - genshi_template.generate(**context).render('html', strip_whitespace=False) - -try: - from Cheetah.Template import Template as CheetahTemplate -except ImportError: - test_cheetah = None -else: - cheetah_template = CheetahTemplate("""\ -#import cgi -<!doctype html> -<html> - <head> - <title>$cgi.escape($page_title)</title> - </head> - <body> - <div class="header"> - <h1>$cgi.escape($page_title)</h1> - </div> - <ul class="navigation"> - #for $href, $caption in [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]: - <li><a href="$cgi.escape($href)">$cgi.escape($caption)</a></li> - #end for - </ul> - <div class="table"> - <table> - #for $row in $table: - <tr> - #for $cell in $row: - <td>$cell</td> - #end for - </tr> - #end for - </table> - </div> - </body> -</html>\ -""", searchList=[dict(context)]) - - def test_cheetah(): - unicode(cheetah_template) - -try: - import tenjin -except ImportError: - test_tenjin = None -else: - tenjin_template = tenjin.Template() - tenjin_template.convert("""\ -<!doctype html> -<html> - <head> - <title>${page_title}</title> - </head> - <body> - <div class="header"> - <h1>${page_title}</h1> - </div> - <ul class="navigation"> -<?py for href, caption in [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]: ?> - <li><a href="${href}">${caption}</a></li> -<?py #end ?> - </ul> - <div class="table"> - <table> -<?py for row in table: ?> - <tr> -<?py for cell in row: ?> - <td>#{cell}</td> -<?py #end ?> - </tr> -<?py #end ?> - </table> - </div> - </body> -</html>\ -""") - - def test_tenjin(): - from tenjin.helpers import escape, to_str - tenjin_template.render(context, locals()) - -try: - from spitfire.compiler import util as SpitfireTemplate - from spitfire.compiler.analyzer import o2_options as spitfire_optimizer -except ImportError: - test_spitfire = None -else: - spitfire_template = SpitfireTemplate.load_template("""\ -<!doctype html> -<html> - <head> - <title>$cgi.escape($page_title)</title> - </head> - <body> - <div class="header"> - <h1>$cgi.escape($page_title)</h1> - </div> - <ul class="navigation"> - #for $href, $caption in [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')] - <li><a href="$cgi.escape($href)">$cgi.escape($caption)</a></li> - #end for - </ul> - <div class="table"> - <table> - #for $row in $table - <tr> - #for $cell in $row - <td>$cell</td> - #end for - </tr> - #end for - </table> - </div> - </body> -</html>\ -""", 'spitfire_tmpl', spitfire_optimizer, {'enable_filters': False}) - spitfire_context = dict(context, **{'cgi': cgi}) - - def test_spitfire(): - spitfire_template(search_list=[spitfire_context]).main() - - -try: - from chameleon.zpt.template import PageTemplate -except ImportError: - test_chameleon = None -else: - chameleon_template = PageTemplate("""\ -<html xmlns:tal="http://xml.zope.org/namespaces/tal"> - <head> - <title tal:content="page_title">Page Title</title> - </head> - <body> - <div class="header"> - <h1 tal:content="page_title">Page Title</h1> - </div> - <ul class="navigation"> - <li tal:repeat="item sections"><a tal:attributes="href item[0]" tal:content="item[1]">caption</a></li> - </ul> - <div class="table"> - <table> - <tr tal:repeat="row table"> - <td tal:repeat="cell row" tal:content="row[cell]">cell</td> - </tr> - </table> - </div> - </body> -</html>\ -""") - chameleon_context = dict(context) - chameleon_context['sections'] = [ - ('index.html', 'Index'), - ('downloads.html', 'Downloads'), - ('products.html', 'Products') - ] - def test_chameleon(): - chameleon_template.render(**chameleon_context) - -try: - from chameleon.zpt.template import PageTemplate - from chameleon.genshi import language -except ImportError: - test_chameleon_genshi = None -else: - chameleon_genshi_template = PageTemplate("""\ -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/"> - <head> - <title>${page_title}</title> - </head> - <body> - <div class="header"> - <h1>${page_title}</h1> - </div> - <ul class="navigation"> - <li py:for="info in sections"><a href="${info[0]}">${info[1]}</a></li> - </ul> - <div class="table"> - <table> - <tr py:for="row in table"> - <td py:for="cell in row">${row[cell]}</td> - </tr> - </table> - </div> - </body> -</html>\ -""", parser=language.Parser()) - chameleon_genshi_context = dict(context) - chameleon_genshi_context['sections'] = [ - ('index.html', 'Index'), - ('downloads.html', 'Downloads'), - ('products.html', 'Products') - ] - def test_chameleon_genshi(): - chameleon_genshi_template.render(**chameleon_genshi_context) - - -sys.stdout.write('\r' + '\n'.join(( - '=' * 80, - 'Template Engine BigTable Benchmark'.center(80), - '=' * 80, - __doc__, - '-' * 80 -)) + '\n') - - -for test in 'jinja', 'mako', 'tornado', 'tenjin', 'spitfire', 'django', 'genshi', 'cheetah', 'chameleon', 'chameleon_genshi': - if locals()['test_' + test] is None: - sys.stdout.write(' %-20s*not installed*\n' % test) - continue - t = Timer(setup='from __main__ import test_%s as bench' % test, - stmt='bench()') - sys.stdout.write(' >> %-20s<running>' % test) - sys.stdout.flush() - sys.stdout.write('\r %-20s%.4f seconds\n' % (test, t.timeit(number=50) / 50)) -sys.stdout.write('-' * 80 + '\n') -sys.stdout.write('''\ - WARNING: The results of this benchmark are useless to compare the - performance of template engines and should not be taken seriously in any - way. It's testing the performance of simple loops and has no real-world - usefulnes. It only used to check if changes on the Jinja code affect - performance in a good or bad way and how it roughly compares to others. -''' + '=' * 80 + '\n') diff --git a/examples/profile.py b/examples/profile.py deleted file mode 100644 index 0c907ae..0000000 --- a/examples/profile.py +++ /dev/null @@ -1,52 +0,0 @@ -try: - from cProfile import Profile -except ImportError: - from profile import Profile -from pstats import Stats -from jinja2 import Environment as JinjaEnvironment - -context = { - 'page_title': 'mitsuhiko\'s benchmark', - 'table': [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10) for x in range(1000)] -} - -source = """\ -% macro testmacro(x) - <span>${x}</span> -% endmacro -<!doctype html> -<html> - <head> - <title>${page_title|e}</title> - </head> - <body> - <div class="header"> - <h1>${page_title|e}</h1> - </div> - <div class="table"> - <table> - % for row in table - <tr> - % for cell in row - <td>${testmacro(cell)}</td> - % endfor - </tr> - % endfor - </table> - </div> - </body> -</html>\ -""" -jinja_template = JinjaEnvironment( - line_statement_prefix='%', - variable_start_string="${", - variable_end_string="}" -).from_string(source) -print jinja_template.environment.compile(source, raw=True) - - -p = Profile() -p.runcall(lambda: jinja_template.render(context)) -stats = Stats(p) -stats.sort_stats('time', 'calls') -stats.print_stats() diff --git a/examples/rwbench/django/_form.html b/examples/rwbench/django/_form.html deleted file mode 100644 index 9c4f710..0000000 --- a/examples/rwbench/django/_form.html +++ /dev/null @@ -1 +0,0 @@ -<form action="{{ action }}" method="{{ method }}">{{ body }}</form> diff --git a/examples/rwbench/django/_input_field.html b/examples/rwbench/django/_input_field.html deleted file mode 100644 index 290fdbd..0000000 --- a/examples/rwbench/django/_input_field.html +++ /dev/null @@ -1 +0,0 @@ -<input type="{{ type }}" value="{{ value }}" name="{{ name }}"> diff --git a/examples/rwbench/django/_textarea.html b/examples/rwbench/django/_textarea.html deleted file mode 100644 index 7f10424..0000000 --- a/examples/rwbench/django/_textarea.html +++ /dev/null @@ -1 +0,0 @@ -<textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">{{ value }}</textarea> diff --git a/examples/rwbench/django/index.html b/examples/rwbench/django/index.html deleted file mode 100644 index 6f620bb..0000000 --- a/examples/rwbench/django/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "layout.html" %} -{% block page_title %}Index Page{% endblock %} -{% block body %} - {% for article in articles %} - {% if article.published %} - <div class="article"> - <h2><a href="{{ article.href }}">{{ article.title }}</a></h2> - <p class="meta">written by <a href="{{ article.user.href }}">{{ article.user.username }}</a> on {{ article.pub_date|dateformat }}</p> - <div class="text">{{ article.body|safe }}</div> - </div> - {% endif %} - {% endfor %} - {% form %} - <dl> - <dt>Name</dt> - <dd>{% input_field 'name' %}</dd> - <dt>E-Mail</dt> - <dd>{% input_field 'email' %}</dd> - <dt>URL</dt> - <dd>{% input_field 'url' %}</dd> - <dt>Comment</dt> - <dd>{% textarea 'comment' %}</dd> - <dt>Captcha</dt> - <dd>{% input_field 'captcha' %}</dd> - </dl> - {% input_field '' 'submit' 'Submit' %} - {% input_field 'cancel' 'submit' 'Cancel' %} - {% endform %} -{% endblock %} diff --git a/examples/rwbench/django/layout.html b/examples/rwbench/django/layout.html deleted file mode 100644 index 60039ce..0000000 --- a/examples/rwbench/django/layout.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html> -<head> - <title>{% block page_title %}{% endblock %} | RealWorld Benchmark</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> -</head> -<body> - <div class="contents"> - <div class="header"> - <h1>RealWorld Benchmark</h1> - <blockquote><p> - A less stupid benchmark for Mako and Jinja2 to get an impression how - code changes affect runtime performance. - </p></blockquote> - </div> - <ul class="navigation"> - {% for href, caption in page_navigation %} - <li><a href="{{ href }}">{{ caption }}</a></li> - {% endfor %} - </ul> - <div class="body"> - {% block body %}{% endblock %} - </div> - <div class="footer"> - © Copyright 2008 by I don't know who. - </div> - </div> -</body> -</html> diff --git a/examples/rwbench/djangoext.py b/examples/rwbench/djangoext.py deleted file mode 100644 index 9e9fa6c..0000000 --- a/examples/rwbench/djangoext.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -from rwbench import ROOT -from os.path import join -from django.conf import settings -settings.configure( - TEMPLATE_DIRS=(join(ROOT, 'django'),), - TEMPLATE_LOADERS=( - ('django.template.loaders.cached.Loader', ( - 'django.template.loaders.filesystem.Loader', - )), - ) -) -from django.template import loader as django_loader, Context as DjangoContext, \ - Node, NodeList, Variable, TokenParser -from django import template as django_template_module -from django.template import Library - - -# for django extensions. We monkey patch our extensions in so that -# we don't have to initialize a more complex django setup. -django_extensions = django_template_module.Library() -django_template_module.builtins.append(django_extensions) - - -from rwbench import dateformat -django_extensions.filter(dateformat) - - -def var_or_none(x): - if x is not None: - return Variable(x) - - -# and more django extensions -@django_extensions.tag -def input_field(parser, token): - p = TokenParser(token.contents) - args = [p.value()] - while p.more(): - args.append(p.value()) - return InputFieldNode(*args) - - -@django_extensions.tag -def textarea(parser, token): - p = TokenParser(token.contents) - args = [p.value()] - while p.more(): - args.append(p.value()) - return TextareaNode(*args) - - -@django_extensions.tag -def form(parser, token): - p = TokenParser(token.contents) - args = [] - while p.more(): - args.append(p.value()) - body = parser.parse(('endform',)) - parser.delete_first_token() - return FormNode(body, *args) - - -class InputFieldNode(Node): - - def __init__(self, name, type=None, value=None): - self.name = var_or_none(name) - self.type = var_or_none(type) - self.value = var_or_none(value) - - def render(self, context): - name = self.name.resolve(context) - type = 'text' - value = '' - if self.type is not None: - type = self.type.resolve(context) - if self.value is not None: - value = self.value.resolve(context) - tmpl = django_loader.get_template('_input_field.html') - return tmpl.render(DjangoContext({ - 'name': name, - 'type': type, - 'value': value - })) - - -class TextareaNode(Node): - - def __init__(self, name, rows=None, cols=None, value=None): - self.name = var_or_none(name) - self.rows = var_or_none(rows) - self.cols = var_or_none(cols) - self.value = var_or_none(value) - - def render(self, context): - name = self.name.resolve(context) - rows = 10 - cols = 40 - value = '' - if self.rows is not None: - rows = int(self.rows.resolve(context)) - if self.cols is not None: - cols = int(self.cols.resolve(context)) - if self.value is not None: - value = self.value.resolve(context) - tmpl = django_loader.get_template('_textarea.html') - return tmpl.render(DjangoContext({ - 'name': name, - 'rows': rows, - 'cols': cols, - 'value': value - })) - - -class FormNode(Node): - - def __init__(self, body, action=None, method=None): - self.body = body - self.action = action - self.method = method - - def render(self, context): - body = self.body.render(context) - action = '' - method = 'post' - if self.action is not None: - action = self.action.resolve(context) - if self.method is not None: - method = self.method.resolve(context) - tmpl = django_loader.get_template('_form.html') - return tmpl.render(DjangoContext({ - 'body': body, - 'action': action, - 'method': method - })) diff --git a/examples/rwbench/genshi/helpers.html b/examples/rwbench/genshi/helpers.html deleted file mode 100644 index ecc6dc4..0000000 --- a/examples/rwbench/genshi/helpers.html +++ /dev/null @@ -1,12 +0,0 @@ -<div xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" - py:strip=""> - - <py:def function="input_field(name='', value='', type='text')"> - <input type="$type" value="$value" name="$name" /> - </py:def> - - <py:def function="textarea(name, value='', rows=10, cols=40)"> - <textarea name="$name" rows="$rows" cols="cols">$value</textarea> - </py:def> - -</div> diff --git a/examples/rwbench/genshi/index.html b/examples/rwbench/genshi/index.html deleted file mode 100644 index 70f697d..0000000 --- a/examples/rwbench/genshi/index.html +++ /dev/null @@ -1,41 +0,0 @@ -<?python - from rwbench import dateformat -?> -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xi="http://www.w3.org/2001/XInclude" - xmlns:py="http://genshi.edgewall.org/"> - <xi:include href="layout.html" /> - <xi:include href="helpers.html" /> - <head><title>Index Page</title></head> - <body> - <div class="article" py:for="article in articles"> - <py:if test="article.published"> - <h2><a href="${article.href}">${article.title}</a></h2> - <p class="meta">written by <a href="${article.user.href}" - >${article.user.username}</a> on ${dateformat(article.pub_date)}</p> - <div class="text">${Markup(article.body)}</div> - </py:if> - </div> - <!-- - For a fair and balanced comparison we would have to use a def here - that wraps the form data but I don't know what would be the best - Genshi equivalent for that. Quite frankly I doubt that this makes - sense in Genshi anyways. - --> - <form action="" method="post"> - <dl> - <dt>Name</dt> - <dd>${input_field('name')}</dd> - <dt>E-Mail</dt> - <dd>${input_field('email')}</dd> - <dt>URL</dt> - <dd>${input_field('url')}</dd> - <dt>Comment</dt> - <dd>${textarea('comment')}</dd> - <dt>Captcha</dt> - <dd>${input_field('captcha')}</dd> - </dl> - ${input_field(type='submit', value='Submit')} - ${input_field(name='cancel', type='submit', value='Cancel')} - </form> - </body> -</html> diff --git a/examples/rwbench/genshi/layout.html b/examples/rwbench/genshi/layout.html deleted file mode 100644 index b12aec4..0000000 --- a/examples/rwbench/genshi/layout.html +++ /dev/null @@ -1,30 +0,0 @@ -<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" > - <py:match path="head" once="true"> - <head> - <title>${select('title/text()')} | RealWorld Benchmark</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - </head> - </py:match> - <py:match path="body" once="true"> - <body> - <div class="contents"> - <div class="header"> - <h1>RealWorld Benchmark</h1> - <blockquote><p> - A less stupid benchmark for Mako and Jinja2 to get an impression how - code changes affect runtime performance. - </p></blockquote> - </div> - <ul class="navigation"> - <li py:for="href, caption in page_navigation"><a href="$href">$caption</a></li> - </ul> - <div class="body"> - ${select('*|text()')} - </div> - <div class="footer"> - © Copyright 2008 by I don't know who. - </div> - </div> - </body> - </py:match> -</html> diff --git a/examples/rwbench/jinja/helpers.html b/examples/rwbench/jinja/helpers.html deleted file mode 100644 index 89976aa..0000000 --- a/examples/rwbench/jinja/helpers.html +++ /dev/null @@ -1,12 +0,0 @@ -{% macro input_field(name, value='', type='text') -%} - <input type="{{ type }}" value="{{ value|e }}" name="{{ name }}"> -{%- endmacro %} - -{% macro textarea(name, value='', rows=10, cols=40) -%} - <textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">{{ - value|e }}</textarea> -{%- endmacro %} - -{% macro form(action='', method='post') -%} - <form action="{{ action|e }}" method="{{ method }}">{{ caller() }}</form> -{%- endmacro %} diff --git a/examples/rwbench/jinja/index.html b/examples/rwbench/jinja/index.html deleted file mode 100644 index b006d05..0000000 --- a/examples/rwbench/jinja/index.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "layout.html" %} -{% from "helpers.html" import input_field, textarea, form %} -{% block page_title %}Index Page{% endblock %} -{% block body %} - {%- for article in articles if article.published %} - <div class="article"> - <h2><a href="{{ article.href|e }}">{{ article.title|e }}</a></h2> - <p class="meta">written by <a href="{{ article.user.href|e - }}">{{ article.user.username|e }}</a> on {{ article.pub_date|dateformat }}</p> - <div class="text">{{ article.body }}</div> - </div> - {%- endfor %} - {%- call form() %} - <dl> - <dt>Name</dt> - <dd>{{ input_field('name') }}</dd> - <dt>E-Mail</dt> - <dd>{{ input_field('email') }}</dd> - <dt>URL</dt> - <dd>{{ input_field('url') }}</dd> - <dt>Comment</dt> - <dd>{{ textarea('comment') }}</dd> - <dt>Captcha</dt> - <dd>{{ input_field('captcha') }}</dd> - </dl> - {{ input_field(type='submit', value='Submit') }} - {{ input_field('cancel', type='submit', value='Cancel') }} - {%- endcall %} -{% endblock %} diff --git a/examples/rwbench/jinja/layout.html b/examples/rwbench/jinja/layout.html deleted file mode 100644 index 755789e..0000000 --- a/examples/rwbench/jinja/layout.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html> -<head> - <title>{% block page_title %}{% endblock %} | RealWorld Benchmark</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> -</head> -<body> - <div class="contents"> - <div class="header"> - <h1>RealWorld Benchmark</h1> - <blockquote><p> - A less stupid benchmark for Mako and Jinja2 to get an impression how - code changes affect runtime performance. - </p></blockquote> - </div> - <ul class="navigation"> - {%- for href, caption in page_navigation %} - <li><a href="{{ href|e }}">{{ caption }}</a></li> - {%- endfor %} - </ul> - <div class="body"> - {% block body %}{% endblock %} - </div> - <div class="footer"> - © Copyright 2008 by I don't know who. - </div> - </div> -</body> -</html> diff --git a/examples/rwbench/mako/helpers.html b/examples/rwbench/mako/helpers.html deleted file mode 100644 index a0290eb..0000000 --- a/examples/rwbench/mako/helpers.html +++ /dev/null @@ -1,11 +0,0 @@ -<%def name="input_field(name='', value='', type='text')"> - <input type="${type}" value="${value|h}" name="${name}"> -</%def> - -<%def name="textarea(name, value='', rows=10, cols=40)"> - <textarea name="${name}" rows="${rows}" cols="${cols}">${value|h}</textarea> -</%def> - -<%def name="form(action='', method='post')"> - <form action="${action|h}" method="${method}">${caller.body()}</form> -</%def> diff --git a/examples/rwbench/mako/index.html b/examples/rwbench/mako/index.html deleted file mode 100644 index c4c6303..0000000 --- a/examples/rwbench/mako/index.html +++ /dev/null @@ -1,31 +0,0 @@ -<%! - from rwbench import dateformat -%> -<%inherit file="layout.html" /> -<%namespace file="helpers.html" import="input_field, textarea, form" /> -<%def name="page_title()">Index Page</%def> -% for article in articles: - <% if not article.published: continue %> -<div class="article"> - <h2><a href="${article.href|h}">${article.title|h}</a></h2> - <p class="meta">written by <a href="${article.user.href|h - }">${article.user.username|h}</a> on ${dateformat(article.pub_date)}</p> - <div class="text">${article.body}</div> -</div> -% endfor -<%call expr="form()"> - <dl> - <dt>Name</dt> - <dd>${input_field('name')}</dd> - <dt>E-Mail</dt> - <dd>${input_field('email')}</dd> - <dt>URL</dt> - <dd>${input_field('url')}</dd> - <dt>Comment</dt> - <dd>${textarea('comment')}</dd> - <dt>Captcha</dt> - <dd>${input_field('captcha')}</dd> - </dl> - ${input_field(type='submit', value='Submit')} - ${input_field(name='cancel', type='submit', value='Cancel')} -</%call> diff --git a/examples/rwbench/mako/layout.html b/examples/rwbench/mako/layout.html deleted file mode 100644 index a9c353e..0000000 --- a/examples/rwbench/mako/layout.html +++ /dev/null @@ -1,30 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html> -<head> - <title>${self.page_title()} | RealWorld Benchmark</title> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> -</head> -<body> - <div class="contents"> - <div class="header"> - <h1>RealWorld Benchmark</h1> - <blockquote><p> - A less stupid benchmark for Mako and Jinja2 to get an impression how - code changes affect runtime performance. - </p></blockquote> - </div> - <ul class="navigation"> - % for href, caption in page_navigation: - <li><a href="${href|h}">${caption}</a></li> - % endfor - </ul> - <div class="body"> - ${self.body()} - </div> - <div class="footer"> - © Copyright 2008 by I don't know who. - </div> - </div> -</body> -</html> -<%def name="page_title()"></%def> diff --git a/examples/rwbench/rwbench.py b/examples/rwbench/rwbench.py deleted file mode 100644 index 813dd56..0000000 --- a/examples/rwbench/rwbench.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -""" - RealWorldish Benchmark - ~~~~~~~~~~~~~~~~~~~~~~ - - A more real-world benchmark of Jinja2. Like the other benchmark in the - Jinja2 repository this has no real-world usefulnes (despite the name). - Just go away and ignore it. NOW! - - :copyright: (c) 2009 by the Jinja Team. - :license: BSD. -""" -import sys -from os.path import join, dirname, abspath -try: - from cProfile import Profile -except ImportError: - from profile import Profile -from pstats import Stats -ROOT = abspath(dirname(__file__)) - -from random import choice, randrange -from datetime import datetime -from timeit import Timer -from jinja2 import Environment, FileSystemLoader -from jinja2.utils import generate_lorem_ipsum -from mako.lookup import TemplateLookup -from genshi.template import TemplateLoader as GenshiTemplateLoader - - -def dateformat(x): - return x.strftime('%Y-%m-%d') - - -jinja_env = Environment(loader=FileSystemLoader(join(ROOT, 'jinja'))) -jinja_env.filters['dateformat'] = dateformat -mako_lookup = TemplateLookup(directories=[join(ROOT, 'mako')]) -genshi_loader = GenshiTemplateLoader([join(ROOT, 'genshi')]) - -class Article(object): - - def __init__(self, id): - self.id = id - self.href = '/article/%d' % self.id - self.title = generate_lorem_ipsum(1, False, 5, 10) - self.user = choice(users) - self.body = generate_lorem_ipsum() - self.pub_date = datetime.utcfromtimestamp(randrange(10 ** 9, 2 * 10 ** 9)) - self.published = True - - -class User(object): - - def __init__(self, username): - self.href = '/user/%s' % username - self.username = username - - -users = map(User, [u'John Doe', u'Jane Doe', u'Peter Somewhat']) -articles = map(Article, range(20)) -navigation = [ - ('index', 'Index'), - ('about', 'About'), - ('foo?bar=1', 'Foo with Bar'), - ('foo?bar=2&s=x', 'Foo with X'), - ('blah', 'Blub Blah'), - ('hehe', 'Haha'), -] * 5 - -context = dict(users=users, articles=articles, page_navigation=navigation) - - -jinja_template = jinja_env.get_template('index.html') -mako_template = mako_lookup.get_template('index.html') -genshi_template = genshi_loader.load('index.html') - - -def test_jinja(): - jinja_template.render(context) - -def test_mako(): - mako_template.render_unicode(**context) - - -from djangoext import django_loader, DjangoContext -def test_django(): - # not cached because django is not thread safe and does - # not cache by itself so it would be unfair to cache it here. - django_template = django_loader.get_template('index.html') - django_template.render(DjangoContext(context)) - - -def test_genshi(): - genshi_template.generate(**context).render('html', doctype='html') - - -if __name__ == '__main__': - sys.stdout.write('Realworldish Benchmark:\n') - for test in 'jinja', 'mako', 'django', 'genshi': - t = Timer(setup='from __main__ import test_%s as bench' % test, - stmt='bench()') - sys.stdout.write(' >> %-20s<running>' % test) - sys.stdout.flush() - sys.stdout.write('\r %-20s%.4f seconds\n' % (test, t.timeit(number=200) / 200)) - - if '-p' in sys.argv: - print 'Jinja profile' - p = Profile() - p.runcall(test_jinja) - stats = Stats(p) - stats.sort_stats('time', 'calls') - stats.print_stats() diff --git a/ext/Vim/jinja.vim b/ext/Vim/jinja.vim index 2fd5838..e2a5bbf 100644 --- a/ext/Vim/jinja.vim +++ b/ext/Vim/jinja.vim @@ -10,27 +10,27 @@ " " Changes: " -" 2008 May 9: Added support for Jinja2 changes (new keyword rules) +" 2008 May 9: Added support for Jinja 2 changes (new keyword rules) " .vimrc variable to disable html highlighting if !exists('g:jinja_syntax_html') - let g:jinja_syntax_html=1 + 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 + if v:version < 600 syntax clear elseif exists("b:current_syntax") - finish -endif + finish + endif let main_syntax = 'jinja' endif " Pull in the HTML syntax. if g:jinja_syntax_html - if version < 600 + if v:version < 600 so <sfile>:p:h/html.vim else runtime! syntax/html.vim @@ -82,6 +82,9 @@ syn region jinjaRaw matchgroup=jinjaRawDelim start="{%\s*raw\s*%}" end="{%\s*end " Jinja comments syn region jinjaComment matchgroup=jinjaCommentDelim start="{#" end="#}" containedin=ALLBUT,jinjaTagBlock,jinjaVarBlock,jinjaString,jinjaComment +" help support folding for some setups +setlocal commentstring={#%s#} +setlocal comments=s:{#,e:#} " 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 @@ -95,8 +98,8 @@ syn match jinjaStatement containedin=jinjaTagBlock contained /\<with\(out\)\?\s\ " 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 +if v:version >= 508 || !exists("did_jinja_syn_inits") + if v:version < 508 let did_jinja_syn_inits = 1 command -nargs=+ HiLink hi link <args> else @@ -130,6 +133,6 @@ endif let b:current_syntax = "jinja" -if main_syntax == 'jinja' +if main_syntax ==# 'jinja' unlet main_syntax endif diff --git a/ext/django2jinja/django2jinja.py b/ext/django2jinja/django2jinja.py deleted file mode 100644 index ad9627f..0000000 --- a/ext/django2jinja/django2jinja.py +++ /dev/null @@ -1,768 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Django to Jinja - ~~~~~~~~~~~~~~~ - - Helper module that can convert django templates into Jinja2 templates. - - This file is not intended to be used as stand alone application but to - be used as library. To convert templates you basically create your own - writer, add extra conversion logic for your custom template tags, - configure your django environment and run the `convert_templates` - function. - - Here a simple example:: - - # configure django (or use settings.configure) - import os - os.environ['DJANGO_SETTINGS_MODULE'] = 'yourapplication.settings' - from yourapplication.foo.templatetags.bar import MyNode - - from django2jinja import Writer, convert_templates - - def write_my_node(writer, node): - writer.start_variable() - writer.write('myfunc(') - for idx, arg in enumerate(node.args): - if idx: - writer.write(', ') - writer.node(arg) - writer.write(')') - writer.end_variable() - - writer = Writer() - writer.node_handlers[MyNode] = write_my_node - convert_templates('/path/to/output/folder', writer=writer) - - Here is an example hos to automatically translate your django - variables to jinja2:: - - import re - # List of tuple (Match pattern, Replace pattern, Exclusion pattern) - - var_re = ((re.compile(r"(u|user)\.is_authenticated"), r"\1.is_authenticated()", None), - (re.compile(r"\.non_field_errors"), r".non_field_errors()", None), - (re.compile(r"\.label_tag"), r".label_tag()", None), - (re.compile(r"\.as_dl"), r".as_dl()", None), - (re.compile(r"\.as_table"), r".as_table()", None), - (re.compile(r"\.as_widget"), r".as_widget()", None), - (re.compile(r"\.as_hidden"), r".as_hidden()", None), - - (re.compile(r"\.get_([0-9_\w]+)_url"), r".get_\1_url()", None), - (re.compile(r"\.url"), r".url()", re.compile(r"(form|calendar).url")), - (re.compile(r"\.get_([0-9_\w]+)_display"), r".get_\1_display()", None), - (re.compile(r"loop\.counter"), r"loop.index", None), - (re.compile(r"loop\.revcounter"), r"loop.revindex", None), - (re.compile(r"request\.GET\.([0-9_\w]+)"), r"request.GET.get('\1', '')", None), - (re.compile(r"request\.get_host"), r"request.get_host()", None), - - (re.compile(r"\.all(?!_)"), r".all()", None), - (re.compile(r"\.all\.0"), r".all()[0]", None), - (re.compile(r"\.([0-9])($|\s+)"), r"[\1]\2", None), - (re.compile(r"\.items"), r".items()", None), - ) - writer = Writer(var_re=var_re) - - For details about the writing process have a look at the module code. - - :copyright: (c) 2009 by the Jinja Team. - :license: BSD. -""" -import re -import os -import sys -from jinja2.defaults import * -from django.conf import settings -from django.template import defaulttags as core_tags, loader, TextNode, \ - FilterExpression, libraries, Variable, loader_tags, TOKEN_TEXT, \ - TOKEN_VAR -from django.template.debug import DebugVariableNode as VariableNode -from django.templatetags import i18n as i18n_tags -from StringIO import StringIO - - -_node_handlers = {} -_resolved_filters = {} -_newline_re = re.compile(r'(?:\r\n|\r|\n)') - - -# Django stores an itertools object on the cycle node. Not only is this -# thread unsafe but also a problem for the converter which needs the raw -# string values passed to the constructor to create a jinja loop.cycle() -# call from it. -_old_cycle_init = core_tags.CycleNode.__init__ -def _fixed_cycle_init(self, cyclevars, variable_name=None): - self.raw_cycle_vars = map(Variable, cyclevars) - _old_cycle_init(self, cyclevars, variable_name) -core_tags.CycleNode.__init__ = _fixed_cycle_init - - -def node(cls): - def proxy(f): - _node_handlers[cls] = f - return f - return proxy - - -def convert_templates(output_dir, extensions=('.html', '.txt'), writer=None, - callback=None): - """Iterates over all templates in the template dirs configured and - translates them and writes the new templates into the output directory. - """ - if writer is None: - writer = Writer() - - def filter_templates(files): - for filename in files: - ifilename = filename.lower() - for extension in extensions: - if ifilename.endswith(extension): - yield filename - - def translate(f, loadname): - template = loader.get_template(loadname) - original = writer.stream - writer.stream = f - writer.body(template.nodelist) - writer.stream = original - - if callback is None: - def callback(template): - print template - - for directory in settings.TEMPLATE_DIRS: - for dirname, _, files in os.walk(directory): - dirname = dirname[len(directory) + 1:] - for filename in filter_templates(files): - source = os.path.normpath(os.path.join(dirname, filename)) - target = os.path.join(output_dir, dirname, filename) - basetarget = os.path.dirname(target) - if not os.path.exists(basetarget): - os.makedirs(basetarget) - callback(source) - f = file(target, 'w') - try: - translate(f, source) - finally: - f.close() - - -class Writer(object): - """The core writer class.""" - - def __init__(self, stream=None, error_stream=None, - block_start_string=BLOCK_START_STRING, - block_end_string=BLOCK_END_STRING, - variable_start_string=VARIABLE_START_STRING, - variable_end_string=VARIABLE_END_STRING, - comment_start_string=COMMENT_START_STRING, - comment_end_string=COMMENT_END_STRING, - initial_autoescape=True, - use_jinja_autoescape=False, - custom_node_handlers=None, - var_re=[], - env=None): - if stream is None: - stream = sys.stdout - if error_stream is None: - error_stream = sys.stderr - self.stream = stream - self.error_stream = error_stream - self.block_start_string = block_start_string - self.block_end_string = block_end_string - self.variable_start_string = variable_start_string - self.variable_end_string = variable_end_string - self.comment_start_string = comment_start_string - self.comment_end_string = comment_end_string - self.autoescape = initial_autoescape - self.spaceless = False - self.use_jinja_autoescape = use_jinja_autoescape - self.node_handlers = dict(_node_handlers, - **(custom_node_handlers or {})) - self._loop_depth = 0 - self._filters_warned = set() - self.var_re = var_re - self.env = env - - def enter_loop(self): - """Increments the loop depth so that write functions know if they - are in a loop. - """ - self._loop_depth += 1 - - def leave_loop(self): - """Reverse of enter_loop.""" - self._loop_depth -= 1 - - @property - def in_loop(self): - """True if we are in a loop.""" - return self._loop_depth > 0 - - def write(self, s): - """Writes stuff to the stream.""" - self.stream.write(s.encode(settings.FILE_CHARSET)) - - def print_expr(self, expr): - """Open a variable tag, write to the string to the stream and close.""" - self.start_variable() - self.write(expr) - self.end_variable() - - def _post_open(self): - if self.spaceless: - self.write('- ') - else: - self.write(' ') - - def _pre_close(self): - if self.spaceless: - self.write(' -') - else: - self.write(' ') - - def start_variable(self): - """Start a variable.""" - self.write(self.variable_start_string) - self._post_open() - - def end_variable(self, always_safe=False): - """End a variable.""" - if not always_safe and self.autoescape and \ - not self.use_jinja_autoescape: - self.write('|e') - self._pre_close() - self.write(self.variable_end_string) - - def start_block(self): - """Starts a block.""" - self.write(self.block_start_string) - self._post_open() - - def end_block(self): - """Ends a block.""" - self._pre_close() - self.write(self.block_end_string) - - def tag(self, name): - """Like `print_expr` just for blocks.""" - self.start_block() - self.write(name) - self.end_block() - - def variable(self, name): - """Prints a variable. This performs variable name transformation.""" - self.write(self.translate_variable_name(name)) - - def literal(self, value): - """Writes a value as literal.""" - value = repr(value) - if value[:2] in ('u"', "u'"): - value = value[1:] - self.write(value) - - def filters(self, filters, is_block=False): - """Dumps a list of filters.""" - want_pipe = not is_block - for filter, args in filters: - name = self.get_filter_name(filter) - if name is None: - self.warn('Could not find filter %s' % name) - continue - if name not in DEFAULT_FILTERS and \ - name not in self._filters_warned: - self._filters_warned.add(name) - self.warn('Filter %s probably doesn\'t exist in Jinja' % - name) - if not want_pipe: - want_pipe = True - else: - self.write('|') - self.write(name) - if args: - self.write('(') - for idx, (is_var, value) in enumerate(args): - if idx: - self.write(', ') - if is_var: - self.node(value) - else: - self.literal(value) - self.write(')') - - def get_location(self, origin, position): - """Returns the location for an origin and position tuple as name - and lineno. - """ - if hasattr(origin, 'source'): - source = origin.source - name = '<unknown source>' - else: - source = origin.loader(origin.loadname, origin.dirs)[0] - name = origin.loadname - lineno = len(_newline_re.findall(source[:position[0]])) + 1 - return name, lineno - - def warn(self, message, node=None): - """Prints a warning to the error stream.""" - if node is not None and hasattr(node, 'source'): - filename, lineno = self.get_location(*node.source) - message = '[%s:%d] %s' % (filename, lineno, message) - print >> self.error_stream, message - - def translate_variable_name(self, var): - """Performs variable name translation.""" - if self.in_loop and var == 'forloop' or var.startswith('forloop.'): - var = var[3:] - - for reg, rep, unless in self.var_re: - no_unless = unless and unless.search(var) or True - if reg.search(var) and no_unless: - var = reg.sub(rep, var) - break - return var - - def get_filter_name(self, filter): - """Returns the filter name for a filter function or `None` if there - is no such filter. - """ - if filter not in _resolved_filters: - for library in libraries.values(): - for key, value in library.filters.iteritems(): - _resolved_filters[value] = key - return _resolved_filters.get(filter, None) - - def node(self, node): - """Invokes the node handler for a node.""" - for cls, handler in self.node_handlers.iteritems(): - if type(node) is cls or type(node).__name__ == cls: - handler(self, node) - break - else: - self.warn('Untranslatable node %s.%s found' % ( - node.__module__, - node.__class__.__name__ - ), node) - - def body(self, nodes): - """Calls node() for every node in the iterable passed.""" - for node in nodes: - self.node(node) - - -@node(TextNode) -def text_node(writer, node): - writer.write(node.s) - - -@node(Variable) -def variable(writer, node): - if node.translate: - writer.warn('i18n system used, make sure to install translations', node) - writer.write('_(') - if node.literal is not None: - writer.literal(node.literal) - else: - writer.variable(node.var) - if node.translate: - writer.write(')') - - -@node(VariableNode) -def variable_node(writer, node): - writer.start_variable() - if node.filter_expression.var.var == 'block.super' \ - and not node.filter_expression.filters: - writer.write('super()') - else: - writer.node(node.filter_expression) - writer.end_variable() - - -@node(FilterExpression) -def filter_expression(writer, node): - writer.node(node.var) - writer.filters(node.filters) - - -@node(core_tags.CommentNode) -def comment_tag(writer, node): - pass - - -@node(core_tags.DebugNode) -def comment_tag(writer, node): - writer.warn('Debug tag detected. Make sure to add a global function ' - 'called debug to the namespace.', node=node) - writer.print_expr('debug()') - - -@node(core_tags.ForNode) -def for_loop(writer, node): - writer.start_block() - writer.write('for ') - for idx, var in enumerate(node.loopvars): - if idx: - writer.write(', ') - writer.variable(var) - writer.write(' in ') - if node.is_reversed: - writer.write('(') - writer.node(node.sequence) - if node.is_reversed: - writer.write(')|reverse') - writer.end_block() - writer.enter_loop() - writer.body(node.nodelist_loop) - writer.leave_loop() - writer.tag('endfor') - - -@node(core_tags.IfNode) -def if_condition(writer, node): - writer.start_block() - writer.write('if ') - join_with = 'and' - if node.link_type == core_tags.IfNode.LinkTypes.or_: - join_with = 'or' - - for idx, (ifnot, expr) in enumerate(node.bool_exprs): - if idx: - writer.write(' %s ' % join_with) - if ifnot: - writer.write('not ') - writer.node(expr) - writer.end_block() - writer.body(node.nodelist_true) - if node.nodelist_false: - writer.tag('else') - writer.body(node.nodelist_false) - writer.tag('endif') - - -@node(core_tags.IfEqualNode) -def if_equal(writer, node): - writer.start_block() - writer.write('if ') - writer.node(node.var1) - if node.negate: - writer.write(' != ') - else: - writer.write(' == ') - writer.node(node.var2) - writer.end_block() - writer.body(node.nodelist_true) - if node.nodelist_false: - writer.tag('else') - writer.body(node.nodelist_false) - writer.tag('endif') - - -@node(loader_tags.BlockNode) -def block(writer, node): - writer.tag('block ' + node.name.replace('-', '_').rstrip('_')) - node = node - while node.parent is not None: - node = node.parent - writer.body(node.nodelist) - writer.tag('endblock') - - -@node(loader_tags.ExtendsNode) -def extends(writer, node): - writer.start_block() - writer.write('extends ') - if node.parent_name_expr: - writer.node(node.parent_name_expr) - else: - writer.literal(node.parent_name) - writer.end_block() - writer.body(node.nodelist) - - -@node(loader_tags.ConstantIncludeNode) -@node(loader_tags.IncludeNode) -def include(writer, node): - writer.start_block() - writer.write('include ') - if hasattr(node, 'template'): - writer.literal(node.template.name) - else: - writer.node(node.template_name) - writer.end_block() - - -@node(core_tags.CycleNode) -def cycle(writer, node): - if not writer.in_loop: - writer.warn('Untranslatable free cycle (cycle outside loop)', node=node) - return - if node.variable_name is not None: - writer.start_block() - writer.write('set %s = ' % node.variable_name) - else: - writer.start_variable() - writer.write('loop.cycle(') - for idx, var in enumerate(node.raw_cycle_vars): - if idx: - writer.write(', ') - writer.node(var) - writer.write(')') - if node.variable_name is not None: - writer.end_block() - else: - writer.end_variable() - - -@node(core_tags.FilterNode) -def filter(writer, node): - writer.start_block() - writer.write('filter ') - writer.filters(node.filter_expr.filters, True) - writer.end_block() - writer.body(node.nodelist) - writer.tag('endfilter') - - -@node(core_tags.AutoEscapeControlNode) -def autoescape_control(writer, node): - original = writer.autoescape - writer.autoescape = node.setting - writer.body(node.nodelist) - writer.autoescape = original - - -@node(core_tags.SpacelessNode) -def spaceless(writer, node): - original = writer.spaceless - writer.spaceless = True - writer.warn('entering spaceless mode with different semantics', node) - # do the initial stripping - nodelist = list(node.nodelist) - if nodelist: - if isinstance(nodelist[0], TextNode): - nodelist[0] = TextNode(nodelist[0].s.lstrip()) - if isinstance(nodelist[-1], TextNode): - nodelist[-1] = TextNode(nodelist[-1].s.rstrip()) - writer.body(nodelist) - writer.spaceless = original - - -@node(core_tags.TemplateTagNode) -def template_tag(writer, node): - tag = { - 'openblock': writer.block_start_string, - 'closeblock': writer.block_end_string, - 'openvariable': writer.variable_start_string, - 'closevariable': writer.variable_end_string, - 'opencomment': writer.comment_start_string, - 'closecomment': writer.comment_end_string, - 'openbrace': '{', - 'closebrace': '}' - }.get(node.tagtype) - if tag: - writer.start_variable() - writer.literal(tag) - writer.end_variable() - - -@node(core_tags.URLNode) -def url_tag(writer, node): - writer.warn('url node used. make sure to provide a proper url() ' - 'function', node) - if node.asvar: - writer.start_block() - writer.write('set %s = ' % node.asvar) - else: - writer.start_variable() - autoescape = writer.autoescape - writer.write('url(') - writer.literal(node.view_name) - for arg in node.args: - writer.write(', ') - writer.node(arg) - for key, arg in node.kwargs.items(): - writer.write(', %s=' % key) - writer.node(arg) - writer.write(')') - if node.asvar: - writer.end_block() - else: - writer.end_variable() - - -@node(core_tags.WidthRatioNode) -def width_ratio(writer, node): - writer.warn('widthratio expanded into formula. You may want to provide ' - 'a helper function for this calculation', node) - writer.start_variable() - writer.write('(') - writer.node(node.val_expr) - writer.write(' / ') - writer.node(node.max_expr) - writer.write(' * ') - writer.write(str(int(node.max_width))) - writer.write(')|round|int') - writer.end_variable(always_safe=True) - - -@node(core_tags.WithNode) -def with_block(writer, node): - writer.warn('with block expanded into set statement. This could cause ' - 'variables following that block to be overridden.', node) - writer.start_block() - writer.write('set %s = ' % node.name) - writer.node(node.var) - writer.end_block() - writer.body(node.nodelist) - - -@node(core_tags.RegroupNode) -def regroup(writer, node): - if node.expression.var.literal: - writer.warn('literal in groupby filter used. Behavior in that ' - 'situation is undefined and translation is skipped.', node) - return - elif node.expression.filters: - writer.warn('filters in groupby filter used. Behavior in that ' - 'situation is undefined which is most likely a bug ' - 'in your code. Filters were ignored.', node) - writer.start_block() - writer.write('set %s = ' % node.var_name) - writer.node(node.target) - writer.write('|groupby(') - writer.literal(node.expression.var.var) - writer.write(')') - writer.end_block() - - -@node(core_tags.LoadNode) -def warn_load(writer, node): - writer.warn('load statement used which was ignored on conversion', node) - - -@node(i18n_tags.GetAvailableLanguagesNode) -def get_available_languages(writer, node): - writer.warn('make sure to provide a get_available_languages function', node) - writer.tag('set %s = get_available_languages()' % - writer.translate_variable_name(node.variable)) - - -@node(i18n_tags.GetCurrentLanguageNode) -def get_current_language(writer, node): - writer.warn('make sure to provide a get_current_language function', node) - writer.tag('set %s = get_current_language()' % - writer.translate_variable_name(node.variable)) - - -@node(i18n_tags.GetCurrentLanguageBidiNode) -def get_current_language_bidi(writer, node): - writer.warn('make sure to provide a get_current_language_bidi function', node) - writer.tag('set %s = get_current_language_bidi()' % - writer.translate_variable_name(node.variable)) - - -@node(i18n_tags.TranslateNode) -def simple_gettext(writer, node): - writer.warn('i18n system used, make sure to install translations', node) - writer.start_variable() - writer.write('_(') - writer.node(node.value) - writer.write(')') - writer.end_variable() - - -@node(i18n_tags.BlockTranslateNode) -def translate_block(writer, node): - first_var = [] - variables = set() - - def touch_var(name): - variables.add(name) - if not first_var: - first_var.append(name) - - def dump_token_list(tokens): - for token in tokens: - if token.token_type == TOKEN_TEXT: - writer.write(token.contents) - elif token.token_type == TOKEN_VAR: - writer.print_expr(token.contents) - touch_var(token.contents) - - writer.warn('i18n system used, make sure to install translations', node) - writer.start_block() - writer.write('trans') - idx = -1 - for idx, (key, var) in enumerate(node.extra_context.items()): - if idx: - writer.write(',') - writer.write(' %s=' % key) - touch_var(key) - writer.node(var.filter_expression) - - have_plural = False - plural_var = None - if node.plural and node.countervar and node.counter: - have_plural = True - plural_var = node.countervar - if plural_var not in variables: - if idx > -1: - writer.write(',') - touch_var(plural_var) - writer.write(' %s=' % plural_var) - writer.node(node.counter) - - writer.end_block() - dump_token_list(node.singular) - if node.plural and node.countervar and node.counter: - writer.start_block() - writer.write('pluralize') - if node.countervar != first_var[0]: - writer.write(' ' + node.countervar) - writer.end_block() - dump_token_list(node.plural) - writer.tag('endtrans') - -@node("SimpleNode") -def simple_tag(writer, node): - """Check if the simple tag exist as a filter in """ - name = node.tag_name - if writer.env and \ - name not in writer.env.filters and \ - name not in writer._filters_warned: - writer._filters_warned.add(name) - writer.warn('Filter %s probably doesn\'t exist in Jinja' % - name) - - if not node.vars_to_resolve: - # No argument, pass the request - writer.start_variable() - writer.write('request|') - writer.write(name) - writer.end_variable() - return - - first_var = node.vars_to_resolve[0] - args = node.vars_to_resolve[1:] - writer.start_variable() - - # Copied from Writer.filters() - writer.node(first_var) - - writer.write('|') - writer.write(name) - if args: - writer.write('(') - for idx, var in enumerate(args): - if idx: - writer.write(', ') - if var.var: - writer.node(var) - else: - writer.literal(var.literal) - writer.write(')') - writer.end_variable() - -# get rid of node now, it shouldn't be used normally -del node diff --git a/ext/django2jinja/example.py b/ext/django2jinja/example.py deleted file mode 100644 index 2d4ab9a..0000000 --- a/ext/django2jinja/example.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf import settings -settings.configure(TEMPLATE_DIRS=['templates'], TEMPLATE_DEBUG=True) - -from django2jinja import convert_templates, Writer - -writer = Writer(use_jinja_autoescape=True) -convert_templates('converted', writer=writer) diff --git a/ext/django2jinja/templates/index.html b/ext/django2jinja/templates/index.html deleted file mode 100644 index d0fbe38..0000000 --- a/ext/django2jinja/templates/index.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "layout.html" %} -{% load i18n %} -{% block title %}Foo{% endblock %} -{% block page-body %} - {{ block.super }} - Hello {{ name|cut:"d" }}! - - {% for item in seq reversed %} - {% if forloop.index|divisibleby:2 %} - <li class="{% cycle 'a' 'b' %}">{{ item }}</li> - {% endif %} - {% endfor %} - {% ifequal foo bar %} - haha - {% else %} - hmm - {% endifequal %} - {% filter upper %} - {% include "subtemplate.html" %} - {% include foo %} - {% endfilter %} - {% spaceless %} - Hello World - {{ foo }} - Hmm - {% endspaceless %} - {% templatetag opencomment %}...{% templatetag closecomment %} - {% url foo a, b, c=d %} - {% url foo a, b, c=d as hmm %} - - {% with object.value as value %} - <img src='bar.gif' height='10' width='{% widthratio value 200 100 %}'> - {% endwith %} - - <pre>{% debug %}</pre> - - {% blocktrans with book|title as book_t and author|title as author_t %} - This is {{ book_t }} by {{ author_t }} - {% endblocktrans %} - - {% blocktrans count list|length as counter %} - There is only one {{ name }} object. - {% plural %} - There are {{ counter }} {{ name }} objects. - {% endblocktrans %} - - {% blocktrans with name|escape as name count list|length as counter %} - There is only one {{ name }} object. - {% plural %} - There are {{ counter }} {{ name }} objects. - {% endblocktrans %} - - {% blocktrans %}This string will have {{ value }} inside.{% endblocktrans %} - - <p>{% trans "This is the title." %}</p> - - {% regroup people by gender as grouped %} -{% endblock %} diff --git a/ext/django2jinja/templates/layout.html b/ext/django2jinja/templates/layout.html deleted file mode 100644 index 3f21a12..0000000 --- a/ext/django2jinja/templates/layout.html +++ /dev/null @@ -1,4 +0,0 @@ -<title>{% block title %}{% endblock %}</title> -<div class="body"> - {% block page-body %}{% endblock %} -</div> diff --git a/ext/django2jinja/templates/subtemplate.html b/ext/django2jinja/templates/subtemplate.html deleted file mode 100644 index 980a0d5..0000000 --- a/ext/django2jinja/templates/subtemplate.html +++ /dev/null @@ -1 +0,0 @@ -Hello World! diff --git a/ext/djangojinja2.py b/ext/djangojinja2.py deleted file mode 100644 index c7b4884..0000000 --- a/ext/djangojinja2.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -""" - djangojinja2 - ~~~~~~~~~~~~ - - Adds support for Jinja2 to Django. - - Configuration variables: - - ======================= ============================================= - Key Description - ======================= ============================================= - `JINJA2_TEMPLATE_DIRS` List of template folders - `JINJA2_EXTENSIONS` List of Jinja2 extensions to use - `JINJA2_CACHE_SIZE` The size of the Jinja2 template cache. - ======================= ============================================= - - :copyright: (c) 2009 by the Jinja Team. - :license: BSD. -""" -from itertools import chain -from django.conf import settings -from django.http import HttpResponse -from django.core.exceptions import ImproperlyConfigured -from django.template.context import get_standard_processors -from django.template import TemplateDoesNotExist -from jinja2 import Environment, FileSystemLoader, TemplateNotFound -from jinja2.defaults import DEFAULT_NAMESPACE - - -# the environment is unconfigured until the first template is loaded. -_jinja_env = None - - -def get_env(): - """Get the Jinja2 env and initialize it if necessary.""" - global _jinja_env - if _jinja_env is None: - _jinja_env = create_env() - return _jinja_env - - -def create_env(): - """Create a new Jinja2 environment.""" - searchpath = list(settings.JINJA2_TEMPLATE_DIRS) - return Environment(loader=FileSystemLoader(searchpath), - auto_reload=settings.TEMPLATE_DEBUG, - cache_size=getattr(settings, 'JINJA2_CACHE_SIZE', 400), - extensions=getattr(settings, 'JINJA2_EXTENSIONS', ())) - - -def get_template(template_name, globals=None): - """Load a template.""" - try: - return get_env().get_template(template_name, globals=globals) - except TemplateNotFound, e: - raise TemplateDoesNotExist(str(e)) - - -def select_template(templates, globals=None): - """Try to load one of the given templates.""" - env = get_env() - for template in templates: - try: - return env.get_template(template, globals=globals) - except TemplateNotFound: - continue - raise TemplateDoesNotExist(', '.join(templates)) - - -def render_to_string(template_name, context=None, request=None, - processors=None): - """Render a template into a string.""" - context = dict(context or {}) - if request is not None: - context['request'] = request - for processor in chain(get_standard_processors(), processors or ()): - context.update(processor(request)) - return get_template(template_name).render(context) - - -def render_to_response(template_name, context=None, request=None, - processors=None, mimetype=None): - """Render a template into a response object.""" - return HttpResponse(render_to_string(template_name, context, request, - processors), mimetype=mimetype) diff --git a/ext/inlinegettext.py b/ext/inlinegettext.py deleted file mode 100644 index cf4ed5e..0000000 --- a/ext/inlinegettext.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Inline Gettext - ~~~~~~~~~~~~~~ - - An example extension for Jinja2 that supports inline gettext calls. - Requires the i18n extension to be loaded. - - :copyright: (c) 2009 by the Jinja Team. - :license: BSD. -""" -import re -from jinja2.ext import Extension -from jinja2.lexer import Token, count_newlines -from jinja2.exceptions import TemplateSyntaxError - - -_outside_re = re.compile(r'\\?(gettext|_)\(') -_inside_re = re.compile(r'\\?[()]') - - -class InlineGettext(Extension): - """This extension implements support for inline gettext blocks:: - - <h1>_(Welcome)</h1> - <p>_(This is a paragraph)</p> - - Requires the i18n extension to be loaded and configured. - """ - - def filter_stream(self, stream): - paren_stack = 0 - - for token in stream: - if token.type is not 'data': - yield token - continue - - pos = 0 - lineno = token.lineno - - while 1: - if not paren_stack: - match = _outside_re.search(token.value, pos) - else: - match = _inside_re.search(token.value, pos) - if match is None: - break - new_pos = match.start() - if new_pos > pos: - preval = token.value[pos:new_pos] - yield Token(lineno, 'data', preval) - lineno += count_newlines(preval) - gtok = match.group() - if gtok[0] == '\\': - yield Token(lineno, 'data', gtok[1:]) - elif not paren_stack: - yield Token(lineno, 'block_begin', None) - yield Token(lineno, 'name', 'trans') - yield Token(lineno, 'block_end', None) - paren_stack = 1 - else: - if gtok == '(' or paren_stack > 1: - yield Token(lineno, 'data', gtok) - paren_stack += gtok == ')' and -1 or 1 - if not paren_stack: - yield Token(lineno, 'block_begin', None) - yield Token(lineno, 'name', 'endtrans') - yield Token(lineno, 'block_end', None) - pos = match.end() - - if pos < len(token.value): - yield Token(lineno, 'data', token.value[pos:]) - - if paren_stack: - raise TemplateSyntaxError('unclosed gettext expression', - token.lineno, stream.name, - stream.filename) diff --git a/ext/jinja.el b/ext/jinja.el deleted file mode 100644 index 4cf0c72..0000000 --- a/ext/jinja.el +++ /dev/null @@ -1,128 +0,0 @@ -;;; jinja.el --- Jinja mode highlighting -;; -;; Author: Georg Brandl -;; Copyright: (c) 2009 by the Jinja Team -;; Last modified: 2008-05-22 23:04 by gbr -;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; -;;; Commentary: -;; -;; Mostly ripped off django-mode by Lennart Borgman. -;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; -;; This program is free software; you can redistribute it and/or -;; modify it under the terms of the GNU General Public License as -;; published by the Free Software Foundation; either version 2, or -;; (at your option) any later version. -;; -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -;; General Public License for more details. -;; -;; You should have received a copy of the GNU General Public License -;; along with this program; see the file COPYING. If not, write to -;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth -;; Floor, Boston, MA 02110-1301, USA. -;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; -;;; Code: - -(defconst jinja-font-lock-keywords - (list -; (cons (rx "{% comment %}" (submatch (0+ anything)) -; "{% endcomment %}") (list 1 font-lock-comment-face)) - '("{# ?\\(.*?\\) ?#}" . (1 font-lock-comment-face)) - '("{%-?\\|-?%}\\|{{\\|}}" . font-lock-preprocessor-face) - '("{#\\|#}" . font-lock-comment-delimiter-face) - ;; first word in a block is a command - '("{%-?[ \t\n]*\\([a-zA-Z_]+\\)" . (1 font-lock-keyword-face)) - ;; variables - '("\\({{ ?\\)\\([^|]*?\\)\\(|.*?\\)? ?}}" . (1 font-lock-variable-name-face)) - ;; keywords and builtins - (cons (rx word-start - (or "in" "as" "recursive" "not" "and" "or" "if" "else" - "import" "with" "without" "context") - word-end) - font-lock-keyword-face) - (cons (rx word-start - (or "true" "false" "none" "loop" "self" "super") - word-end) - font-lock-builtin-face) - ;; tests - '("\\(is\\)[ \t]*\\(not\\)[ \t]*\\([a-zA-Z_]+\\)" - (1 font-lock-keyword-face) (2 font-lock-keyword-face) - (3 font-lock-function-name-face)) - ;; builtin filters - (cons (rx - "|" (* space) - (submatch - (or "abs" "batch" "capitalize" "capture" "center" "count" "default" - "dformat" "dictsort" "e" "escape" "filesizeformat" "first" - "float" "format" "getattribute" "getitem" "groupby" "indent" - "int" "join" "jsonencode" "last" "length" "lower" "markdown" - "pprint" "random" "replace" "reverse" "round" "rst" "slice" - "sort" "string" "striptags" "sum" "textile" "title" "trim" - "truncate" "upper" "urlencode" "urlize" "wordcount" "wordwrap" - "xmlattr"))) - (list 1 font-lock-builtin-face)) - ) - "Minimal highlighting expressions for Jinja mode") - -(define-derived-mode jinja-mode nil "Jinja" - "Simple Jinja mode for use with `mumamo-mode'. -This mode only provides syntax highlighting." - ;;(set (make-local-variable 'comment-start) "{#") - ;;(set (make-local-variable 'comment-end) "#}") - (setq font-lock-defaults '(jinja-font-lock-keywords))) - -;; mumamo stuff -(when (require 'mumamo nil t) - - (defun mumamo-chunk-jinja3 (pos max) - "Find {# ... #}" - (mumamo-quick-chunk-forward pos max "{#" "#}" 'borders 'jinja-mode)) - - (defun mumamo-chunk-jinja2 (pos max) - "Find {{ ... }}" - (mumamo-quick-chunk-forward pos max "{{" "}}" 'borders 'jinja-mode)) - - (defun mumamo-chunk-jinja (pos max) - "Find {% ... %}" - (mumamo-quick-chunk-forward pos max "{%" "%}" 'borders 'jinja-mode)) - -;;;###autoload - (define-mumamo-multi-major-mode jinja-html-mumamo - "Turn on multiple major modes for Jinja with main mode `html-mode'. -This also covers inlined style and javascript." - ("Jinja HTML Family" html-mode - (mumamo-chunk-jinja - mumamo-chunk-jinja2 - mumamo-chunk-jinja3 - mumamo-chunk-inlined-style - mumamo-chunk-inlined-script - mumamo-chunk-style= - mumamo-chunk-onjs= - ))) - -;;;###autoload - (define-mumamo-multi-major-mode jinja-nxhtml-mumamo - "Turn on multiple major modes for Jinja with main mode `nxhtml-mode'. -This also covers inlined style and javascript." - ("Jinja nXhtml Family" nxhtml-mode - (mumamo-chunk-jinja - mumamo-chunk-jinja2 - mumamo-chunk-jinja3 - mumamo-chunk-inlined-style - mumamo-chunk-inlined-script - mumamo-chunk-style= - mumamo-chunk-onjs= - ))) - ) - -(provide 'jinja) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; jinja.el ends here diff --git a/jinja2/__init__.py b/jinja2/__init__.py deleted file mode 100644 index 3ce857a..0000000 --- a/jinja2/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2 - ~~~~~~ - - 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. - - Nutshell - -------- - - Here a small example of a Jinja2 template:: - - {% extends 'base.html' %} - {% block title %}Memberlist{% endblock %} - {% block content %} - <ul> - {% for user in users %} - <li><a href="{{ user.url }}">{{ user.username }}</a></li> - {% endfor %} - </ul> - {% endblock %} - - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -__docformat__ = 'restructuredtext en' -__version__ = "2.10.3" - -# high level interface -from jinja2.environment import Environment, Template - -# loaders -from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \ - DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader, \ - ModuleLoader - -# bytecode caches -from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \ - MemcachedBytecodeCache - -# undefined types -from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined, \ - make_logging_undefined - -# exceptions -from jinja2.exceptions import TemplateError, UndefinedError, \ - TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \ - TemplateAssertionError, TemplateRuntimeError - -# decorators and public utilities -from jinja2.filters import environmentfilter, contextfilter, \ - evalcontextfilter -from jinja2.utils import clear_caches, \ - environmentfunction, evalcontextfunction, contextfunction, \ - is_undefined, select_autoescape -from markupsafe import Markup, escape - -__all__ = [ - 'Environment', 'Template', 'BaseLoader', 'FileSystemLoader', - 'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader', - 'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache', - 'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined', - 'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound', - 'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError', - 'TemplateRuntimeError', - 'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape', - 'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined', - 'evalcontextfilter', 'evalcontextfunction', 'make_logging_undefined', - 'select_autoescape', -] - - -def _patch_async(): - from jinja2.utils import have_async_gen - if have_async_gen: - from jinja2.asyncsupport import patch_all - patch_all() - - -_patch_async() -del _patch_async diff --git a/jinja2/_identifier.py b/jinja2/_identifier.py deleted file mode 100644 index 2eac35d..0000000 --- a/jinja2/_identifier.py +++ /dev/null @@ -1,2 +0,0 @@ -# generated by scripts/generate_identifier_pattern.py -pattern = '·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯' diff --git a/jinja2/debug.py b/jinja2/debug.py deleted file mode 100644 index d3c1a3a..0000000 --- a/jinja2/debug.py +++ /dev/null @@ -1,378 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.debug - ~~~~~~~~~~~~ - - Implements the debug interface for Jinja. This module does some pretty - ugly stuff with the Python traceback system in order to achieve tracebacks - with correct line numbers, locals and contents. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import sys -import traceback -from types import TracebackType, CodeType -from jinja2.utils import missing, internal_code -from jinja2.exceptions import TemplateSyntaxError -from jinja2._compat import iteritems, reraise, PY2 - -# on pypy we can take advantage of transparent proxies -try: - from __pypy__ import tproxy -except ImportError: - tproxy = None - - -# how does the raise helper look like? -try: - exec("raise TypeError, 'foo'") -except SyntaxError: - raise_helper = 'raise __jinja_exception__[1]' -except TypeError: - raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]' - - -class TracebackFrameProxy(object): - """Proxies a traceback frame.""" - - def __init__(self, tb): - self.tb = tb - self._tb_next = None - - @property - def tb_next(self): - return self._tb_next - - def set_next(self, next): - if tb_set_next is not None: - try: - tb_set_next(self.tb, next and next.tb or None) - except Exception: - # this function can fail due to all the hackery it does - # on various python implementations. We just catch errors - # down and ignore them if necessary. - pass - self._tb_next = next - - @property - def is_jinja_frame(self): - return '__jinja_template__' in self.tb.tb_frame.f_globals - - def __getattr__(self, name): - return getattr(self.tb, name) - - -def make_frame_proxy(frame): - proxy = TracebackFrameProxy(frame) - if tproxy is None: - return proxy - def operation_handler(operation, *args, **kwargs): - if operation in ('__getattribute__', '__getattr__'): - return getattr(proxy, args[0]) - elif operation == '__setattr__': - proxy.__setattr__(*args, **kwargs) - else: - return getattr(proxy, operation)(*args, **kwargs) - return tproxy(TracebackType, operation_handler) - - -class ProcessedTraceback(object): - """Holds a Jinja preprocessed traceback for printing or reraising.""" - - def __init__(self, exc_type, exc_value, frames): - assert frames, 'no frames for this traceback?' - self.exc_type = exc_type - self.exc_value = exc_value - self.frames = frames - - # newly concatenate the frames (which are proxies) - prev_tb = None - for tb in self.frames: - if prev_tb is not None: - prev_tb.set_next(tb) - prev_tb = tb - prev_tb.set_next(None) - - def render_as_text(self, limit=None): - """Return a string with the traceback.""" - lines = traceback.format_exception(self.exc_type, self.exc_value, - self.frames[0], limit=limit) - return ''.join(lines).rstrip() - - def render_as_html(self, full=False): - """Return a unicode string with the traceback as rendered HTML.""" - from jinja2.debugrenderer import render_traceback - return u'%s\n\n<!--\n%s\n-->' % ( - render_traceback(self, full=full), - self.render_as_text().decode('utf-8', 'replace') - ) - - @property - def is_template_syntax_error(self): - """`True` if this is a template syntax error.""" - return isinstance(self.exc_value, TemplateSyntaxError) - - @property - def exc_info(self): - """Exception info tuple with a proxy around the frame objects.""" - return self.exc_type, self.exc_value, self.frames[0] - - @property - def standard_exc_info(self): - """Standard python exc_info for re-raising""" - tb = self.frames[0] - # the frame will be an actual traceback (or transparent proxy) if - # we are on pypy or a python implementation with support for tproxy - if type(tb) is not TracebackType: - tb = tb.tb - return self.exc_type, self.exc_value, tb - - -def make_traceback(exc_info, source_hint=None): - """Creates a processed traceback object from the exc_info.""" - exc_type, exc_value, tb = exc_info - if isinstance(exc_value, TemplateSyntaxError): - exc_info = translate_syntax_error(exc_value, source_hint) - initial_skip = 0 - else: - initial_skip = 1 - return translate_exception(exc_info, initial_skip) - - -def translate_syntax_error(error, source=None): - """Rewrites a syntax error to please traceback systems.""" - error.source = source - error.translated = True - exc_info = (error.__class__, error, None) - filename = error.filename - if filename is None: - filename = '<unknown>' - return fake_exc_info(exc_info, filename, error.lineno) - - -def translate_exception(exc_info, initial_skip=0): - """If passed an exc_info it will automatically rewrite the exceptions - all the way down to the correct line numbers and frames. - """ - tb = exc_info[2] - frames = [] - - # skip some internal frames if wanted - for x in range(initial_skip): - if tb is not None: - tb = tb.tb_next - initial_tb = tb - - while tb is not None: - # skip frames decorated with @internalcode. These are internal - # calls we can't avoid and that are useless in template debugging - # output. - if tb.tb_frame.f_code in internal_code: - tb = tb.tb_next - continue - - # save a reference to the next frame if we override the current - # one with a faked one. - next = tb.tb_next - - # fake template exceptions - template = tb.tb_frame.f_globals.get('__jinja_template__') - if template is not None: - lineno = template.get_corresponding_lineno(tb.tb_lineno) - tb = fake_exc_info(exc_info[:2] + (tb,), template.filename, - lineno)[2] - - frames.append(make_frame_proxy(tb)) - tb = next - - # if we don't have any exceptions in the frames left, we have to - # reraise it unchanged. - # XXX: can we backup here? when could this happen? - if not frames: - reraise(exc_info[0], exc_info[1], exc_info[2]) - - return ProcessedTraceback(exc_info[0], exc_info[1], frames) - - -def get_jinja_locals(real_locals): - ctx = real_locals.get('context') - if ctx: - locals = ctx.get_all().copy() - else: - locals = {} - - local_overrides = {} - - for name, value in iteritems(real_locals): - if not name.startswith('l_') or value is missing: - continue - try: - _, depth, name = name.split('_', 2) - depth = int(depth) - except ValueError: - continue - cur_depth = local_overrides.get(name, (-1,))[0] - if cur_depth < depth: - local_overrides[name] = (depth, value) - - for name, (_, value) in iteritems(local_overrides): - if value is missing: - locals.pop(name, None) - else: - locals[name] = value - - return locals - - -def fake_exc_info(exc_info, filename, lineno): - """Helper for `translate_exception`.""" - exc_type, exc_value, tb = exc_info - - # figure the real context out - if tb is not None: - locals = get_jinja_locals(tb.tb_frame.f_locals) - - # if there is a local called __jinja_exception__, we get - # rid of it to not break the debug functionality. - locals.pop('__jinja_exception__', None) - else: - locals = {} - - # assamble fake globals we need - globals = { - '__name__': filename, - '__file__': filename, - '__jinja_exception__': exc_info[:2], - - # we don't want to keep the reference to the template around - # to not cause circular dependencies, but we mark it as Jinja - # frame for the ProcessedTraceback - '__jinja_template__': None - } - - # and fake the exception - code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec') - - # if it's possible, change the name of the code. This won't work - # on some python environments such as google appengine - try: - if tb is None: - location = 'template' - else: - function = tb.tb_frame.f_code.co_name - if function == 'root': - location = 'top-level template code' - elif function.startswith('block_'): - location = 'block "%s"' % function[6:] - else: - location = 'template' - - if PY2: - code = CodeType(0, code.co_nlocals, code.co_stacksize, - code.co_flags, code.co_code, code.co_consts, - code.co_names, code.co_varnames, filename, - location, code.co_firstlineno, - code.co_lnotab, (), ()) - else: - code = CodeType(0, code.co_kwonlyargcount, - code.co_nlocals, code.co_stacksize, - code.co_flags, code.co_code, code.co_consts, - code.co_names, code.co_varnames, filename, - location, code.co_firstlineno, - code.co_lnotab, (), ()) - except Exception as e: - pass - - # execute the code and catch the new traceback - try: - exec(code, globals, locals) - except: - exc_info = sys.exc_info() - new_tb = exc_info[2].tb_next - - # return without this frame - return exc_info[:2] + (new_tb,) - - -def _init_ugly_crap(): - """This function implements a few ugly things so that we can patch the - traceback objects. The function returned allows resetting `tb_next` on - any python traceback object. Do not attempt to use this on non cpython - interpreters - """ - import ctypes - from types import TracebackType - - if PY2: - # figure out size of _Py_ssize_t for Python 2: - if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): - _Py_ssize_t = ctypes.c_int64 - else: - _Py_ssize_t = ctypes.c_int - else: - # platform ssize_t on Python 3 - _Py_ssize_t = ctypes.c_ssize_t - - # regular python - class _PyObject(ctypes.Structure): - pass - _PyObject._fields_ = [ - ('ob_refcnt', _Py_ssize_t), - ('ob_type', ctypes.POINTER(_PyObject)) - ] - - # python with trace - if hasattr(sys, 'getobjects'): - class _PyObject(ctypes.Structure): - pass - _PyObject._fields_ = [ - ('_ob_next', ctypes.POINTER(_PyObject)), - ('_ob_prev', ctypes.POINTER(_PyObject)), - ('ob_refcnt', _Py_ssize_t), - ('ob_type', ctypes.POINTER(_PyObject)) - ] - - class _Traceback(_PyObject): - pass - _Traceback._fields_ = [ - ('tb_next', ctypes.POINTER(_Traceback)), - ('tb_frame', ctypes.POINTER(_PyObject)), - ('tb_lasti', ctypes.c_int), - ('tb_lineno', ctypes.c_int) - ] - - def tb_set_next(tb, next): - """Set the tb_next attribute of a traceback object.""" - if not (isinstance(tb, TracebackType) and - (next is None or isinstance(next, TracebackType))): - raise TypeError('tb_set_next arguments must be traceback objects') - obj = _Traceback.from_address(id(tb)) - if tb.tb_next is not None: - old = _Traceback.from_address(id(tb.tb_next)) - old.ob_refcnt -= 1 - if next is None: - obj.tb_next = ctypes.POINTER(_Traceback)() - else: - next = _Traceback.from_address(id(next)) - next.ob_refcnt += 1 - obj.tb_next = ctypes.pointer(next) - - return tb_set_next - - -# try to get a tb_set_next implementation if we don't have transparent -# proxies. -tb_set_next = None -if tproxy is None: - # traceback.tb_next can be modified since CPython 3.7 - if sys.version_info >= (3, 7): - def tb_set_next(tb, next): - tb.tb_next = next - else: - # On Python 3.6 and older, use ctypes - try: - tb_set_next = _init_ugly_crap() - except Exception: - pass -del _init_ugly_crap diff --git a/jinja2/defaults.py b/jinja2/defaults.py deleted file mode 100644 index 7c93dec..0000000 --- a/jinja2/defaults.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.defaults - ~~~~~~~~~~~~~~~ - - Jinja default filters and tags. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -from jinja2._compat import range_type -from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner, Namespace - - -# defaults for the parser / lexer -BLOCK_START_STRING = '{%' -BLOCK_END_STRING = '%}' -VARIABLE_START_STRING = '{{' -VARIABLE_END_STRING = '}}' -COMMENT_START_STRING = '{#' -COMMENT_END_STRING = '#}' -LINE_STATEMENT_PREFIX = None -LINE_COMMENT_PREFIX = None -TRIM_BLOCKS = False -LSTRIP_BLOCKS = False -NEWLINE_SEQUENCE = '\n' -KEEP_TRAILING_NEWLINE = False - - -# default filters, tests and namespace -from jinja2.filters import FILTERS as DEFAULT_FILTERS -from jinja2.tests import TESTS as DEFAULT_TESTS -DEFAULT_NAMESPACE = { - 'range': range_type, - 'dict': dict, - 'lipsum': generate_lorem_ipsum, - 'cycler': Cycler, - 'joiner': Joiner, - 'namespace': Namespace -} - - -# default policies -DEFAULT_POLICIES = { - 'compiler.ascii_str': True, - 'urlize.rel': 'noopener', - 'urlize.target': None, - 'truncate.leeway': 5, - 'json.dumps_function': None, - 'json.dumps_kwargs': {'sort_keys': True}, - 'ext.i18n.trimmed': False, -} - - -# export all constants -__all__ = tuple(x for x in locals().keys() if x.isupper()) diff --git a/jinja2/lexer.py b/jinja2/lexer.py deleted file mode 100644 index 6fd135d..0000000 --- a/jinja2/lexer.py +++ /dev/null @@ -1,739 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.lexer - ~~~~~~~~~~~~ - - This module implements a Jinja / Python combination lexer. The - `Lexer` class provided by this module is used to do some preprocessing - for Jinja. - - On the one hand it filters out invalid operators like the bitshift - operators we don't allow in templates. On the other hand it separates - template code and python code in expressions. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import re -from collections import deque -from operator import itemgetter - -from jinja2._compat import implements_iterator, intern, iteritems, text_type -from jinja2.exceptions import TemplateSyntaxError -from jinja2.utils import LRUCache - -# cache for the lexers. Exists in order to be able to have multiple -# environments with the same lexer -_lexer_cache = LRUCache(50) - -# static regular expressions -whitespace_re = re.compile(r'\s+', re.U) -string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'" - r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) -integer_re = re.compile(r'\d+') - -try: - # check if this Python supports Unicode identifiers - compile('föö', '<unknown>', 'eval') -except SyntaxError: - # no Unicode support, use ASCII identifiers - name_re = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*') - check_ident = False -else: - # Unicode support, build a pattern to match valid characters, and set flag - # to use str.isidentifier to validate during lexing - from jinja2 import _identifier - name_re = re.compile(r'[\w{0}]+'.format(_identifier.pattern)) - check_ident = True - # remove the pattern from memory after building the regex - import sys - del sys.modules['jinja2._identifier'] - import jinja2 - del jinja2._identifier - del _identifier - -float_re = re.compile(r'(?<!\.)\d+\.\d+') -newline_re = re.compile(r'(\r\n|\r|\n)') - -# internal the tokens and keep references to them -TOKEN_ADD = intern('add') -TOKEN_ASSIGN = intern('assign') -TOKEN_COLON = intern('colon') -TOKEN_COMMA = intern('comma') -TOKEN_DIV = intern('div') -TOKEN_DOT = intern('dot') -TOKEN_EQ = intern('eq') -TOKEN_FLOORDIV = intern('floordiv') -TOKEN_GT = intern('gt') -TOKEN_GTEQ = intern('gteq') -TOKEN_LBRACE = intern('lbrace') -TOKEN_LBRACKET = intern('lbracket') -TOKEN_LPAREN = intern('lparen') -TOKEN_LT = intern('lt') -TOKEN_LTEQ = intern('lteq') -TOKEN_MOD = intern('mod') -TOKEN_MUL = intern('mul') -TOKEN_NE = intern('ne') -TOKEN_PIPE = intern('pipe') -TOKEN_POW = intern('pow') -TOKEN_RBRACE = intern('rbrace') -TOKEN_RBRACKET = intern('rbracket') -TOKEN_RPAREN = intern('rparen') -TOKEN_SEMICOLON = intern('semicolon') -TOKEN_SUB = intern('sub') -TOKEN_TILDE = intern('tilde') -TOKEN_WHITESPACE = intern('whitespace') -TOKEN_FLOAT = intern('float') -TOKEN_INTEGER = intern('integer') -TOKEN_NAME = intern('name') -TOKEN_STRING = intern('string') -TOKEN_OPERATOR = intern('operator') -TOKEN_BLOCK_BEGIN = intern('block_begin') -TOKEN_BLOCK_END = intern('block_end') -TOKEN_VARIABLE_BEGIN = intern('variable_begin') -TOKEN_VARIABLE_END = intern('variable_end') -TOKEN_RAW_BEGIN = intern('raw_begin') -TOKEN_RAW_END = intern('raw_end') -TOKEN_COMMENT_BEGIN = intern('comment_begin') -TOKEN_COMMENT_END = intern('comment_end') -TOKEN_COMMENT = intern('comment') -TOKEN_LINESTATEMENT_BEGIN = intern('linestatement_begin') -TOKEN_LINESTATEMENT_END = intern('linestatement_end') -TOKEN_LINECOMMENT_BEGIN = intern('linecomment_begin') -TOKEN_LINECOMMENT_END = intern('linecomment_end') -TOKEN_LINECOMMENT = intern('linecomment') -TOKEN_DATA = intern('data') -TOKEN_INITIAL = intern('initial') -TOKEN_EOF = intern('eof') - -# bind operators to token types -operators = { - '+': TOKEN_ADD, - '-': TOKEN_SUB, - '/': TOKEN_DIV, - '//': TOKEN_FLOORDIV, - '*': TOKEN_MUL, - '%': TOKEN_MOD, - '**': TOKEN_POW, - '~': TOKEN_TILDE, - '[': TOKEN_LBRACKET, - ']': TOKEN_RBRACKET, - '(': TOKEN_LPAREN, - ')': TOKEN_RPAREN, - '{': TOKEN_LBRACE, - '}': TOKEN_RBRACE, - '==': TOKEN_EQ, - '!=': TOKEN_NE, - '>': TOKEN_GT, - '>=': TOKEN_GTEQ, - '<': TOKEN_LT, - '<=': TOKEN_LTEQ, - '=': TOKEN_ASSIGN, - '.': TOKEN_DOT, - ':': TOKEN_COLON, - '|': TOKEN_PIPE, - ',': TOKEN_COMMA, - ';': TOKEN_SEMICOLON -} - -reverse_operators = dict([(v, k) for k, v in iteritems(operators)]) -assert len(operators) == len(reverse_operators), 'operators dropped' -operator_re = re.compile('(%s)' % '|'.join(re.escape(x) for x in - sorted(operators, key=lambda x: -len(x)))) - -ignored_tokens = frozenset([TOKEN_COMMENT_BEGIN, TOKEN_COMMENT, - TOKEN_COMMENT_END, TOKEN_WHITESPACE, - TOKEN_LINECOMMENT_BEGIN, TOKEN_LINECOMMENT_END, - TOKEN_LINECOMMENT]) -ignore_if_empty = frozenset([TOKEN_WHITESPACE, TOKEN_DATA, - TOKEN_COMMENT, TOKEN_LINECOMMENT]) - - -def _describe_token_type(token_type): - if token_type in reverse_operators: - return reverse_operators[token_type] - return { - TOKEN_COMMENT_BEGIN: 'begin of comment', - TOKEN_COMMENT_END: 'end of comment', - TOKEN_COMMENT: 'comment', - TOKEN_LINECOMMENT: 'comment', - TOKEN_BLOCK_BEGIN: 'begin of statement block', - TOKEN_BLOCK_END: 'end of statement block', - TOKEN_VARIABLE_BEGIN: 'begin of print statement', - TOKEN_VARIABLE_END: 'end of print statement', - TOKEN_LINESTATEMENT_BEGIN: 'begin of line statement', - TOKEN_LINESTATEMENT_END: 'end of line statement', - TOKEN_DATA: 'template data / text', - TOKEN_EOF: 'end of template' - }.get(token_type, token_type) - - -def describe_token(token): - """Returns a description of the token.""" - if token.type == 'name': - return token.value - return _describe_token_type(token.type) - - -def describe_token_expr(expr): - """Like `describe_token` but for token expressions.""" - if ':' in expr: - type, value = expr.split(':', 1) - if type == 'name': - return value - else: - type = expr - return _describe_token_type(type) - - -def count_newlines(value): - """Count the number of newline characters in the string. This is - useful for extensions that filter a stream. - """ - return len(newline_re.findall(value)) - - -def compile_rules(environment): - """Compiles all the rules from the environment into a list of rules.""" - e = re.escape - rules = [ - (len(environment.comment_start_string), 'comment', - e(environment.comment_start_string)), - (len(environment.block_start_string), 'block', - e(environment.block_start_string)), - (len(environment.variable_start_string), 'variable', - e(environment.variable_start_string)) - ] - - if environment.line_statement_prefix is not None: - rules.append((len(environment.line_statement_prefix), 'linestatement', - r'^[ \t\v]*' + e(environment.line_statement_prefix))) - if environment.line_comment_prefix is not None: - rules.append((len(environment.line_comment_prefix), 'linecomment', - r'(?:^|(?<=\S))[^\S\r\n]*' + - e(environment.line_comment_prefix))) - - return [x[1:] for x in sorted(rules, reverse=True)] - - -class Failure(object): - """Class that raises a `TemplateSyntaxError` if called. - Used by the `Lexer` to specify known errors. - """ - - def __init__(self, message, cls=TemplateSyntaxError): - self.message = message - self.error_class = cls - - def __call__(self, lineno, filename): - raise self.error_class(self.message, lineno, filename) - - -class Token(tuple): - """Token class.""" - __slots__ = () - lineno, type, value = (property(itemgetter(x)) for x in range(3)) - - def __new__(cls, lineno, type, value): - return tuple.__new__(cls, (lineno, intern(str(type)), value)) - - def __str__(self): - if self.type in reverse_operators: - return reverse_operators[self.type] - elif self.type == 'name': - return self.value - return self.type - - def test(self, expr): - """Test a token against a token expression. This can either be a - token type or ``'token_type:token_value'``. This can only test - against string values and types. - """ - # here we do a regular string equality check as test_any is usually - # passed an iterable of not interned strings. - if self.type == expr: - return True - elif ':' in expr: - return expr.split(':', 1) == [self.type, self.value] - return False - - def test_any(self, *iterable): - """Test against multiple token expressions.""" - for expr in iterable: - if self.test(expr): - return True - return False - - def __repr__(self): - return 'Token(%r, %r, %r)' % ( - self.lineno, - self.type, - self.value - ) - - -@implements_iterator -class TokenStreamIterator(object): - """The iterator for tokenstreams. Iterate over the stream - until the eof token is reached. - """ - - def __init__(self, stream): - self.stream = stream - - def __iter__(self): - return self - - def __next__(self): - token = self.stream.current - if token.type is TOKEN_EOF: - self.stream.close() - raise StopIteration() - next(self.stream) - return token - - -@implements_iterator -class TokenStream(object): - """A token stream is an iterable that yields :class:`Token`\\s. The - parser however does not iterate over it but calls :meth:`next` to go - one token ahead. The current active token is stored as :attr:`current`. - """ - - def __init__(self, generator, name, filename): - self._iter = iter(generator) - self._pushed = deque() - self.name = name - self.filename = filename - self.closed = False - self.current = Token(1, TOKEN_INITIAL, '') - next(self) - - def __iter__(self): - return TokenStreamIterator(self) - - def __bool__(self): - return bool(self._pushed) or self.current.type is not TOKEN_EOF - __nonzero__ = __bool__ # py2 - - eos = property(lambda x: not x, doc="Are we at the end of the stream?") - - def push(self, token): - """Push a token back to the stream.""" - self._pushed.append(token) - - def look(self): - """Look at the next token.""" - old_token = next(self) - result = self.current - self.push(result) - self.current = old_token - return result - - def skip(self, n=1): - """Got n tokens ahead.""" - for x in range(n): - next(self) - - def next_if(self, expr): - """Perform the token test and return the token if it matched. - Otherwise the return value is `None`. - """ - if self.current.test(expr): - return next(self) - - def skip_if(self, expr): - """Like :meth:`next_if` but only returns `True` or `False`.""" - return self.next_if(expr) is not None - - def __next__(self): - """Go one token ahead and return the old one. - - Use the built-in :func:`next` instead of calling this directly. - """ - rv = self.current - if self._pushed: - self.current = self._pushed.popleft() - elif self.current.type is not TOKEN_EOF: - try: - self.current = next(self._iter) - except StopIteration: - self.close() - return rv - - def close(self): - """Close the stream.""" - self.current = Token(self.current.lineno, TOKEN_EOF, '') - self._iter = None - self.closed = True - - def expect(self, expr): - """Expect a given token type and return it. This accepts the same - argument as :meth:`jinja2.lexer.Token.test`. - """ - if not self.current.test(expr): - expr = describe_token_expr(expr) - if self.current.type is TOKEN_EOF: - raise TemplateSyntaxError('unexpected end of template, ' - 'expected %r.' % expr, - self.current.lineno, - self.name, self.filename) - raise TemplateSyntaxError("expected token %r, got %r" % - (expr, describe_token(self.current)), - self.current.lineno, - self.name, self.filename) - try: - return self.current - finally: - next(self) - - -def get_lexer(environment): - """Return a lexer which is probably cached.""" - key = (environment.block_start_string, - environment.block_end_string, - environment.variable_start_string, - environment.variable_end_string, - environment.comment_start_string, - environment.comment_end_string, - environment.line_statement_prefix, - environment.line_comment_prefix, - environment.trim_blocks, - environment.lstrip_blocks, - environment.newline_sequence, - environment.keep_trailing_newline) - lexer = _lexer_cache.get(key) - if lexer is None: - lexer = Lexer(environment) - _lexer_cache[key] = lexer - return lexer - - -class Lexer(object): - """Class that implements a lexer for a given environment. Automatically - created by the environment class, usually you don't have to do that. - - Note that the lexer is not automatically bound to an environment. - Multiple environments can share the same lexer. - """ - - def __init__(self, environment): - # shortcuts - c = lambda x: re.compile(x, re.M | re.S) - e = re.escape - - # lexing rules for tags - tag_rules = [ - (whitespace_re, TOKEN_WHITESPACE, None), - (float_re, TOKEN_FLOAT, None), - (integer_re, TOKEN_INTEGER, None), - (name_re, TOKEN_NAME, None), - (string_re, TOKEN_STRING, None), - (operator_re, TOKEN_OPERATOR, None) - ] - - # assemble the root lexing rule. because "|" is ungreedy - # we have to sort by length so that the lexer continues working - # as expected when we have parsing rules like <% for block and - # <%= for variables. (if someone wants asp like syntax) - # variables are just part of the rules if variable processing - # is required. - root_tag_rules = compile_rules(environment) - - # block suffix if trimming is enabled - block_suffix_re = environment.trim_blocks and '\\n?' or '' - - # strip leading spaces if lstrip_blocks is enabled - prefix_re = {} - if environment.lstrip_blocks: - # use '{%+' to manually disable lstrip_blocks behavior - no_lstrip_re = e('+') - # detect overlap between block and variable or comment strings - block_diff = c(r'^%s(.*)' % e(environment.block_start_string)) - # make sure we don't mistake a block for a variable or a comment - m = block_diff.match(environment.comment_start_string) - no_lstrip_re += m and r'|%s' % e(m.group(1)) or '' - m = block_diff.match(environment.variable_start_string) - no_lstrip_re += m and r'|%s' % e(m.group(1)) or '' - - # detect overlap between comment and variable strings - comment_diff = c(r'^%s(.*)' % e(environment.comment_start_string)) - m = comment_diff.match(environment.variable_start_string) - no_variable_re = m and r'(?!%s)' % e(m.group(1)) or '' - - lstrip_re = r'^[ \t]*' - block_prefix_re = r'%s%s(?!%s)|%s\+?' % ( - lstrip_re, - e(environment.block_start_string), - no_lstrip_re, - e(environment.block_start_string), - ) - comment_prefix_re = r'%s%s%s|%s\+?' % ( - lstrip_re, - e(environment.comment_start_string), - no_variable_re, - e(environment.comment_start_string), - ) - prefix_re['block'] = block_prefix_re - prefix_re['comment'] = comment_prefix_re - else: - block_prefix_re = '%s' % e(environment.block_start_string) - - self.newline_sequence = environment.newline_sequence - self.keep_trailing_newline = environment.keep_trailing_newline - - # global lexing rules - self.rules = { - 'root': [ - # directives - (c('(.*?)(?:%s)' % '|'.join( - [r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % ( - e(environment.block_start_string), - block_prefix_re, - e(environment.block_end_string), - e(environment.block_end_string) - )] + [ - r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, prefix_re.get(n,r)) - for n, r in root_tag_rules - ])), (TOKEN_DATA, '#bygroup'), '#bygroup'), - # data - (c('.+'), TOKEN_DATA, None) - ], - # comments - TOKEN_COMMENT_BEGIN: [ - (c(r'(.*?)((?:\-%s\s*|%s)%s)' % ( - e(environment.comment_end_string), - e(environment.comment_end_string), - block_suffix_re - )), (TOKEN_COMMENT, TOKEN_COMMENT_END), '#pop'), - (c('(.)'), (Failure('Missing end of comment tag'),), None) - ], - # blocks - TOKEN_BLOCK_BEGIN: [ - (c(r'(?:\-%s\s*|%s)%s' % ( - e(environment.block_end_string), - e(environment.block_end_string), - block_suffix_re - )), TOKEN_BLOCK_END, '#pop'), - ] + tag_rules, - # variables - TOKEN_VARIABLE_BEGIN: [ - (c(r'\-%s\s*|%s' % ( - e(environment.variable_end_string), - e(environment.variable_end_string) - )), TOKEN_VARIABLE_END, '#pop') - ] + tag_rules, - # raw block - TOKEN_RAW_BEGIN: [ - (c(r'(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % ( - e(environment.block_start_string), - block_prefix_re, - e(environment.block_end_string), - e(environment.block_end_string), - block_suffix_re - )), (TOKEN_DATA, TOKEN_RAW_END), '#pop'), - (c('(.)'), (Failure('Missing end of raw directive'),), None) - ], - # line statements - TOKEN_LINESTATEMENT_BEGIN: [ - (c(r'\s*(\n|$)'), TOKEN_LINESTATEMENT_END, '#pop') - ] + tag_rules, - # line comments - TOKEN_LINECOMMENT_BEGIN: [ - (c(r'(.*?)()(?=\n|$)'), (TOKEN_LINECOMMENT, - TOKEN_LINECOMMENT_END), '#pop') - ] - } - - def _normalize_newlines(self, value): - """Called for strings and template data to normalize it to unicode.""" - return newline_re.sub(self.newline_sequence, value) - - def tokenize(self, source, name=None, filename=None, state=None): - """Calls tokeniter + tokenize and wraps it in a token stream. - """ - stream = self.tokeniter(source, name, filename, state) - return TokenStream(self.wrap(stream, name, filename), name, filename) - - def wrap(self, stream, name=None, filename=None): - """This is called with the stream as returned by `tokenize` and wraps - every token in a :class:`Token` and converts the value. - """ - for lineno, token, value in stream: - if token in ignored_tokens: - continue - elif token == 'linestatement_begin': - token = 'block_begin' - elif token == 'linestatement_end': - token = 'block_end' - # we are not interested in those tokens in the parser - elif token in ('raw_begin', 'raw_end'): - continue - elif token == 'data': - value = self._normalize_newlines(value) - elif token == 'keyword': - token = value - elif token == 'name': - value = str(value) - if check_ident and not value.isidentifier(): - raise TemplateSyntaxError( - 'Invalid character in identifier', - lineno, name, filename) - elif token == 'string': - # try to unescape string - try: - value = self._normalize_newlines(value[1:-1]) \ - .encode('ascii', 'backslashreplace') \ - .decode('unicode-escape') - except Exception as e: - msg = str(e).split(':')[-1].strip() - raise TemplateSyntaxError(msg, lineno, name, filename) - elif token == 'integer': - value = int(value) - elif token == 'float': - value = float(value) - elif token == 'operator': - token = operators[value] - yield Token(lineno, token, value) - - def tokeniter(self, source, name, filename=None, state=None): - """This method tokenizes the text and returns the tokens in a - generator. Use this method if you just want to tokenize a template. - """ - source = text_type(source) - lines = source.splitlines() - if self.keep_trailing_newline and source: - for newline in ('\r\n', '\r', '\n'): - if source.endswith(newline): - lines.append('') - break - source = '\n'.join(lines) - pos = 0 - lineno = 1 - stack = ['root'] - if state is not None and state != 'root': - assert state in ('variable', 'block'), 'invalid state' - stack.append(state + '_begin') - else: - state = 'root' - statetokens = self.rules[stack[-1]] - source_length = len(source) - - balancing_stack = [] - - while 1: - # tokenizer loop - for regex, tokens, new_state in statetokens: - m = regex.match(source, pos) - # if no match we try again with the next rule - if m is None: - continue - - # we only match blocks and variables if braces / parentheses - # are balanced. continue parsing with the lower rule which - # is the operator rule. do this only if the end tags look - # like operators - if balancing_stack and \ - tokens in ('variable_end', 'block_end', - 'linestatement_end'): - continue - - # tuples support more options - if isinstance(tokens, tuple): - for idx, token in enumerate(tokens): - # failure group - if token.__class__ is Failure: - raise token(lineno, filename) - # bygroup is a bit more complex, in that case we - # yield for the current token the first named - # group that matched - elif token == '#bygroup': - for key, value in iteritems(m.groupdict()): - if value is not None: - yield lineno, key, value - lineno += value.count('\n') - break - else: - raise RuntimeError('%r wanted to resolve ' - 'the token dynamically' - ' but no group matched' - % regex) - # normal group - else: - data = m.group(idx + 1) - if data or token not in ignore_if_empty: - yield lineno, token, data - lineno += data.count('\n') - - # strings as token just are yielded as it. - else: - data = m.group() - # update brace/parentheses balance - if tokens == 'operator': - if data == '{': - balancing_stack.append('}') - elif data == '(': - balancing_stack.append(')') - elif data == '[': - balancing_stack.append(']') - elif data in ('}', ')', ']'): - if not balancing_stack: - raise TemplateSyntaxError('unexpected \'%s\'' % - data, lineno, name, - filename) - expected_op = balancing_stack.pop() - if expected_op != data: - raise TemplateSyntaxError('unexpected \'%s\', ' - 'expected \'%s\'' % - (data, expected_op), - lineno, name, - filename) - # yield items - if data or tokens not in ignore_if_empty: - yield lineno, tokens, data - lineno += data.count('\n') - - # fetch new position into new variable so that we can check - # if there is a internal parsing error which would result - # in an infinite loop - pos2 = m.end() - - # handle state changes - if new_state is not None: - # remove the uppermost state - if new_state == '#pop': - stack.pop() - # resolve the new state by group checking - elif new_state == '#bygroup': - for key, value in iteritems(m.groupdict()): - if value is not None: - stack.append(key) - break - else: - raise RuntimeError('%r wanted to resolve the ' - 'new state dynamically but' - ' no group matched' % - regex) - # direct state name given - else: - stack.append(new_state) - statetokens = self.rules[stack[-1]] - # we are still at the same position and no stack change. - # this means a loop without break condition, avoid that and - # raise error - elif pos2 == pos: - raise RuntimeError('%r yielded empty string without ' - 'stack change' % regex) - # publish new function and start again - pos = pos2 - break - # if loop terminated without break we haven't found a single match - # either we are at the end of the file or we have a problem - else: - # end of text - if pos >= source_length: - return - # something went wrong - raise TemplateSyntaxError('unexpected char %r at %d' % - (source[pos], pos), lineno, - name, filename) diff --git a/jinja2/nativetypes.py b/jinja2/nativetypes.py deleted file mode 100644 index eab95d6..0000000 --- a/jinja2/nativetypes.py +++ /dev/null @@ -1,223 +0,0 @@ -import sys -from ast import literal_eval - -from markupsafe import escape - -from itertools import islice, chain -from jinja2 import nodes -from jinja2._compat import text_type -from jinja2.compiler import CodeGenerator, has_safe_repr -from jinja2.environment import Environment, Template -from jinja2.utils import concat - - -def native_concat(nodes): - """Return a native Python type from the list of compiled nodes. If the - result is a single node, its value is returned. Otherwise, the nodes are - concatenated as strings. If the result can be parsed with - :func:`ast.literal_eval`, the parsed value is returned. Otherwise, the - string is returned. - """ - head = list(islice(nodes, 2)) - - if not head: - return None - - if len(head) == 1: - out = head[0] - else: - out = u''.join([text_type(v) for v in chain(head, nodes)]) - - try: - return literal_eval(out) - except (ValueError, SyntaxError, MemoryError): - return out - - -class NativeCodeGenerator(CodeGenerator): - """A code generator which avoids injecting ``to_string()`` calls around the - internal code Jinja uses to render templates. - """ - - def visit_Output(self, node, frame): - """Same as :meth:`CodeGenerator.visit_Output`, but do not call - ``to_string`` on output nodes in generated code. - """ - if self.has_known_extends and frame.require_output_check: - return - - finalize = self.environment.finalize - finalize_context = getattr(finalize, 'contextfunction', False) - finalize_eval = getattr(finalize, 'evalcontextfunction', False) - finalize_env = getattr(finalize, 'environmentfunction', False) - - if finalize is not None: - if finalize_context or finalize_eval: - const_finalize = None - elif finalize_env: - def const_finalize(x): - return finalize(self.environment, x) - else: - const_finalize = finalize - else: - def const_finalize(x): - return x - - # If we are inside a frame that requires output checking, we do so. - outdent_later = False - - if frame.require_output_check: - self.writeline('if parent_template is None:') - self.indent() - outdent_later = True - - # Try to evaluate as many chunks as possible into a static string at - # compile time. - body = [] - - for child in node.nodes: - try: - if const_finalize is None: - raise nodes.Impossible() - - const = child.as_const(frame.eval_ctx) - if not has_safe_repr(const): - raise nodes.Impossible() - except nodes.Impossible: - body.append(child) - continue - - # the frame can't be volatile here, because otherwise the as_const - # function would raise an Impossible exception at that point - try: - if frame.eval_ctx.autoescape: - if hasattr(const, '__html__'): - const = const.__html__() - else: - const = escape(const) - - const = const_finalize(const) - except Exception: - # if something goes wrong here we evaluate the node at runtime - # for easier debugging - body.append(child) - continue - - if body and isinstance(body[-1], list): - body[-1].append(const) - else: - body.append([const]) - - # if we have less than 3 nodes or a buffer we yield or extend/append - if len(body) < 3 or frame.buffer is not None: - if frame.buffer is not None: - # for one item we append, for more we extend - if len(body) == 1: - self.writeline('%s.append(' % frame.buffer) - else: - self.writeline('%s.extend((' % frame.buffer) - - self.indent() - - for item in body: - if isinstance(item, list): - val = repr(native_concat(item)) - - if frame.buffer is None: - self.writeline('yield ' + val) - else: - self.writeline(val + ',') - else: - if frame.buffer is None: - self.writeline('yield ', item) - else: - self.newline(item) - - close = 0 - - if finalize is not None: - self.write('environment.finalize(') - - if finalize_context: - self.write('context, ') - - close += 1 - - self.visit(item, frame) - - if close > 0: - self.write(')' * close) - - if frame.buffer is not None: - self.write(',') - - if frame.buffer is not None: - # close the open parentheses - self.outdent() - self.writeline(len(body) == 1 and ')' or '))') - - # otherwise we create a format string as this is faster in that case - else: - format = [] - arguments = [] - - for item in body: - if isinstance(item, list): - format.append(native_concat(item).replace('%', '%%')) - else: - format.append('%s') - arguments.append(item) - - self.writeline('yield ') - self.write(repr(concat(format)) + ' % (') - self.indent() - - for argument in arguments: - self.newline(argument) - close = 0 - - if finalize is not None: - self.write('environment.finalize(') - - if finalize_context: - self.write('context, ') - elif finalize_eval: - self.write('context.eval_ctx, ') - elif finalize_env: - self.write('environment, ') - - close += 1 - - self.visit(argument, frame) - self.write(')' * close + ', ') - - self.outdent() - self.writeline(')') - - if outdent_later: - self.outdent() - - -class NativeTemplate(Template): - def render(self, *args, **kwargs): - """Render the template to produce a native Python type. If the result - is a single node, its value is returned. Otherwise, the nodes are - concatenated as strings. If the result can be parsed with - :func:`ast.literal_eval`, the parsed value is returned. Otherwise, the - string is returned. - """ - vars = dict(*args, **kwargs) - - try: - return native_concat(self.root_render_func(self.new_context(vars))) - except Exception: - exc_info = sys.exc_info() - - return self.environment.handle_exception(exc_info, True) - - -class NativeEnvironment(Environment): - """An environment that renders templates to native Python types.""" - - code_generator_class = NativeCodeGenerator - template_class = NativeTemplate diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py deleted file mode 100644 index 65ab3ce..0000000 --- a/jinja2/optimizer.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.optimizer - ~~~~~~~~~~~~~~~~ - - The jinja optimizer is currently trying to constant fold a few expressions - and modify the AST in place so that it should be easier to evaluate it. - - Because the AST does not contain all the scoping information and the - compiler has to find that out, we cannot do all the optimizations we - want. For example loop unrolling doesn't work because unrolled loops would - have a different scoping. - - The solution would be a second syntax tree that has the scoping rules stored. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. -""" -from jinja2 import nodes -from jinja2.visitor import NodeTransformer - - -def optimize(node, environment): - """The context hint can be used to perform an static optimization - based on the context given.""" - optimizer = Optimizer(environment) - return optimizer.visit(node) - - -class Optimizer(NodeTransformer): - - def __init__(self, environment): - self.environment = environment - - def fold(self, node, eval_ctx=None): - """Do constant folding.""" - node = self.generic_visit(node) - try: - return nodes.Const.from_untrusted(node.as_const(eval_ctx), - lineno=node.lineno, - environment=self.environment) - except nodes.Impossible: - return node - - visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \ - visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \ - visit_Not = visit_Compare = visit_Getitem = visit_Getattr = visit_Call = \ - visit_Filter = visit_Test = visit_CondExpr = fold - del fold diff --git a/scripts/generate_identifier_pattern.py b/scripts/generate_identifier_pattern.py index 7db5f4a..5813199 100755 --- a/scripts/generate_identifier_pattern.py +++ b/scripts/generate_identifier_pattern.py @@ -5,14 +5,14 @@ import re import sys if sys.version_info[0] < 3: - raise RuntimeError('This needs to run on Python 3.') + raise RuntimeError("This needs to run on Python 3.") def get_characters(): """Find every Unicode character that is valid in a Python `identifier`_ but - is not matched by the regex ``\w`` group. + is not matched by the regex ``\\w`` group. - ``\w`` matches some characters that aren't valid in identifiers, but + ``\\w`` matches some characters that aren't valid in identifiers, but :meth:`str.isidentifier` will catch that later in lexing. All start characters are valid continue characters, so we only test for @@ -23,7 +23,7 @@ def get_characters(): for cp in range(sys.maxunicode + 1): s = chr(cp) - if ('a' + s).isidentifier() and not re.match(r'\w', s): + if ("a" + s).isidentifier() and not re.match(r"\w", s): yield s @@ -33,10 +33,7 @@ def collapse_ranges(data): Source: https://stackoverflow.com/a/4629241/400617 """ - for a, b in itertools.groupby( - enumerate(data), - lambda x: ord(x[1]) - x[0] - ): + for _, b in itertools.groupby(enumerate(data), lambda x: ord(x[1]) - x[0]): b = list(b) yield b[0][1], b[-1][1] @@ -55,23 +52,27 @@ def build_pattern(ranges): out.append(a) out.append(b) else: - out.append(f'{a}-{b}') + out.append("{}-{}".format(a, b)) - return ''.join(out) + return "".join(out) def main(): - """Build the regex pattern and write it to the file - :file:`jinja2/_identifier.py`.""" + """Build the regex pattern and write it to + ``jinja2/_identifier.py``. + """ pattern = build_pattern(collapse_ranges(get_characters())) - filename = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..', 'jinja2', '_identifier.py' - )) + filename = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "src", "jinja2", "_identifier.py") + ) - with open(filename, 'w', encoding='utf8') as f: - f.write('# generated by scripts/generate_identifier_pattern.py\n') - f.write(f'pattern = \'{pattern}\'\n') + with open(filename, "w", encoding="utf8") as f: + f.write("import re\n\n") + f.write("# generated by scripts/generate_identifier_pattern.py\n") + f.write("pattern = re.compile(\n") + f.write(' r"[\\w{}]+" # noqa: B950\n'.format(pattern)) + f.write(")\n") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/scripts/jinja2-debug.py b/scripts/jinja2-debug.py deleted file mode 100755 index d052adc..0000000 --- a/scripts/jinja2-debug.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - Jinja2 Debug Interface - ~~~~~~~~~~~~~~~~~~~~~~ - - Helper script for internal Jinja2 debugging. Requires Werkzeug. - - :copyright: Copyright 2010 by Armin Ronacher. - :license: BSD. -""" -import sys -import jinja2 -from werkzeug import script - -env = jinja2.Environment(extensions=['jinja2.ext.i18n', 'jinja2.ext.do', - 'jinja2.ext.loopcontrols', - 'jinja2.ext.with_', - 'jinja2.ext.autoescape'], - autoescape=True) - -def shell_init_func(): - def _compile(x): - print(env.compile(x, raw=True)) - result = { - 'e': env, - 'c': _compile, - 't': env.from_string, - 'p': env.parse - } - for key in jinja2.__all__: - result[key] = getattr(jinja2, key) - return result - - -def action_compile(): - print(env.compile(sys.stdin.read(), raw=True)) - -action_shell = script.make_shell(shell_init_func) - - -if __name__ == '__main__': - script.run() diff --git a/scripts/make-release.py b/scripts/make-release.py deleted file mode 100644 index 51c3700..0000000 --- a/scripts/make-release.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" - make-release - ~~~~~~~~~~~~ - - Helper script that performs a release. Does pretty much everything - automatically for us. - - :copyright: (c) 2011 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import print_function - -import os -import re -import sys -from datetime import date, datetime -from subprocess import PIPE, Popen - -_date_strip_re = re.compile(r'(?<=\d)(st|nd|rd|th)') - - -def parse_changelog(): - with open('CHANGES.rst') as f: - lineiter = iter(f) - for line in lineiter: - match = re.search('^Version\s+(.*)', line.strip()) - - if match is None: - continue - - version = match.group(1).strip() - - if next(lineiter).count('-') != len(match.group(0)): - continue - - while 1: - change_info = next(lineiter).strip() - - if change_info: - break - - match = re.search( - r'(?:codename (.*),\s*)?released on (\w+\s+\d+\w+\s+\d+)(?i)', - change_info - ) - - if match is None: - continue - - codename, datestr = match.groups() - return version, parse_date(datestr), codename - - -def bump_version(version): - try: - parts = [int(i) for i in version.split('.')] - except ValueError: - fail('Current version is not numeric') - - parts[-1] += 1 - return '.'.join(map(str, parts)) - - -def parse_date(string): - string = _date_strip_re.sub('', string) - return datetime.strptime(string, '%B %d %Y') - - -def set_filename_version(filename, version_number, pattern): - changed = [] - - def inject_version(match): - before, old, after = match.groups() - changed.append(True) - return before + version_number + after - - with open(filename) as f: - contents = re.sub( - r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, - inject_version, f.read() - ) - - if not changed: - fail('Could not find %s in %s', pattern, filename) - - with open(filename, 'w') as f: - f.write(contents) - - -def set_init_version(version): - info('Setting __init__.py version to %s', version) - set_filename_version('jinja2/__init__.py', version, '__version__') - - -def set_setup_version(version): - info('Setting setup.py version to %s', version) - set_filename_version('setup.py', version, 'version') - - -def build_and_upload(): - cmd = [sys.executable, 'setup.py', 'sdist', 'bdist_wheel'] - Popen(cmd).wait() - - -def fail(message, *args): - print('Error:', message % args, file=sys.stderr) - sys.exit(1) - - -def info(message, *args): - print(message % args, file=sys.stderr) - - -def get_git_tags(): - return set( - Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines() - ) - - -def git_is_clean(): - return Popen(['git', 'diff', '--quiet']).wait() == 0 - - -def make_git_commit(message, *args): - message = message % args - Popen(['git', 'commit', '-am', message]).wait() - - -def make_git_tag(tag): - info('Tagging "%s"', tag) - Popen(['git', 'tag', tag]).wait() - - -def main(): - os.chdir(os.path.join(os.path.dirname(__file__), '..')) - - rv = parse_changelog() - - if rv is None: - fail('Could not parse changelog') - - version, release_date, codename = rv - dev_version = bump_version(version) + '.dev' - - info( - 'Releasing %s (codename %s, release date %s)', - version, codename, release_date.strftime('%d/%m/%Y') - ) - tags = get_git_tags() - - if version in tags: - fail('Version "%s" is already tagged', version) - - if release_date.date() != date.today(): - fail( - 'Release date is not today (%s != %s)', - release_date.date(), date.today() - ) - - if not git_is_clean(): - fail('You have uncommitted changes in git') - - try: - import wheel - except ImportError: - fail('You need to install the wheel package.') - - set_init_version(version) - set_setup_version(version) - make_git_commit('Bump version number to %s', version) - make_git_tag(version) - build_and_upload() - set_init_version(dev_version) - set_setup_version(dev_version) - - -if __name__ == '__main__': - main() diff --git a/scripts/pylintrc b/scripts/pylintrc deleted file mode 100644 index 4f85b49..0000000 --- a/scripts/pylintrc +++ /dev/null @@ -1,301 +0,0 @@ -# lint Python modules using external checkers. -# -# This is the main checker controling the other ones and the reports -# generation. It is itself both a raw checker and an astng checker in order -# to: -# * handle message activation / deactivation at the module level -# * handle some basic but necessary stats'data (number of classes, methods...) -# -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Profiled execution. -profile=no - -# Add <file or directory> to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=.svn - -# Pickle collected data for later comparisons. -persistent=yes - -# Set the cache size for astng objects. -cache-size=500 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable only checker(s) with the given id(s). This option conflict with the -# disable-checker option -#enable-checker= - -# Enable all checker(s) except those with the given id(s). This option conflict -# with the disable-checker option -#disable-checker= - -# Enable all messages in the listed categories. -#enable-msg-cat= - -# Disable all messages in the listed categories. -#disable-msg-cat= - -# Enable the message(s) with the given id(s). -#enable-msg= - -# Disable the message(s) with the given id(s). -disable-msg=C0323,W0142,C0301,C0103,C0111,E0213,C0302,C0203,W0703,R0201 - - -[REPORTS] - -# set the output format. Available formats are text, parseable, colorized and -# html -output-format=colorized - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells wether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note).You have access to the variables errors warning, statement which -# respectivly contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - -# Enable the report(s) with the given id(s). -#enable-report= - -# Disable the report(s) with the given id(s). -#disable-report= - - -# checks for -# * unused variables / imports -# * undefined variables -# * redefinition of variable from builtins or from an outer scope -# * use of variable before assigment -# -[VARIABLES] - -# Tells wether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -# try to find bugs in the code using type inference -# -[TYPECHECK] - -# Tells wether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# When zope mode is activated, consider the acquired-members option to ignore -# access to some undefined attributes. -zope=no - -# List of members which are usually get through zope's acquisition mecanism and -# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). -acquired-members=REQUEST,acl_users,aq_parent - - -# checks for : -# * doc strings -# * modules / classes / functions / methods / arguments / variables name -# * number of arguments, local variables, branchs, returns and statements in -# functions, methods -# * required module attributes -# * dangerous default values as arguments -# * redefinition of function / method / class -# * uses of the global statement -# -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=__.*__ - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]*$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# List of builtins function names that should not be used, separated by a comma -bad-functions=apply,input - - -# checks for sign of poor/misdesign: -# * number of methods, attributes, local variables... -# * size, complexity of functions, methods -# -[DESIGN] - -# Maximum number of arguments for function / method -max-args=12 - -# Maximum number of locals for function / method body -max-locals=30 - -# Maximum number of return / yield for function / method body -max-returns=12 - -# Maximum number of branch for function / method body -max-branchs=30 - -# Maximum number of statements in function / method body -max-statements=60 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=20 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=0 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -# checks for -# * external modules dependencies -# * relative / wildcard imports -# * cyclic imports -# * uses of deprecated modules -# -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report R0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report R0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report R0402 must -# not be disabled) -int-import-graph= - - -# checks for : -# * methods without self as first argument -# * overridden methods signature -# * access only to existant members via self -# * attributes not defined in the __init__ method -# * supported interfaces implementation -# * unreachable code -# -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -# checks for similarities and duplicated code. This computation may be -# memory / CPU intensive, so you should disable it if you experiments some -# problems. -# -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=10 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -# checks for: -# * warning notes in the code like FIXME, XXX -# * PEP 263: source code with non ascii character but no encoding declaration -# -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -# checks for : -# * unauthorized constructions -# * strict indentation -# * line length -# * use of <> instead of != -# -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=90 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' @@ -1,11 +1,15 @@ [metadata] license_file = LICENSE.rst +long_description_content_type = text/x-rst [bdist_wheel] universal = true [tool:pytest] testpaths = tests +filterwarnings = + error + ignore:the sets module:DeprecationWarning:jinja2.sandbox [coverage:run] branch = True @@ -15,6 +19,27 @@ source = [coverage:paths] source = - src/jinja2 - .tox/*/lib/python*/site-packages/jinja2 - .tox/*/site-packages/jinja2 + src + */site-packages + +[flake8] +# B = bugbear +# E = pycodestyle errors +# F = flake8 pyflakes +# W = pycodestyle warnings +# B9 = bugbear opinions +select = B, E, F, W, B9 +ignore = + # slice notation whitespace, invalid + E203 + # line length, handled by bugbear B950 + E501 + # bare except, handled by bugbear B001 + E722 + # bin op line break, invalid + W503 +# up to 88 allowed by bugbear B950 +max-line-length = 80 +per-file-ignores = + # __init__ module exports names + src/jinja2/__init__.py: F401 @@ -7,7 +7,7 @@ from setuptools import setup with io.open("README.rst", "rt", encoding="utf8") as f: readme = f.read() -with io.open("jinja2/__init__.py", "rt", encoding="utf8") as f: +with io.open("src/jinja2/__init__.py", "rt", encoding="utf8") as f: version = re.search(r'__version__ = "(.*?)"', f.read(), re.M).group(1) setup( @@ -39,14 +39,17 @@ setup( "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Markup :: HTML", ], - packages=find_packages(), + packages=find_packages("src"), + package_dir={"": "src"}, include_package_data=True, + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", install_requires=["MarkupSafe>=0.23"], extras_require={"i18n": ["Babel>=0.8"]}, entry_points={"babel.extractors": ["jinja2 = jinja2.ext:babel_extract[i18n]"]}, diff --git a/src/jinja2/__init__.py b/src/jinja2/__init__.py new file mode 100644 index 0000000..f17866f --- /dev/null +++ b/src/jinja2/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Jinja is a template engine written in pure Python. It provides a +non-XML syntax that supports inline expressions and an optional +sandboxed environment. +""" +from markupsafe import escape +from markupsafe import Markup + +from .bccache import BytecodeCache +from .bccache import FileSystemBytecodeCache +from .bccache import MemcachedBytecodeCache +from .environment import Environment +from .environment import Template +from .exceptions import TemplateAssertionError +from .exceptions import TemplateError +from .exceptions import TemplateNotFound +from .exceptions import TemplateRuntimeError +from .exceptions import TemplatesNotFound +from .exceptions import TemplateSyntaxError +from .exceptions import UndefinedError +from .filters import contextfilter +from .filters import environmentfilter +from .filters import evalcontextfilter +from .loaders import BaseLoader +from .loaders import ChoiceLoader +from .loaders import DictLoader +from .loaders import FileSystemLoader +from .loaders import FunctionLoader +from .loaders import ModuleLoader +from .loaders import PackageLoader +from .loaders import PrefixLoader +from .runtime import ChainableUndefined +from .runtime import DebugUndefined +from .runtime import make_logging_undefined +from .runtime import StrictUndefined +from .runtime import Undefined +from .utils import clear_caches +from .utils import contextfunction +from .utils import environmentfunction +from .utils import evalcontextfunction +from .utils import is_undefined +from .utils import select_autoescape + +__version__ = "2.11.3" diff --git a/jinja2/_compat.py b/src/jinja2/_compat.py index 4dbf6ea..1f04495 100644 --- a/jinja2/_compat.py +++ b/src/jinja2/_compat.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- -""" - jinja2._compat - ~~~~~~~~~~~~~~ - - Some py2/py3 compatibility support based on a stripped down - version of six so we don't have to depend on a specific version - of it. - - :copyright: Copyright 2013 by the Jinja team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" +# flake8: noqa +import marshal import sys PY2 = sys.version_info[0] == 2 -PYPY = hasattr(sys, 'pypy_translation_info') +PYPY = hasattr(sys, "pypy_translation_info") _identity = lambda x: x - if not PY2: unichr = chr range_type = range @@ -30,6 +20,7 @@ if not PY2: import pickle from io import BytesIO, StringIO + NativeStringIO = StringIO def reraise(tp, value, tb=None): @@ -46,6 +37,9 @@ if not PY2: implements_to_string = _identity encode_filename = _identity + marshal_dump = marshal.dump + marshal_load = marshal.load + else: unichr = unichr text_type = unicode @@ -59,11 +53,13 @@ else: import cPickle as pickle from cStringIO import StringIO as BytesIO, StringIO + NativeStringIO = BytesIO - exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') + exec("def reraise(tp, value, tb=None):\n raise tp, value, tb") from itertools import imap, izip, ifilter + intern = intern def implements_iterator(cls): @@ -73,14 +69,25 @@ else: def implements_to_string(cls): cls.__unicode__ = cls.__str__ - cls.__str__ = lambda x: x.__unicode__().encode('utf-8') + cls.__str__ = lambda x: x.__unicode__().encode("utf-8") return cls def encode_filename(filename): if isinstance(filename, unicode): - return filename.encode('utf-8') + return filename.encode("utf-8") return filename + def marshal_dump(code, f): + if isinstance(f, file): + marshal.dump(code, f) + else: + f.write(marshal.dumps(code)) + + def marshal_load(f): + if isinstance(f, file): + return marshal.load(f) + return marshal.loads(f.read()) + def with_metaclass(meta, *bases): """Create a base class with a metaclass.""" @@ -90,7 +97,8 @@ def with_metaclass(meta, *bases): class metaclass(type): def __new__(cls, name, this_bases, d): return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) + + return type.__new__(metaclass, "temporary_class", (), {}) try: @@ -103,3 +111,22 @@ try: from collections import abc except ImportError: import collections as abc + + +try: + from os import fspath +except ImportError: + try: + from pathlib import PurePath + except ImportError: + PurePath = None + + def fspath(path): + if hasattr(path, "__fspath__"): + return path.__fspath__() + + # Python 3.5 doesn't have __fspath__ yet, use str. + if PurePath is not None and isinstance(path, PurePath): + return str(path) + + return path diff --git a/src/jinja2/_identifier.py b/src/jinja2/_identifier.py new file mode 100644 index 0000000..224d544 --- /dev/null +++ b/src/jinja2/_identifier.py @@ -0,0 +1,6 @@ +import re + +# generated by scripts/generate_identifier_pattern.py +pattern = re.compile( + r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 +) diff --git a/jinja2/asyncfilters.py b/src/jinja2/asyncfilters.py index 5c1f46d..3d98dbc 100644 --- a/jinja2/asyncfilters.py +++ b/src/jinja2/asyncfilters.py @@ -1,12 +1,13 @@ from functools import wraps -from jinja2.asyncsupport import auto_aiter -from jinja2 import filters +from . import filters +from .asyncsupport import auto_aiter +from .asyncsupport import auto_await async def auto_to_seq(value): seq = [] - if hasattr(value, '__aiter__'): + if hasattr(value, "__aiter__"): async for item in value: seq.append(item) else: @@ -16,8 +17,7 @@ async def auto_to_seq(value): async def async_select_or_reject(args, kwargs, modfunc, lookup_attr): - seq, func = filters.prepare_select_or_reject( - args, kwargs, modfunc, lookup_attr) + seq, func = filters.prepare_select_or_reject(args, kwargs, modfunc, lookup_attr) if seq: async for item in auto_aiter(seq): if func(item): @@ -26,14 +26,19 @@ async def async_select_or_reject(args, kwargs, modfunc, lookup_attr): def dualfilter(normal_filter, async_filter): wrap_evalctx = False - if getattr(normal_filter, 'environmentfilter', False): - is_async = lambda args: args[0].is_async + if getattr(normal_filter, "environmentfilter", False) is True: + + def is_async(args): + return args[0].is_async + wrap_evalctx = False else: - if not getattr(normal_filter, 'evalcontextfilter', False) and \ - not getattr(normal_filter, 'contextfilter', False): - wrap_evalctx = True - is_async = lambda args: args[0].environment.is_async + has_evalctxfilter = getattr(normal_filter, "evalcontextfilter", False) is True + has_ctxfilter = getattr(normal_filter, "contextfilter", False) is True + wrap_evalctx = not has_evalctxfilter and not has_ctxfilter + + def is_async(args): + return args[0].environment.is_async @wraps(normal_filter) def wrapper(*args, **kwargs): @@ -55,6 +60,7 @@ def dualfilter(normal_filter, async_filter): def asyncfiltervariant(original): def decorator(f): return dualfilter(original, f) + return decorator @@ -63,19 +69,22 @@ async def do_first(environment, seq): try: return await auto_aiter(seq).__anext__() except StopAsyncIteration: - return environment.undefined('No first item, sequence was empty.') + return environment.undefined("No first item, sequence was empty.") @asyncfiltervariant(filters.do_groupby) async def do_groupby(environment, value, attribute): expr = filters.make_attrgetter(environment, attribute) - return [filters._GroupTuple(key, await auto_to_seq(values)) - for key, values in filters.groupby(sorted( - await auto_to_seq(value), key=expr), expr)] + return [ + filters._GroupTuple(key, await auto_to_seq(values)) + for key, values in filters.groupby( + sorted(await auto_to_seq(value), key=expr), expr + ) + ] @asyncfiltervariant(filters.do_join) -async def do_join(eval_ctx, value, d=u'', attribute=None): +async def do_join(eval_ctx, value, d=u"", attribute=None): return filters.do_join(eval_ctx, await auto_to_seq(value), d, attribute) @@ -109,7 +118,7 @@ async def do_map(*args, **kwargs): seq, func = filters.prepare_map(args, kwargs) if seq: async for item in auto_aiter(seq): - yield func(item) + yield await auto_await(func(item)) @asyncfiltervariant(filters.do_sum) @@ -118,7 +127,10 @@ async def do_sum(environment, iterable, attribute=None, start=0): if attribute is not None: func = filters.make_attrgetter(environment, attribute) else: - func = lambda x: x + + def func(x): + return x + async for item in auto_aiter(iterable): rv += func(item) return rv @@ -130,17 +142,17 @@ async def do_slice(value, slices, fill_with=None): ASYNC_FILTERS = { - 'first': do_first, - 'groupby': do_groupby, - 'join': do_join, - 'list': do_list, + "first": do_first, + "groupby": do_groupby, + "join": do_join, + "list": do_list, # we intentionally do not support do_last because that would be # ridiculous - 'reject': do_reject, - 'rejectattr': do_rejectattr, - 'map': do_map, - 'select': do_select, - 'selectattr': do_selectattr, - 'sum': do_sum, - 'slice': do_slice, + "reject": do_reject, + "rejectattr": do_rejectattr, + "map": do_map, + "select": do_select, + "selectattr": do_selectattr, + "sum": do_sum, + "slice": do_slice, } diff --git a/jinja2/asyncsupport.py b/src/jinja2/asyncsupport.py index 7ceddf4..78ba373 100644 --- a/jinja2/asyncsupport.py +++ b/src/jinja2/asyncsupport.py @@ -1,31 +1,27 @@ # -*- coding: utf-8 -*- +"""The code for async support. Importing this patches Jinja on supported +Python versions. """ - jinja2.asyncsupport - ~~~~~~~~~~~~~~~~~~~ - - Has all the code for async support which is implemented as a patch - for supported Python versions. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import sys import asyncio import inspect from functools import update_wrapper from markupsafe import Markup -from jinja2.utils import concat, internalcode -from jinja2.environment import TemplateModule -from jinja2.runtime import LoopContextBase, _last_iteration +from .environment import TemplateModule +from .runtime import LoopContext +from .utils import concat +from .utils import internalcode +from .utils import missing async def concat_async(async_gen): rv = [] + async def collect(): async for event in async_gen: rv.append(event) + await collect() return concat(rv) @@ -36,10 +32,7 @@ async def generate_async(self, *args, **kwargs): async for event in self.root_render_func(self.new_context(vars)): yield event except Exception: - exc_info = sys.exc_info() - else: - return - yield self.environment.handle_exception(exc_info, True) + yield self.environment.handle_exception() def wrap_generate_func(original_generate): @@ -50,17 +43,18 @@ def wrap_generate_func(original_generate): yield loop.run_until_complete(async_gen.__anext__()) except StopAsyncIteration: pass + def generate(self, *args, **kwargs): if not self.environment.is_async: return original_generate(self, *args, **kwargs) return _convert_generator(self, asyncio.get_event_loop(), args, kwargs) + return update_wrapper(generate, original_generate) async def render_async(self, *args, **kwargs): if not self.environment.is_async: - raise RuntimeError('The environment was not created with async mode ' - 'enabled.') + raise RuntimeError("The environment was not created with async mode enabled.") vars = dict(*args, **kwargs) ctx = self.new_context(vars) @@ -68,8 +62,7 @@ async def render_async(self, *args, **kwargs): try: return await concat_async(self.root_render_func(ctx)) except Exception: - exc_info = sys.exc_info() - return self.environment.handle_exception(exc_info, True) + return self.environment.handle_exception() def wrap_render_func(original_render): @@ -78,6 +71,7 @@ def wrap_render_func(original_render): return original_render(self, *args, **kwargs) loop = asyncio.get_event_loop() return loop.run_until_complete(self.render_async(*args, **kwargs)) + return update_wrapper(render, original_render) @@ -111,6 +105,7 @@ def wrap_macro_invoke(original_invoke): if not self._environment.is_async: return original_invoke(self, arguments, autoescape) return async_invoke(self, arguments, autoescape) + return update_wrapper(_invoke, original_invoke) @@ -126,9 +121,9 @@ def wrap_default_module(original_default_module): @internalcode def _get_default_module(self): if self.environment.is_async: - raise RuntimeError('Template module attribute is unavailable ' - 'in async mode') + raise RuntimeError("Template module attribute is unavailable in async mode") return original_default_module(self) + return _get_default_module @@ -141,30 +136,30 @@ async def make_module_async(self, vars=None, shared=False, locals=None): def patch_template(): - from jinja2 import Template + from . import Template + Template.generate = wrap_generate_func(Template.generate) - Template.generate_async = update_wrapper( - generate_async, Template.generate_async) - Template.render_async = update_wrapper( - render_async, Template.render_async) + Template.generate_async = update_wrapper(generate_async, Template.generate_async) + Template.render_async = update_wrapper(render_async, Template.render_async) Template.render = wrap_render_func(Template.render) - Template._get_default_module = wrap_default_module( - Template._get_default_module) + Template._get_default_module = wrap_default_module(Template._get_default_module) Template._get_default_module_async = get_default_module_async Template.make_module_async = update_wrapper( - make_module_async, Template.make_module_async) + make_module_async, Template.make_module_async + ) def patch_runtime(): - from jinja2.runtime import BlockReference, Macro - BlockReference.__call__ = wrap_block_reference_call( - BlockReference.__call__) + from .runtime import BlockReference, Macro + + BlockReference.__call__ = wrap_block_reference_call(BlockReference.__call__) Macro._invoke = wrap_macro_invoke(Macro._invoke) def patch_filters(): - from jinja2.filters import FILTERS - from jinja2.asyncfilters import ASYNC_FILTERS + from .filters import FILTERS + from .asyncfilters import ASYNC_FILTERS + FILTERS.update(ASYNC_FILTERS) @@ -181,7 +176,7 @@ async def auto_await(value): async def auto_aiter(iterable): - if hasattr(iterable, '__aiter__'): + if hasattr(iterable, "__aiter__"): async for item in iterable: yield item return @@ -189,70 +184,81 @@ async def auto_aiter(iterable): yield item -class AsyncLoopContext(LoopContextBase): - - def __init__(self, async_iterator, undefined, after, length, recurse=None, - depth0=0): - LoopContextBase.__init__(self, undefined, recurse, depth0) - self._async_iterator = async_iterator - self._after = after - self._length = length +class AsyncLoopContext(LoopContext): + _to_iterator = staticmethod(auto_aiter) @property - def length(self): - if self._length is None: - raise TypeError('Loop length for some iterators cannot be ' - 'lazily calculated in async mode') + async def length(self): + if self._length is not None: + return self._length + + try: + self._length = len(self._iterable) + except TypeError: + iterable = [x async for x in self._iterator] + self._iterator = self._to_iterator(iterable) + self._length = len(iterable) + self.index + (self._after is not missing) + return self._length - def __aiter__(self): - return AsyncLoopContextIterator(self) + @property + async def revindex0(self): + return await self.length - self.index + @property + async def revindex(self): + return await self.length - self.index0 -class AsyncLoopContextIterator(object): - __slots__ = ('context',) + async def _peek_next(self): + if self._after is not missing: + return self._after - def __init__(self, context): - self.context = context + try: + self._after = await self._iterator.__anext__() + except StopAsyncIteration: + self._after = missing + + return self._after + + @property + async def last(self): + return await self._peek_next() is missing + + @property + async def nextitem(self): + rv = await self._peek_next() + + if rv is missing: + return self._undefined("there is no next item") + + return rv def __aiter__(self): return self async def __anext__(self): - ctx = self.context - ctx.index0 += 1 - if ctx._after is _last_iteration: - raise StopAsyncIteration() - ctx._before = ctx._current - ctx._current = ctx._after - try: - ctx._after = await ctx._async_iterator.__anext__() - except StopAsyncIteration: - ctx._after = _last_iteration - return ctx._current, ctx + if self._after is not missing: + rv = self._after + self._after = missing + else: + rv = await self._iterator.__anext__() + + self.index0 += 1 + self._before = self._current + self._current = rv + return rv, self async def make_async_loop_context(iterable, undefined, recurse=None, depth0=0): - # Length is more complicated and less efficient in async mode. The - # reason for this is that we cannot know if length will be used - # upfront but because length is a property we cannot lazily execute it - # later. This means that we need to buffer it up and measure :( - # - # We however only do this for actual iterators, not for async - # iterators as blocking here does not seem like the best idea in the - # world. - try: - length = len(iterable) - except (TypeError, AttributeError): - if not hasattr(iterable, '__aiter__'): - iterable = tuple(iterable) - length = len(iterable) - else: - length = None - async_iterator = auto_aiter(iterable) - try: - after = await async_iterator.__anext__() - except StopAsyncIteration: - after = _last_iteration - return AsyncLoopContext(async_iterator, undefined, after, length, recurse, - depth0) + import warnings + + warnings.warn( + "This template must be recompiled with at least Jinja 2.11, or" + " it will fail in 3.0.", + DeprecationWarning, + stacklevel=2, + ) + return AsyncLoopContext(iterable, undefined, recurse, depth0) + + +patch_all() diff --git a/jinja2/bccache.py b/src/jinja2/bccache.py index 507a9b3..9c06610 100644 --- a/jinja2/bccache.py +++ b/src/jinja2/bccache.py @@ -1,60 +1,37 @@ # -*- coding: utf-8 -*- -""" - jinja2.bccache - ~~~~~~~~~~~~~~ - - This module implements the bytecode cache system Jinja is optionally - using. This is useful if you have very complex template situations and - the compiliation of all those templates slow down your application too - much. - - Situations where this is useful are often forking web applications that - are initialized on the first request. +"""The optional bytecode cache system. This is useful if you have very +complex template situations and the compilation of all those templates +slows down your application too much. - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. +Situations where this is useful are often forking web applications that +are initialized on the first request. """ -from os import path, listdir +import errno +import fnmatch import os -import sys import stat -import errno -import marshal +import sys import tempfile -import fnmatch from hashlib import sha1 -from jinja2.utils import open_if_exists -from jinja2._compat import BytesIO, pickle, PY2, text_type - - -# marshal works better on 3.x, one hack less required -if not PY2: - marshal_dump = marshal.dump - marshal_load = marshal.load -else: - - def marshal_dump(code, f): - if isinstance(f, file): - marshal.dump(code, f) - else: - f.write(marshal.dumps(code)) - - def marshal_load(f): - if isinstance(f, file): - return marshal.load(f) - return marshal.loads(f.read()) - - -bc_version = 3 - -# magic version used to only change with new jinja versions. With 2.6 -# we change this to also take Python version changes into account. The -# reason for this is that Python tends to segfault if fed earlier bytecode -# versions because someone thought it would be a good idea to reuse opcodes -# or make Python incompatible with earlier versions. -bc_magic = 'j2'.encode('ascii') + \ - pickle.dumps(bc_version, 2) + \ - pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1]) +from os import listdir +from os import path + +from ._compat import BytesIO +from ._compat import marshal_dump +from ._compat import marshal_load +from ._compat import pickle +from ._compat import text_type +from .utils import open_if_exists + +bc_version = 4 +# Magic bytes to identify Jinja bytecode cache files. Contains the +# Python major and minor version to avoid loading incompatible bytecode +# if a project upgrades its Python version. +bc_magic = ( + b"j2" + + pickle.dumps(bc_version, 2) + + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2) +) class Bucket(object): @@ -98,7 +75,7 @@ class Bucket(object): def write_bytecode(self, f): """Dump the bytecode into the file or file like object passed.""" if self.code is None: - raise TypeError('can\'t write empty bucket') + raise TypeError("can't write empty bucket") f.write(bc_magic) pickle.dump(self.checksum, f, 2) marshal_dump(self.code, f) @@ -140,7 +117,7 @@ class BytecodeCache(object): bucket.write_bytecode(f) A more advanced version of a filesystem based bytecode cache is part of - Jinja2. + Jinja. """ def load_bytecode(self, bucket): @@ -158,24 +135,24 @@ class BytecodeCache(object): raise NotImplementedError() def clear(self): - """Clears the cache. This method is not used by Jinja2 but should be + """Clears the cache. This method is not used by Jinja but should be implemented to allow applications to clear the bytecode cache used by a particular environment. """ def get_cache_key(self, name, filename=None): """Returns the unique hash key for this template name.""" - hash = sha1(name.encode('utf-8')) + hash = sha1(name.encode("utf-8")) if filename is not None: - filename = '|' + filename + filename = "|" + filename if isinstance(filename, text_type): - filename = filename.encode('utf-8') + filename = filename.encode("utf-8") hash.update(filename) return hash.hexdigest() def get_source_checksum(self, source): """Returns a checksum for the source.""" - return sha1(source.encode('utf-8')).hexdigest() + return sha1(source.encode("utf-8")).hexdigest() def get_bucket(self, environment, name, filename, source): """Return a cache bucket for the given template. All arguments are @@ -210,7 +187,7 @@ class FileSystemBytecodeCache(BytecodeCache): This bytecode cache supports clearing of the cache using the clear method. """ - def __init__(self, directory=None, pattern='__jinja2_%s.cache'): + def __init__(self, directory=None, pattern="__jinja2_%s.cache"): if directory is None: directory = self._get_default_cache_dir() self.directory = directory @@ -218,19 +195,21 @@ class FileSystemBytecodeCache(BytecodeCache): def _get_default_cache_dir(self): def _unsafe_dir(): - raise RuntimeError('Cannot determine safe temp directory. You ' - 'need to explicitly provide one.') + raise RuntimeError( + "Cannot determine safe temp directory. You " + "need to explicitly provide one." + ) tmpdir = tempfile.gettempdir() # On windows the temporary directory is used specific unless # explicitly forced otherwise. We can just use that. - if os.name == 'nt': + if os.name == "nt": return tmpdir - if not hasattr(os, 'getuid'): + if not hasattr(os, "getuid"): _unsafe_dir() - dirname = '_jinja2-cache-%d' % os.getuid() + dirname = "_jinja2-cache-%d" % os.getuid() actual_dir = os.path.join(tmpdir, dirname) try: @@ -241,18 +220,22 @@ class FileSystemBytecodeCache(BytecodeCache): try: os.chmod(actual_dir, stat.S_IRWXU) actual_dir_stat = os.lstat(actual_dir) - if actual_dir_stat.st_uid != os.getuid() \ - or not stat.S_ISDIR(actual_dir_stat.st_mode) \ - or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU: + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): _unsafe_dir() except OSError as e: if e.errno != errno.EEXIST: raise actual_dir_stat = os.lstat(actual_dir) - if actual_dir_stat.st_uid != os.getuid() \ - or not stat.S_ISDIR(actual_dir_stat.st_mode) \ - or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU: + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): _unsafe_dir() return actual_dir @@ -261,7 +244,7 @@ class FileSystemBytecodeCache(BytecodeCache): return path.join(self.directory, self.pattern % bucket.key) def load_bytecode(self, bucket): - f = open_if_exists(self._get_cache_filename(bucket), 'rb') + f = open_if_exists(self._get_cache_filename(bucket), "rb") if f is not None: try: bucket.load_bytecode(f) @@ -269,7 +252,7 @@ class FileSystemBytecodeCache(BytecodeCache): f.close() def dump_bytecode(self, bucket): - f = open(self._get_cache_filename(bucket), 'wb') + f = open(self._get_cache_filename(bucket), "wb") try: bucket.write_bytecode(f) finally: @@ -280,7 +263,8 @@ class FileSystemBytecodeCache(BytecodeCache): # write access on the file system and the function does not exist # normally. from os import remove - files = fnmatch.filter(listdir(self.directory), self.pattern % '*') + + files = fnmatch.filter(listdir(self.directory), self.pattern % "*") for filename in files: try: remove(path.join(self.directory, filename)) @@ -333,8 +317,13 @@ class MemcachedBytecodeCache(BytecodeCache): `ignore_memcache_errors` parameter. """ - def __init__(self, client, prefix='jinja2/bytecode/', timeout=None, - ignore_memcache_errors=True): + def __init__( + self, + client, + prefix="jinja2/bytecode/", + timeout=None, + ignore_memcache_errors=True, + ): self.client = client self.prefix = prefix self.timeout = timeout diff --git a/jinja2/compiler.py b/src/jinja2/compiler.py index 10aed13..63297b4 100644 --- a/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -1,60 +1,62 @@ # -*- coding: utf-8 -*- -""" - jinja2.compiler - ~~~~~~~~~~~~~~~ - - Compiles nodes into python code. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -from itertools import chain +"""Compiles nodes from the parser into Python code.""" +from collections import namedtuple from functools import update_wrapper +from itertools import chain from keyword import iskeyword as is_python_keyword -from markupsafe import Markup, escape - -from jinja2 import nodes -from jinja2.nodes import EvalContext -from jinja2.visitor import NodeVisitor -from jinja2.optimizer import Optimizer -from jinja2.exceptions import TemplateAssertionError -from jinja2.utils import concat -from jinja2._compat import range_type, text_type, string_types, \ - iteritems, NativeStringIO, imap, izip -from jinja2.idtracking import Symbols, VAR_LOAD_PARAMETER, \ - VAR_LOAD_RESOLVE, VAR_LOAD_ALIAS, VAR_LOAD_UNDEFINED +from markupsafe import escape +from markupsafe import Markup + +from . import nodes +from ._compat import imap +from ._compat import iteritems +from ._compat import izip +from ._compat import NativeStringIO +from ._compat import range_type +from ._compat import string_types +from ._compat import text_type +from .exceptions import TemplateAssertionError +from .idtracking import Symbols +from .idtracking import VAR_LOAD_ALIAS +from .idtracking import VAR_LOAD_PARAMETER +from .idtracking import VAR_LOAD_RESOLVE +from .idtracking import VAR_LOAD_UNDEFINED +from .nodes import EvalContext +from .optimizer import Optimizer +from .utils import concat +from .visitor import NodeVisitor operators = { - 'eq': '==', - 'ne': '!=', - 'gt': '>', - 'gteq': '>=', - 'lt': '<', - 'lteq': '<=', - 'in': 'in', - 'notin': 'not in' + "eq": "==", + "ne": "!=", + "gt": ">", + "gteq": ">=", + "lt": "<", + "lteq": "<=", + "in": "in", + "notin": "not in", } # what method to iterate over items do we want to use for dict iteration # in generated code? on 2.x let's go with iteritems, on 3.x with items -if hasattr(dict, 'iteritems'): - dict_item_iter = 'iteritems' +if hasattr(dict, "iteritems"): + dict_item_iter = "iteritems" else: - dict_item_iter = 'items' + dict_item_iter = "items" -code_features = ['division'] +code_features = ["division"] # does this python version support generator stops? (PEP 0479) try: - exec('from __future__ import generator_stop') - code_features.append('generator_stop') + exec("from __future__ import generator_stop") + code_features.append("generator_stop") except SyntaxError: pass # does this python version support yield from? try: - exec('def f(): yield from x()') + exec("def f(): yield from x()") except SyntaxError: supports_yield_from = False else: @@ -69,17 +71,19 @@ def optimizeconst(f): if new_node != node: return self.visit(new_node, frame) return f(self, node, frame, **kwargs) + return update_wrapper(new_func, f) -def generate(node, environment, name, filename, stream=None, - defer_init=False, optimized=True): +def generate( + node, environment, name, filename, stream=None, defer_init=False, optimized=True +): """Generate the python source for a node tree.""" if not isinstance(node, nodes.Template): - raise TypeError('Can\'t compile non template nodes') - generator = environment.code_generator_class(environment, name, filename, - stream, defer_init, - optimized) + raise TypeError("Can't compile non template nodes") + generator = environment.code_generator_class( + environment, name, filename, stream, defer_init, optimized + ) generator.visit(node) if stream is None: return generator.stream.getvalue() @@ -120,7 +124,6 @@ def find_undeclared(nodes, names): class MacroRef(object): - def __init__(self, node): self.node = node self.accesses_caller = False @@ -133,8 +136,7 @@ class Frame(object): def __init__(self, eval_ctx, parent=None, level=None): self.eval_ctx = eval_ctx - self.symbols = Symbols(parent and parent.symbols or None, - level=level) + self.symbols = Symbols(parent and parent.symbols or None, level=level) # a toplevel frame is the root + soft frames such as if conditions. self.toplevel = False @@ -224,7 +226,7 @@ class UndeclaredNameVisitor(NodeVisitor): self.undeclared = set() def visit_Name(self, node): - if node.ctx == 'load' and node.name in self.names: + if node.ctx == "load" and node.name in self.names: self.undeclared.add(node.name) if self.undeclared == self.names: raise VisitorExit() @@ -243,9 +245,9 @@ class CompilerExit(Exception): class CodeGenerator(NodeVisitor): - - def __init__(self, environment, name, filename, stream=None, - defer_init=False, optimized=True): + def __init__( + self, environment, name, filename, stream=None, defer_init=False, optimized=True + ): if stream is None: stream = NativeStringIO() self.environment = environment @@ -307,7 +309,7 @@ class CodeGenerator(NodeVisitor): self._param_def_block = [] # Tracks the current context. - self._context_reference_stack = ['context'] + self._context_reference_stack = ["context"] # -- Various compilation helpers @@ -318,30 +320,30 @@ class CodeGenerator(NodeVisitor): def temporary_identifier(self): """Get a new unique identifier.""" self._last_identifier += 1 - return 't_%d' % self._last_identifier + return "t_%d" % self._last_identifier def buffer(self, frame): """Enable buffering for the frame from that point onwards.""" frame.buffer = self.temporary_identifier() - self.writeline('%s = []' % frame.buffer) + self.writeline("%s = []" % frame.buffer) def return_buffer_contents(self, frame, force_unescaped=False): """Return the buffer contents of the frame.""" if not force_unescaped: if frame.eval_ctx.volatile: - self.writeline('if context.eval_ctx.autoescape:') + self.writeline("if context.eval_ctx.autoescape:") self.indent() - self.writeline('return Markup(concat(%s))' % frame.buffer) + self.writeline("return Markup(concat(%s))" % frame.buffer) self.outdent() - self.writeline('else:') + self.writeline("else:") self.indent() - self.writeline('return concat(%s)' % frame.buffer) + self.writeline("return concat(%s)" % frame.buffer) self.outdent() return elif frame.eval_ctx.autoescape: - self.writeline('return Markup(concat(%s))' % frame.buffer) + self.writeline("return Markup(concat(%s))" % frame.buffer) return - self.writeline('return concat(%s)' % frame.buffer) + self.writeline("return concat(%s)" % frame.buffer) def indent(self): """Indent by one.""" @@ -354,14 +356,14 @@ class CodeGenerator(NodeVisitor): def start_write(self, frame, node=None): """Yield or write into the frame buffer.""" if frame.buffer is None: - self.writeline('yield ', node) + self.writeline("yield ", node) else: - self.writeline('%s.append(' % frame.buffer, node) + self.writeline("%s.append(" % frame.buffer, node) def end_write(self, frame): """End the writing process started by `start_write`.""" if frame.buffer is not None: - self.write(')') + self.write(")") def simple_write(self, s, frame, node=None): """Simple shortcut for start_write + write + end_write.""" @@ -374,7 +376,7 @@ class CodeGenerator(NodeVisitor): is no buffer a dummy ``if 0: yield None`` is written automatically. """ try: - self.writeline('pass') + self.writeline("pass") for node in nodes: self.visit(node, frame) except CompilerExit: @@ -384,14 +386,13 @@ class CodeGenerator(NodeVisitor): """Write a string into the output stream.""" if self._new_lines: if not self._first_write: - self.stream.write('\n' * self._new_lines) + self.stream.write("\n" * self._new_lines) self.code_lineno += self._new_lines if self._write_debug_info is not None: - self.debug_info.append((self._write_debug_info, - self.code_lineno)) + self.debug_info.append((self._write_debug_info, self.code_lineno)) self._write_debug_info = None self._first_write = False - self.stream.write(' ' * self._indentation) + self.stream.write(" " * self._indentation) self._new_lines = 0 self.stream.write(x) @@ -411,7 +412,7 @@ class CodeGenerator(NodeVisitor): """Writes a function call to the stream for the current node. A leading comma is added automatically. The extra keyword arguments may not include python keywords otherwise a syntax - error could occour. The extra keyword arguments should be given + error could occur. The extra keyword arguments should be given as python dict. """ # if any of the given keyword arguments is a python keyword @@ -423,41 +424,41 @@ class CodeGenerator(NodeVisitor): break for arg in node.args: - self.write(', ') + self.write(", ") self.visit(arg, frame) if not kwarg_workaround: for kwarg in node.kwargs: - self.write(', ') + self.write(", ") self.visit(kwarg, frame) if extra_kwargs is not None: for key, value in iteritems(extra_kwargs): - self.write(', %s=%s' % (key, value)) + self.write(", %s=%s" % (key, value)) if node.dyn_args: - self.write(', *') + self.write(", *") self.visit(node.dyn_args, frame) if kwarg_workaround: if node.dyn_kwargs is not None: - self.write(', **dict({') + self.write(", **dict({") else: - self.write(', **{') + self.write(", **{") for kwarg in node.kwargs: - self.write('%r: ' % kwarg.key) + self.write("%r: " % kwarg.key) self.visit(kwarg.value, frame) - self.write(', ') + self.write(", ") if extra_kwargs is not None: for key, value in iteritems(extra_kwargs): - self.write('%r: %s, ' % (key, value)) + self.write("%r: %s, " % (key, value)) if node.dyn_kwargs is not None: - self.write('}, **') + self.write("}, **") self.visit(node.dyn_kwargs, frame) - self.write(')') + self.write(")") else: - self.write('}') + self.write("}") elif node.dyn_kwargs is not None: - self.write(', **') + self.write(", **") self.visit(node.dyn_kwargs, frame) def pull_dependencies(self, nodes): @@ -465,13 +466,14 @@ class CodeGenerator(NodeVisitor): visitor = DependencyFinderVisitor() for node in nodes: visitor.visit(node) - for dependency in 'filters', 'tests': + for dependency in "filters", "tests": mapping = getattr(self, dependency) for name in getattr(visitor, dependency): if name not in mapping: mapping[name] = self.temporary_identifier() - self.writeline('%s = environment.%s[%r]' % - (mapping[name], dependency, name)) + self.writeline( + "%s = environment.%s[%r]" % (mapping[name], dependency, name) + ) def enter_frame(self, frame): undefs = [] @@ -479,16 +481,15 @@ class CodeGenerator(NodeVisitor): if action == VAR_LOAD_PARAMETER: pass elif action == VAR_LOAD_RESOLVE: - self.writeline('%s = %s(%r)' % - (target, self.get_resolve_func(), param)) + self.writeline("%s = %s(%r)" % (target, self.get_resolve_func(), param)) elif action == VAR_LOAD_ALIAS: - self.writeline('%s = %s' % (target, param)) + self.writeline("%s = %s" % (target, param)) elif action == VAR_LOAD_UNDEFINED: undefs.append(target) else: - raise NotImplementedError('unknown load instruction') + raise NotImplementedError("unknown load instruction") if undefs: - self.writeline('%s = missing' % ' = '.join(undefs)) + self.writeline("%s = missing" % " = ".join(undefs)) def leave_frame(self, frame, with_python_scope=False): if not with_python_scope: @@ -496,12 +497,12 @@ class CodeGenerator(NodeVisitor): for target, _ in iteritems(frame.symbols.loads): undefs.append(target) if undefs: - self.writeline('%s = missing' % ' = '.join(undefs)) + self.writeline("%s = missing" % " = ".join(undefs)) def func(self, name): if self.environment.is_async: - return 'async def %s' % name - return 'def %s' % name + return "async def %s" % name + return "def %s" % name def macro_body(self, node, frame): """Dump the function def of a macro or call block.""" @@ -513,16 +514,16 @@ class CodeGenerator(NodeVisitor): skip_special_params = set() args = [] for idx, arg in enumerate(node.args): - if arg.name == 'caller': + if arg.name == "caller": explicit_caller = idx - if arg.name in ('kwargs', 'varargs'): + if arg.name in ("kwargs", "varargs"): skip_special_params.add(arg.name) args.append(frame.symbols.ref(arg.name)) - undeclared = find_undeclared(node.body, ('caller', 'kwargs', 'varargs')) + undeclared = find_undeclared(node.body, ("caller", "kwargs", "varargs")) - if 'caller' in undeclared: - # In older Jinja2 versions there was a bug that allowed caller + if "caller" in undeclared: + # In older Jinja versions there was a bug that allowed caller # to retain the special behavior even if it was mentioned in # the argument list. However thankfully this was only really # working if it was the last argument. So we are explicitly @@ -532,23 +533,26 @@ class CodeGenerator(NodeVisitor): try: node.defaults[explicit_caller - len(node.args)] except IndexError: - self.fail('When defining macros or call blocks the ' - 'special "caller" argument must be omitted ' - 'or be given a default.', node.lineno) + self.fail( + "When defining macros or call blocks the " + 'special "caller" argument must be omitted ' + "or be given a default.", + node.lineno, + ) else: - args.append(frame.symbols.declare_parameter('caller')) + args.append(frame.symbols.declare_parameter("caller")) macro_ref.accesses_caller = True - if 'kwargs' in undeclared and not 'kwargs' in skip_special_params: - args.append(frame.symbols.declare_parameter('kwargs')) + if "kwargs" in undeclared and "kwargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("kwargs")) macro_ref.accesses_kwargs = True - if 'varargs' in undeclared and not 'varargs' in skip_special_params: - args.append(frame.symbols.declare_parameter('varargs')) + if "varargs" in undeclared and "varargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("varargs")) macro_ref.accesses_varargs = True # macros are delayed, they never require output checks frame.require_output_check = False frame.symbols.analyze_node(node) - self.writeline('%s(%s):' % (self.func('macro'), ', '.join(args)), node) + self.writeline("%s(%s):" % (self.func("macro"), ", ".join(args)), node) self.indent() self.buffer(frame) @@ -557,17 +561,17 @@ class CodeGenerator(NodeVisitor): self.push_parameter_definitions(frame) for idx, arg in enumerate(node.args): ref = frame.symbols.ref(arg.name) - self.writeline('if %s is missing:' % ref) + self.writeline("if %s is missing:" % ref) self.indent() try: default = node.defaults[idx - len(node.args)] except IndexError: - self.writeline('%s = undefined(%r, name=%r)' % ( - ref, - 'parameter %r was not provided' % arg.name, - arg.name)) + self.writeline( + "%s = undefined(%r, name=%r)" + % (ref, "parameter %r was not provided" % arg.name, arg.name) + ) else: - self.writeline('%s = ' % ref) + self.writeline("%s = " % ref) self.visit(default, frame) self.mark_parameter_stored(ref) self.outdent() @@ -582,35 +586,46 @@ class CodeGenerator(NodeVisitor): def macro_def(self, macro_ref, frame): """Dump the macro definition for the def created by macro_body.""" - arg_tuple = ', '.join(repr(x.name) for x in macro_ref.node.args) - name = getattr(macro_ref.node, 'name', None) + arg_tuple = ", ".join(repr(x.name) for x in macro_ref.node.args) + name = getattr(macro_ref.node, "name", None) if len(macro_ref.node.args) == 1: - arg_tuple += ',' - self.write('Macro(environment, macro, %r, (%s), %r, %r, %r, ' - 'context.eval_ctx.autoescape)' % - (name, arg_tuple, macro_ref.accesses_kwargs, - macro_ref.accesses_varargs, macro_ref.accesses_caller)) + arg_tuple += "," + self.write( + "Macro(environment, macro, %r, (%s), %r, %r, %r, " + "context.eval_ctx.autoescape)" + % ( + name, + arg_tuple, + macro_ref.accesses_kwargs, + macro_ref.accesses_varargs, + macro_ref.accesses_caller, + ) + ) def position(self, node): """Return a human readable position for the node.""" - rv = 'line %d' % node.lineno + rv = "line %d" % node.lineno if self.name is not None: - rv += ' in ' + repr(self.name) + rv += " in " + repr(self.name) return rv def dump_local_context(self, frame): - return '{%s}' % ', '.join( - '%r: %s' % (name, target) for name, target - in iteritems(frame.symbols.dump_stores())) + return "{%s}" % ", ".join( + "%r: %s" % (name, target) + for name, target in iteritems(frame.symbols.dump_stores()) + ) def write_commons(self): """Writes a common preamble that is used by root and block functions. Primarily this sets up common local helpers and enforces a generator through a dead branch. """ - self.writeline('resolve = context.resolve_or_missing') - self.writeline('undefined = environment.undefined') - self.writeline('if 0: yield None') + self.writeline("resolve = context.resolve_or_missing") + self.writeline("undefined = environment.undefined") + # always use the standard Undefined class for the implicit else of + # conditional expressions + self.writeline("cond_expr_undefined = Undefined") + self.writeline("if 0: yield None") def push_parameter_definitions(self, frame): """Pushes all parameter targets from the given frame into a local @@ -643,12 +658,12 @@ class CodeGenerator(NodeVisitor): def get_resolve_func(self): target = self._context_reference_stack[-1] - if target == 'context': - return 'resolve' - return '%s.resolve' % target + if target == "context": + return "resolve" + return "%s.resolve" % target def derive_context(self, frame): - return '%s.derived(%s)' % ( + return "%s.derived(%s)" % ( self.get_context_ref(), self.dump_local_context(frame), ) @@ -670,44 +685,48 @@ class CodeGenerator(NodeVisitor): vars = self._assign_stack.pop() if not frame.toplevel or not vars: return - public_names = [x for x in vars if x[:1] != '_'] + public_names = [x for x in vars if x[:1] != "_"] if len(vars) == 1: name = next(iter(vars)) ref = frame.symbols.ref(name) - self.writeline('context.vars[%r] = %s' % (name, ref)) + self.writeline("context.vars[%r] = %s" % (name, ref)) else: - self.writeline('context.vars.update({') + self.writeline("context.vars.update({") for idx, name in enumerate(vars): if idx: - self.write(', ') + self.write(", ") ref = frame.symbols.ref(name) - self.write('%r: %s' % (name, ref)) - self.write('})') + self.write("%r: %s" % (name, ref)) + self.write("})") if public_names: if len(public_names) == 1: - self.writeline('context.exported_vars.add(%r)' % - public_names[0]) + self.writeline("context.exported_vars.add(%r)" % public_names[0]) else: - self.writeline('context.exported_vars.update((%s))' % - ', '.join(imap(repr, public_names))) + self.writeline( + "context.exported_vars.update((%s))" + % ", ".join(imap(repr, public_names)) + ) # -- Statement Visitors def visit_Template(self, node, frame=None): - assert frame is None, 'no root frame allowed' + assert frame is None, "no root frame allowed" eval_ctx = EvalContext(self.environment, self.name) - from jinja2.runtime import __all__ as exported - self.writeline('from __future__ import %s' % ', '.join(code_features)) - self.writeline('from jinja2.runtime import ' + ', '.join(exported)) + from .runtime import exported + + self.writeline("from __future__ import %s" % ", ".join(code_features)) + self.writeline("from jinja2.runtime import " + ", ".join(exported)) if self.environment.is_async: - self.writeline('from jinja2.asyncsupport import auto_await, ' - 'auto_aiter, make_async_loop_context') + self.writeline( + "from jinja2.asyncsupport import auto_await, " + "auto_aiter, AsyncLoopContext" + ) # if we want a deferred initialization we cannot move the # environment into a local name - envenv = not self.defer_init and ', environment=environment' or '' + envenv = not self.defer_init and ", environment=environment" or "" # do we have an extends tag at all? If not, we can save some # overhead by just not processing any inheritance code. @@ -716,7 +735,7 @@ class CodeGenerator(NodeVisitor): # find all blocks for block in node.find_all(nodes.Block): if block.name in self.blocks: - self.fail('block %r defined twice' % block.name, block.lineno) + self.fail("block %r defined twice" % block.name, block.lineno) self.blocks[block.name] = block # find all imports and import them @@ -724,32 +743,32 @@ class CodeGenerator(NodeVisitor): if import_.importname not in self.import_aliases: imp = import_.importname self.import_aliases[imp] = alias = self.temporary_identifier() - if '.' in imp: - module, obj = imp.rsplit('.', 1) - self.writeline('from %s import %s as %s' % - (module, obj, alias)) + if "." in imp: + module, obj = imp.rsplit(".", 1) + self.writeline("from %s import %s as %s" % (module, obj, alias)) else: - self.writeline('import %s as %s' % (imp, alias)) + self.writeline("import %s as %s" % (imp, alias)) # add the load name - self.writeline('name = %r' % self.name) + self.writeline("name = %r" % self.name) # generate the root render function. - self.writeline('%s(context, missing=missing%s):' % - (self.func('root'), envenv), extra=1) + self.writeline( + "%s(context, missing=missing%s):" % (self.func("root"), envenv), extra=1 + ) self.indent() self.write_commons() # process the root frame = Frame(eval_ctx) - if 'self' in find_undeclared(node.body, ('self',)): - ref = frame.symbols.declare_parameter('self') - self.writeline('%s = TemplateReference(context)' % ref) + if "self" in find_undeclared(node.body, ("self",)): + ref = frame.symbols.declare_parameter("self") + self.writeline("%s = TemplateReference(context)" % ref) frame.symbols.analyze_node(node) frame.toplevel = frame.rootlevel = True frame.require_output_check = have_extends and not self.has_known_extends if have_extends: - self.writeline('parent_template = None') + self.writeline("parent_template = None") self.enter_frame(frame) self.pull_dependencies(node.body) self.blockvisit(node.body, frame) @@ -760,39 +779,42 @@ class CodeGenerator(NodeVisitor): if have_extends: if not self.has_known_extends: self.indent() - self.writeline('if parent_template is not None:') + self.writeline("if parent_template is not None:") self.indent() if supports_yield_from and not self.environment.is_async: - self.writeline('yield from parent_template.' - 'root_render_func(context)') + self.writeline("yield from parent_template.root_render_func(context)") else: - self.writeline('%sfor event in parent_template.' - 'root_render_func(context):' % - (self.environment.is_async and 'async ' or '')) + self.writeline( + "%sfor event in parent_template." + "root_render_func(context):" + % (self.environment.is_async and "async " or "") + ) self.indent() - self.writeline('yield event') + self.writeline("yield event") self.outdent() self.outdent(1 + (not self.has_known_extends)) # at this point we now have the blocks collected and can visit them too. for name, block in iteritems(self.blocks): - self.writeline('%s(context, missing=missing%s):' % - (self.func('block_' + name), envenv), - block, 1) + self.writeline( + "%s(context, missing=missing%s):" + % (self.func("block_" + name), envenv), + block, + 1, + ) self.indent() self.write_commons() # It's important that we do not make this frame a child of the # toplevel template. This would cause a variety of # interesting issues with identifier tracking. block_frame = Frame(eval_ctx) - undeclared = find_undeclared(block.body, ('self', 'super')) - if 'self' in undeclared: - ref = block_frame.symbols.declare_parameter('self') - self.writeline('%s = TemplateReference(context)' % ref) - if 'super' in undeclared: - ref = block_frame.symbols.declare_parameter('super') - self.writeline('%s = context.super(%r, ' - 'block_%s)' % (ref, name, name)) + undeclared = find_undeclared(block.body, ("self", "super")) + if "self" in undeclared: + ref = block_frame.symbols.declare_parameter("self") + self.writeline("%s = TemplateReference(context)" % ref) + if "super" in undeclared: + ref = block_frame.symbols.declare_parameter("super") + self.writeline("%s = context.super(%r, block_%s)" % (ref, name, name)) block_frame.symbols.analyze_node(block) block_frame.block = name self.enter_frame(block_frame) @@ -801,13 +823,15 @@ class CodeGenerator(NodeVisitor): self.leave_frame(block_frame, with_python_scope=True) self.outdent() - self.writeline('blocks = {%s}' % ', '.join('%r: block_%s' % (x, x) - for x in self.blocks), - extra=1) + self.writeline( + "blocks = {%s}" % ", ".join("%r: block_%s" % (x, x) for x in self.blocks), + extra=1, + ) # add a function that returns the debug info - self.writeline('debug_info = %r' % '&'.join('%s=%s' % x for x - in self.debug_info)) + self.writeline( + "debug_info = %r" % "&".join("%s=%s" % x for x in self.debug_info) + ) def visit_Block(self, node, frame): """Call a block and register it for the template.""" @@ -818,7 +842,7 @@ class CodeGenerator(NodeVisitor): if self.has_known_extends: return if self.extends_so_far > 0: - self.writeline('if parent_template is None:') + self.writeline("if parent_template is None:") self.indent() level += 1 @@ -827,16 +851,22 @@ class CodeGenerator(NodeVisitor): else: context = self.get_context_ref() - if supports_yield_from and not self.environment.is_async and \ - frame.buffer is None: - self.writeline('yield from context.blocks[%r][0](%s)' % ( - node.name, context), node) + if ( + supports_yield_from + and not self.environment.is_async + and frame.buffer is None + ): + self.writeline( + "yield from context.blocks[%r][0](%s)" % (node.name, context), node + ) else: - loop = self.environment.is_async and 'async for' or 'for' - self.writeline('%s event in context.blocks[%r][0](%s):' % ( - loop, node.name, context), node) + loop = self.environment.is_async and "async for" or "for" + self.writeline( + "%s event in context.blocks[%r][0](%s):" % (loop, node.name, context), + node, + ) self.indent() - self.simple_write('event', frame) + self.simple_write("event", frame) self.outdent() self.outdent(level) @@ -844,8 +874,7 @@ class CodeGenerator(NodeVisitor): def visit_Extends(self, node, frame): """Calls the extender.""" if not frame.toplevel: - self.fail('cannot use extend from a non top-level scope', - node.lineno) + self.fail("cannot use extend from a non top-level scope", node.lineno) # if the number of extends statements in general is zero so # far, we don't have to add a check if something extended @@ -857,10 +886,9 @@ class CodeGenerator(NodeVisitor): # time too, but i welcome it not to confuse users by throwing the # same error at different times just "because we can". if not self.has_known_extends: - self.writeline('if parent_template is not None:') + self.writeline("if parent_template is not None:") self.indent() - self.writeline('raise TemplateRuntimeError(%r)' % - 'extended multiple times') + self.writeline("raise TemplateRuntimeError(%r)" % "extended multiple times") # if we have a known extends already we don't need that code here # as we know that the template execution will end here. @@ -869,14 +897,14 @@ class CodeGenerator(NodeVisitor): else: self.outdent() - self.writeline('parent_template = environment.get_template(', node) + self.writeline("parent_template = environment.get_template(", node) self.visit(node.template, frame) - self.write(', %r)' % self.name) - self.writeline('for name, parent_block in parent_template.' - 'blocks.%s():' % dict_item_iter) + self.write(", %r)" % self.name) + self.writeline( + "for name, parent_block in parent_template.blocks.%s():" % dict_item_iter + ) self.indent() - self.writeline('context.blocks.setdefault(name, []).' - 'append(parent_block)') + self.writeline("context.blocks.setdefault(name, []).append(parent_block)") self.outdent() # if this extends statement was in the root level we can take @@ -891,52 +919,56 @@ class CodeGenerator(NodeVisitor): def visit_Include(self, node, frame): """Handles includes.""" if node.ignore_missing: - self.writeline('try:') + self.writeline("try:") self.indent() - func_name = 'get_or_select_template' + func_name = "get_or_select_template" if isinstance(node.template, nodes.Const): if isinstance(node.template.value, string_types): - func_name = 'get_template' + func_name = "get_template" elif isinstance(node.template.value, (tuple, list)): - func_name = 'select_template' + func_name = "select_template" elif isinstance(node.template, (nodes.Tuple, nodes.List)): - func_name = 'select_template' + func_name = "select_template" - self.writeline('template = environment.%s(' % func_name, node) + self.writeline("template = environment.%s(" % func_name, node) self.visit(node.template, frame) - self.write(', %r)' % self.name) + self.write(", %r)" % self.name) if node.ignore_missing: self.outdent() - self.writeline('except TemplateNotFound:') + self.writeline("except TemplateNotFound:") self.indent() - self.writeline('pass') + self.writeline("pass") self.outdent() - self.writeline('else:') + self.writeline("else:") self.indent() skip_event_yield = False if node.with_context: - loop = self.environment.is_async and 'async for' or 'for' - self.writeline('%s event in template.root_render_func(' - 'template.new_context(context.get_all(), True, ' - '%s)):' % (loop, self.dump_local_context(frame))) + loop = self.environment.is_async and "async for" or "for" + self.writeline( + "%s event in template.root_render_func(" + "template.new_context(context.get_all(), True, " + "%s)):" % (loop, self.dump_local_context(frame)) + ) elif self.environment.is_async: - self.writeline('for event in (await ' - 'template._get_default_module_async())' - '._body_stream:') + self.writeline( + "for event in (await " + "template._get_default_module_async())" + "._body_stream:" + ) else: if supports_yield_from: - self.writeline('yield from template._get_default_module()' - '._body_stream') + self.writeline("yield from template._get_default_module()._body_stream") skip_event_yield = True else: - self.writeline('for event in template._get_default_module()' - '._body_stream:') + self.writeline( + "for event in template._get_default_module()._body_stream:" + ) if not skip_event_yield: self.indent() - self.simple_write('event', frame) + self.simple_write("event", frame) self.outdent() if node.ignore_missing: @@ -944,40 +976,50 @@ class CodeGenerator(NodeVisitor): def visit_Import(self, node, frame): """Visit regular imports.""" - self.writeline('%s = ' % frame.symbols.ref(node.target), node) + self.writeline("%s = " % frame.symbols.ref(node.target), node) if frame.toplevel: - self.write('context.vars[%r] = ' % node.target) + self.write("context.vars[%r] = " % node.target) if self.environment.is_async: - self.write('await ') - self.write('environment.get_template(') + self.write("await ") + self.write("environment.get_template(") self.visit(node.template, frame) - self.write(', %r).' % self.name) + self.write(", %r)." % self.name) if node.with_context: - self.write('make_module%s(context.get_all(), True, %s)' - % (self.environment.is_async and '_async' or '', - self.dump_local_context(frame))) + self.write( + "make_module%s(context.get_all(), True, %s)" + % ( + self.environment.is_async and "_async" or "", + self.dump_local_context(frame), + ) + ) elif self.environment.is_async: - self.write('_get_default_module_async()') + self.write("_get_default_module_async()") else: - self.write('_get_default_module()') - if frame.toplevel and not node.target.startswith('_'): - self.writeline('context.exported_vars.discard(%r)' % node.target) + self.write("_get_default_module()") + if frame.toplevel and not node.target.startswith("_"): + self.writeline("context.exported_vars.discard(%r)" % node.target) def visit_FromImport(self, node, frame): """Visit named imports.""" self.newline(node) - self.write('included_template = %senvironment.get_template(' - % (self.environment.is_async and 'await ' or '')) + self.write( + "included_template = %senvironment.get_template(" + % (self.environment.is_async and "await " or "") + ) self.visit(node.template, frame) - self.write(', %r).' % self.name) + self.write(", %r)." % self.name) if node.with_context: - self.write('make_module%s(context.get_all(), True, %s)' - % (self.environment.is_async and '_async' or '', - self.dump_local_context(frame))) + self.write( + "make_module%s(context.get_all(), True, %s)" + % ( + self.environment.is_async and "_async" or "", + self.dump_local_context(frame), + ) + ) elif self.environment.is_async: - self.write('_get_default_module_async()') + self.write("_get_default_module_async()") else: - self.write('_get_default_module()') + self.write("_get_default_module()") var_names = [] discarded_names = [] @@ -986,41 +1028,51 @@ class CodeGenerator(NodeVisitor): name, alias = name else: alias = name - self.writeline('%s = getattr(included_template, ' - '%r, missing)' % (frame.symbols.ref(alias), name)) - self.writeline('if %s is missing:' % frame.symbols.ref(alias)) + self.writeline( + "%s = getattr(included_template, " + "%r, missing)" % (frame.symbols.ref(alias), name) + ) + self.writeline("if %s is missing:" % frame.symbols.ref(alias)) self.indent() - self.writeline('%s = undefined(%r %% ' - 'included_template.__name__, ' - 'name=%r)' % - (frame.symbols.ref(alias), - 'the template %%r (imported on %s) does ' - 'not export the requested name %s' % ( - self.position(node), - repr(name) - ), name)) + self.writeline( + "%s = undefined(%r %% " + "included_template.__name__, " + "name=%r)" + % ( + frame.symbols.ref(alias), + "the template %%r (imported on %s) does " + "not export the requested name %s" + % (self.position(node), repr(name)), + name, + ) + ) self.outdent() if frame.toplevel: var_names.append(alias) - if not alias.startswith('_'): + if not alias.startswith("_"): discarded_names.append(alias) if var_names: if len(var_names) == 1: name = var_names[0] - self.writeline('context.vars[%r] = %s' % - (name, frame.symbols.ref(name))) + self.writeline( + "context.vars[%r] = %s" % (name, frame.symbols.ref(name)) + ) else: - self.writeline('context.vars.update({%s})' % ', '.join( - '%r: %s' % (name, frame.symbols.ref(name)) for name in var_names - )) + self.writeline( + "context.vars.update({%s})" + % ", ".join( + "%r: %s" % (name, frame.symbols.ref(name)) for name in var_names + ) + ) if discarded_names: if len(discarded_names) == 1: - self.writeline('context.exported_vars.discard(%r)' % - discarded_names[0]) + self.writeline("context.exported_vars.discard(%r)" % discarded_names[0]) else: - self.writeline('context.exported_vars.difference_' - 'update((%s))' % ', '.join(imap(repr, discarded_names))) + self.writeline( + "context.exported_vars.difference_" + "update((%s))" % ", ".join(imap(repr, discarded_names)) + ) def visit_For(self, node, frame): loop_frame = frame.inner() @@ -1030,35 +1082,35 @@ class CodeGenerator(NodeVisitor): # try to figure out if we have an extended loop. An extended loop # is necessary if the loop is in recursive mode if the special loop # variable is accessed in the body. - extended_loop = node.recursive or 'loop' in \ - find_undeclared(node.iter_child_nodes( - only=('body',)), ('loop',)) + extended_loop = node.recursive or "loop" in find_undeclared( + node.iter_child_nodes(only=("body",)), ("loop",) + ) loop_ref = None if extended_loop: - loop_ref = loop_frame.symbols.declare_parameter('loop') + loop_ref = loop_frame.symbols.declare_parameter("loop") - loop_frame.symbols.analyze_node(node, for_branch='body') + loop_frame.symbols.analyze_node(node, for_branch="body") if node.else_: - else_frame.symbols.analyze_node(node, for_branch='else') + else_frame.symbols.analyze_node(node, for_branch="else") if node.test: loop_filter_func = self.temporary_identifier() - test_frame.symbols.analyze_node(node, for_branch='test') - self.writeline('%s(fiter):' % self.func(loop_filter_func), node.test) + test_frame.symbols.analyze_node(node, for_branch="test") + self.writeline("%s(fiter):" % self.func(loop_filter_func), node.test) self.indent() self.enter_frame(test_frame) - self.writeline(self.environment.is_async and 'async for ' or 'for ') + self.writeline(self.environment.is_async and "async for " or "for ") self.visit(node.target, loop_frame) - self.write(' in ') - self.write(self.environment.is_async and 'auto_aiter(fiter)' or 'fiter') - self.write(':') + self.write(" in ") + self.write(self.environment.is_async and "auto_aiter(fiter)" or "fiter") + self.write(":") self.indent() - self.writeline('if ', node.test) + self.writeline("if ", node.test) self.visit(node.test, test_frame) - self.write(':') + self.write(":") self.indent() - self.writeline('yield ') + self.writeline("yield ") self.visit(node.target, loop_frame) self.outdent(3) self.leave_frame(test_frame, with_python_scope=True) @@ -1067,8 +1119,9 @@ class CodeGenerator(NodeVisitor): # variables at that point. Because loops can be nested but the loop # variable is a special one we have to enforce aliasing for it. if node.recursive: - self.writeline('%s(reciter, loop_render_func, depth=0):' % - self.func('loop'), node) + self.writeline( + "%s(reciter, loop_render_func, depth=0):" % self.func("loop"), node + ) self.indent() self.buffer(loop_frame) @@ -1078,57 +1131,60 @@ class CodeGenerator(NodeVisitor): # make sure the loop variable is a special one and raise a template # assertion error if a loop tries to write to loop if extended_loop: - self.writeline('%s = missing' % loop_ref) + self.writeline("%s = missing" % loop_ref) for name in node.find_all(nodes.Name): - if name.ctx == 'store' and name.name == 'loop': - self.fail('Can\'t assign to special loop variable ' - 'in for-loop target', name.lineno) + if name.ctx == "store" and name.name == "loop": + self.fail( + "Can't assign to special loop variable in for-loop target", + name.lineno, + ) if node.else_: iteration_indicator = self.temporary_identifier() - self.writeline('%s = 1' % iteration_indicator) + self.writeline("%s = 1" % iteration_indicator) - self.writeline(self.environment.is_async and 'async for ' or 'for ', node) + self.writeline(self.environment.is_async and "async for " or "for ", node) self.visit(node.target, loop_frame) if extended_loop: if self.environment.is_async: - self.write(', %s in await make_async_loop_context(' % loop_ref) + self.write(", %s in AsyncLoopContext(" % loop_ref) else: - self.write(', %s in LoopContext(' % loop_ref) + self.write(", %s in LoopContext(" % loop_ref) else: - self.write(' in ') + self.write(" in ") if node.test: - self.write('%s(' % loop_filter_func) + self.write("%s(" % loop_filter_func) if node.recursive: - self.write('reciter') + self.write("reciter") else: if self.environment.is_async and not extended_loop: - self.write('auto_aiter(') + self.write("auto_aiter(") self.visit(node.iter, frame) if self.environment.is_async and not extended_loop: - self.write(')') + self.write(")") if node.test: - self.write(')') + self.write(")") if node.recursive: - self.write(', undefined, loop_render_func, depth):') + self.write(", undefined, loop_render_func, depth):") else: - self.write(extended_loop and ', undefined):' or ':') + self.write(extended_loop and ", undefined):" or ":") self.indent() self.enter_frame(loop_frame) self.blockvisit(node.body, loop_frame) if node.else_: - self.writeline('%s = 0' % iteration_indicator) + self.writeline("%s = 0" % iteration_indicator) self.outdent() - self.leave_frame(loop_frame, with_python_scope=node.recursive - and not node.else_) + self.leave_frame( + loop_frame, with_python_scope=node.recursive and not node.else_ + ) if node.else_: - self.writeline('if %s:' % iteration_indicator) + self.writeline("if %s:" % iteration_indicator) self.indent() self.enter_frame(else_frame) self.blockvisit(node.else_, else_frame) @@ -1142,33 +1198,33 @@ class CodeGenerator(NodeVisitor): self.outdent() self.start_write(frame, node) if self.environment.is_async: - self.write('await ') - self.write('loop(') + self.write("await ") + self.write("loop(") if self.environment.is_async: - self.write('auto_aiter(') + self.write("auto_aiter(") self.visit(node.iter, frame) if self.environment.is_async: - self.write(')') - self.write(', loop)') + self.write(")") + self.write(", loop)") self.end_write(frame) def visit_If(self, node, frame): if_frame = frame.soft() - self.writeline('if ', node) + self.writeline("if ", node) self.visit(node.test, if_frame) - self.write(':') + self.write(":") self.indent() self.blockvisit(node.body, if_frame) self.outdent() for elif_ in node.elif_: - self.writeline('elif ', elif_) + self.writeline("elif ", elif_) self.visit(elif_.test, if_frame) - self.write(':') + self.write(":") self.indent() self.blockvisit(elif_.body, if_frame) self.outdent() if node.else_: - self.writeline('else:') + self.writeline("else:") self.indent() self.blockvisit(node.else_, if_frame) self.outdent() @@ -1177,16 +1233,15 @@ class CodeGenerator(NodeVisitor): macro_frame, macro_ref = self.macro_body(node, frame) self.newline() if frame.toplevel: - if not node.name.startswith('_'): - self.write('context.exported_vars.add(%r)' % node.name) - ref = frame.symbols.ref(node.name) - self.writeline('context.vars[%r] = ' % node.name) - self.write('%s = ' % frame.symbols.ref(node.name)) + if not node.name.startswith("_"): + self.write("context.exported_vars.add(%r)" % node.name) + self.writeline("context.vars[%r] = " % node.name) + self.write("%s = " % frame.symbols.ref(node.name)) self.macro_def(macro_ref, macro_frame) def visit_CallBlock(self, node, frame): call_frame, macro_ref = self.macro_body(node, frame) - self.writeline('caller = ') + self.writeline("caller = ") self.macro_def(macro_ref, call_frame) self.start_write(frame, node) self.visit_Call(node.call, frame, forward_caller=True) @@ -1207,10 +1262,10 @@ class CodeGenerator(NodeVisitor): with_frame = frame.inner() with_frame.symbols.analyze_node(node) self.enter_frame(with_frame) - for idx, (target, expr) in enumerate(izip(node.targets, node.values)): + for target, expr in izip(node.targets, node.values): self.newline() self.visit(target, with_frame) - self.write(' = ') + self.write(" = ") self.visit(expr, frame) self.blockvisit(node.body, with_frame) self.leave_frame(with_frame) @@ -1219,156 +1274,187 @@ class CodeGenerator(NodeVisitor): self.newline(node) self.visit(node.node, frame) - def visit_Output(self, node, frame): - # if we have a known extends statement, we don't output anything - # if we are in a require_output_check section - if self.has_known_extends and frame.require_output_check: - return + _FinalizeInfo = namedtuple("_FinalizeInfo", ("const", "src")) + #: The default finalize function if the environment isn't configured + #: with one. Or if the environment has one, this is called on that + #: function's output for constants. + _default_finalize = text_type + _finalize = None + + def _make_finalize(self): + """Build the finalize function to be used on constants and at + runtime. Cached so it's only created once for all output nodes. + + Returns a ``namedtuple`` with the following attributes: + + ``const`` + A function to finalize constant data at compile time. + + ``src`` + Source code to output around nodes to be evaluated at + runtime. + """ + if self._finalize is not None: + return self._finalize + + finalize = default = self._default_finalize + src = None - allow_constant_finalize = True if self.environment.finalize: - func = self.environment.finalize - if getattr(func, 'contextfunction', False) or \ - getattr(func, 'evalcontextfunction', False): - allow_constant_finalize = False - elif getattr(func, 'environmentfunction', False): - finalize = lambda x: text_type( - self.environment.finalize(self.environment, x)) - else: - finalize = lambda x: text_type(self.environment.finalize(x)) + src = "environment.finalize(" + env_finalize = self.environment.finalize + + def finalize(value): + return default(env_finalize(value)) + + if getattr(env_finalize, "contextfunction", False) is True: + src += "context, " + finalize = None # noqa: F811 + elif getattr(env_finalize, "evalcontextfunction", False) is True: + src += "context.eval_ctx, " + finalize = None + elif getattr(env_finalize, "environmentfunction", False) is True: + src += "environment, " + + def finalize(value): + return default(env_finalize(self.environment, value)) + + self._finalize = self._FinalizeInfo(finalize, src) + return self._finalize + + def _output_const_repr(self, group): + """Given a group of constant values converted from ``Output`` + child nodes, produce a string to write to the template module + source. + """ + return repr(concat(group)) + + def _output_child_to_const(self, node, frame, finalize): + """Try to optimize a child of an ``Output`` node by trying to + convert it to constant, finalized data at compile time. + + If :exc:`Impossible` is raised, the node is not constant and + will be evaluated at runtime. Any other exception will also be + evaluated at runtime for easier debugging. + """ + const = node.as_const(frame.eval_ctx) + + if frame.eval_ctx.autoescape: + const = escape(const) + + # Template data doesn't go through finalize. + if isinstance(node, nodes.TemplateData): + return text_type(const) + + return finalize.const(const) + + def _output_child_pre(self, node, frame, finalize): + """Output extra source code before visiting a child of an + ``Output`` node. + """ + if frame.eval_ctx.volatile: + self.write("(escape if context.eval_ctx.autoescape else to_string)(") + elif frame.eval_ctx.autoescape: + self.write("escape(") else: - finalize = text_type + self.write("to_string(") + + if finalize.src is not None: + self.write(finalize.src) + + def _output_child_post(self, node, frame, finalize): + """Output extra source code after visiting a child of an + ``Output`` node. + """ + self.write(")") - # if we are inside a frame that requires output checking, we do so - outdent_later = False + if finalize.src is not None: + self.write(")") + + def visit_Output(self, node, frame): + # If an extends is active, don't render outside a block. if frame.require_output_check: - self.writeline('if parent_template is None:') + # A top-level extends is known to exist at compile time. + if self.has_known_extends: + return + + self.writeline("if parent_template is None:") self.indent() - outdent_later = True - # try to evaluate as many chunks as possible into a static - # string at compile time. + finalize = self._make_finalize() body = [] + + # Evaluate constants at compile time if possible. Each item in + # body will be either a list of static data or a node to be + # evaluated at runtime. for child in node.nodes: try: - if not allow_constant_finalize: + if not ( + # If the finalize function requires runtime context, + # constants can't be evaluated at compile time. + finalize.const + # Unless it's basic template data that won't be + # finalized anyway. + or isinstance(child, nodes.TemplateData) + ): raise nodes.Impossible() - const = child.as_const(frame.eval_ctx) - except nodes.Impossible: - body.append(child) - continue - # the frame can't be volatile here, becaus otherwise the - # as_const() function would raise an Impossible exception - # at that point. - try: - if frame.eval_ctx.autoescape: - if hasattr(const, '__html__'): - const = const.__html__() - else: - const = escape(const) - const = finalize(const) - except Exception: - # if something goes wrong here we evaluate the node - # at runtime for easier debugging + + const = self._output_child_to_const(child, frame, finalize) + except (nodes.Impossible, Exception): + # The node was not constant and needs to be evaluated at + # runtime. Or another error was raised, which is easier + # to debug at runtime. body.append(child) continue + if body and isinstance(body[-1], list): body[-1].append(const) else: body.append([const]) - # if we have less than 3 nodes or a buffer we yield or extend/append - if len(body) < 3 or frame.buffer is not None: - if frame.buffer is not None: - # for one item we append, for more we extend - if len(body) == 1: - self.writeline('%s.append(' % frame.buffer) + if frame.buffer is not None: + if len(body) == 1: + self.writeline("%s.append(" % frame.buffer) + else: + self.writeline("%s.extend((" % frame.buffer) + + self.indent() + + for item in body: + if isinstance(item, list): + # A group of constant data to join and output. + val = self._output_const_repr(item) + + if frame.buffer is None: + self.writeline("yield " + val) else: - self.writeline('%s.extend((' % frame.buffer) - self.indent() - for item in body: - if isinstance(item, list): - val = repr(concat(item)) - if frame.buffer is None: - self.writeline('yield ' + val) - else: - self.writeline(val + ',') + self.writeline(val + ",") + else: + if frame.buffer is None: + self.writeline("yield ", item) else: - if frame.buffer is None: - self.writeline('yield ', item) - else: - self.newline(item) - close = 1 - if frame.eval_ctx.volatile: - self.write('(escape if context.eval_ctx.autoescape' - ' else to_string)(') - elif frame.eval_ctx.autoescape: - self.write('escape(') - else: - self.write('to_string(') - if self.environment.finalize is not None: - self.write('environment.finalize(') - if getattr(self.environment.finalize, - "contextfunction", False): - self.write('context, ') - close += 1 - self.visit(item, frame) - self.write(')' * close) - if frame.buffer is not None: - self.write(',') - if frame.buffer is not None: - # close the open parentheses - self.outdent() - self.writeline(len(body) == 1 and ')' or '))') + self.newline(item) - # otherwise we create a format string as this is faster in that case - else: - format = [] - arguments = [] - for item in body: - if isinstance(item, list): - format.append(concat(item).replace('%', '%%')) - else: - format.append('%s') - arguments.append(item) - self.writeline('yield ') - self.write(repr(concat(format)) + ' % (') - self.indent() - for argument in arguments: - self.newline(argument) - close = 0 - if frame.eval_ctx.volatile: - self.write('(escape if context.eval_ctx.autoescape else' - ' to_string)(') - close += 1 - elif frame.eval_ctx.autoescape: - self.write('escape(') - close += 1 - if self.environment.finalize is not None: - self.write('environment.finalize(') - if getattr(self.environment.finalize, - 'contextfunction', False): - self.write('context, ') - elif getattr(self.environment.finalize, - 'evalcontextfunction', False): - self.write('context.eval_ctx, ') - elif getattr(self.environment.finalize, - 'environmentfunction', False): - self.write('environment, ') - close += 1 - self.visit(argument, frame) - self.write(')' * close + ', ') + # A node to be evaluated at runtime. + self._output_child_pre(item, frame, finalize) + self.visit(item, frame) + self._output_child_post(item, frame, finalize) + + if frame.buffer is not None: + self.write(",") + + if frame.buffer is not None: self.outdent() - self.writeline(')') + self.writeline(")" if len(body) == 1 else "))") - if outdent_later: + if frame.require_output_check: self.outdent() def visit_Assign(self, node, frame): self.push_assign_tracking() self.newline(node) self.visit(node.target, frame) - self.write(' = ') + self.write(" = ") self.visit(node.node, frame) self.pop_assign_tracking(frame) @@ -1385,20 +1471,19 @@ class CodeGenerator(NodeVisitor): self.blockvisit(node.body, block_frame) self.newline(node) self.visit(node.target, frame) - self.write(' = (Markup if context.eval_ctx.autoescape ' - 'else identity)(') + self.write(" = (Markup if context.eval_ctx.autoescape else identity)(") if node.filter is not None: self.visit_Filter(node.filter, block_frame) else: - self.write('concat(%s)' % block_frame.buffer) - self.write(')') + self.write("concat(%s)" % block_frame.buffer) + self.write(")") self.pop_assign_tracking(frame) self.leave_frame(block_frame) # -- Expression Visitors def visit_Name(self, node, frame): - if node.ctx == 'store' and frame.toplevel: + if node.ctx == "store" and frame.toplevel: if self._assign_stack: self._assign_stack[-1].add(node.name) ref = frame.symbols.ref(node.name) @@ -1406,12 +1491,17 @@ class CodeGenerator(NodeVisitor): # If we are looking up a variable we might have to deal with the # case where it's undefined. We can skip that case if the load # instruction indicates a parameter which are always defined. - if node.ctx == 'load': + if node.ctx == "load": load = frame.symbols.find_load(ref) - if not (load is not None and load[0] == VAR_LOAD_PARAMETER and \ - not self.parameter_is_undeclared(ref)): - self.write('(undefined(name=%r) if %s is missing else %s)' % - (node.name, ref, ref)) + if not ( + load is not None + and load[0] == VAR_LOAD_PARAMETER + and not self.parameter_is_undeclared(ref) + ): + self.write( + "(undefined(name=%r) if %s is missing else %s)" + % (node.name, ref, ref) + ) return self.write(ref) @@ -1421,12 +1511,14 @@ class CodeGenerator(NodeVisitor): # `foo.bar` notation they will be parsed as a normal attribute access # when used anywhere but in a `set` context ref = frame.symbols.ref(node.name) - self.writeline('if not isinstance(%s, Namespace):' % ref) + self.writeline("if not isinstance(%s, Namespace):" % ref) self.indent() - self.writeline('raise TemplateRuntimeError(%r)' % - 'cannot assign attribute on non-namespace object') + self.writeline( + "raise TemplateRuntimeError(%r)" + % "cannot assign attribute on non-namespace object" + ) self.outdent() - self.writeline('%s[%r]' % (ref, node.attr)) + self.writeline("%s[%r]" % (ref, node.attr)) def visit_Const(self, node, frame): val = node.as_const(frame.eval_ctx) @@ -1439,230 +1531,256 @@ class CodeGenerator(NodeVisitor): try: self.write(repr(node.as_const(frame.eval_ctx))) except nodes.Impossible: - self.write('(Markup if context.eval_ctx.autoescape else identity)(%r)' - % node.data) + self.write( + "(Markup if context.eval_ctx.autoescape else identity)(%r)" % node.data + ) def visit_Tuple(self, node, frame): - self.write('(') + self.write("(") idx = -1 for idx, item in enumerate(node.items): if idx: - self.write(', ') + self.write(", ") self.visit(item, frame) - self.write(idx == 0 and ',)' or ')') + self.write(idx == 0 and ",)" or ")") def visit_List(self, node, frame): - self.write('[') + self.write("[") for idx, item in enumerate(node.items): if idx: - self.write(', ') + self.write(", ") self.visit(item, frame) - self.write(']') + self.write("]") def visit_Dict(self, node, frame): - self.write('{') + self.write("{") for idx, item in enumerate(node.items): if idx: - self.write(', ') + self.write(", ") self.visit(item.key, frame) - self.write(': ') + self.write(": ") self.visit(item.value, frame) - self.write('}') + self.write("}") - def binop(operator, interceptable=True): + def binop(operator, interceptable=True): # noqa: B902 @optimizeconst def visitor(self, node, frame): - if self.environment.sandboxed and \ - operator in self.environment.intercepted_binops: - self.write('environment.call_binop(context, %r, ' % operator) + if ( + self.environment.sandboxed + and operator in self.environment.intercepted_binops + ): + self.write("environment.call_binop(context, %r, " % operator) self.visit(node.left, frame) - self.write(', ') + self.write(", ") self.visit(node.right, frame) else: - self.write('(') + self.write("(") self.visit(node.left, frame) - self.write(' %s ' % operator) + self.write(" %s " % operator) self.visit(node.right, frame) - self.write(')') + self.write(")") + return visitor - def uaop(operator, interceptable=True): + def uaop(operator, interceptable=True): # noqa: B902 @optimizeconst def visitor(self, node, frame): - if self.environment.sandboxed and \ - operator in self.environment.intercepted_unops: - self.write('environment.call_unop(context, %r, ' % operator) + if ( + self.environment.sandboxed + and operator in self.environment.intercepted_unops + ): + self.write("environment.call_unop(context, %r, " % operator) self.visit(node.node, frame) else: - self.write('(' + operator) + self.write("(" + operator) self.visit(node.node, frame) - self.write(')') + self.write(")") + return visitor - visit_Add = binop('+') - visit_Sub = binop('-') - visit_Mul = binop('*') - visit_Div = binop('/') - visit_FloorDiv = binop('//') - visit_Pow = binop('**') - visit_Mod = binop('%') - visit_And = binop('and', interceptable=False) - visit_Or = binop('or', interceptable=False) - visit_Pos = uaop('+') - visit_Neg = uaop('-') - visit_Not = uaop('not ', interceptable=False) + visit_Add = binop("+") + visit_Sub = binop("-") + visit_Mul = binop("*") + visit_Div = binop("/") + visit_FloorDiv = binop("//") + visit_Pow = binop("**") + visit_Mod = binop("%") + visit_And = binop("and", interceptable=False) + visit_Or = binop("or", interceptable=False) + visit_Pos = uaop("+") + visit_Neg = uaop("-") + visit_Not = uaop("not ", interceptable=False) del binop, uaop @optimizeconst def visit_Concat(self, node, frame): if frame.eval_ctx.volatile: - func_name = '(context.eval_ctx.volatile and' \ - ' markup_join or unicode_join)' + func_name = "(context.eval_ctx.volatile and markup_join or unicode_join)" elif frame.eval_ctx.autoescape: - func_name = 'markup_join' + func_name = "markup_join" else: - func_name = 'unicode_join' - self.write('%s((' % func_name) + func_name = "unicode_join" + self.write("%s((" % func_name) for arg in node.nodes: self.visit(arg, frame) - self.write(', ') - self.write('))') + self.write(", ") + self.write("))") @optimizeconst def visit_Compare(self, node, frame): + self.write("(") self.visit(node.expr, frame) for op in node.ops: self.visit(op, frame) + self.write(")") def visit_Operand(self, node, frame): - self.write(' %s ' % operators[node.op]) + self.write(" %s " % operators[node.op]) self.visit(node.expr, frame) @optimizeconst def visit_Getattr(self, node, frame): - self.write('environment.getattr(') + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getattr(") self.visit(node.node, frame) - self.write(', %r)' % node.attr) + self.write(", %r)" % node.attr) + + if self.environment.is_async: + self.write("))") @optimizeconst def visit_Getitem(self, node, frame): # slices bypass the environment getitem method. if isinstance(node.arg, nodes.Slice): self.visit(node.node, frame) - self.write('[') + self.write("[") self.visit(node.arg, frame) - self.write(']') + self.write("]") else: - self.write('environment.getitem(') + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getitem(") self.visit(node.node, frame) - self.write(', ') + self.write(", ") self.visit(node.arg, frame) - self.write(')') + self.write(")") + + if self.environment.is_async: + self.write("))") def visit_Slice(self, node, frame): if node.start is not None: self.visit(node.start, frame) - self.write(':') + self.write(":") if node.stop is not None: self.visit(node.stop, frame) if node.step is not None: - self.write(':') + self.write(":") self.visit(node.step, frame) @optimizeconst def visit_Filter(self, node, frame): if self.environment.is_async: - self.write('await auto_await(') - self.write(self.filters[node.name] + '(') + self.write("await auto_await(") + self.write(self.filters[node.name] + "(") func = self.environment.filters.get(node.name) if func is None: - self.fail('no filter named %r' % node.name, node.lineno) - if getattr(func, 'contextfilter', False): - self.write('context, ') - elif getattr(func, 'evalcontextfilter', False): - self.write('context.eval_ctx, ') - elif getattr(func, 'environmentfilter', False): - self.write('environment, ') + self.fail("no filter named %r" % node.name, node.lineno) + if getattr(func, "contextfilter", False) is True: + self.write("context, ") + elif getattr(func, "evalcontextfilter", False) is True: + self.write("context.eval_ctx, ") + elif getattr(func, "environmentfilter", False) is True: + self.write("environment, ") # if the filter node is None we are inside a filter block # and want to write to the current buffer if node.node is not None: self.visit(node.node, frame) elif frame.eval_ctx.volatile: - self.write('(context.eval_ctx.autoescape and' - ' Markup(concat(%s)) or concat(%s))' % - (frame.buffer, frame.buffer)) + self.write( + "(context.eval_ctx.autoescape and" + " Markup(concat(%s)) or concat(%s))" % (frame.buffer, frame.buffer) + ) elif frame.eval_ctx.autoescape: - self.write('Markup(concat(%s))' % frame.buffer) + self.write("Markup(concat(%s))" % frame.buffer) else: - self.write('concat(%s)' % frame.buffer) + self.write("concat(%s)" % frame.buffer) self.signature(node, frame) - self.write(')') + self.write(")") if self.environment.is_async: - self.write(')') + self.write(")") @optimizeconst def visit_Test(self, node, frame): - self.write(self.tests[node.name] + '(') + self.write(self.tests[node.name] + "(") if node.name not in self.environment.tests: - self.fail('no test named %r' % node.name, node.lineno) + self.fail("no test named %r" % node.name, node.lineno) self.visit(node.node, frame) self.signature(node, frame) - self.write(')') + self.write(")") @optimizeconst def visit_CondExpr(self, node, frame): def write_expr2(): if node.expr2 is not None: return self.visit(node.expr2, frame) - self.write('undefined(%r)' % ('the inline if-' - 'expression on %s evaluated to false and ' - 'no else section was defined.' % self.position(node))) - - self.write('(') + self.write( + "cond_expr_undefined(%r)" + % ( + "the inline if-" + "expression on %s evaluated to false and " + "no else section was defined." % self.position(node) + ) + ) + + self.write("(") self.visit(node.expr1, frame) - self.write(' if ') + self.write(" if ") self.visit(node.test, frame) - self.write(' else ') + self.write(" else ") write_expr2() - self.write(')') + self.write(")") @optimizeconst def visit_Call(self, node, frame, forward_caller=False): if self.environment.is_async: - self.write('await auto_await(') + self.write("await auto_await(") if self.environment.sandboxed: - self.write('environment.call(context, ') + self.write("environment.call(context, ") else: - self.write('context.call(') + self.write("context.call(") self.visit(node.node, frame) - extra_kwargs = forward_caller and {'caller': 'caller'} or None + extra_kwargs = forward_caller and {"caller": "caller"} or None self.signature(node, frame, extra_kwargs) - self.write(')') + self.write(")") if self.environment.is_async: - self.write(')') + self.write(")") def visit_Keyword(self, node, frame): - self.write(node.key + '=') + self.write(node.key + "=") self.visit(node.value, frame) # -- Unused nodes for extensions def visit_MarkSafe(self, node, frame): - self.write('Markup(') + self.write("Markup(") self.visit(node.expr, frame) - self.write(')') + self.write(")") def visit_MarkSafeIfAutoescape(self, node, frame): - self.write('(context.eval_ctx.autoescape and Markup or identity)(') + self.write("(context.eval_ctx.autoescape and Markup or identity)(") self.visit(node.expr, frame) - self.write(')') + self.write(")") def visit_EnvironmentAttribute(self, node, frame): - self.write('environment.' + node.name) + self.write("environment." + node.name) def visit_ExtensionAttribute(self, node, frame): - self.write('environment.extensions[%r].%s' % (node.identifier, node.name)) + self.write("environment.extensions[%r].%s" % (node.identifier, node.name)) def visit_ImportedName(self, node, frame): self.write(self.import_aliases[node.importname]) @@ -1671,13 +1789,16 @@ class CodeGenerator(NodeVisitor): self.write(node.name) def visit_ContextReference(self, node, frame): - self.write('context') + self.write("context") + + def visit_DerivedContextReference(self, node, frame): + self.write(self.derive_context(frame)) def visit_Continue(self, node, frame): - self.writeline('continue', node) + self.writeline("continue", node) def visit_Break(self, node, frame): - self.writeline('break', node) + self.writeline("break", node) def visit_Scope(self, node, frame): scope_frame = frame.inner() @@ -1688,8 +1809,8 @@ class CodeGenerator(NodeVisitor): def visit_OverlayScope(self, node, frame): ctx = self.temporary_identifier() - self.writeline('%s = %s' % (ctx, self.derive_context(frame))) - self.writeline('%s.vars = ' % ctx) + self.writeline("%s = %s" % (ctx, self.derive_context(frame))) + self.writeline("%s.vars = " % ctx) self.visit(node.context, frame) self.push_context_reference(ctx) @@ -1702,7 +1823,7 @@ class CodeGenerator(NodeVisitor): def visit_EvalContextModifier(self, node, frame): for keyword in node.options: - self.writeline('context.eval_ctx.%s = ' % keyword.key) + self.writeline("context.eval_ctx.%s = " % keyword.key) self.visit(keyword.value, frame) try: val = keyword.value.as_const(frame.eval_ctx) @@ -1714,9 +1835,9 @@ class CodeGenerator(NodeVisitor): def visit_ScopedEvalContextModifier(self, node, frame): old_ctx_name = self.temporary_identifier() saved_ctx = frame.eval_ctx.save() - self.writeline('%s = context.eval_ctx.save()' % old_ctx_name) + self.writeline("%s = context.eval_ctx.save()" % old_ctx_name) self.visit_EvalContextModifier(node, frame) for child in node.body: self.visit(child, frame) frame.eval_ctx.revert(saved_ctx) - self.writeline('context.eval_ctx.revert(%s)' % old_ctx_name) + self.writeline("context.eval_ctx.revert(%s)" % old_ctx_name) diff --git a/jinja2/constants.py b/src/jinja2/constants.py index 11efd1e..bf7f2ca 100644 --- a/jinja2/constants.py +++ b/src/jinja2/constants.py @@ -1,17 +1,6 @@ # -*- coding: utf-8 -*- -""" - jinja.constants - ~~~~~~~~~~~~~~~ - - Various constants. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" - - #: list of lorem ipsum words used by the lipsum() helper function -LOREM_IPSUM_WORDS = u'''\ +LOREM_IPSUM_WORDS = u"""\ a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at auctor augue bibendum blandit class commodo condimentum congue consectetuer consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus @@ -29,4 +18,4 @@ ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus -viverra volutpat vulputate''' +viverra volutpat vulputate""" diff --git a/src/jinja2/debug.py b/src/jinja2/debug.py new file mode 100644 index 0000000..5d8aec3 --- /dev/null +++ b/src/jinja2/debug.py @@ -0,0 +1,268 @@ +import sys +from types import CodeType + +from . import TemplateSyntaxError +from ._compat import PYPY +from .utils import internal_code +from .utils import missing + + +def rewrite_traceback_stack(source=None): + """Rewrite the current exception to replace any tracebacks from + within compiled template code with tracebacks that look like they + came from the template source. + + This must be called within an ``except`` block. + + :param exc_info: A :meth:`sys.exc_info` tuple. If not provided, + the current ``exc_info`` is used. + :param source: For ``TemplateSyntaxError``, the original source if + known. + :return: A :meth:`sys.exc_info` tuple that can be re-raised. + """ + exc_type, exc_value, tb = sys.exc_info() + + if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated: + exc_value.translated = True + exc_value.source = source + + try: + # Remove the old traceback on Python 3, otherwise the frames + # from the compiler still show up. + exc_value.with_traceback(None) + except AttributeError: + pass + + # Outside of runtime, so the frame isn't executing template + # code, but it still needs to point at the template. + tb = fake_traceback( + exc_value, None, exc_value.filename or "<unknown>", exc_value.lineno + ) + else: + # Skip the frame for the render function. + tb = tb.tb_next + + stack = [] + + # Build the stack of traceback object, replacing any in template + # code with the source file and line information. + while tb is not None: + # Skip frames decorated with @internalcode. These are internal + # calls that aren't useful in template debugging output. + if tb.tb_frame.f_code in internal_code: + tb = tb.tb_next + continue + + template = tb.tb_frame.f_globals.get("__jinja_template__") + + if template is not None: + lineno = template.get_corresponding_lineno(tb.tb_lineno) + fake_tb = fake_traceback(exc_value, tb, template.filename, lineno) + stack.append(fake_tb) + else: + stack.append(tb) + + tb = tb.tb_next + + tb_next = None + + # Assign tb_next in reverse to avoid circular references. + for tb in reversed(stack): + tb_next = tb_set_next(tb, tb_next) + + return exc_type, exc_value, tb_next + + +def fake_traceback(exc_value, tb, filename, lineno): + """Produce a new traceback object that looks like it came from the + template source instead of the compiled code. The filename, line + number, and location name will point to the template, and the local + variables will be the current template context. + + :param exc_value: The original exception to be re-raised to create + the new traceback. + :param tb: The original traceback to get the local variables and + code info from. + :param filename: The template filename. + :param lineno: The line number in the template source. + """ + if tb is not None: + # Replace the real locals with the context that would be + # available at that point in the template. + locals = get_template_locals(tb.tb_frame.f_locals) + locals.pop("__jinja_exception__", None) + else: + locals = {} + + globals = { + "__name__": filename, + "__file__": filename, + "__jinja_exception__": exc_value, + } + # Raise an exception at the correct line number. + code = compile("\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec") + + # Build a new code object that points to the template file and + # replaces the location with a block name. + try: + location = "template" + + if tb is not None: + function = tb.tb_frame.f_code.co_name + + if function == "root": + location = "top-level template code" + elif function.startswith("block_"): + location = 'block "%s"' % function[6:] + + # Collect arguments for the new code object. CodeType only + # accepts positional arguments, and arguments were inserted in + # new Python versions. + code_args = [] + + for attr in ( + "argcount", + "posonlyargcount", # Python 3.8 + "kwonlyargcount", # Python 3 + "nlocals", + "stacksize", + "flags", + "code", # codestring + "consts", # constants + "names", + "varnames", + ("filename", filename), + ("name", location), + "firstlineno", + "lnotab", + "freevars", + "cellvars", + ): + if isinstance(attr, tuple): + # Replace with given value. + code_args.append(attr[1]) + continue + + try: + # Copy original value if it exists. + code_args.append(getattr(code, "co_" + attr)) + except AttributeError: + # Some arguments were added later. + continue + + code = CodeType(*code_args) + except Exception: + # Some environments such as Google App Engine don't support + # modifying code objects. + pass + + # Execute the new code, which is guaranteed to raise, and return + # the new traceback without this frame. + try: + exec(code, globals, locals) + except BaseException: + return sys.exc_info()[2].tb_next + + +def get_template_locals(real_locals): + """Based on the runtime locals, get the context that would be + available at that point in the template. + """ + # Start with the current template context. + ctx = real_locals.get("context") + + if ctx: + data = ctx.get_all().copy() + else: + data = {} + + # Might be in a derived context that only sets local variables + # rather than pushing a context. Local variables follow the scheme + # l_depth_name. Find the highest-depth local that has a value for + # each name. + local_overrides = {} + + for name, value in real_locals.items(): + if not name.startswith("l_") or value is missing: + # Not a template variable, or no longer relevant. + continue + + try: + _, depth, name = name.split("_", 2) + depth = int(depth) + except ValueError: + continue + + cur_depth = local_overrides.get(name, (-1,))[0] + + if cur_depth < depth: + local_overrides[name] = (depth, value) + + # Modify the context with any derived context. + for name, (_, value) in local_overrides.items(): + if value is missing: + data.pop(name, None) + else: + 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, tb_next): + tb.tb_next = tb_next + return tb + + +elif PYPY: + # PyPy might have special support, and won't work with ctypes. + try: + import tputil + except ImportError: + # Without tproxy support, use the original traceback. + def tb_set_next(tb, tb_next): + return tb + + else: + # With tproxy support, create a proxy around the traceback that + # returns the new tb_next. + def tb_set_next(tb, tb_next): + def controller(op): + if op.opname == "__getattribute__" and op.args[0] == "tb_next": + return tb_next + + return op.delegate() + + return tputil.make_proxy(controller, obj=tb) + + +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, tb_next): + 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/defaults.py b/src/jinja2/defaults.py new file mode 100644 index 0000000..8e0e7d7 --- /dev/null +++ b/src/jinja2/defaults.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from ._compat import range_type +from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401 +from .tests import TESTS as DEFAULT_TESTS # noqa: F401 +from .utils import Cycler +from .utils import generate_lorem_ipsum +from .utils import Joiner +from .utils import Namespace + +# defaults for the parser / lexer +BLOCK_START_STRING = "{%" +BLOCK_END_STRING = "%}" +VARIABLE_START_STRING = "{{" +VARIABLE_END_STRING = "}}" +COMMENT_START_STRING = "{#" +COMMENT_END_STRING = "#}" +LINE_STATEMENT_PREFIX = None +LINE_COMMENT_PREFIX = None +TRIM_BLOCKS = False +LSTRIP_BLOCKS = False +NEWLINE_SEQUENCE = "\n" +KEEP_TRAILING_NEWLINE = False + +# default filters, tests and namespace + +DEFAULT_NAMESPACE = { + "range": range_type, + "dict": dict, + "lipsum": generate_lorem_ipsum, + "cycler": Cycler, + "joiner": Joiner, + "namespace": Namespace, +} + +# default policies +DEFAULT_POLICIES = { + "compiler.ascii_str": True, + "urlize.rel": "noopener", + "urlize.target": None, + "truncate.leeway": 5, + "json.dumps_function": None, + "json.dumps_kwargs": {"sort_keys": True}, + "ext.i18n.trimmed": False, +} diff --git a/jinja2/environment.py b/src/jinja2/environment.py index b7f8b2f..8430390 100644 --- a/jinja2/environment.py +++ b/src/jinja2/environment.py @@ -1,63 +1,83 @@ # -*- coding: utf-8 -*- -""" - jinja2.environment - ~~~~~~~~~~~~~~~~~~ - - Provides a class that holds runtime and parsing time options. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. +"""Classes for managing templates and their runtime and compile time +options. """ import os import sys import weakref -from functools import reduce, partial +from functools import partial +from functools import reduce from markupsafe import Markup -from jinja2 import nodes -from jinja2.defaults import BLOCK_START_STRING, \ - BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \ - COMMENT_START_STRING, COMMENT_END_STRING, LINE_STATEMENT_PREFIX, \ - LINE_COMMENT_PREFIX, TRIM_BLOCKS, NEWLINE_SEQUENCE, \ - DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE, \ - DEFAULT_POLICIES, KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS -from jinja2.lexer import get_lexer, TokenStream -from jinja2.parser import Parser -from jinja2.nodes import EvalContext -from jinja2.compiler import generate, CodeGenerator -from jinja2.runtime import Undefined, new_context, Context -from jinja2.exceptions import TemplateSyntaxError, TemplateNotFound, \ - TemplatesNotFound, TemplateRuntimeError -from jinja2.utils import import_string, LRUCache, missing, \ - concat, consume, internalcode, have_async_gen -from jinja2._compat import imap, ifilter, string_types, iteritems, \ - text_type, reraise, implements_iterator, implements_to_string, \ - encode_filename, PY2, PYPY - +from . import nodes +from ._compat import encode_filename +from ._compat import implements_iterator +from ._compat import implements_to_string +from ._compat import iteritems +from ._compat import PY2 +from ._compat import PYPY +from ._compat import reraise +from ._compat import string_types +from ._compat import text_type +from .compiler import CodeGenerator +from .compiler import generate +from .defaults import BLOCK_END_STRING +from .defaults import BLOCK_START_STRING +from .defaults import COMMENT_END_STRING +from .defaults import COMMENT_START_STRING +from .defaults import DEFAULT_FILTERS +from .defaults import DEFAULT_NAMESPACE +from .defaults import DEFAULT_POLICIES +from .defaults import DEFAULT_TESTS +from .defaults import KEEP_TRAILING_NEWLINE +from .defaults import LINE_COMMENT_PREFIX +from .defaults import LINE_STATEMENT_PREFIX +from .defaults import LSTRIP_BLOCKS +from .defaults import NEWLINE_SEQUENCE +from .defaults import TRIM_BLOCKS +from .defaults import VARIABLE_END_STRING +from .defaults import VARIABLE_START_STRING +from .exceptions import TemplateNotFound +from .exceptions import TemplateRuntimeError +from .exceptions import TemplatesNotFound +from .exceptions import TemplateSyntaxError +from .exceptions import UndefinedError +from .lexer import get_lexer +from .lexer import TokenStream +from .nodes import EvalContext +from .parser import Parser +from .runtime import Context +from .runtime import new_context +from .runtime import Undefined +from .utils import concat +from .utils import consume +from .utils import have_async_gen +from .utils import import_string +from .utils import internalcode +from .utils import LRUCache +from .utils import missing # for direct template usage we have up to ten living environments _spontaneous_environments = LRUCache(10) -# the function to create jinja traceback objects. This is dynamically -# imported on the first exception in the exception handler. -_make_traceback = None +def get_spontaneous_environment(cls, *args): + """Return a new spontaneous environment. A spontaneous environment + is used for templates created directly rather than through an + existing environment. -def get_spontaneous_environment(*args): - """Return a new spontaneous environment. A spontaneous environment is an - unnamed and unaccessible (in theory) environment that is used for - templates generated from a string and not from the file system. + :param cls: Environment class to create. + :param args: Positional arguments passed to environment. """ + key = (cls, args) + try: - env = _spontaneous_environments.get(args) - except TypeError: - return Environment(*args) - if env is not None: + return _spontaneous_environments[key] + except KeyError: + _spontaneous_environments[key] = env = cls(*args) + env.shared = True return env - _spontaneous_environments[args] = env = Environment(*args) - env.shared = True - return env def create_cache(size): @@ -96,20 +116,25 @@ def fail_for_missing_callable(string, name): try: name._fail_with_undefined_error() except Exception as e: - msg = '%s (%s; did you forget to quote the callable name?)' % (msg, e) + msg = "%s (%s; did you forget to quote the callable name?)" % (msg, e) raise TemplateRuntimeError(msg) def _environment_sanity_check(environment): """Perform a sanity check on the environment.""" - assert issubclass(environment.undefined, Undefined), 'undefined must ' \ - 'be a subclass of undefined because filters depend on it.' - assert environment.block_start_string != \ - environment.variable_start_string != \ - environment.comment_start_string, 'block, variable and comment ' \ - 'start strings must be different' - assert environment.newline_sequence in ('\r', '\r\n', '\n'), \ - 'newline_sequence set to unknown line ending string.' + assert issubclass( + environment.undefined, Undefined + ), "undefined must be a subclass of undefined because filters depend on it." + assert ( + environment.block_start_string + != environment.variable_start_string + != environment.comment_start_string + ), "block, variable and comment start strings must be different" + assert environment.newline_sequence in ( + "\r", + "\r\n", + "\n", + ), "newline_sequence set to unknown line ending string." return environment @@ -192,14 +217,15 @@ class Environment(object): ``None`` implicitly into an empty string here. `autoescape` - If set to ``True`` the HTML/XML autoescaping feature is - enabled by default. For more details about autoescaping see - :ref:`autoescaping`. This can also be a callable that is - passed the template name and returns whether autoescaping - should be enabled. + If set to ``True`` the XML/HTML autoescaping feature is enabled by + default. For more details about autoescaping see + :class:`~markupsafe.Markup`. As of Jinja 2.4 this can also + be a callable that is passed the template name and has to + return ``True`` or ``False`` depending on autoescape should be + enabled by default. .. versionchanged:: 2.4 - Can be a function. + `autoescape` can now be a function `loader` The template loader for this environment. @@ -251,10 +277,6 @@ class Environment(object): #: must not be modified shared = False - #: these are currently EXPERIMENTAL undocumented features. - exception_handler = None - exception_formatter = None - #: the class that is used for code generation. See #: :class:`~jinja2.compiler.CodeGenerator` for more information. code_generator_class = CodeGenerator @@ -263,29 +285,31 @@ class Environment(object): #: :class:`~jinja2.runtime.Context` for more information. context_class = Context - def __init__(self, - block_start_string=BLOCK_START_STRING, - block_end_string=BLOCK_END_STRING, - variable_start_string=VARIABLE_START_STRING, - variable_end_string=VARIABLE_END_STRING, - comment_start_string=COMMENT_START_STRING, - comment_end_string=COMMENT_END_STRING, - line_statement_prefix=LINE_STATEMENT_PREFIX, - line_comment_prefix=LINE_COMMENT_PREFIX, - trim_blocks=TRIM_BLOCKS, - lstrip_blocks=LSTRIP_BLOCKS, - newline_sequence=NEWLINE_SEQUENCE, - keep_trailing_newline=KEEP_TRAILING_NEWLINE, - extensions=(), - optimized=True, - undefined=Undefined, - finalize=None, - autoescape=False, - loader=None, - cache_size=400, - auto_reload=True, - bytecode_cache=None, - enable_async=False): + def __init__( + self, + block_start_string=BLOCK_START_STRING, + block_end_string=BLOCK_END_STRING, + variable_start_string=VARIABLE_START_STRING, + variable_end_string=VARIABLE_END_STRING, + comment_start_string=COMMENT_START_STRING, + comment_end_string=COMMENT_END_STRING, + line_statement_prefix=LINE_STATEMENT_PREFIX, + line_comment_prefix=LINE_COMMENT_PREFIX, + trim_blocks=TRIM_BLOCKS, + lstrip_blocks=LSTRIP_BLOCKS, + newline_sequence=NEWLINE_SEQUENCE, + keep_trailing_newline=KEEP_TRAILING_NEWLINE, + extensions=(), + optimized=True, + undefined=Undefined, + finalize=None, + autoescape=False, + loader=None, + cache_size=400, + auto_reload=True, + bytecode_cache=None, + enable_async=False, + ): # !!Important notice!! # The constructor accepts quite a few arguments that should be # passed by keyword rather than position. However it's important to @@ -336,6 +360,9 @@ class Environment(object): self.enable_async = enable_async self.is_async = self.enable_async and have_async_gen + if self.is_async: + # runs patch_all() to enable async support + from . import asyncsupport # noqa: F401 _environment_sanity_check(self) @@ -355,15 +382,28 @@ class Environment(object): if not hasattr(self, key): setattr(self, key, value) - def overlay(self, block_start_string=missing, block_end_string=missing, - variable_start_string=missing, variable_end_string=missing, - comment_start_string=missing, comment_end_string=missing, - line_statement_prefix=missing, line_comment_prefix=missing, - trim_blocks=missing, lstrip_blocks=missing, - extensions=missing, optimized=missing, - undefined=missing, finalize=missing, autoescape=missing, - loader=missing, cache_size=missing, auto_reload=missing, - bytecode_cache=missing): + def overlay( + self, + block_start_string=missing, + block_end_string=missing, + variable_start_string=missing, + variable_end_string=missing, + comment_start_string=missing, + comment_end_string=missing, + line_statement_prefix=missing, + line_comment_prefix=missing, + trim_blocks=missing, + lstrip_blocks=missing, + extensions=missing, + optimized=missing, + undefined=missing, + finalize=missing, + autoescape=missing, + loader=missing, + cache_size=missing, + auto_reload=missing, + bytecode_cache=missing, + ): """Create a new overlay environment that shares all the data with the current environment except for cache and the overridden attributes. Extensions cannot be removed for an overlayed environment. An overlayed @@ -376,7 +416,7 @@ class Environment(object): through. """ args = dict(locals()) - del args['self'], args['cache_size'], args['extensions'] + del args["self"], args["cache_size"], args["extensions"] rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) @@ -404,8 +444,7 @@ class Environment(object): def iter_extensions(self): """Iterates over the extensions by priority.""" - return iter(sorted(self.extensions.values(), - key=lambda x: x.priority)) + return iter(sorted(self.extensions.values(), key=lambda x: x.priority)) def getitem(self, obj, argument): """Get an item or attribute of an object but prefer the item.""" @@ -437,8 +476,9 @@ class Environment(object): except (TypeError, LookupError, AttributeError): return self.undefined(obj=obj, name=attribute) - def call_filter(self, name, value, args=None, kwargs=None, - context=None, eval_ctx=None): + def call_filter( + self, name, value, args=None, kwargs=None, context=None, eval_ctx=None + ): """Invokes a filter on a value the same way the compiler does it. Note that on Python 3 this might return a coroutine in case the @@ -450,21 +490,22 @@ class Environment(object): """ func = self.filters.get(name) if func is None: - fail_for_missing_callable('no filter named %r', name) + fail_for_missing_callable("no filter named %r", name) args = [value] + list(args or ()) - if getattr(func, 'contextfilter', False): + if getattr(func, "contextfilter", False) is True: if context is None: - raise TemplateRuntimeError('Attempted to invoke context ' - 'filter without context') + raise TemplateRuntimeError( + "Attempted to invoke context filter without context" + ) args.insert(0, context) - elif getattr(func, 'evalcontextfilter', False): + elif getattr(func, "evalcontextfilter", False) is True: if eval_ctx is None: if context is not None: eval_ctx = context.eval_ctx else: eval_ctx = EvalContext(self) args.insert(0, eval_ctx) - elif getattr(func, 'environmentfilter', False): + elif getattr(func, "environmentfilter", False) is True: args.insert(0, self) return func(*args, **(kwargs or {})) @@ -475,7 +516,7 @@ class Environment(object): """ func = self.tests.get(name) if func is None: - fail_for_missing_callable('no test named %r', name) + fail_for_missing_callable("no test named %r", name) return func(value, *(args or ()), **(kwargs or {})) @internalcode @@ -485,14 +526,13 @@ class Environment(object): executable source- or bytecode. This is useful for debugging or to extract information from templates. - If you are :ref:`developing Jinja2 extensions <writing-extensions>` + If you are :ref:`developing Jinja extensions <writing-extensions>` this gives you a good overview of the node tree generated. """ try: return self._parse(source, name, filename) except TemplateSyntaxError: - exc_info = sys.exc_info() - self.handle_exception(exc_info, source_hint=source) + self.handle_exception(source=source) def _parse(self, source, name, filename): """Internal parsing function used by `parse` and `compile`.""" @@ -512,16 +552,18 @@ class Environment(object): try: return self.lexer.tokeniter(source, name, filename) except TemplateSyntaxError: - exc_info = sys.exc_info() - self.handle_exception(exc_info, source_hint=source) + self.handle_exception(source=source) def preprocess(self, source, name=None, filename=None): """Preprocesses the source with all extensions. This is automatically called for all parsing and compiling methods but *not* for :meth:`lex` because there you usually only want the actual source tokenized. """ - return reduce(lambda s, e: e.preprocess(s, name, filename), - self.iter_extensions(), text_type(source)) + return reduce( + lambda s, e: e.preprocess(s, name, filename), + self.iter_extensions(), + text_type(source), + ) def _tokenize(self, source, name, filename=None, state=None): """Called by the parser to do the preprocessing and filtering @@ -541,8 +583,14 @@ class Environment(object): .. versionadded:: 2.5 """ - return generate(source, self, name, filename, defer_init=defer_init, - optimized=self.optimized) + return generate( + source, + self, + name, + filename, + defer_init=defer_init, + optimized=self.optimized, + ) def _compile(self, source, filename): """Internal hook that can be overridden to hook a different compile @@ -550,11 +598,10 @@ class Environment(object): .. versionadded:: 2.5 """ - return compile(source, filename, 'exec') + return compile(source, filename, "exec") @internalcode - def compile(self, source, name=None, filename=None, raw=False, - defer_init=False): + def compile(self, source, name=None, filename=None, raw=False, defer_init=False): """Compile a node or template source code. The `name` parameter is the load name of the template after it was joined using :meth:`join_path` if necessary, not the filename on the file system. @@ -579,18 +626,16 @@ class Environment(object): if isinstance(source, string_types): source_hint = source source = self._parse(source, name, filename) - source = self._generate(source, name, filename, - defer_init=defer_init) + source = self._generate(source, name, filename, defer_init=defer_init) if raw: return source if filename is None: - filename = '<template>' + filename = "<template>" else: filename = encode_filename(filename) return self._compile(source, filename) except TemplateSyntaxError: - exc_info = sys.exc_info() - self.handle_exception(exc_info, source_hint=source_hint) + self.handle_exception(source=source_hint) def compile_expression(self, source, undefined_to_none=True): """A handy helper method that returns a callable that accepts keyword @@ -620,26 +665,32 @@ class Environment(object): .. versionadded:: 2.1 """ - parser = Parser(self, source, state='variable') - exc_info = None + parser = Parser(self, source, state="variable") try: expr = parser.parse_expression() if not parser.stream.eos: - raise TemplateSyntaxError('chunk after expression', - parser.stream.current.lineno, - None, None) + raise TemplateSyntaxError( + "chunk after expression", parser.stream.current.lineno, None, None + ) expr.set_environment(self) except TemplateSyntaxError: - exc_info = sys.exc_info() - if exc_info is not None: - self.handle_exception(exc_info, source_hint=source) - body = [nodes.Assign(nodes.Name('result', 'store'), expr, lineno=1)] + if sys.exc_info() is not None: + self.handle_exception(source=source) + + body = [nodes.Assign(nodes.Name("result", "store"), expr, lineno=1)] template = self.from_string(nodes.Template(body, lineno=1)) return TemplateExpression(template, undefined_to_none) - def compile_templates(self, target, extensions=None, filter_func=None, - zip='deflated', log_function=None, - ignore_errors=True, py_compile=False): + def compile_templates( + self, + target, + extensions=None, + filter_func=None, + zip="deflated", + log_function=None, + ignore_errors=True, + py_compile=False, + ): """Finds all the templates the loader can find, compiles them and stores them in `target`. If `zip` is `None`, instead of in a zipfile, the templates will be stored in a directory. @@ -662,42 +713,52 @@ class Environment(object): .. versionadded:: 2.4 """ - from jinja2.loaders import ModuleLoader + from .loaders import ModuleLoader if log_function is None: - log_function = lambda x: None + + def log_function(x): + pass if py_compile: if not PY2 or PYPY: - from warnings import warn - warn(Warning('py_compile has no effect on pypy or Python 3')) + import warnings + + warnings.warn( + "'py_compile=True' has no effect on PyPy or Python" + " 3 and will be removed in version 3.0", + DeprecationWarning, + stacklevel=2, + ) py_compile = False else: import imp import marshal - py_header = imp.get_magic() + \ - u'\xff\xff\xff\xff'.encode('iso-8859-15') + + py_header = imp.get_magic() + u"\xff\xff\xff\xff".encode("iso-8859-15") # Python 3.3 added a source filesize to the header if sys.version_info >= (3, 3): - py_header += u'\x00\x00\x00\x00'.encode('iso-8859-15') + py_header += u"\x00\x00\x00\x00".encode("iso-8859-15") - def write_file(filename, data, mode): + def write_file(filename, data): if zip: info = ZipInfo(filename) info.external_attr = 0o755 << 16 zip_file.writestr(info, data) else: - f = open(os.path.join(target, filename), mode) - try: + if isinstance(data, text_type): + data = data.encode("utf8") + + with open(os.path.join(target, filename), "wb") as f: f.write(data) - finally: - f.close() if zip is not None: from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED, ZIP_STORED - zip_file = ZipFile(target, 'w', dict(deflated=ZIP_DEFLATED, - stored=ZIP_STORED)[zip]) + + zip_file = ZipFile( + target, "w", dict(deflated=ZIP_DEFLATED, stored=ZIP_STORED)[zip] + ) log_function('Compiling into Zip archive "%s"' % target) else: if not os.path.isdir(target): @@ -719,18 +780,16 @@ class Environment(object): if py_compile: c = self._compile(code, encode_filename(filename)) - write_file(filename + 'c', py_header + - marshal.dumps(c), 'wb') - log_function('Byte-compiled "%s" as %s' % - (name, filename + 'c')) + write_file(filename + "c", py_header + marshal.dumps(c)) + log_function('Byte-compiled "%s" as %s' % (name, filename + "c")) else: - write_file(filename, code, 'w') + write_file(filename, code) log_function('Compiled "%s" as %s' % (name, filename)) finally: if zip: zip_file.close() - log_function('Finished compiling templates') + log_function("Finished compiling templates") def list_templates(self, extensions=None, filter_func=None): """Returns a list of templates for this environment. This requires @@ -748,38 +807,29 @@ class Environment(object): .. versionadded:: 2.4 """ - x = self.loader.list_templates() + names = self.loader.list_templates() + if extensions is not None: if filter_func is not None: - raise TypeError('either extensions or filter_func ' - 'can be passed, but not both') - filter_func = lambda x: '.' in x and \ - x.rsplit('.', 1)[1] in extensions + raise TypeError( + "either extensions or filter_func can be passed, but not both" + ) + + def filter_func(x): + return "." in x and x.rsplit(".", 1)[1] in extensions + if filter_func is not None: - x = list(ifilter(filter_func, x)) - return x + names = [name for name in names if filter_func(name)] + + return names - def handle_exception(self, exc_info=None, rendered=False, source_hint=None): + def handle_exception(self, source=None): """Exception handling helper. This is used internally to either raise rewritten exceptions or return a rendered traceback for the template. """ - global _make_traceback - if exc_info is None: - exc_info = sys.exc_info() - - # the debugging module is imported when it's used for the first time. - # we're doing a lot of stuff there and for applications that do not - # get any exceptions in template rendering there is no need to load - # all of that. - if _make_traceback is None: - from jinja2.debug import make_traceback as _make_traceback - traceback = _make_traceback(exc_info, source_hint) - if rendered and self.exception_formatter is not None: - return self.exception_formatter(traceback) - if self.exception_handler is not None: - self.exception_handler(traceback) - exc_type, exc_value, tb = traceback.standard_exc_info - reraise(exc_type, exc_value, tb) + from .debug import rewrite_traceback_stack + + reraise(*rewrite_traceback_stack(source=source)) def join_path(self, template, parent): """Join a template with the parent. By default all the lookups are @@ -796,12 +846,13 @@ class Environment(object): @internalcode def _load_template(self, name, globals): if self.loader is None: - raise TypeError('no loader for this environment specified') + raise TypeError("no loader for this environment specified") cache_key = (weakref.ref(self.loader), name) if self.cache is not None: template = self.cache.get(cache_key) - if template is not None and (not self.auto_reload or - template.is_up_to_date): + if template is not None and ( + not self.auto_reload or template.is_up_to_date + ): return template template = self.loader.load(self, name, globals) if self.cache is not None: @@ -837,15 +888,24 @@ class Environment(object): before it fails. If it cannot find any of the templates, it will raise a :exc:`TemplatesNotFound` exception. - .. versionadded:: 2.3 + .. versionchanged:: 2.11 + If names is :class:`Undefined`, an :exc:`UndefinedError` is + raised instead. If no templates were found and names + contains :class:`Undefined`, the message is more helpful. .. versionchanged:: 2.4 If `names` contains a :class:`Template` object it is returned from the function unchanged. + + .. versionadded:: 2.3 """ + if isinstance(names, Undefined): + names._fail_with_undefined_error() + if not names: - raise TemplatesNotFound(message=u'Tried to select from an empty list ' - u'of templates.') + raise TemplatesNotFound( + message=u"Tried to select from an empty list " u"of templates." + ) globals = self.make_globals(globals) for name in names: if isinstance(name, Template): @@ -854,20 +914,19 @@ class Environment(object): name = self.join_path(name, parent) try: return self._load_template(name, globals) - except TemplateNotFound: + except (TemplateNotFound, UndefinedError): pass raise TemplatesNotFound(names) @internalcode - def get_or_select_template(self, template_name_or_list, - parent=None, globals=None): + def get_or_select_template(self, template_name_or_list, parent=None, globals=None): """Does a typecheck and dispatches to :meth:`select_template` if an iterable of template names is given, otherwise to :meth:`get_template`. .. versionadded:: 2.3 """ - if isinstance(template_name_or_list, string_types): + if isinstance(template_name_or_list, (string_types, Undefined)): return self.get_template(template_name_or_list, parent, globals) elif isinstance(template_name_or_list, Template): return template_name_or_list @@ -918,32 +977,57 @@ class Template(object): StopIteration """ - def __new__(cls, source, - block_start_string=BLOCK_START_STRING, - block_end_string=BLOCK_END_STRING, - variable_start_string=VARIABLE_START_STRING, - variable_end_string=VARIABLE_END_STRING, - comment_start_string=COMMENT_START_STRING, - comment_end_string=COMMENT_END_STRING, - line_statement_prefix=LINE_STATEMENT_PREFIX, - line_comment_prefix=LINE_COMMENT_PREFIX, - trim_blocks=TRIM_BLOCKS, - lstrip_blocks=LSTRIP_BLOCKS, - newline_sequence=NEWLINE_SEQUENCE, - keep_trailing_newline=KEEP_TRAILING_NEWLINE, - extensions=(), - optimized=True, - undefined=Undefined, - finalize=None, - autoescape=False, - enable_async=False): + #: Type of environment to create when creating a template directly + #: rather than through an existing environment. + environment_class = Environment + + def __new__( + cls, + source, + block_start_string=BLOCK_START_STRING, + block_end_string=BLOCK_END_STRING, + variable_start_string=VARIABLE_START_STRING, + variable_end_string=VARIABLE_END_STRING, + comment_start_string=COMMENT_START_STRING, + comment_end_string=COMMENT_END_STRING, + line_statement_prefix=LINE_STATEMENT_PREFIX, + line_comment_prefix=LINE_COMMENT_PREFIX, + trim_blocks=TRIM_BLOCKS, + lstrip_blocks=LSTRIP_BLOCKS, + newline_sequence=NEWLINE_SEQUENCE, + keep_trailing_newline=KEEP_TRAILING_NEWLINE, + extensions=(), + optimized=True, + undefined=Undefined, + finalize=None, + autoescape=False, + enable_async=False, + ): env = get_spontaneous_environment( - block_start_string, block_end_string, variable_start_string, - variable_end_string, comment_start_string, comment_end_string, - line_statement_prefix, line_comment_prefix, trim_blocks, - lstrip_blocks, newline_sequence, keep_trailing_newline, - frozenset(extensions), optimized, undefined, finalize, autoescape, - None, 0, False, None, enable_async) + cls.environment_class, + block_start_string, + block_end_string, + variable_start_string, + variable_end_string, + comment_start_string, + comment_end_string, + line_statement_prefix, + line_comment_prefix, + trim_blocks, + lstrip_blocks, + newline_sequence, + keep_trailing_newline, + frozenset(extensions), + optimized, + undefined, + finalize, + autoescape, + None, + 0, + False, + None, + enable_async, + ) return env.from_string(source, template_class=cls) @classmethod @@ -951,10 +1035,7 @@ class Template(object): """Creates a template object from compiled code and the globals. This is used by the loaders and environment to create a template object. """ - namespace = { - 'environment': environment, - '__file__': code.co_filename - } + namespace = {"environment": environment, "__file__": code.co_filename} exec(code, namespace) rv = cls._from_namespace(environment, namespace, globals) rv._uptodate = uptodate @@ -974,21 +1055,21 @@ class Template(object): t = object.__new__(cls) t.environment = environment t.globals = globals - t.name = namespace['name'] - t.filename = namespace['__file__'] - t.blocks = namespace['blocks'] + t.name = namespace["name"] + t.filename = namespace["__file__"] + t.blocks = namespace["blocks"] # render function and module - t.root_render_func = namespace['root'] + t.root_render_func = namespace["root"] t._module = None # debug and loader helpers - t._debug_info = namespace['debug_info'] + t._debug_info = namespace["debug_info"] t._uptodate = None # store the reference - namespace['environment'] = environment - namespace['__jinja_template__'] = t + namespace["environment"] = environment + namespace["__jinja_template__"] = t return t @@ -1006,8 +1087,7 @@ class Template(object): try: return concat(self.root_render_func(self.new_context(vars))) except Exception: - exc_info = sys.exc_info() - return self.environment.handle_exception(exc_info, True) + self.environment.handle_exception() def render_async(self, *args, **kwargs): """This works similar to :meth:`render` but returns a coroutine @@ -1019,8 +1099,9 @@ class Template(object): await template.render_async(knights='that say nih; asynchronously') """ # see asyncsupport for the actual implementation - raise NotImplementedError('This feature is not available for this ' - 'version of Python') + raise NotImplementedError( + "This feature is not available for this version of Python" + ) def stream(self, *args, **kwargs): """Works exactly like :meth:`generate` but returns a @@ -1041,29 +1122,28 @@ class Template(object): for event in self.root_render_func(self.new_context(vars)): yield event except Exception: - exc_info = sys.exc_info() - else: - return - yield self.environment.handle_exception(exc_info, True) + yield self.environment.handle_exception() def generate_async(self, *args, **kwargs): """An async version of :meth:`generate`. Works very similarly but returns an async iterator instead. """ # see asyncsupport for the actual implementation - raise NotImplementedError('This feature is not available for this ' - 'version of Python') + raise NotImplementedError( + "This feature is not available for this version of Python" + ) def new_context(self, vars=None, shared=False, locals=None): """Create a new :class:`Context` for this template. The vars provided will be passed to the template. Per default the globals are added to the context. If shared is set to `True` the data - is passed as it to the context without adding the globals. + is passed as is to the context without adding the globals. `locals` can be a dict of local variables for internal usage. """ - return new_context(self.environment, self.name, self.blocks, - vars, shared, self.globals, locals) + return new_context( + self.environment, self.name, self.blocks, vars, shared, self.globals, locals + ) def make_module(self, vars=None, shared=False, locals=None): """This method works like the :attr:`module` attribute when called @@ -1076,13 +1156,14 @@ class Template(object): def make_module_async(self, vars=None, shared=False, locals=None): """As template module creation can invoke template code for - asynchronous exections this method must be used instead of the + asynchronous executions this method must be used instead of the normal :meth:`make_module` one. Likewise the module attribute becomes unavailable in async mode. """ # see asyncsupport for the actual implementation - raise NotImplementedError('This feature is not available for this ' - 'version of Python') + raise NotImplementedError( + "This feature is not available for this version of Python" + ) @internalcode def _get_default_module(self): @@ -1126,15 +1207,16 @@ class Template(object): @property def debug_info(self): """The debug info mapping.""" - return [tuple(imap(int, x.split('='))) for x in - self._debug_info.split('&')] + if self._debug_info: + return [tuple(map(int, x.split("="))) for x in self._debug_info.split("&")] + return [] def __repr__(self): if self.name is None: - name = 'memory:%x' % id(self) + name = "memory:%x" % id(self) else: name = repr(self.name) - return '<%s %s>' % (self.__class__.__name__, name) + return "<%s %s>" % (self.__class__.__name__, name) @implements_to_string @@ -1147,10 +1229,12 @@ class TemplateModule(object): def __init__(self, template, context, body_stream=None): if body_stream is None: if context.environment.is_async: - raise RuntimeError('Async mode requires a body stream ' - 'to be passed to a template module. Use ' - 'the async methods of the API you are ' - 'using.') + raise RuntimeError( + "Async mode requires a body stream " + "to be passed to a template module. Use " + "the async methods of the API you are " + "using." + ) body_stream = list(template.root_render_func(context)) self._body_stream = body_stream self.__dict__.update(context.get_exported()) @@ -1164,10 +1248,10 @@ class TemplateModule(object): def __repr__(self): if self.__name__ is None: - name = 'memory:%x' % id(self) + name = "memory:%x" % id(self) else: name = repr(self.__name__) - return '<%s %s>' % (self.__class__.__name__, name) + return "<%s %s>" % (self.__class__.__name__, name) class TemplateExpression(object): @@ -1183,7 +1267,7 @@ class TemplateExpression(object): def __call__(self, *args, **kwargs): context = self._template.new_context(dict(*args, **kwargs)) consume(self._template.root_render_func(context)) - rv = context.vars['result'] + rv = context.vars["result"] if self._undefined_to_none and isinstance(rv, Undefined): rv = None return rv @@ -1205,7 +1289,7 @@ class TemplateStream(object): self._gen = gen self.disable_buffering() - def dump(self, fp, encoding=None, errors='strict'): + def dump(self, fp, encoding=None, errors="strict"): """Dump the complete stream into a file or file-like object. Per default unicode strings are written, if you want to encode before writing specify an `encoding`. @@ -1217,15 +1301,15 @@ class TemplateStream(object): close = False if isinstance(fp, string_types): if encoding is None: - encoding = 'utf-8' - fp = open(fp, 'wb') + encoding = "utf-8" + fp = open(fp, "wb") close = True try: if encoding is not None: iterable = (x.encode(encoding, errors) for x in self) else: iterable = self - if hasattr(fp, 'writelines'): + if hasattr(fp, "writelines"): fp.writelines(iterable) else: for item in iterable: @@ -1261,7 +1345,7 @@ class TemplateStream(object): def enable_buffering(self, size=5): """Enable buffering. Buffer `size` items before yielding them.""" if size <= 1: - raise ValueError('buffer size too small') + raise ValueError("buffer size too small") self.buffered = True self._next = partial(next, self._buffered_generator(size)) diff --git a/jinja2/exceptions.py b/src/jinja2/exceptions.py index c018a33..0bf2003 100644 --- a/jinja2/exceptions.py +++ b/src/jinja2/exceptions.py @@ -1,23 +1,18 @@ # -*- coding: utf-8 -*- -""" - jinja2.exceptions - ~~~~~~~~~~~~~~~~~ - - Jinja exceptions. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -from jinja2._compat import imap, text_type, PY2, implements_to_string +from ._compat import imap +from ._compat import implements_to_string +from ._compat import PY2 +from ._compat import text_type class TemplateError(Exception): """Baseclass for all template errors.""" if PY2: + def __init__(self, message=None): if message is not None: - message = text_type(message).encode('utf-8') + message = text_type(message).encode("utf-8") Exception.__init__(self, message) @property @@ -25,11 +20,13 @@ class TemplateError(Exception): if self.args: message = self.args[0] if message is not None: - return message.decode('utf-8', 'replace') + return message.decode("utf-8", "replace") def __unicode__(self): - return self.message or u'' + return self.message or u"" + else: + def __init__(self, message=None): Exception.__init__(self, message) @@ -43,16 +40,28 @@ class TemplateError(Exception): @implements_to_string class TemplateNotFound(IOError, LookupError, TemplateError): - """Raised if a template does not exist.""" + """Raised if a template does not exist. + + .. versionchanged:: 2.11 + If the given name is :class:`Undefined` and no message was + provided, an :exc:`UndefinedError` is raised. + """ # looks weird, but removes the warning descriptor that just # bogusly warns us about message being deprecated message = None def __init__(self, name, message=None): - IOError.__init__(self) + IOError.__init__(self, name) + if message is None: + from .runtime import Undefined + + if isinstance(name, Undefined): + name._fail_with_undefined_error() + message = name + self.message = message self.name = name self.templates = [name] @@ -66,13 +75,28 @@ class TemplatesNotFound(TemplateNotFound): are selected. This is a subclass of :class:`TemplateNotFound` exception, so just catching the base exception will catch both. + .. versionchanged:: 2.11 + If a name in the list of names is :class:`Undefined`, a message + about it being undefined is shown rather than the empty string. + .. versionadded:: 2.2 """ def __init__(self, names=(), message=None): if message is None: - message = u'none of the templates given were found: ' + \ - u', '.join(imap(text_type, names)) + from .runtime import Undefined + + parts = [] + + for name in names: + if isinstance(name, Undefined): + parts.append(name._undefined_message) + else: + parts.append(name) + + message = u"none of the templates given were found: " + u", ".join( + imap(text_type, parts) + ) TemplateNotFound.__init__(self, names and names[-1] or None, message) self.templates = list(names) @@ -98,11 +122,11 @@ class TemplateSyntaxError(TemplateError): return self.message # otherwise attach some stuff - location = 'line %d' % self.lineno + location = "line %d" % self.lineno name = self.filename or self.name if name: location = 'File "%s", %s' % (name, location) - lines = [self.message, ' ' + location] + lines = [self.message, " " + location] # if the source is set, add the line to the output if self.source is not None: @@ -111,9 +135,16 @@ class TemplateSyntaxError(TemplateError): except IndexError: line = None if line: - lines.append(' ' + line.strip()) + lines.append(" " + line.strip()) + + return u"\n".join(lines) - return u'\n'.join(lines) + def __reduce__(self): + # https://bugs.python.org/issue1692335 Exceptions that take + # multiple required arguments have problems with pickling. + # Without this, raises TypeError: __init__() missing 1 required + # positional argument: 'lineno' + return self.__class__, (self.message, self.lineno, self.name, self.filename) class TemplateAssertionError(TemplateSyntaxError): diff --git a/jinja2/ext.py b/src/jinja2/ext.py index b70b50b..9141be4 100644 --- a/jinja2/ext.py +++ b/src/jinja2/ext.py @@ -1,44 +1,49 @@ # -*- coding: utf-8 -*- -""" - jinja2.ext - ~~~~~~~~~~ - - Jinja extensions allow to add custom tags similar to the way django custom - tags work. By default two example extensions exist: an i18n and a cache - extension. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. -""" +"""Extension API for adding custom tags and behavior.""" +import pprint import re +from sys import version_info from markupsafe import Markup -from jinja2 import nodes -from jinja2.defaults import BLOCK_START_STRING, \ - BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \ - COMMENT_START_STRING, COMMENT_END_STRING, LINE_STATEMENT_PREFIX, \ - LINE_COMMENT_PREFIX, TRIM_BLOCKS, NEWLINE_SEQUENCE, \ - KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS -from jinja2.environment import Environment -from jinja2.runtime import concat -from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError -from jinja2.utils import contextfunction, import_string -from jinja2._compat import with_metaclass, string_types, iteritems - +from . import nodes +from ._compat import iteritems +from ._compat import string_types +from ._compat import with_metaclass +from .defaults import BLOCK_END_STRING +from .defaults import BLOCK_START_STRING +from .defaults import COMMENT_END_STRING +from .defaults import COMMENT_START_STRING +from .defaults import KEEP_TRAILING_NEWLINE +from .defaults import LINE_COMMENT_PREFIX +from .defaults import LINE_STATEMENT_PREFIX +from .defaults import LSTRIP_BLOCKS +from .defaults import NEWLINE_SEQUENCE +from .defaults import TRIM_BLOCKS +from .defaults import VARIABLE_END_STRING +from .defaults import VARIABLE_START_STRING +from .environment import Environment +from .exceptions import TemplateAssertionError +from .exceptions import TemplateSyntaxError +from .nodes import ContextReference +from .runtime import concat +from .utils import contextfunction +from .utils import import_string # the only real useful gettext functions for a Jinja template. Note # that ugettext must be assigned to gettext as Jinja doesn't support # non unicode strings. -GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') +GETTEXT_FUNCTIONS = ("_", "gettext", "ngettext") + +_ws_re = re.compile(r"\s*\n\s*") class ExtensionRegistry(type): """Gives the extension an unique identifier.""" - def __new__(cls, name, bases, d): - rv = type.__new__(cls, name, bases, d) - rv.identifier = rv.__module__ + '.' + rv.__name__ + def __new__(mcs, name, bases, d): + rv = type.__new__(mcs, name, bases, d) + rv.identifier = rv.__module__ + "." + rv.__name__ return rv @@ -93,10 +98,6 @@ class Extension(with_metaclass(ExtensionRegistry, object)): to filter tokens returned. This method has to return an iterable of :class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a :class:`~jinja2.lexer.TokenStream`. - - In the `ext` folder of the Jinja2 source distribution there is a file - called `inlinegettext.py` which implements a filter that utilizes this - method. """ return stream @@ -118,8 +119,9 @@ class Extension(with_metaclass(ExtensionRegistry, object)): """ return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno) - def call_method(self, name, args=None, kwargs=None, dyn_args=None, - dyn_kwargs=None, lineno=None): + def call_method( + self, name, args=None, kwargs=None, dyn_args=None, dyn_kwargs=None, lineno=None + ): """Call a method of the extension. This is a shortcut for :meth:`attr` + :class:`jinja2.nodes.Call`. """ @@ -127,13 +129,19 @@ class Extension(with_metaclass(ExtensionRegistry, object)): args = [] if kwargs is None: kwargs = [] - return nodes.Call(self.attr(name, lineno=lineno), args, kwargs, - dyn_args, dyn_kwargs, lineno=lineno) + return nodes.Call( + self.attr(name, lineno=lineno), + args, + kwargs, + dyn_args, + dyn_kwargs, + lineno=lineno, + ) @contextfunction def _gettext_alias(__context, *args, **kwargs): - return __context.call(__context.resolve('gettext'), *args, **kwargs) + return __context.call(__context.resolve("gettext"), *args, **kwargs) def _make_new_gettext(func): @@ -142,24 +150,31 @@ def _make_new_gettext(func): rv = __context.call(func, __string) if __context.eval_ctx.autoescape: rv = Markup(rv) + # Always treat as a format string, even if there are no + # variables. This makes translation strings more consistent + # and predictable. This requires escaping return rv % variables + return gettext def _make_new_ngettext(func): @contextfunction def ngettext(__context, __singular, __plural, __num, **variables): - variables.setdefault('num', __num) + variables.setdefault("num", __num) rv = __context.call(func, __singular, __plural, __num) if __context.eval_ctx.autoescape: rv = Markup(rv) + # Always treat as a format string, see gettext comment above. return rv % variables + return ngettext class InternationalizationExtension(Extension): - """This extension adds gettext support to Jinja2.""" - tags = set(['trans']) + """This extension adds gettext support to Jinja.""" + + tags = {"trans"} # TODO: the i18n extension is currently reevaluating values in a few # situations. Take this example: @@ -170,30 +185,28 @@ class InternationalizationExtension(Extension): def __init__(self, environment): Extension.__init__(self, environment) - environment.globals['_'] = _gettext_alias + environment.globals["_"] = _gettext_alias environment.extend( install_gettext_translations=self._install, install_null_translations=self._install_null, install_gettext_callables=self._install_callables, uninstall_gettext_translations=self._uninstall, extract_translations=self._extract, - newstyle_gettext=False + newstyle_gettext=False, ) def _install(self, translations, newstyle=None): - gettext = getattr(translations, 'ugettext', None) + gettext = getattr(translations, "ugettext", None) if gettext is None: gettext = translations.gettext - ngettext = getattr(translations, 'ungettext', None) + ngettext = getattr(translations, "ungettext", None) if ngettext is None: ngettext = translations.ngettext self._install_callables(gettext, ngettext, newstyle) def _install_null(self, newstyle=None): self._install_callables( - lambda x: x, - lambda s, p, n: (n != 1 and (p,) or (s,))[0], - newstyle + lambda x: x, lambda s, p, n: (n != 1 and (p,) or (s,))[0], newstyle ) def _install_callables(self, gettext, ngettext, newstyle=None): @@ -202,13 +215,10 @@ class InternationalizationExtension(Extension): if self.environment.newstyle_gettext: gettext = _make_new_gettext(gettext) ngettext = _make_new_ngettext(ngettext) - self.environment.globals.update( - gettext=gettext, - ngettext=ngettext - ) + self.environment.globals.update(gettext=gettext, ngettext=ngettext) def _uninstall(self, translations): - for key in 'gettext', 'ngettext': + for key in "gettext", "ngettext": self.environment.globals.pop(key, None) def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS): @@ -228,41 +238,44 @@ class InternationalizationExtension(Extension): plural_expr_assignment = None variables = {} trimmed = None - while parser.stream.current.type != 'block_end': + while parser.stream.current.type != "block_end": if variables: - parser.stream.expect('comma') + parser.stream.expect("comma") # skip colon for python compatibility - if parser.stream.skip_if('colon'): + if parser.stream.skip_if("colon"): break - name = parser.stream.expect('name') + name = parser.stream.expect("name") if name.value in variables: - parser.fail('translatable variable %r defined twice.' % - name.value, name.lineno, - exc=TemplateAssertionError) + parser.fail( + "translatable variable %r defined twice." % name.value, + name.lineno, + exc=TemplateAssertionError, + ) # expressions - if parser.stream.current.type == 'assign': + if parser.stream.current.type == "assign": next(parser.stream) variables[name.value] = var = parser.parse_expression() - elif trimmed is None and name.value in ('trimmed', 'notrimmed'): - trimmed = name.value == 'trimmed' + elif trimmed is None and name.value in ("trimmed", "notrimmed"): + trimmed = name.value == "trimmed" continue else: - variables[name.value] = var = nodes.Name(name.value, 'load') + variables[name.value] = var = nodes.Name(name.value, "load") if plural_expr is None: if isinstance(var, nodes.Call): - plural_expr = nodes.Name('_trans', 'load') + plural_expr = nodes.Name("_trans", "load") variables[name.value] = plural_expr plural_expr_assignment = nodes.Assign( - nodes.Name('_trans', 'store'), var) + nodes.Name("_trans", "store"), var + ) else: plural_expr = var - num_called_num = name.value == 'num' + num_called_num = name.value == "num" - parser.stream.expect('block_end') + parser.stream.expect("block_end") plural = None have_plural = False @@ -273,22 +286,24 @@ class InternationalizationExtension(Extension): if singular_names: referenced.update(singular_names) if plural_expr is None: - plural_expr = nodes.Name(singular_names[0], 'load') - num_called_num = singular_names[0] == 'num' + plural_expr = nodes.Name(singular_names[0], "load") + num_called_num = singular_names[0] == "num" # if we have a pluralize block, we parse that too - if parser.stream.current.test('name:pluralize'): + if parser.stream.current.test("name:pluralize"): have_plural = True next(parser.stream) - if parser.stream.current.type != 'block_end': - name = parser.stream.expect('name') + if parser.stream.current.type != "block_end": + name = parser.stream.expect("name") if name.value not in variables: - parser.fail('unknown variable %r for pluralization' % - name.value, name.lineno, - exc=TemplateAssertionError) + parser.fail( + "unknown variable %r for pluralization" % name.value, + name.lineno, + exc=TemplateAssertionError, + ) plural_expr = variables[name.value] - num_called_num = name.value == 'num' - parser.stream.expect('block_end') + num_called_num = name.value == "num" + parser.stream.expect("block_end") plural_names, plural = self._parse_block(parser, False) next(parser.stream) referenced.update(plural_names) @@ -298,88 +313,97 @@ class InternationalizationExtension(Extension): # register free names as simple name expressions for var in referenced: if var not in variables: - variables[var] = nodes.Name(var, 'load') + variables[var] = nodes.Name(var, "load") if not have_plural: plural_expr = None elif plural_expr is None: - parser.fail('pluralize without variables', lineno) + parser.fail("pluralize without variables", lineno) if trimmed is None: - trimmed = self.environment.policies['ext.i18n.trimmed'] + trimmed = self.environment.policies["ext.i18n.trimmed"] if trimmed: singular = self._trim_whitespace(singular) if plural: plural = self._trim_whitespace(plural) - node = self._make_node(singular, plural, variables, plural_expr, - bool(referenced), - num_called_num and have_plural) + node = self._make_node( + singular, + plural, + variables, + plural_expr, + bool(referenced), + num_called_num and have_plural, + ) node.set_lineno(lineno) if plural_expr_assignment is not None: return [plural_expr_assignment, node] else: return node - def _trim_whitespace(self, string, _ws_re=re.compile(r'\s*\n\s*')): - return _ws_re.sub(' ', string.strip()) + def _trim_whitespace(self, string, _ws_re=_ws_re): + return _ws_re.sub(" ", string.strip()) def _parse_block(self, parser, allow_pluralize): """Parse until the next block tag with a given name.""" referenced = [] buf = [] while 1: - if parser.stream.current.type == 'data': - buf.append(parser.stream.current.value.replace('%', '%%')) + if parser.stream.current.type == "data": + buf.append(parser.stream.current.value.replace("%", "%%")) next(parser.stream) - elif parser.stream.current.type == 'variable_begin': + elif parser.stream.current.type == "variable_begin": next(parser.stream) - name = parser.stream.expect('name').value + name = parser.stream.expect("name").value referenced.append(name) - buf.append('%%(%s)s' % name) - parser.stream.expect('variable_end') - elif parser.stream.current.type == 'block_begin': + buf.append("%%(%s)s" % name) + parser.stream.expect("variable_end") + elif parser.stream.current.type == "block_begin": next(parser.stream) - if parser.stream.current.test('name:endtrans'): + if parser.stream.current.test("name:endtrans"): break - elif parser.stream.current.test('name:pluralize'): + elif parser.stream.current.test("name:pluralize"): if allow_pluralize: break - parser.fail('a translatable section can have only one ' - 'pluralize section') - parser.fail('control structures in translatable sections are ' - 'not allowed') + parser.fail( + "a translatable section can have only one pluralize section" + ) + parser.fail( + "control structures in translatable sections are not allowed" + ) elif parser.stream.eos: - parser.fail('unclosed translation block') + parser.fail("unclosed translation block") else: - assert False, 'internal parser error' + raise RuntimeError("internal parser error") return referenced, concat(buf) - def _make_node(self, singular, plural, variables, plural_expr, - vars_referenced, num_called_num): + def _make_node( + self, singular, plural, variables, plural_expr, vars_referenced, num_called_num + ): """Generates a useful node from the data provided.""" # no variables referenced? no need to escape for old style # gettext invocations only if there are vars. if not vars_referenced and not self.environment.newstyle_gettext: - singular = singular.replace('%%', '%') + singular = singular.replace("%%", "%") if plural: - plural = plural.replace('%%', '%') + plural = plural.replace("%%", "%") # singular only: if plural_expr is None: - gettext = nodes.Name('gettext', 'load') - node = nodes.Call(gettext, [nodes.Const(singular)], - [], None, None) + gettext = nodes.Name("gettext", "load") + node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None) # singular and plural else: - ngettext = nodes.Name('ngettext', 'load') - node = nodes.Call(ngettext, [ - nodes.Const(singular), - nodes.Const(plural), - plural_expr - ], [], None, None) + ngettext = nodes.Name("ngettext", "load") + node = nodes.Call( + ngettext, + [nodes.Const(singular), nodes.Const(plural), plural_expr], + [], + None, + None, + ) # in case newstyle gettext is used, the method is powerful # enough to handle the variable expansion and autoescape @@ -388,7 +412,7 @@ class InternationalizationExtension(Extension): for key, value in iteritems(variables): # the function adds that later anyways in case num was # called num, so just skip it. - if num_called_num and key == 'num': + if num_called_num and key == "num": continue node.kwargs.append(nodes.Keyword(key, value)) @@ -398,18 +422,24 @@ class InternationalizationExtension(Extension): # environment with autoescaping turned on node = nodes.MarkSafeIfAutoescape(node) if variables: - node = nodes.Mod(node, nodes.Dict([ - nodes.Pair(nodes.Const(key), value) - for key, value in variables.items() - ])) + node = nodes.Mod( + node, + nodes.Dict( + [ + nodes.Pair(nodes.Const(key), value) + for key, value in variables.items() + ] + ), + ) return nodes.Output([node]) class ExprStmtExtension(Extension): - """Adds a `do` tag to Jinja2 that works like the print statement just + """Adds a `do` tag to Jinja that works like the print statement just that it doesn't print the return value. """ - tags = set(['do']) + + tags = set(["do"]) def parse(self, parser): node = nodes.ExprStmt(lineno=next(parser.stream).lineno) @@ -419,11 +449,12 @@ class ExprStmtExtension(Extension): class LoopControlExtension(Extension): """Adds break and continue to the template engine.""" - tags = set(['break', 'continue']) + + tags = set(["break", "continue"]) def parse(self, parser): token = next(parser.stream) - if token.value == 'break': + if token.value == "break": return nodes.Break(lineno=token.lineno) return nodes.Continue(lineno=token.lineno) @@ -436,8 +467,50 @@ class AutoEscapeExtension(Extension): pass -def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, - babel_style=True): +class DebugExtension(Extension): + """A ``{% debug %}`` tag that dumps the available variables, + filters, and tests. + + .. code-block:: html+jinja + + <pre>{% debug %}</pre> + + .. code-block:: text + + {'context': {'cycler': <class 'jinja2.utils.Cycler'>, + ..., + 'namespace': <class 'jinja2.utils.Namespace'>}, + 'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd', + ..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'], + 'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined', + ..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']} + + .. versionadded:: 2.11.0 + """ + + tags = {"debug"} + + def parse(self, parser): + lineno = parser.stream.expect("name:debug").lineno + context = ContextReference() + result = self.call_method("_render", [context], lineno=lineno) + return nodes.Output([result], lineno=lineno) + + def _render(self, context): + result = { + "context": context.get_all(), + "filters": sorted(self.environment.filters.keys()), + "tests": sorted(self.environment.tests.keys()), + } + + # Set the depth since the intent is to show the top few names. + if version_info[:2] >= (3, 4): + return pprint.pformat(result, depth=3, compact=True) + else: + return pprint.pformat(result, depth=3) + + +def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, babel_style=True): """Extract localizable strings from the given template node. Per default this function returns matches in babel style that means non string parameters as well as keyword arguments are returned as `None`. This @@ -473,19 +546,20 @@ def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, extraction interface or extract comments yourself. """ for node in node.find_all(nodes.Call): - if not isinstance(node.node, nodes.Name) or \ - node.node.name not in gettext_functions: + if ( + not isinstance(node.node, nodes.Name) + or node.node.name not in gettext_functions + ): continue strings = [] for arg in node.args: - if isinstance(arg, nodes.Const) and \ - isinstance(arg.value, string_types): + if isinstance(arg, nodes.Const) and isinstance(arg.value, string_types): strings.append(arg.value) else: strings.append(None) - for arg in node.kwargs: + for _ in node.kwargs: strings.append(None) if node.dyn_args is not None: strings.append(None) @@ -519,9 +593,10 @@ class _CommentFinder(object): def find_backwards(self, offset): try: - for _, token_type, token_value in \ - reversed(self.tokens[self.offset:offset]): - if token_type in ('comment', 'linecomment'): + for _, token_type, token_value in reversed( + self.tokens[self.offset : offset] + ): + if token_type in ("comment", "linecomment"): try: prefix, comment = token_value.split(None, 1) except ValueError: @@ -535,7 +610,7 @@ class _CommentFinder(object): def find_comments(self, lineno): if not self.comment_tags or self.last_lineno > lineno: return [] - for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset:]): + for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]): if token_lineno > lineno: return self.find_backwards(self.offset + idx) return self.find_backwards(len(self.tokens)) @@ -547,7 +622,7 @@ def babel_extract(fileobj, keywords, comment_tags, options): .. versionchanged:: 2.3 Basic support for translation comments was added. If `comment_tags` is now set to a list of keywords for extraction, the extractor will - try to find the best preceeding comment that begins with one of the + try to find the best preceding comment that begins with one of the keywords. For best results, make sure to not have more than one gettext call in one line of code and the matching comment in the same line or the line before. @@ -570,7 +645,7 @@ def babel_extract(fileobj, keywords, comment_tags, options): (comments will be empty currently) """ extensions = set() - for extension in options.get('extensions', '').split(','): + for extension in options.get("extensions", "").split(","): extension = extension.strip() if not extension: continue @@ -579,38 +654,37 @@ def babel_extract(fileobj, keywords, comment_tags, options): extensions.add(InternationalizationExtension) def getbool(options, key, default=False): - return options.get(key, str(default)).lower() in \ - ('1', 'on', 'yes', 'true') + return options.get(key, str(default)).lower() in ("1", "on", "yes", "true") - silent = getbool(options, 'silent', True) + silent = getbool(options, "silent", True) environment = Environment( - options.get('block_start_string', BLOCK_START_STRING), - options.get('block_end_string', BLOCK_END_STRING), - options.get('variable_start_string', VARIABLE_START_STRING), - options.get('variable_end_string', VARIABLE_END_STRING), - options.get('comment_start_string', COMMENT_START_STRING), - options.get('comment_end_string', COMMENT_END_STRING), - options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX, - options.get('line_comment_prefix') or LINE_COMMENT_PREFIX, - getbool(options, 'trim_blocks', TRIM_BLOCKS), - getbool(options, 'lstrip_blocks', LSTRIP_BLOCKS), + options.get("block_start_string", BLOCK_START_STRING), + options.get("block_end_string", BLOCK_END_STRING), + options.get("variable_start_string", VARIABLE_START_STRING), + options.get("variable_end_string", VARIABLE_END_STRING), + options.get("comment_start_string", COMMENT_START_STRING), + options.get("comment_end_string", COMMENT_END_STRING), + options.get("line_statement_prefix") or LINE_STATEMENT_PREFIX, + options.get("line_comment_prefix") or LINE_COMMENT_PREFIX, + getbool(options, "trim_blocks", TRIM_BLOCKS), + getbool(options, "lstrip_blocks", LSTRIP_BLOCKS), NEWLINE_SEQUENCE, - getbool(options, 'keep_trailing_newline', KEEP_TRAILING_NEWLINE), + getbool(options, "keep_trailing_newline", KEEP_TRAILING_NEWLINE), frozenset(extensions), cache_size=0, - auto_reload=False + auto_reload=False, ) - if getbool(options, 'trimmed'): - environment.policies['ext.i18n.trimmed'] = True - if getbool(options, 'newstyle_gettext'): + if getbool(options, "trimmed"): + environment.policies["ext.i18n.trimmed"] = True + if getbool(options, "newstyle_gettext"): environment.newstyle_gettext = True - source = fileobj.read().decode(options.get('encoding', 'utf-8')) + source = fileobj.read().decode(options.get("encoding", "utf-8")) try: node = environment.parse(source) tokens = list(environment.lex(environment.preprocess(source))) - except TemplateSyntaxError as e: + except TemplateSyntaxError: if not silent: raise # skip templates with syntax errors @@ -627,3 +701,4 @@ do = ExprStmtExtension loopcontrols = LoopControlExtension with_ = WithExtension autoescape = AutoEscapeExtension +debug = DebugExtension diff --git a/jinja2/filters.py b/src/jinja2/filters.py index bcb651f..74b108d 100644 --- a/jinja2/filters.py +++ b/src/jinja2/filters.py @@ -1,31 +1,31 @@ # -*- coding: utf-8 -*- -""" - jinja2.filters - ~~~~~~~~~~~~~~ - - Bundled jinja filters. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import re +"""Built-in template filters used with the ``|`` operator.""" import math import random +import re import warnings -from itertools import groupby, chain from collections import namedtuple +from itertools import chain +from itertools import groupby -from markupsafe import Markup, escape, soft_unicode +from markupsafe import escape +from markupsafe import Markup +from markupsafe import soft_unicode -from jinja2.utils import pformat, urlize, \ - unicode_urlencode, htmlsafe_json_dumps -from jinja2.runtime import Undefined -from jinja2.exceptions import FilterArgumentError -from jinja2._compat import imap, string_types, text_type, iteritems +from ._compat import abc +from ._compat import imap +from ._compat import iteritems +from ._compat import string_types +from ._compat import text_type +from .exceptions import FilterArgumentError +from .runtime import Undefined +from .utils import htmlsafe_json_dumps +from .utils import pformat +from .utils import unicode_urlencode +from .utils import urlize - -_word_re = re.compile(r'\w+', re.UNICODE) -_word_beginning_split_re = re.compile(r'([-\s\(\{\[\<]+)', re.UNICODE) +_word_re = re.compile(r"\w+", re.UNICODE) +_word_beginning_split_re = re.compile(r"([-\s\(\{\[\<]+)", re.UNICODE) def contextfilter(f): @@ -61,23 +61,21 @@ def ignore_case(value): return value.lower() if isinstance(value, string_types) else value -def make_attrgetter(environment, attribute, postprocess=None): +def make_attrgetter(environment, attribute, postprocess=None, default=None): """Returns a callable that looks up the given attribute from a passed object with the rules of the environment. Dots are allowed to access attributes of attributes. Integer parts in paths are looked up as integers. """ - if attribute is None: - attribute = [] - elif isinstance(attribute, string_types): - attribute = [int(x) if x.isdigit() else x for x in attribute.split('.')] - else: - attribute = [attribute] + attribute = _prepare_attribute_parts(attribute) def attrgetter(item): for part in attribute: item = environment.getitem(item, part) + if default and isinstance(item, Undefined): + item = default + if postprocess is not None: item = postprocess(item) @@ -86,32 +84,84 @@ def make_attrgetter(environment, attribute, postprocess=None): return attrgetter +def make_multi_attrgetter(environment, attribute, postprocess=None): + """Returns a callable that looks up the given comma separated + attributes from a passed object with the rules of the environment. + Dots are allowed to access attributes of each attribute. Integer + parts in paths are looked up as integers. + + The value returned by the returned callable is a list of extracted + attribute values. + + Examples of attribute: "attr1,attr2", "attr1.inner1.0,attr2.inner2.0", etc. + """ + attribute_parts = ( + attribute.split(",") if isinstance(attribute, string_types) else [attribute] + ) + attribute = [ + _prepare_attribute_parts(attribute_part) for attribute_part in attribute_parts + ] + + def attrgetter(item): + items = [None] * len(attribute) + for i, attribute_part in enumerate(attribute): + item_i = item + for part in attribute_part: + item_i = environment.getitem(item_i, part) + + if postprocess is not None: + item_i = postprocess(item_i) + + items[i] = item_i + return items + + return attrgetter + + +def _prepare_attribute_parts(attr): + if attr is None: + return [] + elif isinstance(attr, string_types): + return [int(x) if x.isdigit() else x for x in attr.split(".")] + else: + return [attr] + + def do_forceescape(value): """Enforce HTML escaping. This will probably double escape variables.""" - if hasattr(value, '__html__'): + if hasattr(value, "__html__"): value = value.__html__() return escape(text_type(value)) def do_urlencode(value): - """Escape strings for use in URLs (uses UTF-8 encoding). It accepts both - dictionaries and regular strings as well as pairwise iterables. + """Quote data for use in a URL path or query using UTF-8. + + Basic wrapper around :func:`urllib.parse.quote` when given a + string, or :func:`urllib.parse.urlencode` for a dict or iterable. + + :param value: Data to quote. A string will be quoted directly. A + dict or iterable of ``(key, value)`` pairs will be joined as a + query string. + + When given a string, "/" is not quoted. HTTP servers treat "/" and + "%2F" equivalently in paths. If you need quoted slashes, use the + ``|replace("/", "%2F")`` filter. .. versionadded:: 2.7 """ - itemiter = None - if isinstance(value, dict): - itemiter = iteritems(value) - elif not isinstance(value, string_types): - try: - itemiter = iter(value) - except TypeError: - pass - if itemiter is None: + if isinstance(value, string_types) or not isinstance(value, abc.Iterable): return unicode_urlencode(value) - return u'&'.join(unicode_urlencode(k) + '=' + - unicode_urlencode(v, for_qs=True) - for k, v in itemiter) + + if isinstance(value, dict): + items = iteritems(value) + else: + items = iter(value) + + return u"&".join( + "%s=%s" % (unicode_urlencode(k, for_qs=True), unicode_urlencode(v, for_qs=True)) + for k, v in items + ) @evalcontextfilter @@ -134,8 +184,11 @@ def do_replace(eval_ctx, s, old, new, count=None): count = -1 if not eval_ctx.autoescape: return text_type(s).replace(text_type(old), text_type(new), count) - if hasattr(old, '__html__') or hasattr(new, '__html__') and \ - not hasattr(s, '__html__'): + if ( + hasattr(old, "__html__") + or hasattr(new, "__html__") + and not hasattr(s, "__html__") + ): s = escape(s) else: s = soft_unicode(s) @@ -176,13 +229,13 @@ def do_xmlattr(_eval_ctx, d, autospace=True): As you can see it automatically prepends a space in front of the item if the filter returned something unless the second parameter is false. """ - rv = u' '.join( + rv = u" ".join( u'%s="%s"' % (escape(key), escape(value)) for key, value in iteritems(d) if value is not None and not isinstance(value, Undefined) ) if autospace and rv: - rv = u' ' + rv + rv = u" " + rv if _eval_ctx.autoescape: rv = Markup(rv) return rv @@ -199,39 +252,40 @@ def do_title(s): """Return a titlecased version of the value. I.e. words will start with uppercase letters, all remaining characters are lowercase. """ - return ''.join( - [item[0].upper() + item[1:].lower() - for item in _word_beginning_split_re.split(soft_unicode(s)) - if item]) + return "".join( + [ + item[0].upper() + item[1:].lower() + for item in _word_beginning_split_re.split(soft_unicode(s)) + if item + ] + ) -def do_dictsort(value, case_sensitive=False, by='key', reverse=False): +def do_dictsort(value, case_sensitive=False, by="key", reverse=False): """Sort a dict and yield (key, value) pairs. Because python dicts are unsorted you may want to use this function to order them by either key or value: .. sourcecode:: jinja - {% for item in mydict|dictsort %} + {% for key, value in mydict|dictsort %} sort the dict by key, case insensitive - {% for item in mydict|dictsort(reverse=true) %} + {% for key, value in mydict|dictsort(reverse=true) %} sort the dict by key, case insensitive, reverse order - {% for item in mydict|dictsort(true) %} + {% for key, value in mydict|dictsort(true) %} sort the dict by key, case sensitive - {% for item in mydict|dictsort(false, 'value') %} + {% for key, value in mydict|dictsort(false, 'value') %} sort the dict by value, case insensitive """ - if by == 'key': + if by == "key": pos = 0 - elif by == 'value': + elif by == "value": pos = 1 else: - raise FilterArgumentError( - 'You can only sort by either "key" or "value"' - ) + raise FilterArgumentError('You can only sort by either "key" or "value"') def sort_func(item): value = item[pos] @@ -245,48 +299,62 @@ def do_dictsort(value, case_sensitive=False, by='key', reverse=False): @environmentfilter -def do_sort( - environment, value, reverse=False, case_sensitive=False, attribute=None -): - """Sort an iterable. Per default it sorts ascending, if you pass it - true as first argument it will reverse the sorting. +def do_sort(environment, value, reverse=False, case_sensitive=False, attribute=None): + """Sort an iterable using Python's :func:`sorted`. + + .. sourcecode:: jinja + + {% for city in cities|sort %} + ... + {% endfor %} - If the iterable is made of strings the third parameter can be used to - control the case sensitiveness of the comparison which is disabled by - default. + :param reverse: Sort descending instead of ascending. + :param case_sensitive: When sorting strings, sort upper and lower + case separately. + :param attribute: When sorting objects or dicts, an attribute or + key to sort by. Can use dot notation like ``"address.city"``. + Can be a list of attributes like ``"age,name"``. + + The sort is stable, it does not change the relative order of + elements that compare equal. This makes it is possible to chain + sorts on different attributes and ordering. .. sourcecode:: jinja - {% for item in iterable|sort %} + {% for user in users|sort(attribute="name") + |sort(reverse=true, attribute="age") %} ... {% endfor %} - It is also possible to sort by an attribute (for example to sort - by the date of an object) by specifying the `attribute` parameter: + As a shortcut to chaining when the direction is the same for all + attributes, pass a comma separate list of attributes. .. sourcecode:: jinja - {% for item in iterable|sort(attribute='date') %} + {% for user users|sort(attribute="age,name") %} ... {% endfor %} + .. versionchanged:: 2.11.0 + The ``attribute`` parameter can be a comma separated list of + attributes, e.g. ``"age,name"``. + .. versionchanged:: 2.6 - The `attribute` parameter was added. + The ``attribute`` parameter was added. """ - key_func = make_attrgetter( - environment, attribute, - postprocess=ignore_case if not case_sensitive else None + key_func = make_multi_attrgetter( + environment, attribute, postprocess=ignore_case if not case_sensitive else None ) return sorted(value, key=key_func, reverse=reverse) @environmentfilter def do_unique(environment, value, case_sensitive=False, attribute=None): - """Returns a list of unique items from the the given iterable. + """Returns a list of unique items from the given iterable. .. sourcecode:: jinja - {{ ['foo', 'bar', 'foobar', 'FooBar']|unique }} + {{ ['foo', 'bar', 'foobar', 'FooBar']|unique|list }} -> ['foo', 'bar', 'foobar'] The unique items are yielded in the same order as their first occurrence in @@ -296,8 +364,7 @@ def do_unique(environment, value, case_sensitive=False, attribute=None): :param attribute: Filter objects with unique values for this attribute. """ getter = make_attrgetter( - environment, attribute, - postprocess=ignore_case if not case_sensitive else None + environment, attribute, postprocess=ignore_case if not case_sensitive else None ) seen = set() @@ -315,11 +382,10 @@ def _min_or_max(environment, value, func, case_sensitive, attribute): try: first = next(it) except StopIteration: - return environment.undefined('No aggregated item, sequence was empty.') + return environment.undefined("No aggregated item, sequence was empty.") key_func = make_attrgetter( - environment, attribute, - ignore_case if not case_sensitive else None + environment, attribute, postprocess=ignore_case if not case_sensitive else None ) return func(chain([first], it), key=key_func) @@ -334,7 +400,7 @@ def do_min(environment, value, case_sensitive=False, attribute=None): -> 1 :param case_sensitive: Treat upper and lower case strings as distinct. - :param attribute: Get the object with the max value of this attribute. + :param attribute: Get the object with the min value of this attribute. """ return _min_or_max(environment, value, min, case_sensitive, attribute) @@ -354,7 +420,7 @@ def do_max(environment, value, case_sensitive=False, attribute=None): return _min_or_max(environment, value, max, case_sensitive, attribute) -def do_default(value, default_value=u'', boolean=False): +def do_default(value, default_value=u"", boolean=False): """If the value is undefined it will return the passed default value, otherwise the value of the variable: @@ -370,6 +436,12 @@ def do_default(value, default_value=u'', boolean=False): .. sourcecode:: jinja {{ ''|default('the string was empty', true) }} + + .. versionchanged:: 2.11 + It's now possible to configure the :class:`~jinja2.Environment` with + :class:`~jinja2.ChainableUndefined` to make the `default` filter work + on nested elements and attributes that may contain undefined values + in the chain without getting an :exc:`~jinja2.UndefinedError`. """ if isinstance(value, Undefined) or (boolean and not value): return default_value @@ -377,7 +449,7 @@ def do_default(value, default_value=u'', boolean=False): @evalcontextfilter -def do_join(eval_ctx, value, d=u'', attribute=None): +def do_join(eval_ctx, value, d=u"", attribute=None): """Return a string which is the concatenation of the strings in the sequence. The separator between elements is an empty string per default, you can define it with the optional parameter: @@ -402,17 +474,17 @@ def do_join(eval_ctx, value, d=u'', attribute=None): if attribute is not None: value = imap(make_attrgetter(eval_ctx.environment, attribute), value) - # no automatic escaping? joining is a lot eaiser then + # no automatic escaping? joining is a lot easier then if not eval_ctx.autoescape: return text_type(d).join(imap(text_type, value)) # if the delimiter doesn't have an html representation we check # if any of the items has. If yes we do a coercion to Markup - if not hasattr(d, '__html__'): + if not hasattr(d, "__html__"): value = list(value) do_escape = False for idx, item in enumerate(value): - if hasattr(item, '__html__'): + if hasattr(item, "__html__"): do_escape = True else: value[idx] = text_type(item) @@ -437,16 +509,25 @@ def do_first(environment, seq): try: return next(iter(seq)) except StopIteration: - return environment.undefined('No first item, sequence was empty.') + return environment.undefined("No first item, sequence was empty.") @environmentfilter def do_last(environment, seq): - """Return the last item of a sequence.""" + """ + Return the last item of a sequence. + + Note: Does not work with generators. You may want to explicitly + convert it to a list: + + .. sourcecode:: jinja + + {{ data | selectattr('name', '==', 'Jinja') | list | last }} + """ try: return next(iter(reversed(seq))) except StopIteration: - return environment.undefined('No last item, sequence was empty.') + return environment.undefined("No last item, sequence was empty.") @contextfilter @@ -455,7 +536,7 @@ def do_random(context, seq): try: return random.choice(seq) except IndexError: - return context.environment.undefined('No random item, sequence was empty.') + return context.environment.undefined("No random item, sequence was empty.") def do_filesizeformat(value, binary=False): @@ -467,25 +548,25 @@ def do_filesizeformat(value, binary=False): bytes = float(value) base = binary and 1024 or 1000 prefixes = [ - (binary and 'KiB' or 'kB'), - (binary and 'MiB' or 'MB'), - (binary and 'GiB' or 'GB'), - (binary and 'TiB' or 'TB'), - (binary and 'PiB' or 'PB'), - (binary and 'EiB' or 'EB'), - (binary and 'ZiB' or 'ZB'), - (binary and 'YiB' or 'YB') + (binary and "KiB" or "kB"), + (binary and "MiB" or "MB"), + (binary and "GiB" or "GB"), + (binary and "TiB" or "TB"), + (binary and "PiB" or "PB"), + (binary and "EiB" or "EB"), + (binary and "ZiB" or "ZB"), + (binary and "YiB" or "YB"), ] if bytes == 1: - return '1 Byte' + return "1 Byte" elif bytes < base: - return '%d Bytes' % bytes + return "%d Bytes" % bytes else: for i, prefix in enumerate(prefixes): unit = base ** (i + 2) if bytes < unit: - return '%.1f %s' % ((base * bytes / unit), prefix) - return '%.1f %s' % ((base * bytes / unit), prefix) + return "%.1f %s" % ((base * bytes / unit), prefix) + return "%.1f %s" % ((base * bytes / unit), prefix) def do_pprint(value, verbose=False): @@ -498,8 +579,9 @@ def do_pprint(value, verbose=False): @evalcontextfilter -def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False, - target=None, rel=None): +def do_urlize( + eval_ctx, value, trim_url_limit=None, nofollow=False, target=None, rel=None +): """Converts URLs in plain text into clickable links. If you pass the filter an additional integer it will shorten the urls @@ -522,22 +604,20 @@ def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False, The *target* parameter was added. """ policies = eval_ctx.environment.policies - rel = set((rel or '').split() or []) + rel = set((rel or "").split() or []) if nofollow: - rel.add('nofollow') - rel.update((policies['urlize.rel'] or '').split()) + rel.add("nofollow") + rel.update((policies["urlize.rel"] or "").split()) if target is None: - target = policies['urlize.target'] - rel = ' '.join(sorted(rel)) or None + target = policies["urlize.target"] + rel = " ".join(sorted(rel)) or None rv = urlize(value, trim_url_limit, rel=rel, target=target) if eval_ctx.autoescape: rv = Markup(rv) return rv -def do_indent( - s, width=4, first=False, blank=False, indentfirst=None -): +def do_indent(s, width=4, first=False, blank=False, indentfirst=None): """Return a copy of the string with each line indented by 4 spaces. The first line and blank lines are not indented by default. @@ -551,22 +631,31 @@ def do_indent( Rename the ``indentfirst`` argument to ``first``. """ if indentfirst is not None: - warnings.warn(DeprecationWarning( - 'The "indentfirst" argument is renamed to "first".' - ), stacklevel=2) + warnings.warn( + "The 'indentfirst' argument is renamed to 'first' and will" + " be removed in version 3.0.", + DeprecationWarning, + stacklevel=2, + ) first = indentfirst - s += u'\n' # this quirk is necessary for splitlines method - indention = u' ' * width + indention = u" " * width + newline = u"\n" + + if isinstance(s, Markup): + indention = Markup(indention) + newline = Markup(newline) + + s += newline # this quirk is necessary for splitlines method if blank: - rv = (u'\n' + indention).join(s.splitlines()) + rv = (newline + indention).join(s.splitlines()) else: lines = s.splitlines() rv = lines.pop(0) if lines: - rv += u'\n' + u'\n'.join( + rv += newline + newline.join( indention + line if line else line for line in lines ) @@ -577,7 +666,7 @@ def do_indent( @environmentfilter -def do_truncate(env, s, length=255, killwords=False, end='...', leeway=None): +def do_truncate(env, s, length=255, killwords=False, end="...", leeway=None): """Return a truncated copy of the string. The length is specified with the first parameter which defaults to ``255``. If the second parameter is ``true`` the filter will cut the text at length. Otherwise @@ -598,46 +687,81 @@ def do_truncate(env, s, length=255, killwords=False, end='...', leeway=None): {{ "foo bar baz qux"|truncate(11, False, '...', 0) }} -> "foo bar..." - The default leeway on newer Jinja2 versions is 5 and was 0 before but + The default leeway on newer Jinja versions is 5 and was 0 before but can be reconfigured globally. """ if leeway is None: - leeway = env.policies['truncate.leeway'] - assert length >= len(end), 'expected length >= %s, got %s' % (len(end), length) - assert leeway >= 0, 'expected leeway >= 0, got %s' % leeway + leeway = env.policies["truncate.leeway"] + assert length >= len(end), "expected length >= %s, got %s" % (len(end), length) + assert leeway >= 0, "expected leeway >= 0, got %s" % leeway if len(s) <= length + leeway: return s if killwords: - return s[:length - len(end)] + end - result = s[:length - len(end)].rsplit(' ', 1)[0] + return s[: length - len(end)] + end + result = s[: length - len(end)].rsplit(" ", 1)[0] return result + end @environmentfilter -def do_wordwrap(environment, s, width=79, break_long_words=True, - wrapstring=None): +def do_wordwrap( + environment, + s, + width=79, + break_long_words=True, + wrapstring=None, + break_on_hyphens=True, +): + """Wrap a string to the given width. Existing newlines are treated + as paragraphs to be wrapped separately. + + :param s: Original text to wrap. + :param width: Maximum length of wrapped lines. + :param break_long_words: If a word is longer than ``width``, break + it across lines. + :param break_on_hyphens: If a word contains hyphens, it may be split + across lines. + :param wrapstring: String to join each wrapped line. Defaults to + :attr:`Environment.newline_sequence`. + + .. versionchanged:: 2.11 + Existing newlines are treated as paragraphs wrapped separately. + + .. versionchanged:: 2.11 + Added the ``break_on_hyphens`` parameter. + + .. versionchanged:: 2.7 + Added the ``wrapstring`` parameter. """ - Return a copy of the string passed to the filter wrapped after - ``79`` characters. You can override this default using the first - parameter. If you set the second parameter to `false` Jinja will not - split words apart if they are longer than `width`. By default, the newlines - will be the default newlines for the environment, but this can be changed - using the wrapstring keyword argument. - .. versionadded:: 2.7 - Added support for the `wrapstring` parameter. - """ + import textwrap + if not wrapstring: wrapstring = environment.newline_sequence - import textwrap - return wrapstring.join(textwrap.wrap(s, width=width, expand_tabs=False, - replace_whitespace=False, - break_long_words=break_long_words)) + + # textwrap.wrap doesn't consider existing newlines when wrapping. + # If the string has a newline before width, wrap will still insert + # a newline at width, resulting in a short line. Instead, split and + # wrap each paragraph individually. + return wrapstring.join( + [ + wrapstring.join( + textwrap.wrap( + line, + width=width, + expand_tabs=False, + replace_whitespace=False, + break_long_words=break_long_words, + break_on_hyphens=break_on_hyphens, + ) + ) + for line in s.splitlines() + ] + ) def do_wordcount(s): """Count the words in that string.""" - return len(_word_re.findall(s)) + return len(_word_re.findall(soft_unicode(s))) def do_int(value, default=0, base=10): @@ -673,29 +797,40 @@ def do_float(value, default=0.0): def do_format(value, *args, **kwargs): - """ - Apply python string formatting on an object: + """Apply the given values to a `printf-style`_ format string, like + ``string % values``. .. sourcecode:: jinja - {{ "%s - %s"|format("Hello?", "Foo!") }} - -> Hello? - Foo! + {{ "%s, %s!"|format(greeting, name) }} + Hello, World! + + In most cases it should be more convenient and efficient to use the + ``%`` operator or :meth:`str.format`. + + .. code-block:: text + + {{ "%s, %s!" % (greeting, name) }} + {{ "{}, {}!".format(greeting, name) }} + + .. _printf-style: https://docs.python.org/library/stdtypes.html + #printf-style-string-formatting """ if args and kwargs: - raise FilterArgumentError('can\'t handle positional and keyword ' - 'arguments at the same time') + raise FilterArgumentError( + "can't handle positional and keyword arguments at the same time" + ) return soft_unicode(value) % (kwargs or args) -def do_trim(value): - """Strip leading and trailing whitespace.""" - return soft_unicode(value).strip() +def do_trim(value, chars=None): + """Strip leading and trailing characters, by default whitespace.""" + return soft_unicode(value).strip(chars) def do_striptags(value): - """Strip SGML/XML tags and replace adjacent whitespace by one space. - """ - if hasattr(value, '__html__'): + """Strip SGML/XML tags and replace adjacent whitespace by one space.""" + if hasattr(value, "__html__"): value = value.__html__() return Markup(text_type(value)).striptags() @@ -707,7 +842,7 @@ def do_slice(value, slices, fill_with=None): .. sourcecode:: html+jinja - <div class="columwrapper"> + <div class="columnwrapper"> {%- for column in items|slice(3) %} <ul class="column-{{ loop.index }}"> {%- for item in column %} @@ -767,7 +902,7 @@ def do_batch(value, linecount, fill_with=None): yield tmp -def do_round(value, precision=0, method='common'): +def do_round(value, precision=0, method="common"): """Round the number to a given precision. The first parameter specifies the precision (default is ``0``), the second the rounding method: @@ -793,9 +928,9 @@ def do_round(value, precision=0, method='common'): {{ 42.55|round|int }} -> 43 """ - if not method in ('common', 'ceil', 'floor'): - raise FilterArgumentError('method must be common, ceil or floor') - if method == 'common': + if method not in {"common", "ceil", "floor"}: + raise FilterArgumentError("method must be common, ceil or floor") + if method == "common": return round(value, precision) func = getattr(math, method) return func(value * (10 ** precision)) / (10 ** precision) @@ -806,52 +941,51 @@ def do_round(value, precision=0, method='common'): # we do not want to accidentally expose an auto generated repr in case # people start to print this out in comments or something similar for # debugging. -_GroupTuple = namedtuple('_GroupTuple', ['grouper', 'list']) +_GroupTuple = namedtuple("_GroupTuple", ["grouper", "list"]) _GroupTuple.__repr__ = tuple.__repr__ _GroupTuple.__str__ = tuple.__str__ + @environmentfilter def do_groupby(environment, value, attribute): - """Group a sequence of objects by a common attribute. + """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``, + the values are sorted first so only one group is returned for each + unique value. - If you for example have a list of dicts or objects that represent persons - with `gender`, `first_name` and `last_name` attributes and you want to - group all users by genders you can do something like the following - snippet: + For example, a list of ``User`` objects with a ``city`` attribute + can be rendered in groups. In this example, ``grouper`` refers to + the ``city`` value of the group. .. sourcecode:: html+jinja - <ul> - {% for group in persons|groupby('gender') %} - <li>{{ group.grouper }}<ul> - {% for person in group.list %} - <li>{{ person.first_name }} {{ person.last_name }}</li> - {% endfor %}</ul></li> - {% endfor %} - </ul> + <ul>{% for city, items in users|groupby("city") %} + <li>{{ city }} + <ul>{% for user in items %} + <li>{{ user.name }} + {% endfor %}</ul> + </li> + {% endfor %}</ul> - Additionally it's possible to use tuple unpacking for the grouper and - list: + ``groupby`` yields namedtuples of ``(grouper, list)``, which + can be used instead of the tuple unpacking above. ``grouper`` is the + value of the attribute, and ``list`` is the items with that value. .. sourcecode:: html+jinja - <ul> - {% for grouper, list in persons|groupby('gender') %} - ... - {% endfor %} - </ul> - - As you can see the item we're grouping by is stored in the `grouper` - attribute and the `list` contains all the objects that have this grouper - in common. + <ul>{% for group in users|groupby("city") %} + <li>{{ group.grouper }}: {{ group.list|join(", ") }} + {% endfor %}</ul> .. versionchanged:: 2.6 - It's now possible to use dotted notation to group by the child - attribute of another attribute. + The attribute supports dot notation for nested access. """ expr = make_attrgetter(environment, attribute) - return [_GroupTuple(key, list(values)) for key, values - in groupby(sorted(value, key=expr), expr)] + return [ + _GroupTuple(key, list(values)) + for key, values in groupby(sorted(value, key=expr), expr) + ] @environmentfilter @@ -908,7 +1042,7 @@ def do_reverse(value): rv.reverse() return rv except TypeError: - raise FilterArgumentError('argument must be iterable') + raise FilterArgumentError("argument must be iterable") @environmentfilter @@ -929,8 +1063,9 @@ def do_attr(environment, obj, name): except AttributeError: pass else: - if environment.sandboxed and not \ - environment.is_safe_attribute(obj, name, value): + if environment.sandboxed and not environment.is_safe_attribute( + obj, name, value + ): return environment.unsafe_undefined(obj, name) return value return environment.undefined(obj=obj, name=name) @@ -949,6 +1084,13 @@ def do_map(*args, **kwargs): Users on this page: {{ users|map(attribute='username')|join(', ') }} + You can specify a ``default`` value to use if an object in the list + does not have the given attribute. + + .. sourcecode:: jinja + + {{ users|map(attribute="username", default="Anonymous")|join(", ") }} + Alternatively you can let it invoke a filter by passing the name of the filter and the arguments afterwards. A good example would be applying a text conversion filter on a sequence: @@ -957,6 +1099,17 @@ def do_map(*args, **kwargs): Users on this page: {{ titles|map('lower')|join(', ') }} + Similar to a generator comprehension such as: + + .. code-block:: python + + (u.username for u in users) + (u.username or "Anonymous" for u in users) + (do_lower(x) for x in titles) + + .. versionchanged:: 2.11.0 + Added the ``default`` parameter. + .. versionadded:: 2.7 """ seq, func = prepare_map(args, kwargs) @@ -982,6 +1135,13 @@ def do_select(*args, **kwargs): {{ numbers|select("lessthan", 42) }} {{ strings|select("equalto", "mystring") }} + Similar to a generator comprehension such as: + + .. code-block:: python + + (n for n in numbers if test_odd(n)) + (n for n in numbers if test_divisibleby(n, 3)) + .. versionadded:: 2.7 """ return select_or_reject(args, kwargs, lambda x: x, False) @@ -1000,6 +1160,12 @@ def do_reject(*args, **kwargs): {{ numbers|reject("odd") }} + Similar to a generator comprehension such as: + + .. code-block:: python + + (n for n in numbers if not test_odd(n)) + .. versionadded:: 2.7 """ return select_or_reject(args, kwargs, lambda x: not x, False) @@ -1021,6 +1187,13 @@ def do_selectattr(*args, **kwargs): {{ users|selectattr("is_active") }} {{ users|selectattr("email", "none") }} + Similar to a generator comprehension such as: + + .. code-block:: python + + (u for user in users if user.is_active) + (u for user in users if test_none(user.email)) + .. versionadded:: 2.7 """ return select_or_reject(args, kwargs, lambda x: x, True) @@ -1040,6 +1213,13 @@ def do_rejectattr(*args, **kwargs): {{ users|rejectattr("is_active") }} {{ users|rejectattr("email", "none") }} + Similar to a generator comprehension such as: + + .. code-block:: python + + (u for user in users if not user.is_active) + (u for user in users if not test_none(user.email)) + .. versionadded:: 2.7 """ return select_or_reject(args, kwargs, lambda x: not x, True) @@ -1072,32 +1252,38 @@ def do_tojson(eval_ctx, value, indent=None): .. versionadded:: 2.9 """ policies = eval_ctx.environment.policies - dumper = policies['json.dumps_function'] - options = policies['json.dumps_kwargs'] + dumper = policies["json.dumps_function"] + options = policies["json.dumps_kwargs"] if indent is not None: options = dict(options) - options['indent'] = indent + options["indent"] = indent return htmlsafe_json_dumps(value, dumper=dumper, **options) def prepare_map(args, kwargs): context = args[0] seq = args[1] + default = None - if len(args) == 2 and 'attribute' in kwargs: - attribute = kwargs.pop('attribute') + if len(args) == 2 and "attribute" in kwargs: + attribute = kwargs.pop("attribute") + default = kwargs.pop("default", None) if kwargs: - raise FilterArgumentError('Unexpected keyword argument %r' % - next(iter(kwargs))) - func = make_attrgetter(context.environment, attribute) + raise FilterArgumentError( + "Unexpected keyword argument %r" % next(iter(kwargs)) + ) + func = make_attrgetter(context.environment, attribute, default=default) else: try: name = args[2] args = args[3:] except LookupError: - raise FilterArgumentError('map requires a filter argument') - func = lambda item: context.environment.call_filter( - name, item, args, kwargs, context=context) + raise FilterArgumentError("map requires a filter argument") + + def func(item): + return context.environment.call_filter( + name, item, args, kwargs, context=context + ) return seq, func @@ -1109,18 +1295,22 @@ def prepare_select_or_reject(args, kwargs, modfunc, lookup_attr): try: attr = args[2] except LookupError: - raise FilterArgumentError('Missing parameter for attribute name') + raise FilterArgumentError("Missing parameter for attribute name") transfunc = make_attrgetter(context.environment, attr) off = 1 else: off = 0 - transfunc = lambda x: x + + def transfunc(x): + return x try: name = args[2 + off] - args = args[3 + off:] - func = lambda item: context.environment.call_test( - name, item, args, kwargs) + args = args[3 + off :] + + def func(item): + return context.environment.call_test(name, item, args, kwargs) + except LookupError: func = bool @@ -1136,57 +1326,57 @@ def select_or_reject(args, kwargs, modfunc, lookup_attr): FILTERS = { - 'abs': abs, - 'attr': do_attr, - 'batch': do_batch, - 'capitalize': do_capitalize, - 'center': do_center, - 'count': len, - 'd': do_default, - 'default': do_default, - 'dictsort': do_dictsort, - 'e': escape, - 'escape': escape, - 'filesizeformat': do_filesizeformat, - 'first': do_first, - 'float': do_float, - 'forceescape': do_forceescape, - 'format': do_format, - 'groupby': do_groupby, - 'indent': do_indent, - 'int': do_int, - 'join': do_join, - 'last': do_last, - 'length': len, - 'list': do_list, - 'lower': do_lower, - 'map': do_map, - 'min': do_min, - 'max': do_max, - 'pprint': do_pprint, - 'random': do_random, - 'reject': do_reject, - 'rejectattr': do_rejectattr, - 'replace': do_replace, - 'reverse': do_reverse, - 'round': do_round, - 'safe': do_mark_safe, - 'select': do_select, - 'selectattr': do_selectattr, - 'slice': do_slice, - 'sort': do_sort, - 'string': soft_unicode, - 'striptags': do_striptags, - 'sum': do_sum, - 'title': do_title, - 'trim': do_trim, - 'truncate': do_truncate, - 'unique': do_unique, - 'upper': do_upper, - 'urlencode': do_urlencode, - 'urlize': do_urlize, - 'wordcount': do_wordcount, - 'wordwrap': do_wordwrap, - 'xmlattr': do_xmlattr, - 'tojson': do_tojson, + "abs": abs, + "attr": do_attr, + "batch": do_batch, + "capitalize": do_capitalize, + "center": do_center, + "count": len, + "d": do_default, + "default": do_default, + "dictsort": do_dictsort, + "e": escape, + "escape": escape, + "filesizeformat": do_filesizeformat, + "first": do_first, + "float": do_float, + "forceescape": do_forceescape, + "format": do_format, + "groupby": do_groupby, + "indent": do_indent, + "int": do_int, + "join": do_join, + "last": do_last, + "length": len, + "list": do_list, + "lower": do_lower, + "map": do_map, + "min": do_min, + "max": do_max, + "pprint": do_pprint, + "random": do_random, + "reject": do_reject, + "rejectattr": do_rejectattr, + "replace": do_replace, + "reverse": do_reverse, + "round": do_round, + "safe": do_mark_safe, + "select": do_select, + "selectattr": do_selectattr, + "slice": do_slice, + "sort": do_sort, + "string": soft_unicode, + "striptags": do_striptags, + "sum": do_sum, + "title": do_title, + "trim": do_trim, + "truncate": do_truncate, + "unique": do_unique, + "upper": do_upper, + "urlencode": do_urlencode, + "urlize": do_urlize, + "wordcount": do_wordcount, + "wordwrap": do_wordwrap, + "xmlattr": do_xmlattr, + "tojson": do_tojson, } diff --git a/jinja2/idtracking.py b/src/jinja2/idtracking.py index 491bfe0..9a0d838 100644 --- a/jinja2/idtracking.py +++ b/src/jinja2/idtracking.py @@ -1,11 +1,10 @@ -from jinja2.visitor import NodeVisitor -from jinja2._compat import iteritems +from ._compat import iteritems +from .visitor import NodeVisitor - -VAR_LOAD_PARAMETER = 'param' -VAR_LOAD_RESOLVE = 'resolve' -VAR_LOAD_ALIAS = 'alias' -VAR_LOAD_UNDEFINED = 'undefined' +VAR_LOAD_PARAMETER = "param" +VAR_LOAD_RESOLVE = "resolve" +VAR_LOAD_ALIAS = "alias" +VAR_LOAD_UNDEFINED = "undefined" def find_symbols(nodes, parent_symbols=None): @@ -23,7 +22,6 @@ def symbols_for_node(node, parent_symbols=None): class Symbols(object): - def __init__(self, parent=None, level=None): if level is None: if parent is None: @@ -41,7 +39,7 @@ class Symbols(object): visitor.visit(node, **kwargs) def _define_ref(self, name, load=None): - ident = 'l_%d_%s' % (self.level, name) + ident = "l_%d_%s" % (self.level, name) self.refs[name] = ident if load is not None: self.loads[ident] = load @@ -62,8 +60,10 @@ class Symbols(object): def ref(self, name): rv = self.find_ref(name) if rv is None: - raise AssertionError('Tried to resolve a name to a reference that ' - 'was unknown to the frame (%r)' % name) + raise AssertionError( + "Tried to resolve a name to a reference that " + "was unknown to the frame (%r)" % name + ) return rv def copy(self): @@ -118,7 +118,7 @@ class Symbols(object): if branch_count == len(branch_symbols): continue target = self.find_ref(name) - assert target is not None, 'should not happen' + assert target is not None, "should not happen" if self.parent is not None: outer_target = self.parent.find_ref(name) @@ -149,7 +149,6 @@ class Symbols(object): class RootVisitor(NodeVisitor): - def __init__(self, symbols): self.sym_visitor = FrameSymbolVisitor(symbols) @@ -157,35 +156,39 @@ class RootVisitor(NodeVisitor): for child in node.iter_child_nodes(): self.sym_visitor.visit(child) - visit_Template = visit_Block = visit_Macro = visit_FilterBlock = \ - visit_Scope = visit_If = visit_ScopedEvalContextModifier = \ - _simple_visit + visit_Template = ( + visit_Block + ) = ( + visit_Macro + ) = ( + visit_FilterBlock + ) = visit_Scope = visit_If = visit_ScopedEvalContextModifier = _simple_visit def visit_AssignBlock(self, node, **kwargs): for child in node.body: self.sym_visitor.visit(child) def visit_CallBlock(self, node, **kwargs): - for child in node.iter_child_nodes(exclude=('call',)): + for child in node.iter_child_nodes(exclude=("call",)): self.sym_visitor.visit(child) def visit_OverlayScope(self, node, **kwargs): for child in node.body: self.sym_visitor.visit(child) - def visit_For(self, node, for_branch='body', **kwargs): - if for_branch == 'body': + def visit_For(self, node, for_branch="body", **kwargs): + if for_branch == "body": self.sym_visitor.visit(node.target, store_as_param=True) branch = node.body - elif for_branch == 'else': + elif for_branch == "else": branch = node.else_ - elif for_branch == 'test': + elif for_branch == "test": self.sym_visitor.visit(node.target, store_as_param=True) if node.test is not None: self.sym_visitor.visit(node.test) return else: - raise RuntimeError('Unknown for branch') + raise RuntimeError("Unknown for branch") for item in branch or (): self.sym_visitor.visit(item) @@ -196,8 +199,9 @@ class RootVisitor(NodeVisitor): self.sym_visitor.visit(child) def generic_visit(self, node, *args, **kwargs): - raise NotImplementedError('Cannot find symbols for %r' % - node.__class__.__name__) + raise NotImplementedError( + "Cannot find symbols for %r" % node.__class__.__name__ + ) class FrameSymbolVisitor(NodeVisitor): @@ -208,11 +212,11 @@ class FrameSymbolVisitor(NodeVisitor): def visit_Name(self, node, store_as_param=False, **kwargs): """All assignments to names go through this function.""" - if store_as_param or node.ctx == 'param': + if store_as_param or node.ctx == "param": self.symbols.declare_parameter(node.name) - elif node.ctx == 'store': + elif node.ctx == "store": self.symbols.store(node.name) - elif node.ctx == 'load': + elif node.ctx == "load": self.symbols.load(node.name) def visit_NSRef(self, node, **kwargs): diff --git a/src/jinja2/lexer.py b/src/jinja2/lexer.py new file mode 100644 index 0000000..552356a --- /dev/null +++ b/src/jinja2/lexer.py @@ -0,0 +1,848 @@ +# -*- coding: utf-8 -*- +"""Implements a Jinja / Python combination lexer. The ``Lexer`` class +is used to do some preprocessing. It filters out invalid operators like +the bitshift operators we don't allow in templates. It separates +template code and python code in expressions. +""" +import re +from ast import literal_eval +from collections import deque +from operator import itemgetter + +from ._compat import implements_iterator +from ._compat import intern +from ._compat import iteritems +from ._compat import text_type +from .exceptions import TemplateSyntaxError +from .utils import LRUCache + +# cache for the lexers. Exists in order to be able to have multiple +# environments with the same lexer +_lexer_cache = LRUCache(50) + +# static regular expressions +whitespace_re = re.compile(r"\s+", re.U) +newline_re = re.compile(r"(\r\n|\r|\n)") +string_re = re.compile( + r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S +) +integer_re = re.compile(r"(\d+_)*\d+") +float_re = re.compile( + r""" + (?<!\.) # doesn't start with a . + (\d+_)*\d+ # digits, possibly _ separated + ( + (\.(\d+_)*\d+)? # optional fractional part + e[+\-]?(\d+_)*\d+ # exponent part + | + \.(\d+_)*\d+ # required fractional part + ) + """, + re.IGNORECASE | re.VERBOSE, +) + +try: + # check if this Python supports Unicode identifiers + compile("föö", "<unknown>", "eval") +except SyntaxError: + # Python 2, no Unicode support, use ASCII identifiers + name_re = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*") + check_ident = False +else: + # Unicode support, import generated re pattern and set flag to use + # str.isidentifier to validate during lexing. + from ._identifier import pattern as name_re + + check_ident = True + +# internal the tokens and keep references to them +TOKEN_ADD = intern("add") +TOKEN_ASSIGN = intern("assign") +TOKEN_COLON = intern("colon") +TOKEN_COMMA = intern("comma") +TOKEN_DIV = intern("div") +TOKEN_DOT = intern("dot") +TOKEN_EQ = intern("eq") +TOKEN_FLOORDIV = intern("floordiv") +TOKEN_GT = intern("gt") +TOKEN_GTEQ = intern("gteq") +TOKEN_LBRACE = intern("lbrace") +TOKEN_LBRACKET = intern("lbracket") +TOKEN_LPAREN = intern("lparen") +TOKEN_LT = intern("lt") +TOKEN_LTEQ = intern("lteq") +TOKEN_MOD = intern("mod") +TOKEN_MUL = intern("mul") +TOKEN_NE = intern("ne") +TOKEN_PIPE = intern("pipe") +TOKEN_POW = intern("pow") +TOKEN_RBRACE = intern("rbrace") +TOKEN_RBRACKET = intern("rbracket") +TOKEN_RPAREN = intern("rparen") +TOKEN_SEMICOLON = intern("semicolon") +TOKEN_SUB = intern("sub") +TOKEN_TILDE = intern("tilde") +TOKEN_WHITESPACE = intern("whitespace") +TOKEN_FLOAT = intern("float") +TOKEN_INTEGER = intern("integer") +TOKEN_NAME = intern("name") +TOKEN_STRING = intern("string") +TOKEN_OPERATOR = intern("operator") +TOKEN_BLOCK_BEGIN = intern("block_begin") +TOKEN_BLOCK_END = intern("block_end") +TOKEN_VARIABLE_BEGIN = intern("variable_begin") +TOKEN_VARIABLE_END = intern("variable_end") +TOKEN_RAW_BEGIN = intern("raw_begin") +TOKEN_RAW_END = intern("raw_end") +TOKEN_COMMENT_BEGIN = intern("comment_begin") +TOKEN_COMMENT_END = intern("comment_end") +TOKEN_COMMENT = intern("comment") +TOKEN_LINESTATEMENT_BEGIN = intern("linestatement_begin") +TOKEN_LINESTATEMENT_END = intern("linestatement_end") +TOKEN_LINECOMMENT_BEGIN = intern("linecomment_begin") +TOKEN_LINECOMMENT_END = intern("linecomment_end") +TOKEN_LINECOMMENT = intern("linecomment") +TOKEN_DATA = intern("data") +TOKEN_INITIAL = intern("initial") +TOKEN_EOF = intern("eof") + +# bind operators to token types +operators = { + "+": TOKEN_ADD, + "-": TOKEN_SUB, + "/": TOKEN_DIV, + "//": TOKEN_FLOORDIV, + "*": TOKEN_MUL, + "%": TOKEN_MOD, + "**": TOKEN_POW, + "~": TOKEN_TILDE, + "[": TOKEN_LBRACKET, + "]": TOKEN_RBRACKET, + "(": TOKEN_LPAREN, + ")": TOKEN_RPAREN, + "{": TOKEN_LBRACE, + "}": TOKEN_RBRACE, + "==": TOKEN_EQ, + "!=": TOKEN_NE, + ">": TOKEN_GT, + ">=": TOKEN_GTEQ, + "<": TOKEN_LT, + "<=": TOKEN_LTEQ, + "=": TOKEN_ASSIGN, + ".": TOKEN_DOT, + ":": TOKEN_COLON, + "|": TOKEN_PIPE, + ",": TOKEN_COMMA, + ";": TOKEN_SEMICOLON, +} + +reverse_operators = dict([(v, k) for k, v in iteritems(operators)]) +assert len(operators) == len(reverse_operators), "operators dropped" +operator_re = re.compile( + "(%s)" % "|".join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x))) +) + +ignored_tokens = frozenset( + [ + TOKEN_COMMENT_BEGIN, + TOKEN_COMMENT, + TOKEN_COMMENT_END, + TOKEN_WHITESPACE, + TOKEN_LINECOMMENT_BEGIN, + TOKEN_LINECOMMENT_END, + TOKEN_LINECOMMENT, + ] +) +ignore_if_empty = frozenset( + [TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT] +) + + +def _describe_token_type(token_type): + if token_type in reverse_operators: + return reverse_operators[token_type] + return { + TOKEN_COMMENT_BEGIN: "begin of comment", + TOKEN_COMMENT_END: "end of comment", + TOKEN_COMMENT: "comment", + TOKEN_LINECOMMENT: "comment", + TOKEN_BLOCK_BEGIN: "begin of statement block", + TOKEN_BLOCK_END: "end of statement block", + TOKEN_VARIABLE_BEGIN: "begin of print statement", + TOKEN_VARIABLE_END: "end of print statement", + TOKEN_LINESTATEMENT_BEGIN: "begin of line statement", + TOKEN_LINESTATEMENT_END: "end of line statement", + TOKEN_DATA: "template data / text", + TOKEN_EOF: "end of template", + }.get(token_type, token_type) + + +def describe_token(token): + """Returns a description of the token.""" + if token.type == TOKEN_NAME: + return token.value + return _describe_token_type(token.type) + + +def describe_token_expr(expr): + """Like `describe_token` but for token expressions.""" + if ":" in expr: + type, value = expr.split(":", 1) + if type == TOKEN_NAME: + return value + else: + type = expr + return _describe_token_type(type) + + +def count_newlines(value): + """Count the number of newline characters in the string. This is + useful for extensions that filter a stream. + """ + return len(newline_re.findall(value)) + + +def compile_rules(environment): + """Compiles all the rules from the environment into a list of rules.""" + e = re.escape + rules = [ + ( + len(environment.comment_start_string), + TOKEN_COMMENT_BEGIN, + e(environment.comment_start_string), + ), + ( + len(environment.block_start_string), + TOKEN_BLOCK_BEGIN, + e(environment.block_start_string), + ), + ( + len(environment.variable_start_string), + TOKEN_VARIABLE_BEGIN, + e(environment.variable_start_string), + ), + ] + + if environment.line_statement_prefix is not None: + rules.append( + ( + len(environment.line_statement_prefix), + TOKEN_LINESTATEMENT_BEGIN, + r"^[ \t\v]*" + e(environment.line_statement_prefix), + ) + ) + if environment.line_comment_prefix is not None: + rules.append( + ( + len(environment.line_comment_prefix), + TOKEN_LINECOMMENT_BEGIN, + r"(?:^|(?<=\S))[^\S\r\n]*" + e(environment.line_comment_prefix), + ) + ) + + return [x[1:] for x in sorted(rules, reverse=True)] + + +class Failure(object): + """Class that raises a `TemplateSyntaxError` if called. + Used by the `Lexer` to specify known errors. + """ + + def __init__(self, message, cls=TemplateSyntaxError): + self.message = message + self.error_class = cls + + def __call__(self, lineno, filename): + raise self.error_class(self.message, lineno, filename) + + +class Token(tuple): + """Token class.""" + + __slots__ = () + lineno, type, value = (property(itemgetter(x)) for x in range(3)) + + def __new__(cls, lineno, type, value): + return tuple.__new__(cls, (lineno, intern(str(type)), value)) + + def __str__(self): + if self.type in reverse_operators: + return reverse_operators[self.type] + elif self.type == "name": + return self.value + return self.type + + def test(self, expr): + """Test a token against a token expression. This can either be a + token type or ``'token_type:token_value'``. This can only test + against string values and types. + """ + # here we do a regular string equality check as test_any is usually + # passed an iterable of not interned strings. + if self.type == expr: + return True + elif ":" in expr: + return expr.split(":", 1) == [self.type, self.value] + return False + + def test_any(self, *iterable): + """Test against multiple token expressions.""" + for expr in iterable: + if self.test(expr): + return True + return False + + def __repr__(self): + return "Token(%r, %r, %r)" % (self.lineno, self.type, self.value) + + +@implements_iterator +class TokenStreamIterator(object): + """The iterator for tokenstreams. Iterate over the stream + until the eof token is reached. + """ + + def __init__(self, stream): + self.stream = stream + + def __iter__(self): + return self + + def __next__(self): + token = self.stream.current + if token.type is TOKEN_EOF: + self.stream.close() + raise StopIteration() + next(self.stream) + return token + + +@implements_iterator +class TokenStream(object): + """A token stream is an iterable that yields :class:`Token`\\s. The + parser however does not iterate over it but calls :meth:`next` to go + one token ahead. The current active token is stored as :attr:`current`. + """ + + def __init__(self, generator, name, filename): + self._iter = iter(generator) + self._pushed = deque() + self.name = name + self.filename = filename + self.closed = False + self.current = Token(1, TOKEN_INITIAL, "") + next(self) + + def __iter__(self): + return TokenStreamIterator(self) + + def __bool__(self): + return bool(self._pushed) or self.current.type is not TOKEN_EOF + + __nonzero__ = __bool__ # py2 + + @property + def eos(self): + """Are we at the end of the stream?""" + return not self + + def push(self, token): + """Push a token back to the stream.""" + self._pushed.append(token) + + def look(self): + """Look at the next token.""" + old_token = next(self) + result = self.current + self.push(result) + self.current = old_token + return result + + def skip(self, n=1): + """Got n tokens ahead.""" + for _ in range(n): + next(self) + + def next_if(self, expr): + """Perform the token test and return the token if it matched. + Otherwise the return value is `None`. + """ + if self.current.test(expr): + return next(self) + + def skip_if(self, expr): + """Like :meth:`next_if` but only returns `True` or `False`.""" + return self.next_if(expr) is not None + + def __next__(self): + """Go one token ahead and return the old one. + + Use the built-in :func:`next` instead of calling this directly. + """ + rv = self.current + if self._pushed: + self.current = self._pushed.popleft() + elif self.current.type is not TOKEN_EOF: + try: + self.current = next(self._iter) + except StopIteration: + self.close() + return rv + + def close(self): + """Close the stream.""" + self.current = Token(self.current.lineno, TOKEN_EOF, "") + self._iter = None + self.closed = True + + def expect(self, expr): + """Expect a given token type and return it. This accepts the same + argument as :meth:`jinja2.lexer.Token.test`. + """ + if not self.current.test(expr): + expr = describe_token_expr(expr) + if self.current.type is TOKEN_EOF: + raise TemplateSyntaxError( + "unexpected end of template, expected %r." % expr, + self.current.lineno, + self.name, + self.filename, + ) + raise TemplateSyntaxError( + "expected token %r, got %r" % (expr, describe_token(self.current)), + self.current.lineno, + self.name, + self.filename, + ) + try: + return self.current + finally: + next(self) + + +def get_lexer(environment): + """Return a lexer which is probably cached.""" + key = ( + environment.block_start_string, + environment.block_end_string, + environment.variable_start_string, + environment.variable_end_string, + environment.comment_start_string, + environment.comment_end_string, + environment.line_statement_prefix, + environment.line_comment_prefix, + environment.trim_blocks, + environment.lstrip_blocks, + environment.newline_sequence, + environment.keep_trailing_newline, + ) + lexer = _lexer_cache.get(key) + if lexer is None: + lexer = Lexer(environment) + _lexer_cache[key] = lexer + return lexer + + +class OptionalLStrip(tuple): + """A special tuple for marking a point in the state that can have + lstrip applied. + """ + + __slots__ = () + + # Even though it looks like a no-op, creating instances fails + # without this. + def __new__(cls, *members, **kwargs): + return super(OptionalLStrip, cls).__new__(cls, members) + + +class Lexer(object): + """Class that implements a lexer for a given environment. Automatically + created by the environment class, usually you don't have to do that. + + Note that the lexer is not automatically bound to an environment. + Multiple environments can share the same lexer. + """ + + def __init__(self, environment): + # shortcuts + e = re.escape + + def c(x): + return re.compile(x, re.M | re.S) + + # lexing rules for tags + tag_rules = [ + (whitespace_re, TOKEN_WHITESPACE, None), + (float_re, TOKEN_FLOAT, None), + (integer_re, TOKEN_INTEGER, None), + (name_re, TOKEN_NAME, None), + (string_re, TOKEN_STRING, None), + (operator_re, TOKEN_OPERATOR, None), + ] + + # assemble the root lexing rule. because "|" is ungreedy + # we have to sort by length so that the lexer continues working + # as expected when we have parsing rules like <% for block and + # <%= for variables. (if someone wants asp like syntax) + # variables are just part of the rules if variable processing + # is required. + root_tag_rules = compile_rules(environment) + + # block suffix if trimming is enabled + block_suffix_re = environment.trim_blocks and "\\n?" or "" + + # 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.newline_sequence = environment.newline_sequence + self.keep_trailing_newline = environment.keep_trailing_newline + + # global lexing rules + self.rules = { + "root": [ + # directives + ( + c( + "(.*?)(?:%s)" + % "|".join( + [ + r"(?P<raw_begin>%s(\-|\+|)\s*raw\s*(?:\-%s\s*|%s))" + % ( + e(environment.block_start_string), + e(environment.block_end_string), + e(environment.block_end_string), + ) + ] + + [ + r"(?P<%s>%s(\-|\+|))" % (n, r) + for n, r in root_tag_rules + ] + ) + ), + OptionalLStrip(TOKEN_DATA, "#bygroup"), + "#bygroup", + ), + # data + (c(".+"), TOKEN_DATA, None), + ], + # comments + TOKEN_COMMENT_BEGIN: [ + ( + c( + r"(.*?)((?:\-%s\s*|%s)%s)" + % ( + e(environment.comment_end_string), + e(environment.comment_end_string), + block_suffix_re, + ) + ), + (TOKEN_COMMENT, TOKEN_COMMENT_END), + "#pop", + ), + (c("(.)"), (Failure("Missing end of comment tag"),), None), + ], + # blocks + TOKEN_BLOCK_BEGIN: [ + ( + c( + r"(?:\-%s\s*|%s)%s" + % ( + e(environment.block_end_string), + e(environment.block_end_string), + block_suffix_re, + ) + ), + TOKEN_BLOCK_END, + "#pop", + ), + ] + + tag_rules, + # variables + TOKEN_VARIABLE_BEGIN: [ + ( + c( + r"\-%s\s*|%s" + % ( + e(environment.variable_end_string), + e(environment.variable_end_string), + ) + ), + TOKEN_VARIABLE_END, + "#pop", + ) + ] + + tag_rules, + # raw block + TOKEN_RAW_BEGIN: [ + ( + c( + r"(.*?)((?:%s(\-|\+|))\s*endraw\s*(?:\-%s\s*|%s%s))" + % ( + e(environment.block_start_string), + e(environment.block_end_string), + e(environment.block_end_string), + block_suffix_re, + ) + ), + OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END), + "#pop", + ), + (c("(.)"), (Failure("Missing end of raw directive"),), None), + ], + # line statements + TOKEN_LINESTATEMENT_BEGIN: [ + (c(r"\s*(\n|$)"), TOKEN_LINESTATEMENT_END, "#pop") + ] + + tag_rules, + # line comments + TOKEN_LINECOMMENT_BEGIN: [ + ( + c(r"(.*?)()(?=\n|$)"), + (TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END), + "#pop", + ) + ], + } + + def _normalize_newlines(self, value): + """Called for strings and template data to normalize it to unicode.""" + return newline_re.sub(self.newline_sequence, value) + + def tokenize(self, source, name=None, filename=None, state=None): + """Calls tokeniter + tokenize and wraps it in a token stream.""" + stream = self.tokeniter(source, name, filename, state) + return TokenStream(self.wrap(stream, name, filename), name, filename) + + def wrap(self, stream, name=None, filename=None): + """This is called with the stream as returned by `tokenize` and wraps + every token in a :class:`Token` and converts the value. + """ + for lineno, token, value in stream: + if token in ignored_tokens: + continue + elif token == TOKEN_LINESTATEMENT_BEGIN: + token = TOKEN_BLOCK_BEGIN + elif token == TOKEN_LINESTATEMENT_END: + token = TOKEN_BLOCK_END + # we are not interested in those tokens in the parser + elif token in (TOKEN_RAW_BEGIN, TOKEN_RAW_END): + continue + elif token == TOKEN_DATA: + value = self._normalize_newlines(value) + elif token == "keyword": + token = value + elif token == TOKEN_NAME: + value = str(value) + if check_ident and not value.isidentifier(): + raise TemplateSyntaxError( + "Invalid character in identifier", lineno, name, filename + ) + elif token == TOKEN_STRING: + # try to unescape string + try: + value = ( + self._normalize_newlines(value[1:-1]) + .encode("ascii", "backslashreplace") + .decode("unicode-escape") + ) + except Exception as e: + msg = str(e).split(":")[-1].strip() + raise TemplateSyntaxError(msg, lineno, name, filename) + elif token == TOKEN_INTEGER: + value = int(value.replace("_", "")) + elif token == TOKEN_FLOAT: + # remove all "_" first to support more Python versions + value = literal_eval(value.replace("_", "")) + elif token == TOKEN_OPERATOR: + token = operators[value] + yield Token(lineno, token, value) + + def tokeniter(self, source, name, filename=None, state=None): + """This method tokenizes the text and returns the tokens in a + generator. Use this method if you just want to tokenize a template. + """ + source = text_type(source) + lines = source.splitlines() + if self.keep_trailing_newline and source: + for newline in ("\r\n", "\r", "\n"): + if source.endswith(newline): + lines.append("") + break + source = "\n".join(lines) + pos = 0 + lineno = 1 + stack = ["root"] + if state is not None and state != "root": + assert state in ("variable", "block"), "invalid state" + stack.append(state + "_begin") + statetokens = self.rules[stack[-1]] + source_length = len(source) + balancing_stack = [] + lstrip_unless_re = self.lstrip_unless_re + newlines_stripped = 0 + line_starting = True + + while 1: + # tokenizer loop + for regex, tokens, new_state in statetokens: + m = regex.match(source, pos) + # if no match we try again with the next rule + if m is None: + continue + + # we only match blocks and variables if braces / parentheses + # are balanced. continue parsing with the lower rule which + # is the operator rule. do this only if the end tags look + # like operators + if balancing_stack and tokens in ( + TOKEN_VARIABLE_END, + TOKEN_BLOCK_END, + TOKEN_LINESTATEMENT_END, + ): + continue + + # tuples support more options + if isinstance(tokens, tuple): + groups = m.groups() + + if isinstance(tokens, OptionalLStrip): + # Rule supports lstrip. Match will look like + # text, block type, whitespace control, type, control, ... + text = groups[0] + + # Skipping the text and first type, every other group is the + # whitespace control for each type. One of the groups will be + # -, +, or empty string instead of None. + strip_sign = next(g for g in groups[2::2] if g is not None) + + if strip_sign == "-": + # Strip all whitespace between the text and the tag. + stripped = text.rstrip() + newlines_stripped = text[len(stripped) :].count("\n") + groups = (stripped,) + groups[1:] + elif ( + # Not marked for preserving whitespace. + strip_sign != "+" + # lstrip is enabled. + and lstrip_unless_re is not None + # Not a variable expression. + and not m.groupdict().get(TOKEN_VARIABLE_BEGIN) + ): + # The start of text between the last newline and the tag. + l_pos = text.rfind("\n") + 1 + 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): + groups = (text[:l_pos],) + groups[1:] + + for idx, token in enumerate(tokens): + # failure group + if token.__class__ is Failure: + raise token(lineno, filename) + # bygroup is a bit more complex, in that case we + # yield for the current token the first named + # group that matched + elif token == "#bygroup": + for key, value in iteritems(m.groupdict()): + if value is not None: + yield lineno, key, value + lineno += value.count("\n") + break + else: + raise RuntimeError( + "%r wanted to resolve " + "the token dynamically" + " but no group matched" % regex + ) + # normal group + else: + data = groups[idx] + if data or token not in ignore_if_empty: + yield lineno, token, data + lineno += data.count("\n") + newlines_stripped + newlines_stripped = 0 + + # strings as token just are yielded as it. + else: + data = m.group() + # update brace/parentheses balance + if tokens == TOKEN_OPERATOR: + if data == "{": + balancing_stack.append("}") + elif data == "(": + balancing_stack.append(")") + elif data == "[": + balancing_stack.append("]") + elif data in ("}", ")", "]"): + if not balancing_stack: + raise TemplateSyntaxError( + "unexpected '%s'" % data, lineno, name, filename + ) + expected_op = balancing_stack.pop() + if expected_op != data: + raise TemplateSyntaxError( + "unexpected '%s', " + "expected '%s'" % (data, expected_op), + lineno, + name, + filename, + ) + # yield items + if data or tokens not in ignore_if_empty: + yield lineno, tokens, data + lineno += data.count("\n") + + line_starting = m.group()[-1:] == "\n" + + # fetch new position into new variable so that we can check + # if there is a internal parsing error which would result + # in an infinite loop + pos2 = m.end() + + # handle state changes + if new_state is not None: + # remove the uppermost state + if new_state == "#pop": + stack.pop() + # resolve the new state by group checking + elif new_state == "#bygroup": + for key, value in iteritems(m.groupdict()): + if value is not None: + stack.append(key) + break + else: + raise RuntimeError( + "%r wanted to resolve the " + "new state dynamically but" + " no group matched" % regex + ) + # direct state name given + else: + stack.append(new_state) + statetokens = self.rules[stack[-1]] + # we are still at the same position and no stack change. + # this means a loop without break condition, avoid that and + # raise error + elif pos2 == pos: + raise RuntimeError( + "%r yielded empty string without stack change" % regex + ) + # publish new function and start again + pos = pos2 + break + # if loop terminated without break we haven't found a single match + # either we are at the end of the file or we have a problem + else: + # end of text + if pos >= source_length: + return + # something went wrong + raise TemplateSyntaxError( + "unexpected char %r at %d" % (source[pos], pos), + lineno, + name, + filename, + ) diff --git a/jinja2/loaders.py b/src/jinja2/loaders.py index 4c79793..457c4b5 100644 --- a/jinja2/loaders.py +++ b/src/jinja2/loaders.py @@ -1,22 +1,21 @@ # -*- coding: utf-8 -*- -""" - jinja2.loaders - ~~~~~~~~~~~~~~ - - Jinja loader classes. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. +"""API and implementations for loading templates from different data +sources. """ import os import sys import weakref -from types import ModuleType -from os import path from hashlib import sha1 -from jinja2.exceptions import TemplateNotFound -from jinja2.utils import open_if_exists, internalcode -from jinja2._compat import string_types, iteritems +from os import path +from types import ModuleType + +from ._compat import abc +from ._compat import fspath +from ._compat import iteritems +from ._compat import string_types +from .exceptions import TemplateNotFound +from .utils import internalcode +from .utils import open_if_exists def split_template_path(template): @@ -24,12 +23,14 @@ def split_template_path(template): '..' in the path it will raise a `TemplateNotFound` error. """ pieces = [] - for piece in template.split('/'): - if path.sep in piece \ - or (path.altsep and path.altsep in piece) or \ - piece == path.pardir: + for piece in template.split("/"): + if ( + path.sep in piece + or (path.altsep and path.altsep in piece) + or piece == path.pardir + ): raise TemplateNotFound(template) - elif piece and piece != '.': + elif piece and piece != ".": pieces.append(piece) return pieces @@ -86,15 +87,16 @@ class BaseLoader(object): the template will be reloaded. """ if not self.has_source_access: - raise RuntimeError('%s cannot provide access to the source' % - self.__class__.__name__) + raise RuntimeError( + "%s cannot provide access to the source" % self.__class__.__name__ + ) raise TemplateNotFound(template) def list_templates(self): """Iterates over all templates. If the loader does not support that it should raise a :exc:`TypeError` which is the default behavior. """ - raise TypeError('this loader cannot iterate over all templates') + raise TypeError("this loader cannot iterate over all templates") @internalcode def load(self, environment, name, globals=None): @@ -131,8 +133,9 @@ class BaseLoader(object): bucket.code = code bcc.set_bucket(bucket) - return environment.template_class.from_code(environment, code, - globals, uptodate) + return environment.template_class.from_code( + environment, code, globals, uptodate + ) class FileSystemLoader(BaseLoader): @@ -153,14 +156,20 @@ class FileSystemLoader(BaseLoader): >>> loader = FileSystemLoader('/path/to/templates', followlinks=True) - .. versionchanged:: 2.8+ - The *followlinks* parameter was added. + .. versionchanged:: 2.8 + The ``followlinks`` parameter was added. """ - def __init__(self, searchpath, encoding='utf-8', followlinks=False): - if isinstance(searchpath, string_types): + def __init__(self, searchpath, encoding="utf-8", followlinks=False): + if not isinstance(searchpath, abc.Iterable) or isinstance( + searchpath, string_types + ): searchpath = [searchpath] - self.searchpath = list(searchpath) + + # In Python 3.5, os.path.join doesn't support Path. This can be + # simplified to list(searchpath) when Python 3.5 is dropped. + self.searchpath = [fspath(p) for p in searchpath] + self.encoding = encoding self.followlinks = followlinks @@ -183,6 +192,7 @@ class FileSystemLoader(BaseLoader): return path.getmtime(filename) == mtime except OSError: return False + return contents, filename, uptodate raise TemplateNotFound(template) @@ -190,12 +200,14 @@ class FileSystemLoader(BaseLoader): found = set() for searchpath in self.searchpath: walk_dir = os.walk(searchpath, followlinks=self.followlinks) - for dirpath, dirnames, filenames in walk_dir: + for dirpath, _, filenames in walk_dir: for filename in filenames: - template = os.path.join(dirpath, filename) \ - [len(searchpath):].strip(os.path.sep) \ - .replace(os.path.sep, '/') - if template[:2] == './': + template = ( + os.path.join(dirpath, filename)[len(searchpath) :] + .strip(os.path.sep) + .replace(os.path.sep, "/") + ) + if template[:2] == "./": template = template[2:] if template not in found: found.add(template) @@ -217,10 +229,11 @@ class PackageLoader(BaseLoader): from the file system and not a zip file. """ - def __init__(self, package_name, package_path='templates', - encoding='utf-8'): - from pkg_resources import DefaultProvider, ResourceManager, \ - get_provider + def __init__(self, package_name, package_path="templates", encoding="utf-8"): + from pkg_resources import DefaultProvider + from pkg_resources import get_provider + from pkg_resources import ResourceManager + provider = get_provider(package_name) self.encoding = encoding self.manager = ResourceManager() @@ -230,14 +243,17 @@ class PackageLoader(BaseLoader): def get_source(self, environment, template): pieces = split_template_path(template) - p = '/'.join((self.package_path,) + tuple(pieces)) + p = "/".join((self.package_path,) + tuple(pieces)) + if not self.provider.has_resource(p): raise TemplateNotFound(template) filename = uptodate = None + if self.filesystem_bound: filename = self.provider.get_resource_filename(self.manager, p) mtime = path.getmtime(filename) + def uptodate(): try: return path.getmtime(filename) == mtime @@ -249,19 +265,24 @@ class PackageLoader(BaseLoader): def list_templates(self): path = self.package_path - if path[:2] == './': + + if path[:2] == "./": path = path[2:] - elif path == '.': - path = '' + elif path == ".": + path = "" + offset = len(path) results = [] + def _walk(path): for filename in self.provider.resource_listdir(path): - fullname = path + '/' + filename + fullname = path + "/" + filename + if self.provider.resource_isdir(fullname): _walk(fullname) else: - results.append(fullname[offset:].lstrip('/')) + results.append(fullname[offset:].lstrip("/")) + _walk(path) results.sort() return results @@ -334,7 +355,7 @@ class PrefixLoader(BaseLoader): by loading ``'app2/index.html'`` the file from the second. """ - def __init__(self, mapping, delimiter='/'): + def __init__(self, mapping, delimiter="/"): self.mapping = mapping self.delimiter = delimiter @@ -434,19 +455,20 @@ class ModuleLoader(BaseLoader): has_source_access = False def __init__(self, path): - package_name = '_jinja2_module_templates_%x' % id(self) + package_name = "_jinja2_module_templates_%x" % id(self) # create a fake module that looks for the templates in the # path given. mod = _TemplateModule(package_name) - if isinstance(path, string_types): + + if not isinstance(path, abc.Iterable) or isinstance(path, string_types): path = [path] - else: - path = list(path) - mod.__path__ = path - sys.modules[package_name] = weakref.proxy(mod, - lambda x: sys.modules.pop(package_name, None)) + mod.__path__ = [fspath(p) for p in path] + + sys.modules[package_name] = weakref.proxy( + mod, lambda x: sys.modules.pop(package_name, None) + ) # the only strong reference, the sys.modules entry is weak # so that the garbage collector can remove it once the @@ -456,20 +478,20 @@ class ModuleLoader(BaseLoader): @staticmethod def get_template_key(name): - return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest() + return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() @staticmethod def get_module_filename(name): - return ModuleLoader.get_template_key(name) + '.py' + return ModuleLoader.get_template_key(name) + ".py" @internalcode def load(self, environment, name, globals=None): key = self.get_template_key(name) - module = '%s.%s' % (self.package_name, key) + module = "%s.%s" % (self.package_name, key) mod = getattr(self.module, module, None) if mod is None: try: - mod = __import__(module, None, None, ['root']) + mod = __import__(module, None, None, ["root"]) except ImportError: raise TemplateNotFound(name) @@ -478,4 +500,5 @@ class ModuleLoader(BaseLoader): sys.modules.pop(module, None) return environment.template_class.from_module_dict( - environment, mod.__dict__, globals) + environment, mod.__dict__, globals + ) diff --git a/jinja2/meta.py b/src/jinja2/meta.py index 7421914..3795aac 100644 --- a/jinja2/meta.py +++ b/src/jinja2/meta.py @@ -1,25 +1,18 @@ # -*- coding: utf-8 -*- +"""Functions that expose information about templates that might be +interesting for introspection. """ - jinja2.meta - ~~~~~~~~~~~ - - This module implements various functions that exposes information about - templates that might be interesting for various kinds of applications. - - :copyright: (c) 2017 by the Jinja Team, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. -""" -from jinja2 import nodes -from jinja2.compiler import CodeGenerator -from jinja2._compat import string_types, iteritems +from . import nodes +from ._compat import iteritems +from ._compat import string_types +from .compiler import CodeGenerator class TrackingCodeGenerator(CodeGenerator): """We abuse the code generator for introspection.""" def __init__(self, environment): - CodeGenerator.__init__(self, environment, '<introspection>', - '<introspection>') + CodeGenerator.__init__(self, environment, "<introspection>", "<introspection>") self.undeclared_identifiers = set() def write(self, x): @@ -29,7 +22,7 @@ class TrackingCodeGenerator(CodeGenerator): """Remember all undeclared identifiers.""" CodeGenerator.enter_frame(self, frame) for _, (action, param) in iteritems(frame.symbols.loads): - if action == 'resolve': + if action == "resolve" and param not in self.environment.globals: self.undeclared_identifiers.add(param) @@ -72,8 +65,9 @@ def find_referenced_templates(ast): This function is useful for dependency tracking. For example if you want to rebuild parts of the website after a layout template has changed. """ - for node in ast.find_all((nodes.Extends, nodes.FromImport, nodes.Import, - nodes.Include)): + for node in ast.find_all( + (nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include) + ): if not isinstance(node.template, nodes.Const): # a tuple with some non consts in there if isinstance(node.template, (nodes.Tuple, nodes.List)): @@ -96,8 +90,9 @@ def find_referenced_templates(ast): # a tuple or list (latter *should* not happen) made of consts, # yield the consts that are strings. We could warn here for # non string values - elif isinstance(node, nodes.Include) and \ - isinstance(node.template.value, (tuple, list)): + elif isinstance(node, nodes.Include) and isinstance( + node.template.value, (tuple, list) + ): for template_name in node.template.value: if isinstance(template_name, string_types): yield template_name diff --git a/src/jinja2/nativetypes.py b/src/jinja2/nativetypes.py new file mode 100644 index 0000000..a9ead4e --- /dev/null +++ b/src/jinja2/nativetypes.py @@ -0,0 +1,94 @@ +from ast import literal_eval +from itertools import chain +from itertools import islice + +from . import nodes +from ._compat import text_type +from .compiler import CodeGenerator +from .compiler import has_safe_repr +from .environment import Environment +from .environment import Template + + +def native_concat(nodes): + """Return a native Python type from the list of compiled nodes. If + the result is a single node, its value is returned. Otherwise, the + nodes are concatenated as strings. If the result can be parsed with + :func:`ast.literal_eval`, the parsed value is returned. Otherwise, + the string is returned. + + :param nodes: Iterable of nodes to concatenate. + """ + head = list(islice(nodes, 2)) + + if not head: + return None + + if len(head) == 1: + raw = head[0] + else: + raw = u"".join([text_type(v) for v in chain(head, nodes)]) + + try: + return literal_eval(raw) + except (ValueError, SyntaxError, MemoryError): + return raw + + +class NativeCodeGenerator(CodeGenerator): + """A code generator which renders Python types by not adding + ``to_string()`` around output nodes. + """ + + @staticmethod + def _default_finalize(value): + return value + + def _output_const_repr(self, group): + return repr(u"".join([text_type(v) for v in group])) + + def _output_child_to_const(self, node, frame, finalize): + const = node.as_const(frame.eval_ctx) + + if not has_safe_repr(const): + raise nodes.Impossible() + + if isinstance(node, nodes.TemplateData): + return const + + return finalize.const(const) + + def _output_child_pre(self, node, frame, finalize): + if finalize.src is not None: + self.write(finalize.src) + + def _output_child_post(self, node, frame, finalize): + if finalize.src is not None: + self.write(")") + + +class NativeEnvironment(Environment): + """An environment that renders templates to native Python types.""" + + code_generator_class = NativeCodeGenerator + + +class NativeTemplate(Template): + environment_class = NativeEnvironment + + def render(self, *args, **kwargs): + """Render the template to produce a native Python type. If the + result is a single node, its value is returned. Otherwise, the + nodes are concatenated as strings. If the result can be parsed + with :func:`ast.literal_eval`, the parsed value is returned. + Otherwise, the string is returned. + """ + vars = dict(*args, **kwargs) + + try: + return native_concat(self.root_render_func(self.new_context(vars))) + except Exception: + return self.environment.handle_exception() + + +NativeEnvironment.template_class = NativeTemplate diff --git a/jinja2/nodes.py b/src/jinja2/nodes.py index 801764a..95bd614 100644 --- a/jinja2/nodes.py +++ b/src/jinja2/nodes.py @@ -1,55 +1,39 @@ # -*- coding: utf-8 -*- +"""AST nodes generated by the parser for the compiler. Also provides +some node tree helper functions used by the parser and compiler in order +to normalize nodes. """ - jinja2.nodes - ~~~~~~~~~~~~ - - This module implements additional nodes derived from the ast base node. - - It also provides some node tree helper functions like `in_lineno` and - `get_nodes` used by the parser and translator in order to normalize - python and jinja nodes. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import types import operator - -from markupsafe import Markup - from collections import deque -from jinja2._compat import izip, with_metaclass, text_type, PY2 +from markupsafe import Markup -#: the types we support for context functions -_context_function_types = (types.FunctionType, types.MethodType) - +from ._compat import izip +from ._compat import PY2 +from ._compat import text_type +from ._compat import with_metaclass _binop_to_func = { - '*': operator.mul, - '/': operator.truediv, - '//': operator.floordiv, - '**': operator.pow, - '%': operator.mod, - '+': operator.add, - '-': operator.sub + "*": operator.mul, + "/": operator.truediv, + "//": operator.floordiv, + "**": operator.pow, + "%": operator.mod, + "+": operator.add, + "-": operator.sub, } -_uaop_to_func = { - 'not': operator.not_, - '+': operator.pos, - '-': operator.neg -} +_uaop_to_func = {"not": operator.not_, "+": operator.pos, "-": operator.neg} _cmpop_to_func = { - 'eq': operator.eq, - 'ne': operator.ne, - 'gt': operator.gt, - 'gteq': operator.ge, - 'lt': operator.lt, - 'lteq': operator.le, - 'in': lambda a, b: a in b, - 'notin': lambda a, b: a not in b + "eq": operator.eq, + "ne": operator.ne, + "gt": operator.gt, + "gteq": operator.ge, + "lt": operator.lt, + "lteq": operator.le, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, } @@ -62,16 +46,16 @@ class NodeType(type): inheritance. fields and attributes from the parent class are automatically forwarded to the child.""" - def __new__(cls, name, bases, d): - for attr in 'fields', 'attributes': + def __new__(mcs, name, bases, d): + for attr in "fields", "attributes": storage = [] storage.extend(getattr(bases[0], attr, ())) storage.extend(d.get(attr, ())) - assert len(bases) == 1, 'multiple inheritance not allowed' - assert len(storage) == len(set(storage)), 'layout conflict' + assert len(bases) == 1, "multiple inheritance not allowed" + assert len(storage) == len(set(storage)), "layout conflict" d[attr] = tuple(storage) - d.setdefault('abstract', False) - return type.__new__(cls, name, bases, d) + d.setdefault("abstract", False) + return type.__new__(mcs, name, bases, d) class EvalContext(object): @@ -98,15 +82,17 @@ class EvalContext(object): def get_eval_context(node, ctx): if ctx is None: if node.environment is None: - raise RuntimeError('if no eval context is passed, the ' - 'node must have an attached ' - 'environment.') + raise RuntimeError( + "if no eval context is passed, the " + "node must have an attached " + "environment." + ) return EvalContext(node.environment) return ctx class Node(with_metaclass(NodeType, object)): - """Baseclass for all Jinja2 nodes. There are a number of nodes available + """Baseclass for all Jinja nodes. There are a number of nodes available of different types. There are four major types: - :class:`Stmt`: statements @@ -121,30 +107,32 @@ class Node(with_metaclass(NodeType, object)): The `environment` attribute is set at the end of the parsing process for all nodes automatically. """ + fields = () - attributes = ('lineno', 'environment') + attributes = ("lineno", "environment") abstract = True def __init__(self, *fields, **attributes): if self.abstract: - raise TypeError('abstract nodes are not instanciable') + raise TypeError("abstract nodes are not instantiable") if fields: if len(fields) != len(self.fields): if not self.fields: - raise TypeError('%r takes 0 arguments' % - self.__class__.__name__) - raise TypeError('%r takes 0 or %d argument%s' % ( - self.__class__.__name__, - len(self.fields), - len(self.fields) != 1 and 's' or '' - )) + raise TypeError("%r takes 0 arguments" % self.__class__.__name__) + raise TypeError( + "%r takes 0 or %d argument%s" + % ( + self.__class__.__name__, + len(self.fields), + len(self.fields) != 1 and "s" or "", + ) + ) for name, arg in izip(self.fields, fields): setattr(self, name, arg) for attr in self.attributes: setattr(self, attr, attributes.pop(attr, None)) if attributes: - raise TypeError('unknown attribute %r' % - next(iter(attributes))) + raise TypeError("unknown attribute %r" % next(iter(attributes))) def iter_fields(self, exclude=None, only=None): """This method iterates over all fields that are defined and yields @@ -154,9 +142,11 @@ class Node(with_metaclass(NodeType, object)): should be sets or tuples of field names. """ for name in self.fields: - if (exclude is only is None) or \ - (exclude is not None and name not in exclude) or \ - (only is not None and name in only): + if ( + (exclude is only is None) + or (exclude is not None and name not in exclude) + or (only is not None and name in only) + ): try: yield name, getattr(self, name) except AttributeError: @@ -167,7 +157,7 @@ class Node(with_metaclass(NodeType, object)): over all fields and yields the values of they are nodes. If the value of a field is a list all the nodes in that list are returned. """ - for field, item in self.iter_fields(exclude, only): + for _, item in self.iter_fields(exclude, only): if isinstance(item, list): for n in item: if isinstance(n, Node): @@ -201,7 +191,7 @@ class Node(with_metaclass(NodeType, object)): todo = deque([self]) while todo: node = todo.popleft() - if 'ctx' in node.fields: + if "ctx" in node.fields: node.ctx = ctx todo.extend(node.iter_child_nodes()) return self @@ -211,7 +201,7 @@ class Node(with_metaclass(NodeType, object)): todo = deque([self]) while todo: node = todo.popleft() - if 'lineno' in node.attributes: + if "lineno" in node.attributes: if node.lineno is None or override: node.lineno = lineno todo.extend(node.iter_child_nodes()) @@ -227,8 +217,9 @@ class Node(with_metaclass(NodeType, object)): return self def __eq__(self, other): - return type(self) is type(other) and \ - tuple(self.iter_fields()) == tuple(other.iter_fields()) + return type(self) is type(other) and tuple(self.iter_fields()) == tuple( + other.iter_fields() + ) def __ne__(self, other): return not self.__eq__(other) @@ -237,10 +228,9 @@ class Node(with_metaclass(NodeType, object)): __hash__ = object.__hash__ def __repr__(self): - return '%s(%s)' % ( + return "%s(%s)" % ( self.__class__.__name__, - ', '.join('%s=%r' % (arg, getattr(self, arg, None)) for - arg in self.fields) + ", ".join("%s=%r" % (arg, getattr(self, arg, None)) for arg in self.fields), ) def dump(self): @@ -249,37 +239,39 @@ class Node(with_metaclass(NodeType, object)): buf.append(repr(node)) return - buf.append('nodes.%s(' % node.__class__.__name__) + buf.append("nodes.%s(" % node.__class__.__name__) if not node.fields: - buf.append(')') + buf.append(")") return for idx, field in enumerate(node.fields): if idx: - buf.append(', ') + buf.append(", ") value = getattr(node, field) if isinstance(value, list): - buf.append('[') + buf.append("[") for idx, item in enumerate(value): if idx: - buf.append(', ') + buf.append(", ") _dump(item) - buf.append(']') + buf.append("]") else: _dump(value) - buf.append(')') + buf.append(")") + buf = [] _dump(self) - return ''.join(buf) - + return "".join(buf) class Stmt(Node): """Base node for all statements.""" + abstract = True class Helper(Node): """Nodes that exist in a specific context only.""" + abstract = True @@ -287,19 +279,22 @@ class Template(Node): """Node that represents a template. This must be the outermost node that is passed to the compiler. """ - fields = ('body',) + + fields = ("body",) class Output(Stmt): """A node that holds multiple expressions which are then printed out. This is used both for the `print` statement and the regular template data. """ - fields = ('nodes',) + + fields = ("nodes",) class Extends(Stmt): """Represents an extends statement.""" - fields = ('template',) + + fields = ("template",) class For(Stmt): @@ -310,12 +305,14 @@ class For(Stmt): For filtered nodes an expression can be stored as `test`, otherwise `None`. """ - fields = ('target', 'iter', 'body', 'else_', 'test', 'recursive') + + fields = ("target", "iter", "body", "else_", "test", "recursive") class If(Stmt): """If `test` is true, `body` is rendered, else `else_`.""" - fields = ('test', 'body', 'elif_', 'else_') + + fields = ("test", "body", "elif_", "else_") class Macro(Stmt): @@ -323,19 +320,22 @@ class Macro(Stmt): arguments and `defaults` a list of defaults if there are any. `body` is a list of nodes for the macro body. """ - fields = ('name', 'args', 'defaults', 'body') + + fields = ("name", "args", "defaults", "body") class CallBlock(Stmt): """Like a macro without a name but a call instead. `call` is called with the unnamed macro as `caller` argument this node holds. """ - fields = ('call', 'args', 'defaults', 'body') + + fields = ("call", "args", "defaults", "body") class FilterBlock(Stmt): """Node for filter sections.""" - fields = ('body', 'filter') + + fields = ("body", "filter") class With(Stmt): @@ -344,22 +344,26 @@ class With(Stmt): .. versionadded:: 2.9.3 """ - fields = ('targets', 'values', 'body') + + fields = ("targets", "values", "body") class Block(Stmt): """A node that represents a block.""" - fields = ('name', 'body', 'scoped') + + fields = ("name", "body", "scoped") class Include(Stmt): """A node that represents the include tag.""" - fields = ('template', 'with_context', 'ignore_missing') + + fields = ("template", "with_context", "ignore_missing") class Import(Stmt): """A node that represents the import tag.""" - fields = ('template', 'target', 'with_context') + + fields = ("template", "target", "with_context") class FromImport(Stmt): @@ -373,26 +377,31 @@ class FromImport(Stmt): The list of names may contain tuples if aliases are wanted. """ - fields = ('template', 'names', 'with_context') + + fields = ("template", "names", "with_context") class ExprStmt(Stmt): """A statement that evaluates an expression and discards the result.""" - fields = ('node',) + + fields = ("node",) class Assign(Stmt): """Assigns an expression to a target.""" - fields = ('target', 'node') + + fields = ("target", "node") class AssignBlock(Stmt): """Assigns a block to a target.""" - fields = ('target', 'filter', 'body') + + fields = ("target", "filter", "body") class Expr(Node): """Baseclass for all expressions.""" + abstract = True def as_const(self, eval_ctx=None): @@ -415,15 +424,18 @@ class Expr(Node): class BinExpr(Expr): """Baseclass for all binary expressions.""" - fields = ('left', 'right') + + fields = ("left", "right") operator = None abstract = True def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) # intercepted operators cannot be folded at compile time - if self.environment.sandboxed and \ - self.operator in self.environment.intercepted_binops: + if ( + self.environment.sandboxed + and self.operator in self.environment.intercepted_binops + ): raise Impossible() f = _binop_to_func[self.operator] try: @@ -434,15 +446,18 @@ class BinExpr(Expr): class UnaryExpr(Expr): """Baseclass for all unary expressions.""" - fields = ('node',) + + fields = ("node",) operator = None abstract = True def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) # intercepted operators cannot be folded at compile time - if self.environment.sandboxed and \ - self.operator in self.environment.intercepted_unops: + if ( + self.environment.sandboxed + and self.operator in self.environment.intercepted_unops + ): raise Impossible() f = _uaop_to_func[self.operator] try: @@ -459,16 +474,17 @@ class Name(Expr): - `load`: load that name - `param`: like `store` but if the name was defined as function parameter. """ - fields = ('name', 'ctx') + + fields = ("name", "ctx") def can_assign(self): - return self.name not in ('true', 'false', 'none', - 'True', 'False', 'None') + return self.name not in ("true", "false", "none", "True", "False", "None") class NSRef(Expr): """Reference to a namespace value assignment""" - fields = ('name', 'attr') + + fields = ("name", "attr") def can_assign(self): # We don't need any special checks here; NSRef assignments have a @@ -480,6 +496,7 @@ class NSRef(Expr): class Literal(Expr): """Baseclass for literals.""" + abstract = True @@ -489,14 +506,18 @@ class Const(Literal): complex values such as lists too. Only constants with a safe representation (objects where ``eval(repr(x)) == x`` is true). """ - fields = ('value',) + + fields = ("value",) def as_const(self, eval_ctx=None): rv = self.value - if PY2 and type(rv) is text_type and \ - self.environment.policies['compiler.ascii_str']: + if ( + PY2 + and type(rv) is text_type + and self.environment.policies["compiler.ascii_str"] + ): try: - rv = rv.encode('ascii') + rv = rv.encode("ascii") except UnicodeError: pass return rv @@ -508,6 +529,7 @@ class Const(Literal): an `Impossible` exception. """ from .compiler import has_safe_repr + if not has_safe_repr(value): raise Impossible() return cls(value, lineno=lineno, environment=environment) @@ -515,7 +537,8 @@ class Const(Literal): class TemplateData(Literal): """A constant template string.""" - fields = ('data',) + + fields = ("data",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -531,7 +554,8 @@ class Tuple(Literal): for subscripts. Like for :class:`Name` `ctx` specifies if the tuple is used for loading the names or storing. """ - fields = ('items', 'ctx') + + fields = ("items", "ctx") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -546,7 +570,8 @@ class Tuple(Literal): class List(Literal): """Any list literal such as ``[1, 2, 3]``""" - fields = ('items',) + + fields = ("items",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -557,7 +582,8 @@ class Dict(Literal): """Any dict literal such as ``{1: 2, 3: 4}``. The items must be a list of :class:`Pair` nodes. """ - fields = ('items',) + + fields = ("items",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -566,7 +592,8 @@ class Dict(Literal): class Pair(Helper): """A key, value pair for dicts.""" - fields = ('key', 'value') + + fields = ("key", "value") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -575,7 +602,8 @@ class Pair(Helper): class Keyword(Helper): """A key, value pair for keyword arguments where key is a string.""" - fields = ('key', 'value') + + fields = ("key", "value") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -586,7 +614,8 @@ class CondExpr(Expr): """A conditional expression (inline if expression). (``{{ foo if bar else baz }}``) """ - fields = ('test', 'expr1', 'expr2') + + fields = ("test", "expr1", "expr2") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -627,7 +656,7 @@ class Filter(Expr): filtered. Buffers are created by macros and filter blocks. """ - fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') + fields = ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -637,28 +666,27 @@ class Filter(Expr): # we have to be careful here because we call filter_ below. # if this variable would be called filter, 2to3 would wrap the - # call in a list beause it is assuming we are talking about the + # call in a list because it is assuming we are talking about the # builtin filter function here which no longer returns a list in # python 3. because of that, do not rename filter_ to filter! filter_ = self.environment.filters.get(self.name) - if filter_ is None or getattr(filter_, 'contextfilter', False): + if filter_ is None or getattr(filter_, "contextfilter", False) is True: raise Impossible() # We cannot constant handle async filters, so we need to make sure # to not go down this path. - if ( - eval_ctx.environment.is_async - and getattr(filter_, 'asyncfiltervariant', False) + if eval_ctx.environment.is_async and getattr( + filter_, "asyncfiltervariant", False ): raise Impossible() args, kwargs = args_as_const(self, eval_ctx) args.insert(0, self.node.as_const(eval_ctx)) - if getattr(filter_, 'evalcontextfilter', False): + if getattr(filter_, "evalcontextfilter", False) is True: args.insert(0, eval_ctx) - elif getattr(filter_, 'environmentfilter', False): + elif getattr(filter_, "environmentfilter", False) is True: args.insert(0, self.environment) try: @@ -672,7 +700,7 @@ class Test(Expr): rest of the fields are the same as for :class:`Call`. """ - fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') + fields = ("node", "name", "args", "kwargs", "dyn_args", "dyn_kwargs") def as_const(self, eval_ctx=None): test = self.environment.tests.get(self.name) @@ -697,20 +725,23 @@ class Call(Expr): node for dynamic positional (``*args``) or keyword (``**kwargs``) arguments. """ - fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') + + fields = ("node", "args", "kwargs", "dyn_args", "dyn_kwargs") class Getitem(Expr): """Get an attribute or item from an expression and prefer the item.""" - fields = ('node', 'arg', 'ctx') + + fields = ("node", "arg", "ctx") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) - if self.ctx != 'load': + if self.ctx != "load": raise Impossible() try: - return self.environment.getitem(self.node.as_const(eval_ctx), - self.arg.as_const(eval_ctx)) + return self.environment.getitem( + self.node.as_const(eval_ctx), self.arg.as_const(eval_ctx) + ) except Exception: raise Impossible() @@ -722,15 +753,15 @@ class Getattr(Expr): """Get an attribute or item from an expression that is a ascii-only bytestring and prefer the attribute. """ - fields = ('node', 'attr', 'ctx') + + fields = ("node", "attr", "ctx") def as_const(self, eval_ctx=None): - if self.ctx != 'load': + if self.ctx != "load": raise Impossible() try: eval_ctx = get_eval_context(self, eval_ctx) - return self.environment.getattr(self.node.as_const(eval_ctx), - self.attr) + return self.environment.getattr(self.node.as_const(eval_ctx), self.attr) except Exception: raise Impossible() @@ -742,14 +773,17 @@ class Slice(Expr): """Represents a slice object. This must only be used as argument for :class:`Subscript`. """ - fields = ('start', 'stop', 'step') + + fields = ("start", "stop", "step") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) + def const(obj): if obj is None: return None return obj.as_const(eval_ctx) + return slice(const(self.start), const(self.stop), const(self.step)) @@ -757,82 +791,103 @@ class Concat(Expr): """Concatenates the list of expressions provided after converting them to unicode. """ - fields = ('nodes',) + + fields = ("nodes",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) - return ''.join(text_type(x.as_const(eval_ctx)) for x in self.nodes) + return "".join(text_type(x.as_const(eval_ctx)) for x in self.nodes) class Compare(Expr): """Compares an expression with some other expressions. `ops` must be a list of :class:`Operand`\\s. """ - fields = ('expr', 'ops') + + fields = ("expr", "ops") def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) result = value = self.expr.as_const(eval_ctx) + try: for op in self.ops: new_value = op.expr.as_const(eval_ctx) result = _cmpop_to_func[op.op](value, new_value) + + if not result: + return False + value = new_value except Exception: raise Impossible() + return result class Operand(Helper): """Holds an operator and an expression.""" - fields = ('op', 'expr') + + fields = ("op", "expr") + if __debug__: - Operand.__doc__ += '\nThe following operators are available: ' + \ - ', '.join(sorted('``%s``' % x for x in set(_binop_to_func) | - set(_uaop_to_func) | set(_cmpop_to_func))) + Operand.__doc__ += "\nThe following operators are available: " + ", ".join( + sorted( + "``%s``" % x + for x in set(_binop_to_func) | set(_uaop_to_func) | set(_cmpop_to_func) + ) + ) class Mul(BinExpr): """Multiplies the left with the right node.""" - operator = '*' + + operator = "*" class Div(BinExpr): """Divides the left by the right node.""" - operator = '/' + + operator = "/" class FloorDiv(BinExpr): """Divides the left by the right node and truncates conver the result into an integer by truncating. """ - operator = '//' + + operator = "//" class Add(BinExpr): """Add the left to the right node.""" - operator = '+' + + operator = "+" class Sub(BinExpr): """Subtract the right from the left node.""" - operator = '-' + + operator = "-" class Mod(BinExpr): """Left modulo right.""" - operator = '%' + + operator = "%" class Pow(BinExpr): """Left to the power of right.""" - operator = '**' + + operator = "**" class And(BinExpr): """Short circuited AND.""" - operator = 'and' + + operator = "and" def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -841,7 +896,8 @@ class And(BinExpr): class Or(BinExpr): """Short circuited OR.""" - operator = 'or' + + operator = "or" def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -850,17 +906,20 @@ class Or(BinExpr): class Not(UnaryExpr): """Negate the expression.""" - operator = 'not' + + operator = "not" class Neg(UnaryExpr): """Make the expression negative.""" - operator = '-' + + operator = "-" class Pos(UnaryExpr): """Make the expression positive (noop for most expressions)""" - operator = '+' + + operator = "+" # Helpers for extensions @@ -870,7 +929,8 @@ class EnvironmentAttribute(Expr): """Loads an attribute from the environment object. This is useful for extensions that want to call a callback stored on the environment. """ - fields = ('name',) + + fields = ("name",) class ExtensionAttribute(Expr): @@ -880,7 +940,8 @@ class ExtensionAttribute(Expr): This node is usually constructed by calling the :meth:`~jinja2.ext.Extension.attr` method on an extension. """ - fields = ('identifier', 'name') + + fields = ("identifier", "name") class ImportedName(Expr): @@ -889,7 +950,8 @@ class ImportedName(Expr): function from the cgi module on evaluation. Imports are optimized by the compiler so there is no need to assign them to local variables. """ - fields = ('importname',) + + fields = ("importname",) class InternalName(Expr): @@ -899,16 +961,20 @@ class InternalName(Expr): a new identifier for you. This identifier is not available from the template and is not threated specially by the compiler. """ - fields = ('name',) + + fields = ("name",) def __init__(self): - raise TypeError('Can\'t create internal names. Use the ' - '`free_identifier` method on a parser.') + raise TypeError( + "Can't create internal names. Use the " + "`free_identifier` method on a parser." + ) class MarkSafe(Expr): """Mark the wrapped expression as safe (wrap it as `Markup`).""" - fields = ('expr',) + + fields = ("expr",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -921,7 +987,8 @@ class MarkSafeIfAutoescape(Expr): .. versionadded:: 2.5 """ - fields = ('expr',) + + fields = ("expr",) def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) @@ -943,6 +1010,20 @@ class ContextReference(Expr): Assign(Name('foo', ctx='store'), Getattr(ContextReference(), 'name')) + + This is basically equivalent to using the + :func:`~jinja2.contextfunction` decorator when using the + high-level API, which causes a reference to the context to be passed + as the first argument to a function. + """ + + +class DerivedContextReference(Expr): + """Return the current template context including locals. Behaves + exactly like :class:`ContextReference`, but includes local + variables, such as from a ``for`` loop. + + .. versionadded:: 2.11 """ @@ -956,7 +1037,8 @@ class Break(Stmt): class Scope(Stmt): """An artificial scope.""" - fields = ('body',) + + fields = ("body",) class OverlayScope(Stmt): @@ -972,7 +1054,8 @@ class OverlayScope(Stmt): .. versionadded:: 2.10 """ - fields = ('context', 'body') + + fields = ("context", "body") class EvalContextModifier(Stmt): @@ -983,7 +1066,8 @@ class EvalContextModifier(Stmt): EvalContextModifier(options=[Keyword('autoescape', Const(True))]) """ - fields = ('options',) + + fields = ("options",) class ScopedEvalContextModifier(EvalContextModifier): @@ -991,10 +1075,14 @@ class ScopedEvalContextModifier(EvalContextModifier): :class:`EvalContextModifier` but will only modify the :class:`~jinja2.nodes.EvalContext` for nodes in the :attr:`body`. """ - fields = ('body',) + + fields = ("body",) # make sure nobody creates custom nodes def _failing_new(*args, **kwargs): - raise TypeError('can\'t create custom node types') -NodeType.__new__ = staticmethod(_failing_new); del _failing_new + raise TypeError("can't create custom node types") + + +NodeType.__new__ = staticmethod(_failing_new) +del _failing_new diff --git a/src/jinja2/optimizer.py b/src/jinja2/optimizer.py new file mode 100644 index 0000000..7bc78c4 --- /dev/null +++ b/src/jinja2/optimizer.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""The optimizer tries to constant fold expressions and modify the AST +in place so that it should be faster to evaluate. + +Because the AST does not contain all the scoping information and the +compiler has to find that out, we cannot do all the optimizations we +want. For example, loop unrolling doesn't work because unrolled loops +would have a different scope. The solution would be a second syntax tree +that stored the scoping rules. +""" +from . import nodes +from .visitor import NodeTransformer + + +def optimize(node, environment): + """The context hint can be used to perform an static optimization + based on the context given.""" + optimizer = Optimizer(environment) + return optimizer.visit(node) + + +class Optimizer(NodeTransformer): + def __init__(self, environment): + self.environment = environment + + def generic_visit(self, node, *args, **kwargs): + node = super(Optimizer, self).generic_visit(node, *args, **kwargs) + + # Do constant folding. Some other nodes besides Expr have + # as_const, but folding them causes errors later on. + if isinstance(node, nodes.Expr): + try: + return nodes.Const.from_untrusted( + node.as_const(args[0] if args else None), + lineno=node.lineno, + environment=self.environment, + ) + except nodes.Impossible: + pass + + return node diff --git a/jinja2/parser.py b/src/jinja2/parser.py index ed00d97..d588106 100644 --- a/jinja2/parser.py +++ b/src/jinja2/parser.py @@ -1,41 +1,46 @@ # -*- coding: utf-8 -*- -""" - jinja2.parser - ~~~~~~~~~~~~~ - - Implements the template parser. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -from jinja2 import nodes -from jinja2.exceptions import TemplateSyntaxError, TemplateAssertionError -from jinja2.lexer import describe_token, describe_token_expr -from jinja2._compat import imap - - -_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print', - 'macro', 'include', 'from', 'import', - 'set', 'with', 'autoescape']) -_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq']) +"""Parse tokens from the lexer into nodes for the compiler.""" +from . import nodes +from ._compat import imap +from .exceptions import TemplateAssertionError +from .exceptions import TemplateSyntaxError +from .lexer import describe_token +from .lexer import describe_token_expr + +_statement_keywords = frozenset( + [ + "for", + "if", + "block", + "extends", + "print", + "macro", + "include", + "from", + "import", + "set", + "with", + "autoescape", + ] +) +_compare_operators = frozenset(["eq", "ne", "lt", "lteq", "gt", "gteq"]) _math_nodes = { - 'add': nodes.Add, - 'sub': nodes.Sub, - 'mul': nodes.Mul, - 'div': nodes.Div, - 'floordiv': nodes.FloorDiv, - 'mod': nodes.Mod, + "add": nodes.Add, + "sub": nodes.Sub, + "mul": nodes.Mul, + "div": nodes.Div, + "floordiv": nodes.FloorDiv, + "mod": nodes.Mod, } class Parser(object): - """This is the central parsing class Jinja2 uses. It's passed to + """This is the central parsing class Jinja uses. It's passed to extensions and can be used to parse expressions or statements. """ - def __init__(self, environment, source, name=None, filename=None, - state=None): + def __init__(self, environment, source, name=None, filename=None, state=None): self.environment = environment self.stream = environment._tokenize(source, name, filename, state) self.name = name @@ -63,31 +68,37 @@ class Parser(object): for exprs in end_token_stack: expected.extend(imap(describe_token_expr, exprs)) if end_token_stack: - currently_looking = ' or '.join( - "'%s'" % describe_token_expr(expr) - for expr in end_token_stack[-1]) + currently_looking = " or ".join( + "'%s'" % describe_token_expr(expr) for expr in end_token_stack[-1] + ) else: currently_looking = None if name is None: - message = ['Unexpected end of template.'] + message = ["Unexpected end of template."] else: - message = ['Encountered unknown tag \'%s\'.' % name] + message = ["Encountered unknown tag '%s'." % name] if currently_looking: if name is not None and name in expected: - message.append('You probably made a nesting mistake. Jinja ' - 'is expecting this tag, but currently looking ' - 'for %s.' % currently_looking) + message.append( + "You probably made a nesting mistake. Jinja " + "is expecting this tag, but currently looking " + "for %s." % currently_looking + ) else: - message.append('Jinja was looking for the following tags: ' - '%s.' % currently_looking) + message.append( + "Jinja was looking for the following tags: " + "%s." % currently_looking + ) if self._tag_stack: - message.append('The innermost block that needs to be ' - 'closed is \'%s\'.' % self._tag_stack[-1]) + message.append( + "The innermost block that needs to be " + "closed is '%s'." % self._tag_stack[-1] + ) - self.fail(' '.join(message), lineno) + self.fail(" ".join(message), lineno) def fail_unknown_tag(self, name, lineno=None): """Called if the parser encounters an unknown tag. Tries to fail @@ -105,7 +116,7 @@ class Parser(object): def is_tuple_end(self, extra_end_rules=None): """Are we at the end of a tuple?""" - if self.stream.current.type in ('variable_end', 'block_end', 'rparen'): + if self.stream.current.type in ("variable_end", "block_end", "rparen"): return True elif extra_end_rules is not None: return self.stream.current.test_any(extra_end_rules) @@ -115,22 +126,22 @@ class Parser(object): """Return a new free identifier as :class:`~jinja2.nodes.InternalName`.""" self._last_identifier += 1 rv = object.__new__(nodes.InternalName) - nodes.Node.__init__(rv, 'fi%d' % self._last_identifier, lineno=lineno) + nodes.Node.__init__(rv, "fi%d" % self._last_identifier, lineno=lineno) return rv def parse_statement(self): """Parse a single statement.""" token = self.stream.current - if token.type != 'name': - self.fail('tag name expected', token.lineno) + if token.type != "name": + self.fail("tag name expected", token.lineno) self._tag_stack.append(token.value) pop_tag = True try: if token.value in _statement_keywords: - return getattr(self, 'parse_' + self.stream.current.value)() - if token.value == 'call': + return getattr(self, "parse_" + self.stream.current.value)() + if token.value == "call": return self.parse_call_block() - if token.value == 'filter': + if token.value == "filter": return self.parse_filter_block() ext = self.extensions.get(token.value) if ext is not None: @@ -157,16 +168,16 @@ class Parser(object): can be set to `True` and the end token is removed. """ # the first token may be a colon for python compatibility - self.stream.skip_if('colon') + self.stream.skip_if("colon") # in the future it would be possible to add whole code sections # by adding some sort of end of statement token and parsing those here. - self.stream.expect('block_end') + self.stream.expect("block_end") result = self.subparse(end_tokens) # we reached the end of the template too early, the subparser # does not check for this, so we do that now - if self.stream.current.type == 'eof': + if self.stream.current.type == "eof": self.fail_eof(end_tokens) if drop_needle: @@ -177,50 +188,47 @@ class Parser(object): """Parse an assign statement.""" lineno = next(self.stream).lineno target = self.parse_assign_target(with_namespace=True) - if self.stream.skip_if('assign'): + if self.stream.skip_if("assign"): expr = self.parse_tuple() return nodes.Assign(target, expr, lineno=lineno) filter_node = self.parse_filter(None) - body = self.parse_statements(('name:endset',), - drop_needle=True) + body = self.parse_statements(("name:endset",), drop_needle=True) return nodes.AssignBlock(target, filter_node, body, lineno=lineno) def parse_for(self): """Parse a for loop.""" - lineno = self.stream.expect('name:for').lineno - target = self.parse_assign_target(extra_end_rules=('name:in',)) - self.stream.expect('name:in') - iter = self.parse_tuple(with_condexpr=False, - extra_end_rules=('name:recursive',)) + lineno = self.stream.expect("name:for").lineno + target = self.parse_assign_target(extra_end_rules=("name:in",)) + self.stream.expect("name:in") + iter = self.parse_tuple( + with_condexpr=False, extra_end_rules=("name:recursive",) + ) test = None - if self.stream.skip_if('name:if'): + if self.stream.skip_if("name:if"): test = self.parse_expression() - recursive = self.stream.skip_if('name:recursive') - body = self.parse_statements(('name:endfor', 'name:else')) - if next(self.stream).value == 'endfor': + recursive = self.stream.skip_if("name:recursive") + body = self.parse_statements(("name:endfor", "name:else")) + if next(self.stream).value == "endfor": else_ = [] else: - else_ = self.parse_statements(('name:endfor',), drop_needle=True) - return nodes.For(target, iter, body, else_, test, - recursive, lineno=lineno) + else_ = self.parse_statements(("name:endfor",), drop_needle=True) + return nodes.For(target, iter, body, else_, test, recursive, lineno=lineno) def parse_if(self): """Parse an if construct.""" - node = result = nodes.If(lineno=self.stream.expect('name:if').lineno) + node = result = nodes.If(lineno=self.stream.expect("name:if").lineno) while 1: node.test = self.parse_tuple(with_condexpr=False) - node.body = self.parse_statements(('name:elif', 'name:else', - 'name:endif')) + node.body = self.parse_statements(("name:elif", "name:else", "name:endif")) node.elif_ = [] node.else_ = [] token = next(self.stream) - if token.test('name:elif'): + if token.test("name:elif"): node = nodes.If(lineno=self.stream.current.lineno) result.elif_.append(node) continue - elif token.test('name:else'): - result.else_ = self.parse_statements(('name:endif',), - drop_needle=True) + elif token.test("name:else"): + result.else_ = self.parse_statements(("name:endif",), drop_needle=True) break return result @@ -228,45 +236,42 @@ class Parser(object): node = nodes.With(lineno=next(self.stream).lineno) targets = [] values = [] - while self.stream.current.type != 'block_end': - lineno = self.stream.current.lineno + while self.stream.current.type != "block_end": if targets: - self.stream.expect('comma') + self.stream.expect("comma") target = self.parse_assign_target() - target.set_ctx('param') + target.set_ctx("param") targets.append(target) - self.stream.expect('assign') + self.stream.expect("assign") values.append(self.parse_expression()) node.targets = targets node.values = values - node.body = self.parse_statements(('name:endwith',), - drop_needle=True) + node.body = self.parse_statements(("name:endwith",), drop_needle=True) return node def parse_autoescape(self): node = nodes.ScopedEvalContextModifier(lineno=next(self.stream).lineno) - node.options = [ - nodes.Keyword('autoescape', self.parse_expression()) - ] - node.body = self.parse_statements(('name:endautoescape',), - drop_needle=True) + node.options = [nodes.Keyword("autoescape", self.parse_expression())] + node.body = self.parse_statements(("name:endautoescape",), drop_needle=True) return nodes.Scope([node]) def parse_block(self): node = nodes.Block(lineno=next(self.stream).lineno) - node.name = self.stream.expect('name').value - node.scoped = self.stream.skip_if('name:scoped') + node.name = self.stream.expect("name").value + node.scoped = self.stream.skip_if("name:scoped") # common problem people encounter when switching from django # to jinja. we do not support hyphens in block names, so let's # raise a nicer error message in that case. - if self.stream.current.type == 'sub': - self.fail('Block names in Jinja have to be valid Python ' - 'identifiers and may not contain hyphens, use an ' - 'underscore instead.') - - node.body = self.parse_statements(('name:endblock',), drop_needle=True) - self.stream.skip_if('name:' + node.name) + if self.stream.current.type == "sub": + self.fail( + "Block names in Jinja have to be valid Python " + "identifiers and may not contain hyphens, use an " + "underscore instead." + ) + + node.body = self.parse_statements(("name:endblock",), drop_needle=True) + self.stream.skip_if("name:" + node.name) return node def parse_extends(self): @@ -275,9 +280,10 @@ class Parser(object): return node def parse_import_context(self, node, default): - if self.stream.current.test_any('name:with', 'name:without') and \ - self.stream.look().test('name:context'): - node.with_context = next(self.stream).value == 'with' + if self.stream.current.test_any( + "name:with", "name:without" + ) and self.stream.look().test("name:context"): + node.with_context = next(self.stream).value == "with" self.stream.skip() else: node.with_context = default @@ -286,8 +292,9 @@ class Parser(object): def parse_include(self): node = nodes.Include(lineno=next(self.stream).lineno) node.template = self.parse_expression() - if self.stream.current.test('name:ignore') and \ - self.stream.look().test('name:missing'): + if self.stream.current.test("name:ignore") and self.stream.look().test( + "name:missing" + ): node.ignore_missing = True self.stream.skip(2) else: @@ -297,67 +304,71 @@ class Parser(object): def parse_import(self): node = nodes.Import(lineno=next(self.stream).lineno) node.template = self.parse_expression() - self.stream.expect('name:as') + self.stream.expect("name:as") node.target = self.parse_assign_target(name_only=True).name return self.parse_import_context(node, False) def parse_from(self): node = nodes.FromImport(lineno=next(self.stream).lineno) node.template = self.parse_expression() - self.stream.expect('name:import') + self.stream.expect("name:import") node.names = [] def parse_context(): - if self.stream.current.value in ('with', 'without') and \ - self.stream.look().test('name:context'): - node.with_context = next(self.stream).value == 'with' + 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 return False while 1: if node.names: - self.stream.expect('comma') - if self.stream.current.type == 'name': + self.stream.expect("comma") + if self.stream.current.type == "name": if parse_context(): break target = self.parse_assign_target(name_only=True) - if target.name.startswith('_'): - self.fail('names starting with an underline can not ' - 'be imported', target.lineno, - exc=TemplateAssertionError) - if self.stream.skip_if('name:as'): + if target.name.startswith("_"): + self.fail( + "names starting with an underline can not be imported", + target.lineno, + exc=TemplateAssertionError, + ) + if self.stream.skip_if("name:as"): alias = self.parse_assign_target(name_only=True) node.names.append((target.name, alias.name)) else: node.names.append(target.name) - if parse_context() or self.stream.current.type != 'comma': + if parse_context() or self.stream.current.type != "comma": break else: - self.stream.expect('name') - if not hasattr(node, 'with_context'): + self.stream.expect("name") + if not hasattr(node, "with_context"): node.with_context = False return node def parse_signature(self, node): node.args = args = [] node.defaults = defaults = [] - self.stream.expect('lparen') - while self.stream.current.type != 'rparen': + self.stream.expect("lparen") + while self.stream.current.type != "rparen": if args: - self.stream.expect('comma') + self.stream.expect("comma") arg = self.parse_assign_target(name_only=True) - arg.set_ctx('param') - if self.stream.skip_if('assign'): + arg.set_ctx("param") + if self.stream.skip_if("assign"): defaults.append(self.parse_expression()) elif defaults: - self.fail('non-default argument follows default argument') + self.fail("non-default argument follows default argument") args.append(arg) - self.stream.expect('rparen') + self.stream.expect("rparen") def parse_call_block(self): node = nodes.CallBlock(lineno=next(self.stream).lineno) - if self.stream.current.type == 'lparen': + if self.stream.current.type == "lparen": self.parse_signature(node) else: node.args = [] @@ -365,37 +376,40 @@ class Parser(object): node.call = self.parse_expression() if not isinstance(node.call, nodes.Call): - self.fail('expected call', node.lineno) - node.body = self.parse_statements(('name:endcall',), drop_needle=True) + self.fail("expected call", node.lineno) + node.body = self.parse_statements(("name:endcall",), drop_needle=True) return node def parse_filter_block(self): node = nodes.FilterBlock(lineno=next(self.stream).lineno) node.filter = self.parse_filter(None, start_inline=True) - node.body = self.parse_statements(('name:endfilter',), - drop_needle=True) + node.body = self.parse_statements(("name:endfilter",), drop_needle=True) return node def parse_macro(self): node = nodes.Macro(lineno=next(self.stream).lineno) node.name = self.parse_assign_target(name_only=True).name self.parse_signature(node) - node.body = self.parse_statements(('name:endmacro',), - drop_needle=True) + node.body = self.parse_statements(("name:endmacro",), drop_needle=True) return node def parse_print(self): node = nodes.Output(lineno=next(self.stream).lineno) node.nodes = [] - while self.stream.current.type != 'block_end': + while self.stream.current.type != "block_end": if node.nodes: - self.stream.expect('comma') + self.stream.expect("comma") node.nodes.append(self.parse_expression()) return node - def parse_assign_target(self, with_tuple=True, name_only=False, - extra_end_rules=None, with_namespace=False): - """Parse an assignment target. As Jinja2 allows assignments to + def parse_assign_target( + self, + with_tuple=True, + name_only=False, + extra_end_rules=None, + with_namespace=False, + ): + """Parse an assignment target. As Jinja allows assignments to tuples, this function can parse all allowed assignment targets. Per default assignments to tuples are parsed, that can be disable however by setting `with_tuple` to `False`. If only assignments to names are @@ -403,24 +417,26 @@ class Parser(object): parameter is forwarded to the tuple parsing function. If `with_namespace` is enabled, a namespace assignment may be parsed. """ - if with_namespace and self.stream.look().type == 'dot': - token = self.stream.expect('name') + if with_namespace and self.stream.look().type == "dot": + token = self.stream.expect("name") next(self.stream) # dot - attr = self.stream.expect('name') + attr = self.stream.expect("name") target = nodes.NSRef(token.value, attr.value, lineno=token.lineno) elif name_only: - token = self.stream.expect('name') - target = nodes.Name(token.value, 'store', lineno=token.lineno) + token = self.stream.expect("name") + target = nodes.Name(token.value, "store", lineno=token.lineno) else: if with_tuple: - target = self.parse_tuple(simplified=True, - extra_end_rules=extra_end_rules) + target = self.parse_tuple( + simplified=True, extra_end_rules=extra_end_rules + ) else: target = self.parse_primary() - target.set_ctx('store') + target.set_ctx("store") if not target.can_assign(): - self.fail('can\'t assign to %r' % target.__class__. - __name__.lower(), target.lineno) + self.fail( + "can't assign to %r" % target.__class__.__name__.lower(), target.lineno + ) return target def parse_expression(self, with_condexpr=True): @@ -435,9 +451,9 @@ class Parser(object): def parse_condexpr(self): lineno = self.stream.current.lineno expr1 = self.parse_or() - while self.stream.skip_if('name:if'): + while self.stream.skip_if("name:if"): expr2 = self.parse_or() - if self.stream.skip_if('name:else'): + if self.stream.skip_if("name:else"): expr3 = self.parse_condexpr() else: expr3 = None @@ -448,7 +464,7 @@ class Parser(object): def parse_or(self): lineno = self.stream.current.lineno left = self.parse_and() - while self.stream.skip_if('name:or'): + while self.stream.skip_if("name:or"): right = self.parse_and() left = nodes.Or(left, right, lineno=lineno) lineno = self.stream.current.lineno @@ -457,14 +473,14 @@ class Parser(object): def parse_and(self): lineno = self.stream.current.lineno left = self.parse_not() - while self.stream.skip_if('name:and'): + while self.stream.skip_if("name:and"): right = self.parse_not() left = nodes.And(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_not(self): - if self.stream.current.test('name:not'): + if self.stream.current.test("name:not"): lineno = next(self.stream).lineno return nodes.Not(self.parse_not(), lineno=lineno) return self.parse_compare() @@ -478,12 +494,13 @@ class Parser(object): if token_type in _compare_operators: next(self.stream) ops.append(nodes.Operand(token_type, self.parse_math1())) - elif self.stream.skip_if('name:in'): - ops.append(nodes.Operand('in', self.parse_math1())) - elif (self.stream.current.test('name:not') and - self.stream.look().test('name:in')): + elif self.stream.skip_if("name:in"): + ops.append(nodes.Operand("in", self.parse_math1())) + elif self.stream.current.test("name:not") and self.stream.look().test( + "name:in" + ): self.stream.skip(2) - ops.append(nodes.Operand('notin', self.parse_math1())) + ops.append(nodes.Operand("notin", self.parse_math1())) else: break lineno = self.stream.current.lineno @@ -494,7 +511,7 @@ class Parser(object): def parse_math1(self): lineno = self.stream.current.lineno left = self.parse_concat() - while self.stream.current.type in ('add', 'sub'): + while self.stream.current.type in ("add", "sub"): cls = _math_nodes[self.stream.current.type] next(self.stream) right = self.parse_concat() @@ -505,7 +522,7 @@ class Parser(object): def parse_concat(self): lineno = self.stream.current.lineno args = [self.parse_math2()] - while self.stream.current.type == 'tilde': + while self.stream.current.type == "tilde": next(self.stream) args.append(self.parse_math2()) if len(args) == 1: @@ -515,7 +532,7 @@ class Parser(object): def parse_math2(self): lineno = self.stream.current.lineno left = self.parse_pow() - while self.stream.current.type in ('mul', 'div', 'floordiv', 'mod'): + while self.stream.current.type in ("mul", "div", "floordiv", "mod"): cls = _math_nodes[self.stream.current.type] next(self.stream) right = self.parse_pow() @@ -526,7 +543,7 @@ class Parser(object): def parse_pow(self): lineno = self.stream.current.lineno left = self.parse_unary() - while self.stream.current.type == 'pow': + while self.stream.current.type == "pow": next(self.stream) right = self.parse_unary() left = nodes.Pow(left, right, lineno=lineno) @@ -536,10 +553,10 @@ class Parser(object): def parse_unary(self, with_filter=True): token_type = self.stream.current.type lineno = self.stream.current.lineno - if token_type == 'sub': + if token_type == "sub": next(self.stream) node = nodes.Neg(self.parse_unary(False), lineno=lineno) - elif token_type == 'add': + elif token_type == "add": next(self.stream) node = nodes.Pos(self.parse_unary(False), lineno=lineno) else: @@ -551,40 +568,44 @@ class Parser(object): def parse_primary(self): token = self.stream.current - if token.type == 'name': - if token.value in ('true', 'false', 'True', 'False'): - node = nodes.Const(token.value in ('true', 'True'), - lineno=token.lineno) - elif token.value in ('none', 'None'): + if token.type == "name": + if token.value in ("true", "false", "True", "False"): + node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno) + elif token.value in ("none", "None"): node = nodes.Const(None, lineno=token.lineno) else: - node = nodes.Name(token.value, 'load', lineno=token.lineno) + node = nodes.Name(token.value, "load", lineno=token.lineno) next(self.stream) - elif token.type == 'string': + elif token.type == "string": next(self.stream) buf = [token.value] lineno = token.lineno - while self.stream.current.type == 'string': + while self.stream.current.type == "string": buf.append(self.stream.current.value) next(self.stream) - node = nodes.Const(''.join(buf), lineno=lineno) - elif token.type in ('integer', 'float'): + node = nodes.Const("".join(buf), lineno=lineno) + elif token.type in ("integer", "float"): next(self.stream) node = nodes.Const(token.value, lineno=token.lineno) - elif token.type == 'lparen': + elif token.type == "lparen": next(self.stream) node = self.parse_tuple(explicit_parentheses=True) - self.stream.expect('rparen') - elif token.type == 'lbracket': + self.stream.expect("rparen") + elif token.type == "lbracket": node = self.parse_list() - elif token.type == 'lbrace': + elif token.type == "lbrace": node = self.parse_dict() else: self.fail("unexpected '%s'" % describe_token(token), token.lineno) return node - def parse_tuple(self, simplified=False, with_condexpr=True, - extra_end_rules=None, explicit_parentheses=False): + def parse_tuple( + self, + simplified=False, + with_condexpr=True, + extra_end_rules=None, + explicit_parentheses=False, + ): """Works like `parse_expression` but if multiple expressions are delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created. This method could also return a regular expression instead of a tuple @@ -609,16 +630,19 @@ class Parser(object): elif with_condexpr: parse = self.parse_expression else: - parse = lambda: self.parse_expression(with_condexpr=False) + + def parse(): + return self.parse_expression(with_condexpr=False) + args = [] is_tuple = False while 1: if args: - self.stream.expect('comma') + self.stream.expect("comma") if self.is_tuple_end(extra_end_rules): break args.append(parse()) - if self.stream.current.type == 'comma': + if self.stream.current.type == "comma": is_tuple = True else: break @@ -633,46 +657,48 @@ class Parser(object): # nothing) in the spot of an expression would be an empty # tuple. if not explicit_parentheses: - self.fail('Expected an expression, got \'%s\'' % - describe_token(self.stream.current)) + self.fail( + "Expected an expression, got '%s'" + % describe_token(self.stream.current) + ) - return nodes.Tuple(args, 'load', lineno=lineno) + return nodes.Tuple(args, "load", lineno=lineno) def parse_list(self): - token = self.stream.expect('lbracket') + token = self.stream.expect("lbracket") items = [] - while self.stream.current.type != 'rbracket': + while self.stream.current.type != "rbracket": if items: - self.stream.expect('comma') - if self.stream.current.type == 'rbracket': + self.stream.expect("comma") + if self.stream.current.type == "rbracket": break items.append(self.parse_expression()) - self.stream.expect('rbracket') + self.stream.expect("rbracket") return nodes.List(items, lineno=token.lineno) def parse_dict(self): - token = self.stream.expect('lbrace') + token = self.stream.expect("lbrace") items = [] - while self.stream.current.type != 'rbrace': + while self.stream.current.type != "rbrace": if items: - self.stream.expect('comma') - if self.stream.current.type == 'rbrace': + self.stream.expect("comma") + if self.stream.current.type == "rbrace": break key = self.parse_expression() - self.stream.expect('colon') + self.stream.expect("colon") value = self.parse_expression() items.append(nodes.Pair(key, value, lineno=key.lineno)) - self.stream.expect('rbrace') + self.stream.expect("rbrace") return nodes.Dict(items, lineno=token.lineno) def parse_postfix(self, node): while 1: token_type = self.stream.current.type - if token_type == 'dot' or token_type == 'lbracket': + if token_type == "dot" or token_type == "lbracket": node = self.parse_subscript(node) # calls are valid both after postfix expressions (getattr # and getitem) as well as filters and tests - elif token_type == 'lparen': + elif token_type == "lparen": node = self.parse_call(node) else: break @@ -681,13 +707,13 @@ class Parser(object): def parse_filter_expr(self, node): while 1: token_type = self.stream.current.type - if token_type == 'pipe': + if token_type == "pipe": node = self.parse_filter(node) - elif token_type == 'name' and self.stream.current.value == 'is': + elif token_type == "name" and self.stream.current.value == "is": node = self.parse_test(node) # calls are valid both after postfix expressions (getattr # and getitem) as well as filters and tests - elif token_type == 'lparen': + elif token_type == "lparen": node = self.parse_call(node) else: break @@ -695,53 +721,54 @@ class Parser(object): def parse_subscript(self, node): token = next(self.stream) - if token.type == 'dot': + if token.type == "dot": attr_token = self.stream.current next(self.stream) - if attr_token.type == 'name': - return nodes.Getattr(node, attr_token.value, 'load', - lineno=token.lineno) - elif attr_token.type != 'integer': - self.fail('expected name or number', attr_token.lineno) + if attr_token.type == "name": + return nodes.Getattr( + node, attr_token.value, "load", lineno=token.lineno + ) + elif attr_token.type != "integer": + self.fail("expected name or number", attr_token.lineno) arg = nodes.Const(attr_token.value, lineno=attr_token.lineno) - return nodes.Getitem(node, arg, 'load', lineno=token.lineno) - if token.type == 'lbracket': + return nodes.Getitem(node, arg, "load", lineno=token.lineno) + if token.type == "lbracket": args = [] - while self.stream.current.type != 'rbracket': + while self.stream.current.type != "rbracket": if args: - self.stream.expect('comma') + self.stream.expect("comma") args.append(self.parse_subscribed()) - self.stream.expect('rbracket') + self.stream.expect("rbracket") if len(args) == 1: arg = args[0] else: - arg = nodes.Tuple(args, 'load', lineno=token.lineno) - return nodes.Getitem(node, arg, 'load', lineno=token.lineno) - self.fail('expected subscript expression', self.lineno) + arg = nodes.Tuple(args, "load", lineno=token.lineno) + return nodes.Getitem(node, arg, "load", lineno=token.lineno) + self.fail("expected subscript expression", token.lineno) def parse_subscribed(self): lineno = self.stream.current.lineno - if self.stream.current.type == 'colon': + if self.stream.current.type == "colon": next(self.stream) args = [None] else: node = self.parse_expression() - if self.stream.current.type != 'colon': + if self.stream.current.type != "colon": return node next(self.stream) args = [node] - if self.stream.current.type == 'colon': + if self.stream.current.type == "colon": args.append(None) - elif self.stream.current.type not in ('rbracket', 'comma'): + elif self.stream.current.type not in ("rbracket", "comma"): args.append(self.parse_expression()) else: args.append(None) - if self.stream.current.type == 'colon': + if self.stream.current.type == "colon": next(self.stream) - if self.stream.current.type not in ('rbracket', 'comma'): + if self.stream.current.type not in ("rbracket", "comma"): args.append(self.parse_expression()) else: args.append(None) @@ -751,7 +778,7 @@ class Parser(object): return nodes.Slice(lineno=lineno, *args) def parse_call(self, node): - token = self.stream.expect('lparen') + token = self.stream.expect("lparen") args = [] kwargs = [] dyn_args = dyn_kwargs = None @@ -759,91 +786,100 @@ class Parser(object): def ensure(expr): if not expr: - self.fail('invalid syntax for function call expression', - token.lineno) + self.fail("invalid syntax for function call expression", token.lineno) - while self.stream.current.type != 'rparen': + while self.stream.current.type != "rparen": if require_comma: - self.stream.expect('comma') + self.stream.expect("comma") # support for trailing comma - if self.stream.current.type == 'rparen': + if self.stream.current.type == "rparen": break - if self.stream.current.type == 'mul': + if self.stream.current.type == "mul": ensure(dyn_args is None and dyn_kwargs is None) next(self.stream) dyn_args = self.parse_expression() - elif self.stream.current.type == 'pow': + elif self.stream.current.type == "pow": ensure(dyn_kwargs is None) next(self.stream) dyn_kwargs = self.parse_expression() else: - ensure(dyn_args is None and dyn_kwargs is None) - if self.stream.current.type == 'name' and \ - self.stream.look().type == 'assign': + if ( + self.stream.current.type == "name" + and self.stream.look().type == "assign" + ): + # Parsing a kwarg + ensure(dyn_kwargs is None) key = self.stream.current.value self.stream.skip(2) value = self.parse_expression() - kwargs.append(nodes.Keyword(key, value, - lineno=value.lineno)) + kwargs.append(nodes.Keyword(key, value, lineno=value.lineno)) else: - ensure(not kwargs) + # Parsing an arg + ensure(dyn_args is None and dyn_kwargs is None and not kwargs) args.append(self.parse_expression()) require_comma = True - self.stream.expect('rparen') + self.stream.expect("rparen") if node is None: return args, kwargs, dyn_args, dyn_kwargs - return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, - lineno=token.lineno) + return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno) def parse_filter(self, node, start_inline=False): - while self.stream.current.type == 'pipe' or start_inline: + while self.stream.current.type == "pipe" or start_inline: if not start_inline: next(self.stream) - token = self.stream.expect('name') + token = self.stream.expect("name") name = token.value - while self.stream.current.type == 'dot': + while self.stream.current.type == "dot": next(self.stream) - name += '.' + self.stream.expect('name').value - if self.stream.current.type == 'lparen': + name += "." + self.stream.expect("name").value + if self.stream.current.type == "lparen": args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) else: args = [] kwargs = [] dyn_args = dyn_kwargs = None - node = nodes.Filter(node, name, args, kwargs, dyn_args, - dyn_kwargs, lineno=token.lineno) + node = nodes.Filter( + node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno + ) start_inline = False return node def parse_test(self, node): token = next(self.stream) - if self.stream.current.test('name:not'): + if self.stream.current.test("name:not"): next(self.stream) negated = True else: negated = False - name = self.stream.expect('name').value - while self.stream.current.type == 'dot': + name = self.stream.expect("name").value + while self.stream.current.type == "dot": next(self.stream) - name += '.' + self.stream.expect('name').value + name += "." + self.stream.expect("name").value dyn_args = dyn_kwargs = None kwargs = [] - if self.stream.current.type == 'lparen': + if self.stream.current.type == "lparen": args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) - 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') - args = [self.parse_primary()] + 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() + arg_node = self.parse_postfix(arg_node) + args = [arg_node] else: args = [] - node = nodes.Test(node, name, args, kwargs, dyn_args, - dyn_kwargs, lineno=token.lineno) + node = nodes.Test( + node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno + ) if negated: node = nodes.Not(node, lineno=token.lineno) return node @@ -865,29 +901,29 @@ class Parser(object): try: while self.stream: token = self.stream.current - if token.type == 'data': + if token.type == "data": if token.value: - add_data(nodes.TemplateData(token.value, - lineno=token.lineno)) + add_data(nodes.TemplateData(token.value, lineno=token.lineno)) next(self.stream) - elif token.type == 'variable_begin': + elif token.type == "variable_begin": next(self.stream) add_data(self.parse_tuple(with_condexpr=True)) - self.stream.expect('variable_end') - elif token.type == 'block_begin': + self.stream.expect("variable_end") + elif token.type == "block_begin": flush_data() next(self.stream) - if end_tokens is not None and \ - self.stream.current.test_any(*end_tokens): + if end_tokens is not None and self.stream.current.test_any( + *end_tokens + ): return body rv = self.parse_statement() if isinstance(rv, list): body.extend(rv) else: body.append(rv) - self.stream.expect('block_end') + self.stream.expect("block_end") else: - raise AssertionError('internal parsing error') + raise AssertionError("internal parsing error") flush_data() finally: diff --git a/jinja2/runtime.py b/src/jinja2/runtime.py index c925b71..3ad7968 100644 --- a/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -1,45 +1,62 @@ # -*- coding: utf-8 -*- -""" - jinja2.runtime - ~~~~~~~~~~~~~~ - - Runtime helpers. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. -""" +"""The runtime functions and state used by compiled templates.""" import sys - from itertools import chain from types import MethodType -from markupsafe import Markup, escape, soft_unicode - -from jinja2.nodes import EvalContext, _context_function_types -from jinja2.utils import missing, concat, \ - internalcode, object_type_repr, evalcontextfunction, Namespace -from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ - TemplateNotFound -from jinja2._compat import imap, text_type, iteritems, \ - implements_iterator, implements_to_string, string_types, PY2, \ - with_metaclass, abc - +from markupsafe import escape # noqa: F401 +from markupsafe import Markup +from markupsafe import soft_unicode + +from ._compat import abc +from ._compat import imap +from ._compat import implements_iterator +from ._compat import implements_to_string +from ._compat import iteritems +from ._compat import PY2 +from ._compat import string_types +from ._compat import text_type +from ._compat import with_metaclass +from .exceptions import TemplateNotFound # noqa: F401 +from .exceptions import TemplateRuntimeError # noqa: F401 +from .exceptions import UndefinedError +from .nodes import EvalContext +from .utils import concat +from .utils import evalcontextfunction +from .utils import internalcode +from .utils import missing +from .utils import Namespace # noqa: F401 +from .utils import object_type_repr # these variables are exported to the template runtime -__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup', - 'TemplateRuntimeError', 'missing', 'concat', 'escape', - 'markup_join', 'unicode_join', 'to_string', 'identity', - 'TemplateNotFound', 'Namespace'] +exported = [ + "LoopContext", + "TemplateReference", + "Macro", + "Markup", + "TemplateRuntimeError", + "missing", + "concat", + "escape", + "markup_join", + "unicode_join", + "to_string", + "identity", + "TemplateNotFound", + "Namespace", + "Undefined", +] #: the name of the function that is used to convert something into #: a string. We can just use the text type here. to_string = text_type -#: the identity function. Useful for certain things in the environment -identity = lambda x: x -_first_iteration = object() -_last_iteration = object() +def identity(x): + """Returns its argument. Useful for certain things in the + environment. + """ + return x def markup_join(seq): @@ -48,8 +65,8 @@ def markup_join(seq): iterator = imap(soft_unicode, seq) for arg in iterator: buf.append(arg) - if hasattr(arg, '__html__'): - return Markup(u'').join(chain(buf, iterator)) + if hasattr(arg, "__html__"): + return Markup(u"").join(chain(buf, iterator)) return concat(buf) @@ -58,9 +75,16 @@ def unicode_join(seq): return concat(imap(text_type, seq)) -def new_context(environment, template_name, blocks, vars=None, - shared=None, globals=None, locals=None): - """Internal helper to for context creation.""" +def new_context( + environment, + template_name, + blocks, + vars=None, + shared=None, + globals=None, + locals=None, +): + """Internal helper for context creation.""" if vars is None: vars = {} if shared: @@ -75,8 +99,7 @@ def new_context(environment, template_name, blocks, vars=None, for key, value in iteritems(locals): if value is not missing: parent[key] = value - return environment.context_class(environment, parent, template_name, - blocks) + return environment.context_class(environment, parent, template_name, blocks) class TemplateReference(object): @@ -90,20 +113,16 @@ class TemplateReference(object): return BlockReference(name, self.__context, blocks, 0) def __repr__(self): - return '<%s %r>' % ( - self.__class__.__name__, - self.__context.name - ) + return "<%s %r>" % (self.__class__.__name__, self.__context.name) def _get_func(x): - return getattr(x, '__func__', x) + return getattr(x, "__func__", x) class ContextMeta(type): - - def __new__(cls, name, bases, d): - rv = type.__new__(cls, name, bases, d) + def __new__(mcs, name, bases, d): + rv = type.__new__(mcs, name, bases, d) if bases == (): return rv @@ -114,11 +133,15 @@ class ContextMeta(type): # If we have a changed resolve but no changed default or missing # resolve we invert the call logic. - if resolve is not default_resolve and \ - resolve_or_missing is default_resolve_or_missing: + if ( + resolve is not default_resolve + and resolve_or_missing is default_resolve_or_missing + ): rv._legacy_resolve_mode = True - elif resolve is default_resolve and \ - resolve_or_missing is default_resolve_or_missing: + elif ( + resolve is default_resolve + and resolve_or_missing is default_resolve_or_missing + ): rv._fast_resolve_mode = True return rv @@ -151,6 +174,7 @@ class Context(with_metaclass(ContextMeta)): method that doesn't fail with a `KeyError` but returns an :class:`Undefined` object for missing variables. """ + # XXX: we want to eventually make this be a deprecation warning and # remove it. _legacy_resolve_mode = False @@ -181,9 +205,9 @@ class Context(with_metaclass(ContextMeta)): index = blocks.index(current) + 1 blocks[index] except LookupError: - return self.environment.undefined('there is no parent block ' - 'called %r.' % name, - name='super') + return self.environment.undefined( + "there is no parent block called %r." % name, name="super" + ) return BlockReference(name, self, blocks, index) def get(self, key, default=None): @@ -234,7 +258,7 @@ class Context(with_metaclass(ContextMeta)): return dict(self.parent, **self.vars) @internalcode - def call(__self, __obj, *args, **kwargs): + def call(__self, __obj, *args, **kwargs): # noqa: B902 """Call the callable with the arguments and keyword arguments provided but inject the active context or environment as first argument if the callable is a :func:`contextfunction` or @@ -244,55 +268,62 @@ class Context(with_metaclass(ContextMeta)): __traceback_hide__ = True # noqa # Allow callable classes to take a context - if hasattr(__obj, '__call__'): + if hasattr(__obj, "__call__"): # noqa: B004 fn = __obj.__call__ - for fn_type in ('contextfunction', - 'evalcontextfunction', - 'environmentfunction'): + for fn_type in ( + "contextfunction", + "evalcontextfunction", + "environmentfunction", + ): if hasattr(fn, fn_type): __obj = fn break - if isinstance(__obj, _context_function_types): - if getattr(__obj, 'contextfunction', 0): + if callable(__obj): + if getattr(__obj, "contextfunction", False) is True: args = (__self,) + args - elif getattr(__obj, 'evalcontextfunction', 0): + elif getattr(__obj, "evalcontextfunction", False) is True: args = (__self.eval_ctx,) + args - elif getattr(__obj, 'environmentfunction', 0): + elif getattr(__obj, "environmentfunction", False) is True: args = (__self.environment,) + args try: return __obj(*args, **kwargs) except StopIteration: - return __self.environment.undefined('value was undefined because ' - 'a callable raised a ' - 'StopIteration exception') + return __self.environment.undefined( + "value was undefined because " + "a callable raised a " + "StopIteration exception" + ) def derived(self, locals=None): """Internal helper function to create a derived context. This is used in situations where the system needs a new context in the same template that is independent. """ - context = new_context(self.environment, self.name, {}, - self.get_all(), True, None, locals) + context = new_context( + self.environment, self.name, {}, self.get_all(), True, None, locals + ) context.eval_ctx = self.eval_ctx context.blocks.update((k, list(v)) for k, v in iteritems(self.blocks)) return context - def _all(meth): - proxy = lambda self: getattr(self.get_all(), meth)() + def _all(meth): # noqa: B902 + def proxy(self): + return getattr(self.get_all(), meth)() + proxy.__doc__ = getattr(dict, meth).__doc__ proxy.__name__ = meth return proxy - keys = _all('keys') - values = _all('values') - items = _all('items') + keys = _all("keys") + values = _all("values") + items = _all("items") # not available on python 3 if PY2: - iterkeys = _all('iterkeys') - itervalues = _all('itervalues') - iteritems = _all('iteritems') + iterkeys = _all("iterkeys") + itervalues = _all("itervalues") + iteritems = _all("iteritems") del _all def __contains__(self, name): @@ -308,10 +339,10 @@ class Context(with_metaclass(ContextMeta)): return item def __repr__(self): - return '<%s %s of %r>' % ( + return "<%s %s of %r>" % ( self.__class__.__name__, repr(self.get_all()), - self.name + self.name, ) @@ -331,11 +362,10 @@ class BlockReference(object): def super(self): """Super the block.""" if self._depth + 1 >= len(self._stack): - return self._context.environment. \ - undefined('there is no parent block called %r.' % - self.name, name='super') - return BlockReference(self.name, self._context, self._stack, - self._depth + 1) + return self._context.environment.undefined( + "there is no parent block called %r." % self.name, name="super" + ) + return BlockReference(self.name, self._context, self._stack, self._depth + 1) @internalcode def __call__(self): @@ -345,143 +375,212 @@ class BlockReference(object): return rv -class LoopContextBase(object): - """A loop context for dynamic iteration.""" +@implements_iterator +class LoopContext: + """A wrapper iterable for dynamic ``for`` loops, with information + about the loop and iteration. + """ + + #: Current iteration of the loop, starting at 0. + index0 = -1 - _before = _first_iteration - _current = _first_iteration - _after = _last_iteration _length = None + _after = missing + _current = missing + _before = missing + _last_changed_value = missing - def __init__(self, undefined, recurse=None, depth0=0): + def __init__(self, iterable, undefined, recurse=None, depth0=0): + """ + :param iterable: Iterable to wrap. + :param undefined: :class:`Undefined` class to use for next and + previous items. + :param recurse: The function to render the loop body when the + loop is marked recursive. + :param depth0: Incremented when looping recursively. + """ + self._iterable = iterable + self._iterator = self._to_iterator(iterable) self._undefined = undefined self._recurse = recurse - self.index0 = -1 + #: How many levels deep a recursive loop currently is, starting at 0. self.depth0 = depth0 - self._last_checked_value = missing - def cycle(self, *args): - """Cycles among the arguments with the current loop index.""" - if not args: - raise TypeError('no items for cycling given') - return args[self.index0 % len(args)] + @staticmethod + def _to_iterator(iterable): + return iter(iterable) - def changed(self, *value): - """Checks whether the value has changed since the last call.""" - if self._last_checked_value != value: - self._last_checked_value = value - return True - return False + @property + def length(self): + """Length of the iterable. - first = property(lambda x: x.index0 == 0) - last = property(lambda x: x._after is _last_iteration) - index = property(lambda x: x.index0 + 1) - revindex = property(lambda x: x.length - x.index0) - revindex0 = property(lambda x: x.length - x.index) - depth = property(lambda x: x.depth0 + 1) + If the iterable is a generator or otherwise does not have a + size, it is eagerly evaluated to get a size. + """ + if self._length is not None: + return self._length - @property - def previtem(self): - if self._before is _first_iteration: - return self._undefined('there is no previous item') - return self._before + try: + self._length = len(self._iterable) + except TypeError: + iterable = list(self._iterator) + self._iterator = self._to_iterator(iterable) + self._length = len(iterable) + self.index + (self._after is not missing) - @property - def nextitem(self): - if self._after is _last_iteration: - return self._undefined('there is no next item') - return self._after + return self._length def __len__(self): return self.length - @internalcode - def loop(self, iterable): - if self._recurse is None: - raise TypeError('Tried to call non recursive loop. Maybe you ' - "forgot the 'recursive' modifier.") - return self._recurse(iterable, self._recurse, self.depth0 + 1) + @property + def depth(self): + """How many levels deep a recursive loop currently is, starting at 1.""" + return self.depth0 + 1 - # a nifty trick to enhance the error message if someone tried to call - # the the loop without or with too many arguments. - __call__ = loop - del loop + @property + def index(self): + """Current iteration of the loop, starting at 1.""" + return self.index0 + 1 - def __repr__(self): - return '<%s %r/%r>' % ( - self.__class__.__name__, - self.index, - self.length - ) + @property + def revindex0(self): + """Number of iterations from the end of the loop, ending at 0. + Requires calculating :attr:`length`. + """ + return self.length - self.index -class LoopContext(LoopContextBase): + @property + def revindex(self): + """Number of iterations from the end of the loop, ending at 1. - def __init__(self, iterable, undefined, recurse=None, depth0=0): - LoopContextBase.__init__(self, undefined, recurse, depth0) - self._iterator = iter(iterable) + Requires calculating :attr:`length`. + """ + return self.length - self.index0 - # try to get the length of the iterable early. This must be done - # here because there are some broken iterators around where there - # __len__ is the number of iterations left (i'm looking at your - # listreverseiterator!). - try: - self._length = len(iterable) - except (TypeError, AttributeError): - self._length = None - self._after = self._safe_next() + @property + def first(self): + """Whether this is the first iteration of the loop.""" + return self.index0 == 0 + + def _peek_next(self): + """Return the next element in the iterable, or :data:`missing` + if the iterable is exhausted. Only peeks one item ahead, caching + the result in :attr:`_last` for use in subsequent checks. The + cache is reset when :meth:`__next__` is called. + """ + if self._after is not missing: + return self._after + + self._after = next(self._iterator, missing) + return self._after @property - def length(self): - if self._length is None: - # if was not possible to get the length of the iterator when - # the loop context was created (ie: iterating over a generator) - # we have to convert the iterable into a sequence and use the - # length of that + the number of iterations so far. - iterable = tuple(self._iterator) - self._iterator = iter(iterable) - iterations_done = self.index0 + 2 - self._length = len(iterable) + iterations_done - return self._length + def last(self): + """Whether this is the last iteration of the loop. - def __iter__(self): - return LoopContextIterator(self) + Causes the iterable to advance early. See + :func:`itertools.groupby` for issues this can cause. + The :func:`groupby` filter avoids that issue. + """ + return self._peek_next() is missing - def _safe_next(self): - try: - return next(self._iterator) - except StopIteration: - return _last_iteration + @property + def previtem(self): + """The item in the previous iteration. Undefined during the + first iteration. + """ + if self.first: + return self._undefined("there is no previous item") + return self._before -@implements_iterator -class LoopContextIterator(object): - """The iterator for a loop context.""" - __slots__ = ('context',) + @property + def nextitem(self): + """The item in the next iteration. Undefined during the last + iteration. - def __init__(self, context): - self.context = context + Causes the iterable to advance early. See + :func:`itertools.groupby` for issues this can cause. + The :func:`groupby` filter avoids that issue. + """ + rv = self._peek_next() + + if rv is missing: + return self._undefined("there is no next item") + + return rv + + def cycle(self, *args): + """Return a value from the given args, cycling through based on + the current :attr:`index0`. + + :param args: One or more values to cycle through. + """ + if not args: + raise TypeError("no items for cycling given") + + return args[self.index0 % len(args)] + + def changed(self, *value): + """Return ``True`` if previously called with a different value + (including when called for the first time). + + :param value: One or more values to compare to the last call. + """ + if self._last_changed_value != value: + self._last_changed_value = value + return True + + return False def __iter__(self): return self def __next__(self): - ctx = self.context - ctx.index0 += 1 - if ctx._after is _last_iteration: - raise StopIteration() - ctx._before = ctx._current - ctx._current = ctx._after - ctx._after = ctx._safe_next() - return ctx._current, ctx + if self._after is not missing: + rv = self._after + self._after = missing + else: + rv = next(self._iterator) + + self.index0 += 1 + self._before = self._current + self._current = rv + return rv, self + + @internalcode + def __call__(self, iterable): + """When iterating over nested data, render the body of the loop + recursively with the given inner iterable data. + + The loop must have the ``recursive`` marker for this to work. + """ + if self._recurse is None: + raise TypeError( + "The loop must have the 'recursive' marker to be called recursively." + ) + + return self._recurse(iterable, self._recurse, depth=self.depth) + + def __repr__(self): + return "<%s %d/%d>" % (self.__class__.__name__, self.index, self.length) class Macro(object): """Wraps a macro function.""" - def __init__(self, environment, func, name, arguments, - catch_kwargs, catch_varargs, caller, - default_autoescape=None): + def __init__( + self, + environment, + func, + name, + arguments, + catch_kwargs, + catch_varargs, + caller, + default_autoescape=None, + ): self._environment = environment self._func = func self._argument_count = len(arguments) @@ -490,7 +589,7 @@ class Macro(object): self.catch_kwargs = catch_kwargs self.catch_varargs = catch_varargs self.caller = caller - self.explicit_caller = 'caller' in arguments + self.explicit_caller = "caller" in arguments if default_autoescape is None: default_autoescape = environment.autoescape self._default_autoescape = default_autoescape @@ -502,9 +601,8 @@ class Macro(object): # decide largely based on compile-time information if a macro is # safe or unsafe. While there was a volatile mode it was largely # unused for deciding on escaping. This turns out to be - # problemtic for macros because if a macro is safe or not not so - # much depends on the escape mode when it was defined but when it - # was used. + # problematic for macros because whether a macro is safe depends not + # on the escape mode when it was defined, but rather when it was used. # # Because however we export macros from the module system and # there are historic callers that do not pass an eval context (and @@ -512,7 +610,7 @@ class Macro(object): # check here. # # This is considered safe because an eval context is not a valid - # argument to callables otherwise anwyays. Worst case here is + # argument to callables otherwise anyway. Worst case here is # that if no eval context is passed we fall back to the compile # time autoescape flag. if args and isinstance(args[0], EvalContext): @@ -522,7 +620,7 @@ class Macro(object): autoescape = self._default_autoescape # try to consume the positional arguments - arguments = list(args[:self._argument_count]) + arguments = list(args[: self._argument_count]) off = len(arguments) # For information why this is necessary refer to the handling @@ -533,12 +631,12 @@ class Macro(object): # arguments expected we start filling in keyword arguments # and defaults. if off != self._argument_count: - for idx, name in enumerate(self.arguments[len(arguments):]): + for name in self.arguments[len(arguments) :]: try: value = kwargs.pop(name) except KeyError: value = missing - if name == 'caller': + if name == "caller": found_caller = True arguments.append(value) else: @@ -548,26 +646,31 @@ class Macro(object): # if not also changed in the compiler's `function_scoping` method. # the order is caller, keyword arguments, positional arguments! if self.caller and not found_caller: - caller = kwargs.pop('caller', None) + caller = kwargs.pop("caller", None) if caller is None: - caller = self._environment.undefined('No caller defined', - name='caller') + caller = self._environment.undefined("No caller defined", name="caller") arguments.append(caller) if self.catch_kwargs: arguments.append(kwargs) elif kwargs: - if 'caller' in kwargs: - raise TypeError('macro %r was invoked with two values for ' - 'the special caller argument. This is ' - 'most likely a bug.' % self.name) - raise TypeError('macro %r takes no keyword argument %r' % - (self.name, next(iter(kwargs)))) + if "caller" in kwargs: + raise TypeError( + "macro %r was invoked with two values for " + "the special caller argument. This is " + "most likely a bug." % self.name + ) + raise TypeError( + "macro %r takes no keyword argument %r" + % (self.name, next(iter(kwargs))) + ) if self.catch_varargs: - arguments.append(args[self._argument_count:]) + arguments.append(args[self._argument_count :]) elif len(args) > self._argument_count: - raise TypeError('macro %r takes not more than %d argument(s)' % - (self.name, len(self.arguments))) + raise TypeError( + "macro %r takes not more than %d argument(s)" + % (self.name, len(self.arguments)) + ) return self._invoke(arguments, autoescape) @@ -579,16 +682,16 @@ class Macro(object): return rv def __repr__(self): - return '<%s %s>' % ( + return "<%s %s>" % ( self.__class__.__name__, - self.name is None and 'anonymous' or repr(self.name) + self.name is None and "anonymous" or repr(self.name), ) @implements_to_string class Undefined(object): """The default undefined type. This undefined type can be printed and - iterated over, but every other access will raise an :exc:`jinja2.exceptions.UndefinedError`: + iterated over, but every other access will raise an :exc:`UndefinedError`: >>> foo = Undefined(name='foo') >>> str(foo) @@ -600,8 +703,13 @@ class Undefined(object): ... jinja2.exceptions.UndefinedError: 'foo' is undefined """ - __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name', - '_undefined_exception') + + __slots__ = ( + "_undefined_hint", + "_undefined_obj", + "_undefined_name", + "_undefined_exception", + ) def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError): self._undefined_hint = hint @@ -609,40 +717,86 @@ class Undefined(object): self._undefined_name = name self._undefined_exception = exc + @property + def _undefined_message(self): + """Build a message about the undefined value based on how it was + accessed. + """ + if self._undefined_hint: + return self._undefined_hint + + if self._undefined_obj is missing: + return "%r is undefined" % self._undefined_name + + if not isinstance(self._undefined_name, string_types): + return "%s has no element %r" % ( + object_type_repr(self._undefined_obj), + self._undefined_name, + ) + + return "%r has no attribute %r" % ( + object_type_repr(self._undefined_obj), + self._undefined_name, + ) + @internalcode def _fail_with_undefined_error(self, *args, **kwargs): - """Regular callback function for undefined objects that raises an - `jinja2.exceptions.UndefinedError` on call. + """Raise an :exc:`UndefinedError` when operations are performed + on the undefined value. """ - if self._undefined_hint is None: - if self._undefined_obj is missing: - hint = '%r is undefined' % self._undefined_name - elif not isinstance(self._undefined_name, string_types): - hint = '%s has no element %r' % ( - object_type_repr(self._undefined_obj), - self._undefined_name - ) - else: - hint = '%r has no attribute %r' % ( - object_type_repr(self._undefined_obj), - self._undefined_name - ) - else: - hint = self._undefined_hint - raise self._undefined_exception(hint) + raise self._undefined_exception(self._undefined_message) @internalcode def __getattr__(self, name): - if name[:2] == '__': + if name[:2] == "__": raise AttributeError(name) return self._fail_with_undefined_error() - __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \ - __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \ - __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \ - __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = __int__ = \ - __float__ = __complex__ = __pow__ = __rpow__ = __sub__ = \ - __rsub__ = _fail_with_undefined_error + __add__ = ( + __radd__ + ) = ( + __mul__ + ) = ( + __rmul__ + ) = ( + __div__ + ) = ( + __rdiv__ + ) = ( + __truediv__ + ) = ( + __rtruediv__ + ) = ( + __floordiv__ + ) = ( + __rfloordiv__ + ) = ( + __mod__ + ) = ( + __rmod__ + ) = ( + __pos__ + ) = ( + __neg__ + ) = ( + __call__ + ) = ( + __getitem__ + ) = ( + __lt__ + ) = ( + __le__ + ) = ( + __gt__ + ) = ( + __ge__ + ) = ( + __int__ + ) = ( + __float__ + ) = ( + __complex__ + ) = __pow__ = __rpow__ = __sub__ = __rsub__ = _fail_with_undefined_error def __eq__(self, other): return type(self) is type(other) @@ -654,7 +808,7 @@ class Undefined(object): return id(type(self)) def __str__(self): - return u'' + return u"" def __len__(self): return 0 @@ -665,10 +819,11 @@ class Undefined(object): def __nonzero__(self): return False + __bool__ = __nonzero__ def __repr__(self): - return 'Undefined' + return "Undefined" def make_logging_undefined(logger=None, base=None): @@ -693,6 +848,7 @@ def make_logging_undefined(logger=None, base=None): """ if logger is None: import logging + logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler(sys.stderr)) if base is None: @@ -701,26 +857,27 @@ def make_logging_undefined(logger=None, base=None): def _log_message(undef): if undef._undefined_hint is None: if undef._undefined_obj is missing: - hint = '%s is undefined' % undef._undefined_name + hint = "%s is undefined" % undef._undefined_name elif not isinstance(undef._undefined_name, string_types): - hint = '%s has no element %s' % ( + hint = "%s has no element %s" % ( object_type_repr(undef._undefined_obj), - undef._undefined_name) + undef._undefined_name, + ) else: - hint = '%s has no attribute %s' % ( + hint = "%s has no attribute %s" % ( object_type_repr(undef._undefined_obj), - undef._undefined_name) + undef._undefined_name, + ) else: hint = undef._undefined_hint - logger.warning('Template variable warning: %s', hint) + logger.warning("Template variable warning: %s", hint) class LoggingUndefined(base): - def _fail_with_undefined_error(self, *args, **kwargs): try: return base._fail_with_undefined_error(self, *args, **kwargs) except self._undefined_exception as e: - logger.error('Template variable error: %s', str(e)) + logger.error("Template variable error: %s", str(e)) raise e def __str__(self): @@ -734,6 +891,7 @@ def make_logging_undefined(logger=None, base=None): return rv if PY2: + def __nonzero__(self): rv = base.__nonzero__(self) _log_message(self) @@ -743,7 +901,9 @@ def make_logging_undefined(logger=None, base=None): rv = base.__unicode__(self) _log_message(self) return rv + else: + def __bool__(self): rv = base.__bool__(self) _log_message(self) @@ -752,6 +912,36 @@ def make_logging_undefined(logger=None, base=None): return LoggingUndefined +# No @implements_to_string decorator here because __str__ +# is not overwritten from Undefined in this class. +# This would cause a recursion error in Python 2. +class ChainableUndefined(Undefined): + """An undefined that is chainable, where both ``__getattr__`` and + ``__getitem__`` return itself rather than raising an + :exc:`UndefinedError`. + + >>> foo = ChainableUndefined(name='foo') + >>> str(foo.bar['baz']) + '' + >>> foo.bar['baz'] + 42 + Traceback (most recent call last): + ... + jinja2.exceptions.UndefinedError: 'foo' is undefined + + .. versionadded:: 2.11.0 + """ + + __slots__ = () + + def __html__(self): + return self.__str__() + + def __getattr__(self, _): + return self + + __getitem__ = __getattr__ + + @implements_to_string class DebugUndefined(Undefined): """An undefined that returns the debug info when printed. @@ -766,17 +956,18 @@ class DebugUndefined(Undefined): ... jinja2.exceptions.UndefinedError: 'foo' is undefined """ + __slots__ = () def __str__(self): if self._undefined_hint is None: if self._undefined_obj is missing: - return u'{{ %s }}' % self._undefined_name - return '{{ no such element: %s[%r] }}' % ( + return u"{{ %s }}" % self._undefined_name + return "{{ no such element: %s[%r] }}" % ( object_type_repr(self._undefined_obj), - self._undefined_name + self._undefined_name, ) - return u'{{ undefined value printed: %s }}' % self._undefined_hint + return u"{{ undefined value printed: %s }}" % self._undefined_hint @implements_to_string @@ -799,12 +990,22 @@ class StrictUndefined(Undefined): ... jinja2.exceptions.UndefinedError: 'foo' is undefined """ + __slots__ = () - __iter__ = __str__ = __len__ = __nonzero__ = __eq__ = \ - __ne__ = __bool__ = __hash__ = \ - Undefined._fail_with_undefined_error + __iter__ = ( + __str__ + ) = ( + __len__ + ) = ( + __nonzero__ + ) = __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error # remove remaining slots attributes, after the metaclass did the magic they # are unneeded and irritating as they contain wrong data for the subclasses. -del Undefined.__slots__, DebugUndefined.__slots__, StrictUndefined.__slots__ +del ( + Undefined.__slots__, + ChainableUndefined.__slots__, + DebugUndefined.__slots__, + StrictUndefined.__slots__, +) diff --git a/jinja2/sandbox.py b/src/jinja2/sandbox.py index 88f83ae..cfd7993 100644 --- a/jinja2/sandbox.py +++ b/src/jinja2/sandbox.py @@ -1,70 +1,66 @@ # -*- coding: utf-8 -*- +"""A sandbox layer that ensures unsafe operations cannot be performed. +Useful when the template itself comes from an untrusted source. """ - jinja2.sandbox - ~~~~~~~~~~~~~~ - - Adds a sandbox layer to Jinja as it was the default behavior in the old - Jinja 1 releases. This sandbox is slightly different from Jinja 1 as the - default behavior is easier to use. - - The behavior can be changed by subclassing the environment. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. -""" -import types import operator +import types +import warnings +from collections import deque from string import Formatter -from markupsafe import Markup, EscapeFormatter - -from jinja2.environment import Environment -from jinja2.exceptions import SecurityError -from jinja2._compat import string_types, PY2, abc, range_type +from markupsafe import EscapeFormatter +from markupsafe import Markup +from ._compat import abc +from ._compat import PY2 +from ._compat import range_type +from ._compat import string_types +from .environment import Environment +from .exceptions import SecurityError #: maximum number of items a range may produce MAX_RANGE = 100000 #: attributes of function objects that are considered unsafe. if PY2: - UNSAFE_FUNCTION_ATTRIBUTES = set(['func_closure', 'func_code', 'func_dict', - 'func_defaults', 'func_globals']) + UNSAFE_FUNCTION_ATTRIBUTES = { + "func_closure", + "func_code", + "func_dict", + "func_defaults", + "func_globals", + } else: # On versions > python 2 the special attributes on functions are gone, # but they remain on methods and generators for whatever reason. UNSAFE_FUNCTION_ATTRIBUTES = set() - #: unsafe method attributes. function attributes are unsafe for methods too -UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self']) +UNSAFE_METHOD_ATTRIBUTES = {"im_class", "im_func", "im_self"} -#: unsafe generator attirbutes. -UNSAFE_GENERATOR_ATTRIBUTES = set(['gi_frame', 'gi_code']) +#: unsafe generator attributes. +UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"} #: unsafe attributes on coroutines -UNSAFE_COROUTINE_ATTRIBUTES = set(['cr_frame', 'cr_code']) +UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"} #: unsafe attributes on async generators -UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = set(['ag_code', 'ag_frame']) - -import warnings +UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"} # make sure we don't warn in python 2.6 about stuff we don't care about -warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning, - module='jinja2.sandbox') - -from collections import deque +warnings.filterwarnings( + "ignore", "the sets module", DeprecationWarning, module=__name__ +) _mutable_set_types = (set,) _mutable_mapping_types = (dict,) _mutable_sequence_types = (list,) - # on python 2.x we can register the user collection types try: from UserDict import UserDict, DictMixin from UserList import UserList + _mutable_mapping_types += (UserDict, DictMixin) _mutable_set_types += (UserList,) except ImportError: @@ -73,6 +69,7 @@ except ImportError: # if sets is still available, register the mutable set from there as well try: from sets import Set + _mutable_set_types += (Set,) except ImportError: pass @@ -82,22 +79,46 @@ _mutable_set_types += (abc.MutableSet,) _mutable_mapping_types += (abc.MutableMapping,) _mutable_sequence_types += (abc.MutableSequence,) - _mutable_spec = ( - (_mutable_set_types, frozenset([ - 'add', 'clear', 'difference_update', 'discard', 'pop', 'remove', - 'symmetric_difference_update', 'update' - ])), - (_mutable_mapping_types, frozenset([ - 'clear', 'pop', 'popitem', 'setdefault', 'update' - ])), - (_mutable_sequence_types, frozenset([ - 'append', 'reverse', 'insert', 'sort', 'extend', 'remove' - ])), - (deque, frozenset([ - 'append', 'appendleft', 'clear', 'extend', 'extendleft', 'pop', - 'popleft', 'remove', 'rotate' - ])) + ( + _mutable_set_types, + frozenset( + [ + "add", + "clear", + "difference_update", + "discard", + "pop", + "remove", + "symmetric_difference_update", + "update", + ] + ), + ), + ( + _mutable_mapping_types, + frozenset(["clear", "pop", "popitem", "setdefault", "update"]), + ), + ( + _mutable_sequence_types, + frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]), + ), + ( + deque, + frozenset( + [ + "append", + "appendleft", + "clear", + "extend", + "extendleft", + "pop", + "popleft", + "remove", + "rotate", + ] + ), + ), ) @@ -115,7 +136,7 @@ class _MagicFormatMapping(abc.Mapping): self._last_index = 0 def __getitem__(self, key): - if key == '': + if key == "": idx = self._last_index self._last_index += 1 try: @@ -133,9 +154,9 @@ class _MagicFormatMapping(abc.Mapping): def inspect_format_method(callable): - if not isinstance(callable, (types.MethodType, - types.BuiltinMethodType)) or \ - callable.__name__ not in ('format', 'format_map'): + if not isinstance( + callable, (types.MethodType, types.BuiltinMethodType) + ) or callable.__name__ not in ("format", "format_map"): return None obj = callable.__self__ if isinstance(obj, string_types): @@ -186,24 +207,25 @@ def is_internal_attribute(obj, attr): if attr in UNSAFE_FUNCTION_ATTRIBUTES: return True elif isinstance(obj, types.MethodType): - if attr in UNSAFE_FUNCTION_ATTRIBUTES or \ - attr in UNSAFE_METHOD_ATTRIBUTES: + if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES: return True elif isinstance(obj, type): - if attr == 'mro': + if attr == "mro": return True elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)): return True elif isinstance(obj, types.GeneratorType): if attr in UNSAFE_GENERATOR_ATTRIBUTES: return True - elif hasattr(types, 'CoroutineType') and isinstance(obj, types.CoroutineType): + elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType): if attr in UNSAFE_COROUTINE_ATTRIBUTES: return True - elif hasattr(types, 'AsyncGeneratorType') and isinstance(obj, types.AsyncGeneratorType): + elif hasattr(types, "AsyncGeneratorType") and isinstance( + obj, types.AsyncGeneratorType + ): if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES: return True - return attr.startswith('__') + return attr.startswith("__") def modifies_known_mutable(obj, attr): @@ -244,28 +266,26 @@ class SandboxedEnvironment(Environment): raised. However also other exceptions may occur during the rendering so the caller has to ensure that all exceptions are caught. """ + sandboxed = True #: default callback table for the binary operators. A copy of this is #: available on each instance of a sandboxed environment as #: :attr:`binop_table` default_binop_table = { - '+': operator.add, - '-': operator.sub, - '*': operator.mul, - '/': operator.truediv, - '//': operator.floordiv, - '**': operator.pow, - '%': operator.mod + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "//": operator.floordiv, + "**": operator.pow, + "%": operator.mod, } #: default callback table for the unary operators. A copy of this is #: available on each instance of a sandboxed environment as #: :attr:`unop_table` - default_unop_table = { - '+': operator.pos, - '-': operator.neg - } + default_unop_table = {"+": operator.pos, "-": operator.neg} #: a set of binary operators that should be intercepted. Each operator #: that is added to this set (empty by default) is delegated to the @@ -301,7 +321,7 @@ class SandboxedEnvironment(Environment): def intercept_unop(self, operator): """Called during template compilation with the name of a unary operator to check if it should be intercepted at runtime. If this - method returns `True`, :meth:`call_unop` is excuted for this unary + method returns `True`, :meth:`call_unop` is executed for this unary operator. The default implementation of :meth:`call_unop` will use the :attr:`unop_table` dictionary to perform the operator with the same logic as the builtin one. @@ -315,10 +335,9 @@ class SandboxedEnvironment(Environment): """ return False - def __init__(self, *args, **kwargs): Environment.__init__(self, *args, **kwargs) - self.globals['range'] = safe_range + self.globals["range"] = safe_range self.binop_table = self.default_binop_table.copy() self.unop_table = self.default_unop_table.copy() @@ -329,7 +348,7 @@ class SandboxedEnvironment(Environment): special attributes of internal python objects as returned by the :func:`is_internal_attribute` function. """ - return not (attr.startswith('_') or is_internal_attribute(obj, attr)) + return not (attr.startswith("_") or is_internal_attribute(obj, attr)) def is_safe_callable(self, obj): """Check if an object is safely callable. Per default a function is @@ -337,8 +356,9 @@ class SandboxedEnvironment(Environment): True. Override this method to alter the behavior, but this won't affect the `unsafe` decorator from this module. """ - return not (getattr(obj, 'unsafe_callable', False) or - getattr(obj, 'alters_data', False)) + return not ( + getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False) + ) def call_binop(self, context, operator, left, right): """For intercepted binary operator calls (:meth:`intercepted_binops`) @@ -398,11 +418,13 @@ class SandboxedEnvironment(Environment): def unsafe_undefined(self, obj, attribute): """Return an undefined object for unsafe attributes.""" - return self.undefined('access to attribute %r of %r ' - 'object is unsafe.' % ( - attribute, - obj.__class__.__name__ - ), name=attribute, obj=obj, exc=SecurityError) + return self.undefined( + "access to attribute %r of %r " + "object is unsafe." % (attribute, obj.__class__.__name__), + name=attribute, + obj=obj, + exc=SecurityError, + ) def format_string(self, s, args, kwargs, format_func=None): """If a format call is detected, then this is routed through this @@ -413,10 +435,10 @@ class SandboxedEnvironment(Environment): else: formatter = SandboxedFormatter(self) - if format_func is not None and format_func.__name__ == 'format_map': + if format_func is not None and format_func.__name__ == "format_map": if len(args) != 1 or kwargs: raise TypeError( - 'format_map() takes exactly one argument %d given' + "format_map() takes exactly one argument %d given" % (len(args) + (kwargs is not None)) ) @@ -427,7 +449,7 @@ class SandboxedEnvironment(Environment): rv = formatter.vformat(s, args, kwargs) return type(s)(rv) - def call(__self, __context, __obj, *args, **kwargs): + def call(__self, __context, __obj, *args, **kwargs): # noqa: B902 """Call an object from sandboxed code.""" fmt = inspect_format_method(__obj) if fmt is not None: @@ -436,7 +458,7 @@ class SandboxedEnvironment(Environment): # the double prefixes are to avoid double keyword argument # errors when proxying the call. if not __self.is_safe_callable(__obj): - raise SecurityError('%r is not safely callable' % (__obj,)) + raise SecurityError("%r is not safely callable" % (__obj,)) return __context.call(__obj, *args, **kwargs) @@ -452,16 +474,16 @@ class ImmutableSandboxedEnvironment(SandboxedEnvironment): return not modifies_known_mutable(obj, attr) -# This really is not a public API apparenlty. +# This really is not a public API apparently. try: from _string import formatter_field_name_split except ImportError: + def formatter_field_name_split(field_name): return field_name._formatter_field_name_split() class SandboxedFormatterMixin(object): - def __init__(self, env): self._env = env @@ -475,14 +497,14 @@ class SandboxedFormatterMixin(object): obj = self._env.getitem(obj, i) return obj, first -class SandboxedFormatter(SandboxedFormatterMixin, Formatter): +class SandboxedFormatter(SandboxedFormatterMixin, Formatter): def __init__(self, env): SandboxedFormatterMixin.__init__(self, env) Formatter.__init__(self) -class SandboxedEscapeFormatter(SandboxedFormatterMixin, EscapeFormatter): +class SandboxedEscapeFormatter(SandboxedFormatterMixin, EscapeFormatter): def __init__(self, env, escape): SandboxedFormatterMixin.__init__(self, env) EscapeFormatter.__init__(self, escape) diff --git a/jinja2/tests.py b/src/jinja2/tests.py index bc99d66..fabd4ce 100644 --- a/jinja2/tests.py +++ b/src/jinja2/tests.py @@ -1,23 +1,17 @@ # -*- coding: utf-8 -*- -""" - jinja2.tests - ~~~~~~~~~~~~ - - Jinja test functions. Used with the "is" operator. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" +"""Built-in template tests used with the ``is`` operator.""" +import decimal import operator import re -from jinja2.runtime import Undefined -from jinja2._compat import text_type, string_types, integer_types, abc -import decimal - -number_re = re.compile(r'^-?\d+(\.\d+)?$') -regex_type = type(number_re) +from ._compat import abc +from ._compat import integer_types +from ._compat import string_types +from ._compat import text_type +from .runtime import Undefined +number_re = re.compile(r"^-?\d+(\.\d+)?$") +regex_type = type(number_re) test_callable = callable @@ -63,6 +57,48 @@ def test_none(value): return value is None +def test_boolean(value): + """Return true if the object is a boolean value. + + .. versionadded:: 2.11 + """ + return value is True or value is False + + +def test_false(value): + """Return true if the object is False. + + .. versionadded:: 2.11 + """ + return value is False + + +def test_true(value): + """Return true if the object is True. + + .. versionadded:: 2.11 + """ + return value is True + + +# NOTE: The existing 'number' test matches booleans and floats +def test_integer(value): + """Return true if the object is an integer. + + .. versionadded:: 2.11 + """ + return isinstance(value, integer_types) and value is not True and value is not False + + +# NOTE: The existing 'number' test matches booleans and integers +def test_float(value): + """Return true if the object is a float. + + .. versionadded:: 2.11 + """ + return isinstance(value, float) + + def test_lower(value): """Return true if the variable is lowercased.""" return text_type(value).islower() @@ -98,7 +134,7 @@ def test_sequence(value): try: len(value) value.__getitem__ - except: + except Exception: return False return True @@ -127,7 +163,7 @@ def test_iterable(value): def test_escaped(value): """Check if the value is escaped.""" - return hasattr(value, '__html__') + return hasattr(value, "__html__") def test_in(value, seq): @@ -139,36 +175,41 @@ def test_in(value, seq): TESTS = { - 'odd': test_odd, - 'even': test_even, - 'divisibleby': test_divisibleby, - 'defined': test_defined, - 'undefined': test_undefined, - 'none': test_none, - 'lower': test_lower, - 'upper': test_upper, - 'string': test_string, - 'mapping': test_mapping, - 'number': test_number, - 'sequence': test_sequence, - 'iterable': test_iterable, - 'callable': test_callable, - 'sameas': test_sameas, - 'escaped': test_escaped, - 'in': test_in, - '==': operator.eq, - 'eq': operator.eq, - 'equalto': operator.eq, - '!=': operator.ne, - 'ne': operator.ne, - '>': operator.gt, - 'gt': operator.gt, - 'greaterthan': operator.gt, - 'ge': operator.ge, - '>=': operator.ge, - '<': operator.lt, - 'lt': operator.lt, - 'lessthan': operator.lt, - '<=': operator.le, - 'le': operator.le, + "odd": test_odd, + "even": test_even, + "divisibleby": test_divisibleby, + "defined": test_defined, + "undefined": test_undefined, + "none": test_none, + "boolean": test_boolean, + "false": test_false, + "true": test_true, + "integer": test_integer, + "float": test_float, + "lower": test_lower, + "upper": test_upper, + "string": test_string, + "mapping": test_mapping, + "number": test_number, + "sequence": test_sequence, + "iterable": test_iterable, + "callable": test_callable, + "sameas": test_sameas, + "escaped": test_escaped, + "in": test_in, + "==": operator.eq, + "eq": operator.eq, + "equalto": operator.eq, + "!=": operator.ne, + "ne": operator.ne, + ">": operator.gt, + "gt": operator.gt, + "greaterthan": operator.gt, + "ge": operator.ge, + ">=": operator.ge, + "<": operator.lt, + "lt": operator.lt, + "lessthan": operator.lt, + "<=": operator.le, + "le": operator.le, } diff --git a/jinja2/utils.py b/src/jinja2/utils.py index 8c5978d..6afca81 100644 --- a/jinja2/utils.py +++ b/src/jinja2/utils.py @@ -1,47 +1,32 @@ # -*- coding: utf-8 -*- -""" - jinja2.utils - ~~~~~~~~~~~~ - - Utility functions. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import re import json -import errno +import os +import re +import warnings from collections import deque +from random import choice +from random import randrange +from string import ascii_letters as _letters +from string import digits as _digits from threading import Lock -from markupsafe import Markup, escape - -from jinja2._compat import text_type, string_types, implements_iterator, \ - url_quote, abc +from markupsafe import escape +from markupsafe import Markup - -_word_split_re = re.compile(r'(\s+)') -_punctuation_re = re.compile( - '^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % ( - '|'.join(map(re.escape, ('(', '<', '<'))), - '|'.join(map(re.escape, ('.', ',', ')', '>', '\n', '>'))) - ) -) -_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') -_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)') -_entity_re = re.compile(r'&([^;]+);') -_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' -_digits = '0123456789' +from ._compat import abc +from ._compat import string_types +from ._compat import text_type +from ._compat import url_quote # special singleton representing missing values for the runtime -missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})() +missing = type("MissingType", (), {"__repr__": lambda x: "missing"})() # internal code internal_code = set() -concat = u''.join +concat = u"".join -_slash_escape = '\\/' not in json.dumps('/') +_slash_escape = "\\/" not in json.dumps("/") def contextfunction(f): @@ -101,24 +86,26 @@ def is_undefined(obj): return default return var """ - from jinja2.runtime import Undefined + from .runtime import Undefined + return isinstance(obj, Undefined) def consume(iterable): """Consumes an iterable without doing anything with it.""" - for event in iterable: + for _ in iterable: pass def clear_caches(): - """Jinja2 keeps internal caches for environments and lexers. These are - used so that Jinja2 doesn't have to recreate environments and lexers all + """Jinja keeps internal caches for environments and lexers. These are + used so that Jinja doesn't have to recreate environments and lexers all the time. Normally you don't have to care about that but if you are measuring memory consumption you may want to clean the caches. """ - from jinja2.environment import _spontaneous_environments - from jinja2.lexer import _lexer_cache + from .environment import _spontaneous_environments + from .lexer import _lexer_cache + _spontaneous_environments.clear() _lexer_cache.clear() @@ -135,12 +122,10 @@ def import_string(import_name, silent=False): :return: imported object """ try: - if ':' in import_name: - module, obj = import_name.split(':', 1) - elif '.' in import_name: - items = import_name.split('.') - module = '.'.join(items[:-1]) - obj = items[-1] + if ":" in import_name: + module, obj = import_name.split(":", 1) + elif "." in import_name: + module, _, obj = import_name.rpartition(".") else: return __import__(import_name) return getattr(__import__(module, None, None, [obj]), obj) @@ -149,15 +134,14 @@ def import_string(import_name, silent=False): raise -def open_if_exists(filename, mode='rb'): +def open_if_exists(filename, mode="rb"): """Returns a file descriptor for the filename if that file exists, - otherwise `None`. + otherwise ``None``. """ - try: - return open(filename, mode) - except IOError as e: - if e.errno not in (errno.ENOENT, errno.EISDIR, errno.EINVAL): - raise + if not os.path.isfile(filename): + return None + + return open(filename, mode) def object_type_repr(obj): @@ -166,15 +150,19 @@ def object_type_repr(obj): example for `None` and `Ellipsis`). """ if obj is None: - return 'None' + return "None" elif obj is Ellipsis: - return 'Ellipsis' + return "Ellipsis" + + cls = type(obj) + # __builtin__ in 2.x, builtins in 3.x - if obj.__class__.__module__ in ('__builtin__', 'builtins'): - name = obj.__class__.__name__ + if cls.__module__ in ("__builtin__", "builtins"): + name = cls.__name__ else: - name = obj.__class__.__module__ + '.' + obj.__class__.__name__ - return '%s object' % name + name = cls.__module__ + "." + cls.__name__ + + return "%s object" % name def pformat(obj, verbose=False): @@ -183,9 +171,11 @@ def pformat(obj, verbose=False): """ try: from pretty import pretty + return pretty(obj, verbose=verbose) except ImportError: from pprint import pformat + return pformat(obj) @@ -203,45 +193,77 @@ def urlize(text, trim_url_limit=None, rel=None, target=None): If target is not None, a target attribute will be added to the link. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None \ - and (x[:limit] + (len(x) >=limit and '...' - or '')) or x - words = _word_split_re.split(text_type(escape(text))) - rel_attr = rel and ' rel="%s"' % text_type(escape(rel)) or '' - target_attr = target and ' target="%s"' % escape(target) or '' + trim_url = ( + lambda x, limit=trim_url_limit: limit is not None + and (x[:limit] + (len(x) >= limit and "..." or "")) + or x + ) + words = re.split(r"(\s+)", text_type(escape(text))) + rel_attr = rel and ' rel="%s"' % text_type(escape(rel)) or "" + target_attr = target and ' target="%s"' % escape(target) or "" for i, word in enumerate(words): - match = _punctuation_re.match(word) + head, middle, tail = "", word, "" + match = re.match(r"^([(<]|<)+", middle) + if match: - lead, middle, trail = match.groups() - if middle.startswith('www.') or ( - '@' not in middle and - not middle.startswith('http://') and - not middle.startswith('https://') and - len(middle) > 0 and - middle[0] in _letters + _digits and ( - middle.endswith('.org') or - middle.endswith('.net') or - middle.endswith('.com') - )): - middle = '<a href="http://%s"%s%s>%s</a>' % (middle, - rel_attr, target_attr, trim_url(middle)) - if middle.startswith('http://') or \ - middle.startswith('https://'): - middle = '<a href="%s"%s%s>%s</a>' % (middle, - rel_attr, target_attr, trim_url(middle)) - if '@' in middle and not middle.startswith('www.') and \ - not ':' in middle and _simple_email_re.match(middle): - middle = '<a href="mailto:%s">%s</a>' % (middle, middle) - if lead + middle + trail != word: - words[i] = lead + middle + trail - return u''.join(words) + head = match.group() + middle = middle[match.end() :] + + # Unlike lead, which is anchored to the start of the string, + # need to check that the string ends with any of the characters + # before trying to match all of them, to avoid backtracking. + if middle.endswith((")", ">", ".", ",", "\n", ">")): + match = re.search(r"([)>.,\n]|>)+$", middle) + + if match: + tail = match.group() + middle = middle[: match.start()] + + if middle.startswith("www.") or ( + "@" not in middle + and not middle.startswith("http://") + and not middle.startswith("https://") + and len(middle) > 0 + and middle[0] in _letters + _digits + and ( + middle.endswith(".org") + or middle.endswith(".net") + or middle.endswith(".com") + ) + ): + middle = '<a href="http://%s"%s%s>%s</a>' % ( + middle, + rel_attr, + target_attr, + trim_url(middle), + ) + + if middle.startswith("http://") or middle.startswith("https://"): + middle = '<a href="%s"%s%s>%s</a>' % ( + middle, + rel_attr, + target_attr, + trim_url(middle), + ) + + if ( + "@" in middle + and not middle.startswith("www.") + and ":" not in middle + and re.match(r"^\S+@\w[\w.-]*\.\w+$", middle) + ): + middle = '<a href="mailto:%s">%s</a>' % (middle, middle) + + words[i] = head + middle + tail + + return u"".join(words) def generate_lorem_ipsum(n=5, html=True, min=20, max=100): """Generate some lorem ipsum for the template.""" - from jinja2.constants import LOREM_IPSUM_WORDS - from random import choice, randrange + from .constants import LOREM_IPSUM_WORDS + words = LOREM_IPSUM_WORDS.split() result = [] @@ -266,43 +288,53 @@ def generate_lorem_ipsum(n=5, html=True, min=20, max=100): if idx - randrange(3, 8) > last_comma: last_comma = idx last_fullstop += 2 - word += ',' + word += "," # add end of sentences if idx - randrange(10, 20) > last_fullstop: last_comma = last_fullstop = idx - word += '.' + word += "." next_capitalized = True p.append(word) # ensure that the paragraph ends with a dot. - p = u' '.join(p) - if p.endswith(','): - p = p[:-1] + '.' - elif not p.endswith('.'): - p += '.' + p = u" ".join(p) + if p.endswith(","): + p = p[:-1] + "." + elif not p.endswith("."): + p += "." result.append(p) if not html: - return u'\n\n'.join(result) - return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result)) + return u"\n\n".join(result) + return Markup(u"\n".join(u"<p>%s</p>" % escape(x) for x in result)) -def unicode_urlencode(obj, charset='utf-8', for_qs=False): - """URL escapes a single bytestring or unicode string with the - given charset if applicable to URL safe quoting under all rules - that need to be considered under all supported Python versions. +def unicode_urlencode(obj, charset="utf-8", for_qs=False): + """Quote a string for use in a URL using the given charset. - If non strings are provided they are converted to their unicode - representation first. + This function is misnamed, it is a wrapper around + :func:`urllib.parse.quote`. + + :param obj: String or bytes to quote. Other types are converted to + string then encoded to bytes using the given charset. + :param charset: Encode text to bytes using this charset. + :param for_qs: Quote "/" and use "+" for spaces. """ if not isinstance(obj, string_types): obj = text_type(obj) + if isinstance(obj, text_type): obj = obj.encode(charset) - safe = not for_qs and b'/' or b'' - rv = text_type(url_quote(obj, safe)) + + safe = b"" if for_qs else b"/" + rv = url_quote(obj, safe) + + if not isinstance(rv, text_type): + rv = rv.decode("utf-8") + if for_qs: - rv = rv.replace('%20', '+') + rv = rv.replace("%20", "+") + return rv @@ -329,9 +361,9 @@ class LRUCache(object): def __getstate__(self): return { - 'capacity': self.capacity, - '_mapping': self._mapping, - '_queue': self._queue + "capacity": self.capacity, + "_mapping": self._mapping, + "_queue": self._queue, } def __setstate__(self, d): @@ -345,7 +377,7 @@ class LRUCache(object): """Return a shallow copy of the instance.""" rv = self.__class__(self.capacity) rv._mapping.update(self._mapping) - rv._queue = deque(self._queue) + rv._queue.extend(self._queue) return rv def get(self, key, default=None): @@ -359,15 +391,11 @@ class LRUCache(object): """Set `default` if the key is not in the cache otherwise leave unchanged. Return the value of this key. """ - self._wlock.acquire() try: - try: - return self[key] - except KeyError: - self[key] = default - return default - finally: - self._wlock.release() + return self[key] + except KeyError: + self[key] = default + return default def clear(self): """Clear the cache.""" @@ -387,10 +415,7 @@ class LRUCache(object): return len(self._mapping) def __repr__(self): - return '<%s %r>' % ( - self.__class__.__name__, - self._mapping - ) + return "<%s %r>" % (self.__class__.__name__, self._mapping) def __getitem__(self, key): """Get an item from the cache. Moves the item up so that it has the @@ -439,7 +464,6 @@ class LRUCache(object): try: self._remove(key) except ValueError: - # __getitem__ is not locked, it might happen pass finally: self._wlock.release() @@ -452,6 +476,12 @@ class LRUCache(object): def iteritems(self): """Iterate over all items.""" + warnings.warn( + "'iteritems()' will be removed in version 3.0. Use" + " 'iter(cache.items())' instead.", + DeprecationWarning, + stacklevel=2, + ) return iter(self.items()) def values(self): @@ -460,6 +490,22 @@ class LRUCache(object): def itervalue(self): """Iterate over all values.""" + warnings.warn( + "'itervalue()' will be removed in version 3.0. Use" + " 'iter(cache.values())' instead.", + DeprecationWarning, + stacklevel=2, + ) + return iter(self.values()) + + def itervalues(self): + """Iterate over all values.""" + warnings.warn( + "'itervalues()' will be removed in version 3.0. Use" + " 'iter(cache.values())' instead.", + DeprecationWarning, + stacklevel=2, + ) return iter(self.values()) def keys(self): @@ -470,12 +516,19 @@ class LRUCache(object): """Iterate over all keys in the cache dict, ordered by the most recent usage. """ - return reversed(tuple(self._queue)) + warnings.warn( + "'iterkeys()' will be removed in version 3.0. Use" + " 'iter(cache.keys())' instead.", + DeprecationWarning, + stacklevel=2, + ) + return iter(self) - __iter__ = iterkeys + def __iter__(self): + return reversed(tuple(self._queue)) def __reversed__(self): - """Iterate over the values in the cache dict, oldest items + """Iterate over the keys in the cache dict, oldest items coming first. """ return iter(tuple(self._queue)) @@ -492,22 +545,21 @@ def select_autoescape( default_for_string=True, default=False, ): - """Set the initial value of autoescaping based on the name of the - template, case insensitive. This is the recommended way to configure + """Intelligently sets the initial value of autoescaping based on the + filename of the template. This is the recommended way to configure autoescaping if you do not want to write a custom function yourself. - The defaults will enable autoescaping only for template names ending - with ".html", ".htm", and ".xml", as well as templates from strings. - - .. code-block:: python + If you want to enable it for all templates created from strings or + for all templates with `.html` and `.xml` extensions:: from jinja2 import Environment, select_autoescape - env = Environment(autoescape=select_autoescape()) - - The following configuration enables it for all templates except if - the name ends with ".txt". + env = Environment(autoescape=select_autoescape( + enabled_extensions=('html', 'xml'), + default_for_string=True, + )) - .. code-block:: python + Example configuration to turn it on at all times except if the template + ends with `.txt`:: from jinja2 import Environment, select_autoescape env = Environment(autoescape=select_autoescape( @@ -516,16 +568,14 @@ def select_autoescape( default=True, )) - :param enabled_extensions: Template names ending in these extensions - will have autoescaping enabled. A "." is prepended to each value - if it's missing. - :param disabled_extensions: Template names ending in these - extensions will have autoescaping disabled. A "." is prepended - to each value if it's missing. - :param default_for_string: What to do if the template is loaded from - a string and doesn't have a name. - :param default: What to do if the name does not match any of the - other rules. + The `enabled_extensions` is an iterable of all the extensions that + autoescaping should be enabled for. Likewise `disabled_extensions` is + a list of all templates it should be disabled for. If a template is + loaded from a string then the default from `default_for_string` is used. + If nothing matches then the initial value of autoescaping is set to the + value of `default`. + + For security reasons this function operates case insensitive. .. versionadded:: 2.9 """ @@ -535,15 +585,11 @@ def select_autoescape( def autoescape(template_name): if template_name is None: return default_for_string - template_name = template_name.lower() - if template_name.endswith(enabled_patterns): return True - if template_name.endswith(disabled_patterns): return False - return default return autoescape @@ -569,35 +615,63 @@ def htmlsafe_json_dumps(obj, dumper=None, **kwargs): """ if dumper is None: dumper = json.dumps - rv = dumper(obj, **kwargs) \ - .replace(u'<', u'\\u003c') \ - .replace(u'>', u'\\u003e') \ - .replace(u'&', u'\\u0026') \ - .replace(u"'", u'\\u0027') + rv = ( + dumper(obj, **kwargs) + .replace(u"<", u"\\u003c") + .replace(u">", u"\\u003e") + .replace(u"&", u"\\u0026") + .replace(u"'", u"\\u0027") + ) return Markup(rv) -@implements_iterator class Cycler(object): - """A cycle helper for templates.""" + """Cycle through values by yield them one at a time, then restarting + once the end is reached. Available as ``cycler`` in templates. + + Similar to ``loop.cycle``, but can be used outside loops or across + multiple loops. For example, render a list of folders and files in a + list, alternating giving them "odd" and "even" classes. + + .. code-block:: html+jinja + + {% set row_class = cycler("odd", "even") %} + <ul class="browser"> + {% for folder in folders %} + <li class="folder {{ row_class.next() }}">{{ folder }} + {% endfor %} + {% for file in files %} + <li class="file {{ row_class.next() }}">{{ file }} + {% endfor %} + </ul> + + :param items: Each positional argument will be yielded in the order + given for each cycle. + + .. versionadded:: 2.1 + """ def __init__(self, *items): if not items: - raise RuntimeError('at least one item has to be provided') + raise RuntimeError("at least one item has to be provided") self.items = items - self.reset() + self.pos = 0 def reset(self): - """Resets the cycle.""" + """Resets the current item to the first item.""" self.pos = 0 @property def current(self): - """Returns the current item.""" + """Return the current item. Equivalent to the item that will be + returned next time :meth:`next` is called. + """ return self.items[self.pos] def next(self): - """Goes one item ahead and returns it.""" + """Return the current item, then advance :attr:`current` to the + next item. + """ rv = self.current self.pos = (self.pos + 1) % len(self.items) return rv @@ -608,27 +682,28 @@ class Cycler(object): class Joiner(object): """A joining helper for templates.""" - def __init__(self, sep=u', '): + def __init__(self, sep=u", "): self.sep = sep self.used = False def __call__(self): if not self.used: self.used = True - return u'' + return u"" return self.sep class Namespace(object): """A namespace object that can hold arbitrary attributes. It may be - initialized from a dictionary or with keyword argments.""" + initialized from a dictionary or with keyword arguments.""" - def __init__(*args, **kwargs): + def __init__(*args, **kwargs): # noqa: B902 self, args = args[0], args[1:] self.__attrs = dict(*args, **kwargs) def __getattribute__(self, name): - if name == '_Namespace__attrs': + # __class__ is needed for the awaitable check in async mode + if name in {"_Namespace__attrs", "__class__"}: return object.__getattribute__(self, name) try: return self.__attrs[name] @@ -639,12 +714,24 @@ class Namespace(object): self.__attrs[name] = value def __repr__(self): - return '<Namespace %r>' % self.__attrs + return "<Namespace %r>" % self.__attrs # does this python version support async for in and async generators? try: - exec('async def _():\n async for _ in ():\n yield _') + exec("async def _():\n async for _ in ():\n yield _") have_async_gen = True except SyntaxError: have_async_gen = False + + +def soft_unicode(s): + from markupsafe import soft_unicode + + warnings.warn( + "'jinja2.utils.soft_unicode' will be removed in version 3.0." + " Use 'markupsafe.soft_unicode' instead.", + DeprecationWarning, + stacklevel=2, + ) + return soft_unicode(s) diff --git a/jinja2/visitor.py b/src/jinja2/visitor.py index ba526df..d1365bf 100644 --- a/jinja2/visitor.py +++ b/src/jinja2/visitor.py @@ -1,14 +1,8 @@ # -*- coding: utf-8 -*- +"""API for traversing the AST nodes. Implemented by the compiler and +meta introspection. """ - jinja2.visitor - ~~~~~~~~~~~~~~ - - This module implements a visitor for the nodes. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD. -""" -from jinja2.nodes import Node +from .nodes import Node class NodeVisitor(object): @@ -28,7 +22,7 @@ class NodeVisitor(object): exists for this node. In that case the generic visit function is used instead. """ - method = 'visit_' + node.__class__.__name__ + method = "visit_" + node.__class__.__name__ return getattr(self, method, None) def visit(self, node, *args, **kwargs): diff --git a/tests/conftest.py b/tests/conftest.py index 107659b..23088a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,119 +1,57 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.conftest - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Configuration and Fixtures for the tests +import os - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -import os +from jinja2 import Environment from jinja2 import loaders from jinja2.utils import have_async_gen -from jinja2 import Environment def pytest_ignore_collect(path): - if 'async' in path.basename and not have_async_gen: + if "async" in path.basename and not have_async_gen: return True return False -def pytest_configure(config): - '''Register custom marks for test categories.''' - custom_markers = [ - 'api', - 'byte_code_cache', - 'core_tags', - 'debug', - 'escapeUrlizeTarget', - 'ext', - 'extended', - 'filter', - 'for_loop', - 'helpers', - 'if_condition', - 'imports', - 'includes', - 'inheritance', - 'lexer', - 'lexnparse', - 'loaders', - 'lowlevel', - 'lrucache', - 'lstripblocks', - 'macros', - 'meta', - 'moduleloader', - 'parser', - 'regression', - 'sandbox', - 'set', - 'streaming', - 'syntax', - 'test_tests', - 'tokenstream', - 'undefined', - 'utils', - 'with_', - ] - for mark in custom_markers: - config.addinivalue_line('markers', mark + ': test category') - - @pytest.fixture def env(): - '''returns a new environment. - ''' + """returns a new environment.""" return Environment() @pytest.fixture def dict_loader(): - '''returns DictLoader - ''' - return loaders.DictLoader({ - 'justdict.html': 'FOO' - }) + """returns DictLoader""" + return loaders.DictLoader({"justdict.html": "FOO"}) @pytest.fixture def package_loader(): - '''returns PackageLoader initialized from templates - ''' - return loaders.PackageLoader('res', 'templates') + """returns PackageLoader initialized from templates""" + return loaders.PackageLoader("res", "templates") @pytest.fixture def filesystem_loader(): - '''returns FileSystemLoader initialized to res/templates directory - ''' + """returns FileSystemLoader initialized to res/templates directory""" here = os.path.dirname(os.path.abspath(__file__)) - return loaders.FileSystemLoader(here + '/res/templates') + return loaders.FileSystemLoader(here + "/res/templates") @pytest.fixture def function_loader(): - '''returns a FunctionLoader - ''' - return loaders.FunctionLoader({'justfunction.html': 'FOO'}.get) + """returns a FunctionLoader""" + return loaders.FunctionLoader({"justfunction.html": "FOO"}.get) @pytest.fixture def choice_loader(dict_loader, package_loader): - '''returns a ChoiceLoader - ''' + """returns a ChoiceLoader""" return loaders.ChoiceLoader([dict_loader, package_loader]) @pytest.fixture def prefix_loader(filesystem_loader, dict_loader): - '''returns a PrefixLoader - ''' - return loaders.PrefixLoader({ - 'a': filesystem_loader, - 'b': dict_loader - }) + """returns a PrefixLoader""" + return loaders.PrefixLoader({"a": filesystem_loader, "b": dict_loader}) diff --git a/tests/res/templates/mojibake.txt b/tests/res/templates/mojibake.txt new file mode 100644 index 0000000..4b94aa6 --- /dev/null +++ b/tests/res/templates/mojibake.txt @@ -0,0 +1 @@ +文字化け diff --git a/tests/res/templates2/foo b/tests/res/templates2/foo new file mode 100644 index 0000000..1c4ad3e --- /dev/null +++ b/tests/res/templates2/foo @@ -0,0 +1,2 @@ +Looks like the start of templates/foo/test.html +Tested by test_filesystem_loader_overlapping_names diff --git a/tests/test_api.py b/tests/test_api.py index 26dd4da..7a1cae8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,52 +1,87 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.api - ~~~~~~~~~~~~~~~~~~~~ - - Tests the public API and related stuff. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import os -import tempfile import shutil +import tempfile import pytest -from jinja2 import Environment, Undefined, DebugUndefined, \ - StrictUndefined, UndefinedError, meta, \ - is_undefined, Template, DictLoader, make_logging_undefined + +from jinja2 import ChainableUndefined +from jinja2 import DebugUndefined +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import is_undefined +from jinja2 import make_logging_undefined +from jinja2 import meta +from jinja2 import StrictUndefined +from jinja2 import Template +from jinja2 import TemplatesNotFound +from jinja2 import Undefined +from jinja2 import UndefinedError from jinja2.compiler import CodeGenerator from jinja2.runtime import Context +from jinja2.utils import contextfunction from jinja2.utils import Cycler +from jinja2.utils import environmentfunction +from jinja2.utils import evalcontextfunction -@pytest.mark.api -@pytest.mark.extended class TestExtendedAPI(object): - def test_item_and_attribute(self, env): from jinja2.sandbox import SandboxedEnvironment for env in Environment(), SandboxedEnvironment(): # the |list is necessary for python3 - tmpl = env.from_string('{{ foo.items()|list }}') - assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" + tmpl = env.from_string("{{ foo.items()|list }}") + assert tmpl.render(foo={"items": 42}) == "[('items', 42)]" tmpl = env.from_string('{{ foo|attr("items")()|list }}') - assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" + assert tmpl.render(foo={"items": 42}) == "[('items', 42)]" tmpl = env.from_string('{{ foo["items"] }}') - assert tmpl.render(foo={'items': 42}) == '42' - - def test_finalizer(self, env): - def finalize_none_empty(value): - if value is None: - value = u'' - return value - env = Environment(finalize=finalize_none_empty) - tmpl = env.from_string('{% for item in seq %}|{{ item }}{% endfor %}') - assert tmpl.render(seq=(None, 1, "foo")) == '||1|foo' - tmpl = env.from_string('<{{ none }}>') - assert tmpl.render() == '<>' + assert tmpl.render(foo={"items": 42}) == "42" + + def test_finalize(self): + e = Environment(finalize=lambda v: "" if v is None else v) + t = e.from_string("{% for item in seq %}|{{ item }}{% endfor %}") + assert t.render(seq=(None, 1, "foo")) == "||1|foo" + + def test_finalize_constant_expression(self): + e = Environment(finalize=lambda v: "" if v is None else v) + t = e.from_string("<{{ none }}>") + assert t.render() == "<>" + + def test_no_finalize_template_data(self): + e = Environment(finalize=lambda v: type(v).__name__) + t = e.from_string("<{{ value }}>") + # If template data was finalized, it would print "strintstr". + assert t.render(value=123) == "<int>" + + def test_context_finalize(self): + @contextfunction + def finalize(context, value): + return value * context["scale"] + + e = Environment(finalize=finalize) + t = e.from_string("{{ value }}") + assert t.render(value=5, scale=3) == "15" + + def test_eval_finalize(self): + @evalcontextfunction + def finalize(eval_ctx, value): + return str(eval_ctx.autoescape) + value + + e = Environment(finalize=finalize, autoescape=True) + t = e.from_string("{{ value }}") + assert t.render(value="<script>") == "True<script>" + + def test_env_autoescape(self): + @environmentfunction + def finalize(env, value): + return " ".join( + (env.variable_start_string, repr(value), env.variable_end_string) + ) + + e = Environment(finalize=finalize) + t = e.from_string("{{ value }}") + assert t.render(value="hello") == "{{ 'hello' }}" def test_cycler(self, env): items = 1, 2, 3 @@ -59,17 +94,6 @@ class TestExtendedAPI(object): c.reset() assert c.current == 1 - def test_cycler_nextmethod(self, env): - items = 1, 2, 3 - c = Cycler(*items) - for item in items + items: - assert c.current == item - assert c.next() == item - c.next() - assert c.current == 2 - c.reset() - assert c.current == 1 - def test_expressions(self, env): expr = env.compile_expression("foo") assert expr() is None @@ -81,28 +105,51 @@ class TestExtendedAPI(object): assert expr(foo=42) == 84 def test_template_passthrough(self, env): - t = Template('Content') + t = Template("Content") assert env.get_template(t) is t assert env.select_template([t]) is t assert env.get_or_select_template([t]) is t assert env.get_or_select_template(t) is t + def test_get_template_undefined(self, env): + """Passing Undefined to get/select_template raises an + UndefinedError or shows the undefined message in the list. + """ + env.loader = DictLoader({}) + t = Undefined(name="no_name_1") + + with pytest.raises(UndefinedError): + env.get_template(t) + + with pytest.raises(UndefinedError): + env.get_or_select_template(t) + + with pytest.raises(UndefinedError): + env.select_template(t) + + with pytest.raises(TemplatesNotFound) as exc_info: + env.select_template([t, "no_name_2"]) + + exc_message = str(exc_info.value) + assert "'no_name_1' is undefined" in exc_message + assert "no_name_2" in exc_message + def test_autoescape_autoselect(self, env): def select_autoescape(name): - if name is None or '.' not in name: + if name is None or "." not in name: return False - return name.endswith('.html') - env = Environment(autoescape=select_autoescape, - loader=DictLoader({ - 'test.txt': '{{ foo }}', - 'test.html': '{{ foo }}' - })) - t = env.get_template('test.txt') - assert t.render(foo='<foo>') == '<foo>' - t = env.get_template('test.html') - assert t.render(foo='<foo>') == '<foo>' - t = env.from_string('{{ foo }}') - assert t.render(foo='<foo>') == '<foo>' + return name.endswith(".html") + + env = Environment( + autoescape=select_autoescape, + loader=DictLoader({"test.txt": "{{ foo }}", "test.html": "{{ foo }}"}), + ) + t = env.get_template("test.txt") + assert t.render(foo="<foo>") == "<foo>" + t = env.get_template("test.html") + assert t.render(foo="<foo>") == "<foo>" + t = env.from_string("{{ foo }}") + assert t.render(foo="<foo>") == "<foo>" def test_sandbox_max_range(self, env): from jinja2.sandbox import SandboxedEnvironment, MAX_RANGE @@ -114,76 +161,78 @@ class TestExtendedAPI(object): t.render(total=MAX_RANGE + 1) -@pytest.mark.api -@pytest.mark.meta class TestMeta(object): - def test_find_undeclared_variables(self, env): - ast = env.parse('{% set foo = 42 %}{{ bar + foo }}') + ast = env.parse("{% set foo = 42 %}{{ bar + foo }}") + x = meta.find_undeclared_variables(ast) + assert x == set(["bar"]) + + ast = env.parse( + "{% set foo = 42 %}{{ bar + foo }}" + "{% macro meh(x) %}{{ x }}{% endmacro %}" + "{% for item in seq %}{{ muh(item) + meh(seq) }}" + "{% endfor %}" + ) x = meta.find_undeclared_variables(ast) - assert x == set(['bar']) + assert x == set(["bar", "seq", "muh"]) - ast = env.parse('{% set foo = 42 %}{{ bar + foo }}' - '{% macro meh(x) %}{{ x }}{% endmacro %}' - '{% for item in seq %}{{ muh(item) + meh(seq) }}' - '{% endfor %}') + ast = env.parse("{% for x in range(5) %}{{ x }}{% endfor %}{{ foo }}") x = meta.find_undeclared_variables(ast) - assert x == set(['bar', 'seq', 'muh']) + assert x == set(["foo"]) def test_find_refererenced_templates(self, env): ast = env.parse('{% extends "layout.html" %}{% include helper %}') i = meta.find_referenced_templates(ast) - assert next(i) == 'layout.html' + assert next(i) == "layout.html" assert next(i) is None assert list(i) == [] - ast = env.parse('{% extends "layout.html" %}' - '{% from "test.html" import a, b as c %}' - '{% import "meh.html" as meh %}' - '{% include "muh.html" %}') + ast = env.parse( + '{% extends "layout.html" %}' + '{% from "test.html" import a, b as c %}' + '{% import "meh.html" as meh %}' + '{% include "muh.html" %}' + ) i = meta.find_referenced_templates(ast) - assert list(i) == ['layout.html', 'test.html', 'meh.html', 'muh.html'] + assert list(i) == ["layout.html", "test.html", "meh.html", "muh.html"] def test_find_included_templates(self, env): ast = env.parse('{% include ["foo.html", "bar.html"] %}') i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html'] + assert list(i) == ["foo.html", "bar.html"] ast = env.parse('{% include ("foo.html", "bar.html") %}') i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html'] + assert list(i) == ["foo.html", "bar.html"] ast = env.parse('{% include ["foo.html", "bar.html", foo] %}') i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html', None] + assert list(i) == ["foo.html", "bar.html", None] ast = env.parse('{% include ("foo.html", "bar.html", foo) %}') i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html', None] + assert list(i) == ["foo.html", "bar.html", None] -@pytest.mark.api -@pytest.mark.streaming class TestStreaming(object): - def test_basic_streaming(self, env): - tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index " - "}} - {{ item }}</li>{%- endfor %}</ul>") - stream = tmpl.stream(seq=list(range(4))) - assert next(stream) == '<ul>' - assert next(stream) == '<li>1 - 0</li>' - assert next(stream) == '<li>2 - 1</li>' - assert next(stream) == '<li>3 - 2</li>' - assert next(stream) == '<li>4 - 3</li>' - assert next(stream) == '</ul>' + t = env.from_string( + "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>" + "{%- endfor %}</ul>" + ) + stream = t.stream(seq=list(range(3))) + assert next(stream) == "<ul>" + assert "".join(stream) == "<li>1 - 0</li><li>2 - 1</li><li>3 - 2</li></ul>" def test_buffered_streaming(self, env): - tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index " - "}} - {{ item }}</li>{%- endfor %}</ul>") - stream = tmpl.stream(seq=list(range(4))) + tmpl = env.from_string( + "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>" + "{%- endfor %}</ul>" + ) + stream = tmpl.stream(seq=list(range(3))) stream.enable_buffering(size=3) - assert next(stream) == u'<ul><li>1 - 0</li><li>2 - 1</li>' - assert next(stream) == u'<li>3 - 2</li><li>4 - 3</li></ul>' + assert next(stream) == u"<ul><li>1" + assert next(stream) == u" - 0</li>" def test_streaming_behavior(self, env): tmpl = env.from_string("") @@ -199,133 +248,172 @@ class TestStreaming(object): try: tmpl = env.from_string(u"\u2713") stream = tmpl.stream() - stream.dump(os.path.join(tmp, 'dump.txt'), 'utf-8') - with open(os.path.join(tmp, 'dump.txt'), 'rb') as f: - assert f.read() == b'\xe2\x9c\x93' + stream.dump(os.path.join(tmp, "dump.txt"), "utf-8") + with open(os.path.join(tmp, "dump.txt"), "rb") as f: + assert f.read() == b"\xe2\x9c\x93" finally: shutil.rmtree(tmp) -@pytest.mark.api -@pytest.mark.undefined class TestUndefined(object): - def test_stopiteration_is_undefined(self): def test(): raise StopIteration() - t = Template('A{{ test() }}B') - assert t.render(test=test) == 'AB' - t = Template('A{{ test().missingattribute }}B') + + t = Template("A{{ test() }}B") + assert t.render(test=test) == "AB" + t = Template("A{{ test().missingattribute }}B") pytest.raises(UndefinedError, t.render, test=test) def test_undefined_and_special_attributes(self): - try: - Undefined('Foo').__dict__ - except AttributeError: - pass - else: - assert False, "Expected actual attribute error" + with pytest.raises(AttributeError): + Undefined("Foo").__dict__ + + def test_undefined_attribute_error(self): + # Django's LazyObject turns the __class__ attribute into a + # property that resolves the wrapped function. If that wrapped + # function raises an AttributeError, printing the repr of the + # object in the undefined message would cause a RecursionError. + class Error(object): + @property + def __class__(self): + raise AttributeError() + + u = Undefined(obj=Error(), name="hello") + + with pytest.raises(UndefinedError): + getattr(u, "recursion", None) def test_logging_undefined(self): _messages = [] class DebugLogger(object): def warning(self, msg, *args): - _messages.append('W:' + msg % args) + _messages.append("W:" + msg % args) def error(self, msg, *args): - _messages.append('E:' + msg % args) + _messages.append("E:" + msg % args) logging_undefined = make_logging_undefined(DebugLogger()) env = Environment(undefined=logging_undefined) - assert env.from_string('{{ missing }}').render() == u'' - pytest.raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - assert env.from_string('{{ missing|list }}').render() == '[]' - assert env.from_string('{{ missing is not defined }}').render() \ - == 'True' - assert env.from_string('{{ foo.missing }}').render(foo=42) == '' - assert env.from_string('{{ not missing }}').render() == 'True' + assert env.from_string("{{ missing }}").render() == u"" + pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) + assert env.from_string("{{ missing|list }}").render() == "[]" + assert env.from_string("{{ missing is not defined }}").render() == "True" + assert env.from_string("{{ foo.missing }}").render(foo=42) == "" + assert env.from_string("{{ not missing }}").render() == "True" assert _messages == [ - 'W:Template variable warning: missing is undefined', + "W:Template variable warning: missing is undefined", "E:Template variable error: 'missing' is undefined", - 'W:Template variable warning: missing is undefined', - 'W:Template variable warning: int object has no attribute missing', - 'W:Template variable warning: missing is undefined', + "W:Template variable warning: missing is undefined", + "W:Template variable warning: int object has no attribute missing", + "W:Template variable warning: missing is undefined", ] def test_default_undefined(self): env = Environment(undefined=Undefined) - assert env.from_string('{{ missing }}').render() == u'' - pytest.raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - assert env.from_string('{{ missing|list }}').render() == '[]' - assert env.from_string('{{ missing is not defined }}').render() \ - == 'True' - assert env.from_string('{{ foo.missing }}').render(foo=42) == '' - assert env.from_string('{{ not missing }}').render() == 'True' - pytest.raises(UndefinedError, - env.from_string('{{ missing - 1}}').render) + assert env.from_string("{{ missing }}").render() == u"" + pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) + assert env.from_string("{{ missing|list }}").render() == "[]" + assert env.from_string("{{ missing is not defined }}").render() == "True" + assert env.from_string("{{ foo.missing }}").render(foo=42) == "" + assert env.from_string("{{ not missing }}").render() == "True" + pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render) + und1 = Undefined(name="x") + und2 = Undefined(name="y") + assert und1 == und2 + assert und1 != 42 + assert hash(und1) == hash(und2) == hash(Undefined()) + with pytest.raises(AttributeError): + getattr(Undefined, "__slots__") # noqa: B009 + + def test_chainable_undefined(self): + env = Environment(undefined=ChainableUndefined) + # The following tests are copied from test_default_undefined + assert env.from_string("{{ missing }}").render() == u"" + assert env.from_string("{{ missing|list }}").render() == "[]" + assert env.from_string("{{ missing is not defined }}").render() == "True" + assert env.from_string("{{ foo.missing }}").render(foo=42) == "" + assert env.from_string("{{ not missing }}").render() == "True" + pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render) + with pytest.raises(AttributeError): + getattr(ChainableUndefined, "__slots__") # noqa: B009 + + # The following tests ensure subclass functionality works as expected + assert env.from_string('{{ missing.bar["baz"] }}').render() == u"" + assert ( + env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == u"foo" + ) + assert ( + env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(foo=42) + == u"bar" + ) + assert ( + env.from_string('{{ foo.bar["baz"]._undefined_name }}').render( + foo={"bar": 42} + ) + == u"baz" + ) def test_debug_undefined(self): env = Environment(undefined=DebugUndefined) - assert env.from_string('{{ missing }}').render() == '{{ missing }}' - pytest.raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - assert env.from_string('{{ missing|list }}').render() == '[]' - assert env.from_string('{{ missing is not defined }}').render() \ - == 'True' - assert env.from_string('{{ foo.missing }}').render(foo=42) \ + assert env.from_string("{{ missing }}").render() == "{{ missing }}" + pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) + assert env.from_string("{{ missing|list }}").render() == "[]" + assert env.from_string("{{ missing is not defined }}").render() == "True" + assert ( + env.from_string("{{ foo.missing }}").render(foo=42) == u"{{ no such element: int object['missing'] }}" - assert env.from_string('{{ not missing }}').render() == 'True' + ) + assert env.from_string("{{ not missing }}").render() == "True" + undefined_hint = "this is testing undefined hint of DebugUndefined" + assert ( + str(DebugUndefined(hint=undefined_hint)) + == u"{{ undefined value printed: %s }}" % undefined_hint + ) + with pytest.raises(AttributeError): + getattr(DebugUndefined, "__slots__") # noqa: B009 def test_strict_undefined(self): env = Environment(undefined=StrictUndefined) - pytest.raises(UndefinedError, env.from_string('{{ missing }}').render) - pytest.raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - pytest.raises(UndefinedError, - env.from_string('{{ missing|list }}').render) - assert env.from_string('{{ missing is not defined }}').render() \ - == 'True' - pytest.raises(UndefinedError, - env.from_string('{{ foo.missing }}').render, foo=42) - pytest.raises(UndefinedError, - env.from_string('{{ not missing }}').render) - assert env.from_string('{{ missing|default("default", true) }}')\ - .render() == 'default' + pytest.raises(UndefinedError, env.from_string("{{ missing }}").render) + pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render) + pytest.raises(UndefinedError, env.from_string("{{ missing|list }}").render) + assert env.from_string("{{ missing is not defined }}").render() == "True" + pytest.raises( + UndefinedError, env.from_string("{{ foo.missing }}").render, foo=42 + ) + pytest.raises(UndefinedError, env.from_string("{{ not missing }}").render) + assert ( + env.from_string('{{ missing|default("default", true) }}').render() + == "default" + ) + with pytest.raises(AttributeError): + getattr(StrictUndefined, "__slots__") # noqa: B009 + assert env.from_string('{{ "foo" if false }}').render() == "" def test_indexing_gives_undefined(self): t = Template("{{ var[42].foo }}") pytest.raises(UndefinedError, t.render, var=0) def test_none_gives_proper_error(self): - try: - Environment().getattr(None, 'split')() - except UndefinedError as e: - assert e.message == "'None' has no attribute 'split'" - else: - assert False, 'expected exception' + with pytest.raises(UndefinedError, match="'None' has no attribute 'split'"): + Environment().getattr(None, "split")() def test_object_repr(self): - try: - Undefined(obj=42, name='upper')() - except UndefinedError as e: - assert e.message == "'int object' has no attribute 'upper'" - else: - assert False, 'expected exception' + with pytest.raises( + UndefinedError, match="'int object' has no attribute 'upper'" + ): + Undefined(obj=42, name="upper")() -@pytest.mark.api -@pytest.mark.lowlevel class TestLowLevel(object): - def test_custom_code_generator(self): class CustomCodeGenerator(CodeGenerator): def visit_Const(self, node, frame=None): # This method is pure nonsense, but works fine for testing... - if node.value == 'foo': - self.write(repr('bar')) + if node.value == "foo": + self.write(repr("bar")) else: super(CustomCodeGenerator, self).visit_Const(node, frame) @@ -334,16 +422,16 @@ class TestLowLevel(object): env = CustomEnvironment() tmpl = env.from_string('{% set foo = "foo" %}{{ foo }}') - assert tmpl.render() == 'bar' + assert tmpl.render() == "bar" def test_custom_context(self): class CustomContext(Context): def resolve_or_missing(self, key): - return 'resolve-' + key + return "resolve-" + key class CustomEnvironment(Environment): context_class = CustomContext env = CustomEnvironment() - tmpl = env.from_string('{{ foo }}') - assert tmpl.render() == 'resolve-foo' + tmpl = env.from_string("{{ foo }}") + assert tmpl.render() == "resolve-foo" diff --git a/tests/test_async.py b/tests/test_async.py index 2f17747..2b9974e 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,9 +1,14 @@ -import pytest import asyncio -from jinja2 import Template, Environment, DictLoader -from jinja2.exceptions import TemplateNotFound, TemplatesNotFound, \ - UndefinedError +import pytest + +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import Template +from jinja2.asyncsupport import auto_aiter +from jinja2.exceptions import TemplateNotFound +from jinja2.exceptions import TemplatesNotFound +from jinja2.exceptions import UndefinedError def run(coro): @@ -12,18 +17,19 @@ def run(coro): def test_basic_async(): - t = Template('{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}', - enable_async=True) + t = Template( + "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True + ) + async def func(): return await t.render_async() rv = run(func()) - assert rv == '[1][2][3]' + assert rv == "[1][2][3]" def test_await_on_calls(): - t = Template('{{ async_func() + normal_func() }}', - enable_async=True) + t = Template("{{ async_func() + normal_func() }}", enable_async=True) async def async_func(): return 42 @@ -32,18 +38,14 @@ def test_await_on_calls(): return 23 async def func(): - return await t.render_async( - async_func=async_func, - normal_func=normal_func - ) + return await t.render_async(async_func=async_func, normal_func=normal_func) rv = run(func()) - assert rv == '65' + assert rv == "65" def test_await_on_calls_normal_render(): - t = Template('{{ async_func() + normal_func() }}', - enable_async=True) + t = Template("{{ async_func() + normal_func() }}", enable_async=True) async def async_func(): return 42 @@ -51,17 +53,16 @@ def test_await_on_calls_normal_render(): def normal_func(): return 23 - rv = t.render( - async_func=async_func, - normal_func=normal_func - ) + rv = t.render(async_func=async_func, normal_func=normal_func) - assert rv == '65' + assert rv == "65" def test_await_and_macros(): - t = Template('{% macro foo(x) %}[{{ x }}][{{ async_func() }}]' - '{% endmacro %}{{ foo(42) }}', enable_async=True) + t = Template( + "{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}", + enable_async=True, + ) async def async_func(): return 42 @@ -70,81 +71,87 @@ def test_await_and_macros(): return await t.render_async(async_func=async_func) rv = run(func()) - assert rv == '[42][42]' + assert rv == "[42][42]" def test_async_blocks(): - t = Template('{% block foo %}<Test>{% endblock %}{{ self.foo() }}', - enable_async=True, autoescape=True) + t = Template( + "{% block foo %}<Test>{% endblock %}{{ self.foo() }}", + enable_async=True, + autoescape=True, + ) + async def func(): return await t.render_async() rv = run(func()) - assert rv == '<Test><Test>' + assert rv == "<Test><Test>" def test_async_generate(): - t = Template('{% for x in [1, 2, 3] %}{{ x }}{% endfor %}', - enable_async=True) + t = Template("{% for x in [1, 2, 3] %}{{ x }}{% endfor %}", enable_async=True) rv = list(t.generate()) - assert rv == ['1', '2', '3'] + assert rv == ["1", "2", "3"] def test_async_iteration_in_templates(): - t = Template('{% for x in rng %}{{ x }}{% endfor %}', - enable_async=True) + t = Template("{% for x in rng %}{{ x }}{% endfor %}", enable_async=True) + async def async_iterator(): for item in [1, 2, 3]: yield item + rv = list(t.generate(rng=async_iterator())) - assert rv == ['1', '2', '3'] + assert rv == ["1", "2", "3"] def test_async_iteration_in_templates_extended(): - t = Template('{% for x in rng %}{{ loop.index0 }}/{{ x }}{% endfor %}', - enable_async=True) - async def async_iterator(): - for item in [1, 2, 3]: - yield item - rv = list(t.generate(rng=async_iterator())) - assert rv == ['0/1', '1/2', '2/3'] + t = Template( + "{% for x in rng %}{{ loop.index0 }}/{{ x }}{% endfor %}", enable_async=True + ) + stream = t.generate(rng=auto_aiter(range(1, 4))) + assert next(stream) == "0" + assert "".join(stream) == "/11/22/3" @pytest.fixture def test_env_async(): - env = Environment(loader=DictLoader(dict( - module='{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}', - header='[{{ foo }}|{{ 23 }}]', - o_printer='({{ o }})' - )), enable_async=True) - env.globals['bar'] = 23 + env = Environment( + loader=DictLoader( + dict( + module="{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}", + header="[{{ foo }}|{{ 23 }}]", + o_printer="({{ o }})", + ) + ), + enable_async=True, + ) + env.globals["bar"] = 23 return env -@pytest.mark.imports class TestAsyncImports(object): - def test_context_imports(self, test_env_async): t = test_env_async.from_string('{% import "module" as m %}{{ m.test() }}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env_async.from_string( '{% import "module" as m without context %}{{ m.test() }}' ) - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env_async.from_string( '{% import "module" as m with context %}{{ m.test() }}' ) - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env_async.from_string('{% from "module" import test %}{{ test() }}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env_async.from_string( '{% from "module" import test without context %}{{ test() }}' ) - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env_async.from_string( '{% from "module" import test with context %}{{ test() }}' ) - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" def test_trailing_comma(self, test_env_async): test_env_async.from_string('{% from "foo" import bar, baz with context %}') @@ -154,85 +161,90 @@ class TestAsyncImports(object): 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(''' + m = run( + 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()) == '...' - assert not hasattr(m, '__missing') + """ + )._get_default_module_async() + ) + assert run(m.toplevel()) == "..." + assert not hasattr(m, "__missing") assert m.variable == 42 - assert not hasattr(m, 'notthere') + assert not hasattr(m, "notthere") -@pytest.mark.imports -@pytest.mark.includes class TestAsyncIncludes(object): - def test_context_include(self, test_env_async): t = test_env_async.from_string('{% include "header" %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env_async.from_string('{% include "header" with context %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env_async.from_string('{% include "header" without context %}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" def test_choice_includes(self, test_env_async): t = test_env_async.from_string('{% include ["missing", "header"] %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env_async.from_string( '{% include ["missing", "missing2"] ignore missing %}' ) - assert t.render(foo=42) == '' + assert t.render(foo=42) == "" t = test_env_async.from_string('{% include ["missing", "missing2"] %}') pytest.raises(TemplateNotFound, t.render) - try: + with pytest.raises(TemplatesNotFound) as e: t.render() - except TemplatesNotFound as e: - assert e.templates == ['missing', 'missing2'] - assert e.name == 'missing2' - else: - assert False, 'thou shalt raise' + + assert e.value.templates == ["missing", "missing2"] + assert e.value.name == "missing2" def test_includes(t, **ctx): - ctx['foo'] = 42 - assert t.render(ctx) == '[42|23]' + ctx["foo"] = 42 + assert t.render(ctx) == "[42|23]" t = test_env_async.from_string('{% include ["missing", "header"] %}') test_includes(t) - t = test_env_async.from_string('{% include x %}') - test_includes(t, x=['missing', 'header']) + t = test_env_async.from_string("{% include x %}") + test_includes(t, x=["missing", "header"]) t = test_env_async.from_string('{% include [x, "header"] %}') - test_includes(t, x='missing') - t = test_env_async.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env_async.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env_async.from_string('{% include [x] %}') - test_includes(t, x='header') + test_includes(t, x="missing") + t = test_env_async.from_string("{% include x %}") + test_includes(t, x="header") + t = test_env_async.from_string("{% include x %}") + test_includes(t, x="header") + t = test_env_async.from_string("{% include [x] %}") + test_includes(t, x="header") def test_include_ignoring_missing(self, test_env_async): t = test_env_async.from_string('{% include "missing" %}') pytest.raises(TemplateNotFound, t.render) - for extra in '', 'with context', 'without context': - t = test_env_async.from_string('{% include "missing" ignore missing ' + - extra + ' %}') - assert t.render() == '' + for extra in "", "with context", "without context": + t = test_env_async.from_string( + '{% include "missing" ignore missing ' + extra + " %}" + ) + assert t.render() == "" def test_context_include_with_overrides(self, test_env_async): - env = Environment(loader=DictLoader(dict( - main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", - item="{{ item }}" - ))) + env = Environment( + loader=DictLoader( + dict( + main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", + item="{{ item }}", + ) + ) + ) assert env.get_template("main").render() == "123" def test_unoptimized_scopes(self, test_env_async): - t = test_env_async.from_string(""" + t = test_env_async.from_string( + """ {% macro outer(o) %} {% macro inner() %} {% include "o_printer" %} @@ -240,14 +252,18 @@ class TestAsyncIncludes(object): {{ inner() }} {% endmacro %} {{ outer("FOO") }} - """) - assert t.render().strip() == '(FOO)' + """ + ) + assert t.render().strip() == "(FOO)" def test_unoptimized_scopes_autoescape(self): - env = Environment(loader=DictLoader(dict( - o_printer='({{ o }})', - )), autoescape=True, enable_async=True) - t = env.from_string(""" + env = Environment( + loader=DictLoader(dict(o_printer="({{ o }})",)), + autoescape=True, + enable_async=True, + ) + t = env.from_string( + """ {% macro outer(o) %} {% macro inner() %} {% include "o_printer" %} @@ -255,72 +271,70 @@ class TestAsyncIncludes(object): {{ inner() }} {% endmacro %} {{ outer("FOO") }} - """) - assert t.render().strip() == '(FOO)' + """ + ) + assert t.render().strip() == "(FOO)" -@pytest.mark.core_tags -@pytest.mark.for_loop class TestAsyncForLoop(object): - def test_simple(self, test_env_async): - tmpl = test_env_async.from_string('{% for item in seq %}{{ item }}{% endfor %}') - assert tmpl.render(seq=list(range(10))) == '0123456789' + tmpl = test_env_async.from_string("{% for item in seq %}{{ item }}{% endfor %}") + assert tmpl.render(seq=list(range(10))) == "0123456789" def test_else(self, test_env_async): tmpl = test_env_async.from_string( - '{% for item in seq %}XXX{% else %}...{% endfor %}') - assert tmpl.render() == '...' + "{% for item in seq %}XXX{% else %}...{% endfor %}" + ) + assert tmpl.render() == "..." def test_empty_blocks(self, test_env_async): - tmpl = test_env_async.from_string('<{% for item in seq %}{% else %}{% endfor %}>') - assert tmpl.render() == '<>' - - def test_context_vars(self, test_env_async): - slist = [42, 24] - for seq in [slist, iter(slist), reversed(slist), (_ for _ in slist)]: - tmpl = test_env_async.from_string('''{% for item in seq -%} - {{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ - loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{ - loop.length }}###{% endfor %}''') - one, two, _ = tmpl.render(seq=seq).split('###') - (one_index, one_index0, one_revindex, one_revindex0, one_first, - one_last, one_length) = one.split('|') - (two_index, two_index0, two_revindex, two_revindex0, two_first, - two_last, two_length) = two.split('|') - - assert int(one_index) == 1 and int(two_index) == 2 - assert int(one_index0) == 0 and int(two_index0) == 1 - assert int(one_revindex) == 2 and int(two_revindex) == 1 - assert int(one_revindex0) == 1 and int(two_revindex0) == 0 - assert one_first == 'True' and two_first == 'False' - assert one_last == 'False' and two_last == 'True' - assert one_length == two_length == '2' + tmpl = test_env_async.from_string( + "<{% for item in seq %}{% else %}{% endfor %}>" + ) + assert tmpl.render() == "<>" + + @pytest.mark.parametrize( + "transform", [lambda x: x, iter, reversed, lambda x: (i for i in x), auto_aiter] + ) + def test_context_vars(self, test_env_async, transform): + t = test_env_async.from_string( + "{% for item in seq %}{{ loop.index }}|{{ loop.index0 }}" + "|{{ loop.revindex }}|{{ loop.revindex0 }}|{{ loop.first }}" + "|{{ loop.last }}|{{ loop.length }}\n{% endfor %}" + ) + out = t.render(seq=transform([42, 24])) + assert out == "1|0|2|1|True|False|2\n2|1|1|0|False|True|2\n" def test_cycling(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq %}{{ + tmpl = test_env_async.from_string( + """{% for item in seq %}{{ loop.cycle('<1>', '<2>') }}{% endfor %}{% - for item in seq %}{{ loop.cycle(*through) }}{% endfor %}''') - output = tmpl.render(seq=list(range(4)), through=('<1>', '<2>')) - assert output == '<1><2>' * 4 + for item in seq %}{{ loop.cycle(*through) }}{% endfor %}""" + ) + output = tmpl.render(seq=list(range(4)), through=("<1>", "<2>")) + assert output == "<1><2>" * 4 def test_lookaround(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq -%} + tmpl = test_env_async.from_string( + """{% for item in seq -%} {{ loop.previtem|default('x') }}-{{ item }}-{{ loop.nextitem|default('x') }}| - {%- endfor %}''') + {%- endfor %}""" + ) output = tmpl.render(seq=list(range(4))) - assert output == 'x-0-1|0-1-2|1-2-3|2-3-x|' + assert output == "x-0-1|0-1-2|1-2-3|2-3-x|" def test_changed(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq -%} + tmpl = test_env_async.from_string( + """{% for item in seq -%} {{ loop.changed(item) }}, - {%- endfor %}''') + {%- endfor %}""" + ) output = tmpl.render(seq=[None, None, 1, 2, 2, 3, 4, 4, 4]) - assert output == 'True,False,True,True,False,True,True,False,False,' + assert output == "True,False,True,True,False,True,True,False,False," def test_scope(self, test_env_async): - tmpl = test_env_async.from_string('{% for item in seq %}{% endfor %}{{ item }}') + tmpl = test_env_async.from_string("{% for item in seq %}{% endfor %}{{ item }}") output = tmpl.render(seq=list(range(10))) assert not output @@ -328,111 +342,158 @@ class TestAsyncForLoop(object): def inner(): for item in range(5): yield item - tmpl = test_env_async.from_string('{% for item in iter %}{{ item }}{% endfor %}') + + tmpl = test_env_async.from_string( + "{% for item in iter %}{{ item }}{% endfor %}" + ) output = tmpl.render(iter=inner()) - assert output == '01234' + assert output == "01234" def test_noniter(self, test_env_async): - tmpl = test_env_async.from_string('{% for item in none %}...{% endfor %}') + tmpl = test_env_async.from_string("{% for item in none %}...{% endfor %}") pytest.raises(TypeError, tmpl.render) def test_recursive(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq recursive -%} + tmpl = test_env_async.from_string( + """{% for item in seq recursive -%} [{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]' + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[1<[1][2]>][2<[1][2]>][3<[a]>]" + ) def test_recursive_lookaround(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq recursive -%} + tmpl = test_env_async.from_string( + """{% for item in seq recursive -%} [{{ loop.previtem.a if loop.previtem is defined else 'x' }}.{{ item.a }}.{{ loop.nextitem.a if loop.nextitem is defined else 'x' }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]' + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]" + ) def test_recursive_depth0(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq recursive -%} - [{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[0:1<[1:1][1:2]>][0:2<[1:1][1:2]>][0:3<[1:a]>]' + tmpl = test_env_async.from_string( + "{% for item in seq recursive %}[{{ loop.depth0 }}:{{ item.a }}" + "{% if item.b %}<{{ loop(item.b) }}>{% endif %}]{% endfor %}" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[0:1<[1:1][1:2]>][0:2<[1:1][1:2]>][0:3<[1:a]>]" + ) def test_recursive_depth(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in seq recursive -%} - [{{ loop.depth }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[1:1<[2:1][2:2]>][1:2<[2:1][2:2]>][1:3<[2:a]>]' + tmpl = test_env_async.from_string( + "{% for item in seq recursive %}[{{ loop.depth }}:{{ item.a }}" + "{% if item.b %}<{{ loop(item.b) }}>{% endif %}]{% endfor %}" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[1:1<[2:1][2:2]>][1:2<[2:1][2:2]>][1:3<[2:a]>]" + ) def test_looploop(self, test_env_async): - tmpl = test_env_async.from_string('''{% for row in table %} + tmpl = test_env_async.from_string( + """{% for row in table %} {%- set rowloop = loop -%} {% for cell in row -%} [{{ rowloop.index }}|{{ loop.index }}] {%- endfor %} - {%- endfor %}''') - assert tmpl.render(table=['ab', 'cd']) == '[1|1][1|2][2|1][2|2]' + {%- endfor %}""" + ) + assert tmpl.render(table=["ab", "cd"]) == "[1|1][1|2][2|1][2|2]" def test_reversed_bug(self, test_env_async): - tmpl = test_env_async.from_string('{% for i in items %}{{ i }}' - '{% if not loop.last %}' - ',{% endif %}{% endfor %}') - assert tmpl.render(items=reversed([3, 2, 1])) == '1,2,3' + tmpl = test_env_async.from_string( + "{% for i in items %}{{ i }}" + "{% if not loop.last %}" + ",{% endif %}{% endfor %}" + ) + assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3" def test_loop_errors(self, test_env_async): - tmpl = test_env_async.from_string('''{% for item in [1] if loop.index - == 0 %}...{% endfor %}''') + tmpl = test_env_async.from_string( + """{% for item in [1] if loop.index + == 0 %}...{% endfor %}""" + ) pytest.raises(UndefinedError, tmpl.render) - tmpl = test_env_async.from_string('''{% for item in [] %}...{% else - %}{{ loop }}{% endfor %}''') - assert tmpl.render() == '' + tmpl = test_env_async.from_string( + """{% for item in [] %}...{% else + %}{{ loop }}{% endfor %}""" + ) + assert tmpl.render() == "" def test_loop_filter(self, test_env_async): - tmpl = test_env_async.from_string('{% for item in range(10) if item ' - 'is even %}[{{ item }}]{% endfor %}') - assert tmpl.render() == '[0][2][4][6][8]' - tmpl = test_env_async.from_string(''' + tmpl = test_env_async.from_string( + "{% for item in range(10) if item is even %}[{{ item }}]{% endfor %}" + ) + assert tmpl.render() == "[0][2][4][6][8]" + tmpl = test_env_async.from_string( + """ {%- for item in range(10) if item is even %}[{{ - loop.index }}:{{ item }}]{% endfor %}''') - assert tmpl.render() == '[1:0][2:2][3:4][4:6][5:8]' + loop.index }}:{{ item }}]{% endfor %}""" + ) + assert tmpl.render() == "[1:0][2:2][3:4][4:6][5:8]" def test_scoped_special_var(self, test_env_async): t = test_env_async.from_string( - '{% for s in seq %}[{{ loop.first }}{% for c in s %}' - '|{{ loop.first }}{% endfor %}]{% endfor %}') - assert t.render(seq=('ab', 'cd')) \ - == '[True|True|False][False|True|False]' + "{% for s in seq %}[{{ loop.first }}{% for c in s %}" + "|{{ loop.first }}{% endfor %}]{% endfor %}" + ) + assert t.render(seq=("ab", "cd")) == "[True|True|False][False|True|False]" def test_scoped_loop_var(self, test_env_async): - t = test_env_async.from_string('{% for x in seq %}{{ loop.first }}' - '{% for y in seq %}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalse' - t = test_env_async.from_string('{% for x in seq %}{% for y in seq %}' - '{{ loop.first }}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalseTrueFalse' + t = test_env_async.from_string( + "{% for x in seq %}{{ loop.first }}" + "{% for y in seq %}{% endfor %}{% endfor %}" + ) + assert t.render(seq="ab") == "TrueFalse" + t = test_env_async.from_string( + "{% for x in seq %}{% for y in seq %}" + "{{ loop.first }}{% endfor %}{% endfor %}" + ) + assert t.render(seq="ab") == "TrueFalseTrueFalse" def test_recursive_empty_loop_iter(self, test_env_async): - t = test_env_async.from_string(''' + t = test_env_async.from_string( + """ {%- for item in foo recursive -%}{%- endfor -%} - ''') - assert t.render(dict(foo=[])) == '' + """ + ) + assert t.render(dict(foo=[])) == "" def test_call_in_loop(self, test_env_async): - t = test_env_async.from_string(''' + t = test_env_async.from_string( + """ {%- macro do_something() -%} [{{ caller() }}] {%- endmacro %} @@ -442,24 +503,29 @@ class TestAsyncForLoop(object): {{ i }} {%- endcall %} {%- endfor -%} - ''') - assert t.render() == '[1][2][3]' + """ + ) + assert t.render() == "[1][2][3]" def test_scoping_bug(self, test_env_async): - t = test_env_async.from_string(''' + t = test_env_async.from_string( + """ {%- for item in foo %}...{{ item }}...{% endfor %} {%- macro item(a) %}...{{ a }}...{% endmacro %} {{- item(2) -}} - ''') - assert t.render(foo=(1,)) == '...1......2...' + """ + ) + assert t.render(foo=(1,)) == "...1......2..." def test_unpacking(self, test_env_async): - tmpl = test_env_async.from_string('{% for a, b, c in [[1, 2, 3]] %}' - '{{ a }}|{{ b }}|{{ c }}{% endfor %}') - assert tmpl.render() == '1|2|3' + tmpl = test_env_async.from_string( + "{% for a, b, c in [[1, 2, 3]] %}{{ a }}|{{ b }}|{{ c }}{% endfor %}" + ) + assert tmpl.render() == "1|2|3" def test_recursive_loop_filter(self, test_env_async): - t = test_env_async.from_string(''' + t = test_env_async.from_string( + """ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {%- for page in [site.root] if page.url != this recursive %} @@ -467,46 +533,59 @@ class TestAsyncForLoop(object): {{- loop(page.children) }} {%- endfor %} </urlset> - ''') - sm =t.render(this='/foo', site={'root': { - 'url': '/', - 'children': [ - {'url': '/foo'}, - {'url': '/bar'}, - ] - }}) + """ + ) + sm = t.render( + this="/foo", + site={"root": {"url": "/", "children": [{"url": "/foo"}, {"url": "/bar"}]}}, + ) lines = [x.strip() for x in sm.splitlines() if x.strip()] assert lines == [ '<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', - '<url><loc>/</loc></url>', - '<url><loc>/bar</loc></url>', - '</urlset>', + "<url><loc>/</loc></url>", + "<url><loc>/bar</loc></url>", + "</urlset>", ] def test_nonrecursive_loop_filter(self, test_env_async): - t = test_env_async.from_string(''' + t = test_env_async.from_string( + """ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {%- for page in items if page.url != this %} <url><loc>{{ page.url }}</loc></url> {%- endfor %} </urlset> - ''') - sm =t.render(this='/foo', items=[ - {'url': '/'}, - {'url': '/foo'}, - {'url': '/bar'}, - ]) + """ + ) + sm = t.render( + this="/foo", items=[{"url": "/"}, {"url": "/foo"}, {"url": "/bar"}] + ) lines = [x.strip() for x in sm.splitlines() if x.strip()] assert lines == [ '<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', - '<url><loc>/</loc></url>', - '<url><loc>/bar</loc></url>', - '</urlset>', + "<url><loc>/</loc></url>", + "<url><loc>/bar</loc></url>", + "</urlset>", ] def test_bare_async(self, test_env_async): t = test_env_async.from_string('{% extends "header" %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" + + def test_awaitable_property_slicing(self, test_env_async): + t = test_env_async.from_string("{% for x in a.b[:1] %}{{ x }}{% endfor %}") + assert t.render(a=dict(b=[1, 2, 3])) == "1" + + +def test_namespace_awaitable(test_env_async): + async def _test(): + t = test_env_async.from_string( + '{% set ns = namespace(foo="Bar") %}{{ ns.foo }}' + ) + actual = await t.render_async() + assert actual == "Bar" + + run(_test()) diff --git a/tests/test_asyncfilters.py b/tests/test_asyncfilters.py index 9796caa..7c737c8 100644 --- a/tests/test_asyncfilters.py +++ b/tests/test_asyncfilters.py @@ -1,8 +1,9 @@ -import pytest +from collections import namedtuple -from markupsafe import Markup +import pytest from jinja2 import Environment +from jinja2.utils import Markup async def make_aiter(iter): @@ -12,10 +13,10 @@ async def make_aiter(iter): def mark_dualiter(parameter, factory): def decorator(f): - return pytest.mark.parametrize(parameter, [ - lambda: factory(), - lambda: make_aiter(factory()), - ])(f) + return pytest.mark.parametrize( + parameter, [lambda: factory(), lambda: make_aiter(factory())] + )(f) + return decorator @@ -24,206 +25,200 @@ def env_async(): return Environment(enable_async=True) -@mark_dualiter('foo', lambda: range(10)) +@mark_dualiter("foo", lambda: range(10)) def test_first(env_async, foo): - tmpl = env_async.from_string('{{ foo()|first }}') + tmpl = env_async.from_string("{{ foo()|first }}") out = tmpl.render(foo=foo) - assert out == '0' - - -@mark_dualiter('items', lambda: [ - {'foo': 1, 'bar': 2}, - {'foo': 2, 'bar': 3}, - {'foo': 1, 'bar': 1}, - {'foo': 3, 'bar': 4} -]) + assert out == "0" + + +@mark_dualiter( + "items", + lambda: [ + {"foo": 1, "bar": 2}, + {"foo": 2, "bar": 3}, + {"foo": 1, "bar": 1}, + {"foo": 3, "bar": 4}, + ], +) def test_groupby(env_async, items): - tmpl = env_async.from_string(''' + tmpl = env_async.from_string( + """ {%- for grouper, list in items()|groupby('foo') -%} {{ grouper }}{% for x in list %}: {{ x.foo }}, {{ x.bar }}{% endfor %}| - {%- endfor %}''') - assert tmpl.render(items=items).split('|') == [ + {%- endfor %}""" + ) + assert tmpl.render(items=items).split("|") == [ "1: 1, 2: 1, 1", "2: 2, 3", "3: 3, 4", - "" + "", ] -@mark_dualiter('items', lambda: [('a', 1), ('a', 2), ('b', 1)]) +@mark_dualiter("items", lambda: [("a", 1), ("a", 2), ("b", 1)]) def test_groupby_tuple_index(env_async, items): - tmpl = env_async.from_string(''' + tmpl = env_async.from_string( + """ {%- for grouper, list in items()|groupby(0) -%} {{ grouper }}{% for x in list %}:{{ x.1 }}{% endfor %}| - {%- endfor %}''') - assert tmpl.render(items=items) == 'a:1:2|b:1|' + {%- endfor %}""" + ) + assert tmpl.render(items=items) == "a:1:2|b:1|" def make_articles(): - class Date(object): - def __init__(self, day, month, year): - self.day = day - self.month = month - self.year = year - - class Article(object): - def __init__(self, title, *date): - self.date = Date(*date) - self.title = title - + Date = namedtuple("Date", "day,month,year") + Article = namedtuple("Article", "title,date") return [ - Article('aha', 1, 1, 1970), - Article('interesting', 2, 1, 1970), - Article('really?', 3, 1, 1970), - Article('totally not', 1, 1, 1971) + Article("aha", Date(1, 1, 1970)), + Article("interesting", Date(2, 1, 1970)), + Article("really?", Date(3, 1, 1970)), + Article("totally not", Date(1, 1, 1971)), ] -@mark_dualiter('articles', make_articles) +@mark_dualiter("articles", make_articles) def test_groupby_multidot(env_async, articles): - tmpl = env_async.from_string(''' + tmpl = env_async.from_string( + """ {%- for year, list in articles()|groupby('date.year') -%} {{ year }}{% for x in list %}[{{ x.title }}]{% endfor %}| - {%- endfor %}''') - assert tmpl.render(articles=articles).split('|') == [ - '1970[aha][interesting][really?]', - '1971[totally not]', - '' + {%- endfor %}""" + ) + assert tmpl.render(articles=articles).split("|") == [ + "1970[aha][interesting][really?]", + "1971[totally not]", + "", ] -@mark_dualiter('int_items', lambda: [1, 2, 3]) -def test_join(env_async, int_items): +@mark_dualiter("int_items", lambda: [1, 2, 3]) +def test_join_env_int(env_async, int_items): tmpl = env_async.from_string('{{ items()|join("|") }}') out = tmpl.render(items=int_items) - assert out == '1|2|3' + assert out == "1|2|3" -@mark_dualiter('string_items', lambda: ["<foo>", Markup("<span>foo</span>")]) -def test_join(string_items): +@mark_dualiter("string_items", lambda: ["<foo>", Markup("<span>foo</span>")]) +def test_join_string_list(string_items): env2 = Environment(autoescape=True, enable_async=True) - tmpl = env2.from_string( - '{{ ["<foo>", "<span>foo</span>"|safe]|join }}') - assert tmpl.render(items=string_items) == '<foo><span>foo</span>' + tmpl = env2.from_string('{{ ["<foo>", "<span>foo</span>"|safe]|join }}') + assert tmpl.render(items=string_items) == "<foo><span>foo</span>" def make_users(): - class User(object): - def __init__(self, username): - self.username = username - return map(User, ['foo', 'bar']) + User = namedtuple("User", "username") + return map(User, ["foo", "bar"]) -@mark_dualiter('users', make_users) +@mark_dualiter("users", make_users) def test_join_attribute(env_async, users): - tmpl = env_async.from_string('''{{ users()|join(', ', 'username') }}''') - assert tmpl.render(users=users) == 'foo, bar' + tmpl = env_async.from_string("""{{ users()|join(', ', 'username') }}""") + assert tmpl.render(users=users) == "foo, bar" -@mark_dualiter('items', lambda: [1, 2, 3, 4, 5]) +@mark_dualiter("items", lambda: [1, 2, 3, 4, 5]) def test_simple_reject(env_async, items): tmpl = env_async.from_string('{{ items()|reject("odd")|join("|") }}') - assert tmpl.render(items=items) == '2|4' + assert tmpl.render(items=items) == "2|4" -@mark_dualiter('items', lambda: [None, False, 0, 1, 2, 3, 4, 5]) +@mark_dualiter("items", lambda: [None, False, 0, 1, 2, 3, 4, 5]) def test_bool_reject(env_async, items): - tmpl = env_async.from_string( - '{{ items()|reject|join("|") }}' - ) - assert tmpl.render(items=items) == 'None|False|0' + tmpl = env_async.from_string('{{ items()|reject|join("|") }}') + assert tmpl.render(items=items) == "None|False|0" -@mark_dualiter('items', lambda: [1, 2, 3, 4, 5]) +@mark_dualiter("items", lambda: [1, 2, 3, 4, 5]) def test_simple_select(env_async, items): tmpl = env_async.from_string('{{ items()|select("odd")|join("|") }}') - assert tmpl.render(items=items) == '1|3|5' + assert tmpl.render(items=items) == "1|3|5" -@mark_dualiter('items', lambda: [None, False, 0, 1, 2, 3, 4, 5]) +@mark_dualiter("items", lambda: [None, False, 0, 1, 2, 3, 4, 5]) def test_bool_select(env_async, items): - tmpl = env_async.from_string( - '{{ items()|select|join("|") }}' - ) - assert tmpl.render(items=items) == '1|2|3|4|5' + tmpl = env_async.from_string('{{ items()|select|join("|") }}') + assert tmpl.render(items=items) == "1|2|3|4|5" def make_users(): - class User(object): - def __init__(self, name, is_active): - self.name = name - self.is_active = is_active + User = namedtuple("User", "name,is_active") return [ - User('john', True), - User('jane', True), - User('mike', False), + User("john", True), + User("jane", True), + User("mike", False), ] -@mark_dualiter('users', make_users) +@mark_dualiter("users", make_users) def test_simple_select_attr(env_async, users): tmpl = env_async.from_string( - '{{ users()|selectattr("is_active")|' - 'map(attribute="name")|join("|") }}' + '{{ users()|selectattr("is_active")|map(attribute="name")|join("|") }}' ) - assert tmpl.render(users=users) == 'john|jane' + assert tmpl.render(users=users) == "john|jane" -@mark_dualiter('items', lambda: list('123')) +@mark_dualiter("items", lambda: list("123")) def test_simple_map(env_async, items): tmpl = env_async.from_string('{{ items()|map("int")|sum }}') - assert tmpl.render(items=items) == '6' + assert tmpl.render(items=items) == "6" + +def test_map_sum(env_async): # async map + async filter + tmpl = env_async.from_string('{{ [[1,2], [3], [4,5,6]]|map("sum")|list }}') + assert tmpl.render() == "[3, 3, 15]" -@mark_dualiter('users', make_users) + +@mark_dualiter("users", make_users) def test_attribute_map(env_async, users): tmpl = env_async.from_string('{{ users()|map(attribute="name")|join("|") }}') - assert tmpl.render(users=users) == 'john|jane|mike' + assert tmpl.render(users=users) == "john|jane|mike" def test_empty_map(env_async): tmpl = env_async.from_string('{{ none|map("upper")|list }}') - assert tmpl.render() == '[]' + assert tmpl.render() == "[]" -@mark_dualiter('items', lambda: [1, 2, 3, 4, 5, 6]) +@mark_dualiter("items", lambda: [1, 2, 3, 4, 5, 6]) def test_sum(env_async, items): - tmpl = env_async.from_string('''{{ items()|sum }}''') - assert tmpl.render(items=items) == '21' + tmpl = env_async.from_string("""{{ items()|sum }}""") + assert tmpl.render(items=items) == "21" -@mark_dualiter('items', lambda: [ - {'value': 23}, - {'value': 1}, - {'value': 18}, -]) +@mark_dualiter("items", lambda: [{"value": 23}, {"value": 1}, {"value": 18}]) def test_sum_attributes(env_async, items): - tmpl = env_async.from_string('''{{ items()|sum('value') }}''') + tmpl = env_async.from_string("""{{ items()|sum('value') }}""") assert tmpl.render(items=items) def test_sum_attributes_nested(env_async): - tmpl = env_async.from_string('''{{ values|sum('real.value') }}''') - assert tmpl.render(values=[ - {'real': {'value': 23}}, - {'real': {'value': 1}}, - {'real': {'value': 18}}, - ]) == '42' + tmpl = env_async.from_string("""{{ values|sum('real.value') }}""") + assert ( + tmpl.render( + values=[ + {"real": {"value": 23}}, + {"real": {"value": 1}}, + {"real": {"value": 18}}, + ] + ) + == "42" + ) def test_sum_attributes_tuple(env_async): - tmpl = env_async.from_string('''{{ values.items()|sum('1') }}''') - assert tmpl.render(values={ - 'foo': 23, - 'bar': 1, - 'baz': 18, - }) == '42' + tmpl = env_async.from_string("""{{ values.items()|sum('1') }}""") + assert tmpl.render(values={"foo": 23, "bar": 1, "baz": 18}) == "42" -@mark_dualiter('items', lambda: range(10)) +@mark_dualiter("items", lambda: range(10)) def test_slice(env_async, items): - tmpl = env_async.from_string('{{ items()|slice(3)|list }}|' - '{{ items()|slice(3, "X")|list }}') + tmpl = env_async.from_string( + "{{ items()|slice(3)|list }}|{{ items()|slice(3, 'X')|list }}" + ) out = tmpl.render(items=items) - assert out == ("[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" - "[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]") + assert out == ( + "[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" + "[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]" + ) diff --git a/tests/test_bytecode_cache.py b/tests/test_bytecode_cache.py index caef36b..c7882b1 100644 --- a/tests/test_bytecode_cache.py +++ b/tests/test_bytecode_cache.py @@ -1,38 +1,24 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.bytecode_cache - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Test bytecode caching - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" - import pytest from jinja2 import Environment -from jinja2.bccache import Bucket, FileSystemBytecodeCache, \ - MemcachedBytecodeCache +from jinja2.bccache import Bucket +from jinja2.bccache import FileSystemBytecodeCache +from jinja2.bccache import MemcachedBytecodeCache from jinja2.exceptions import TemplateNotFound @pytest.fixture -def env(package_loader): - bytecode_cache = FileSystemBytecodeCache() - return Environment( - loader=package_loader, - bytecode_cache=bytecode_cache, - ) +def env(package_loader, tmp_path): + bytecode_cache = FileSystemBytecodeCache(str(tmp_path)) + return Environment(loader=package_loader, bytecode_cache=bytecode_cache) -@pytest.mark.byte_code_cache class TestByteCodeCache(object): - def test_simple(self, env): - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') + tmpl = env.get_template("test.html") + assert tmpl.render().strip() == "BAR" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") class MockMemcached(object): @@ -63,22 +49,22 @@ class TestMemcachedBytecodeCache(object): memcached = MockMemcached() m = MemcachedBytecodeCache(memcached) - b = Bucket(None, 'key', '') - b.code = 'code' + b = Bucket(None, "key", "") + b.code = "code" m.dump_bytecode(b) - assert memcached.key == 'jinja2/bytecode/key' + assert memcached.key == "jinja2/bytecode/key" - b = Bucket(None, 'key', '') + b = Bucket(None, "key", "") m.load_bytecode(b) - assert b.code == 'code' + assert b.code == "code" def test_exception(self): memcached = MockMemcached() memcached.get = memcached.get_side_effect memcached.set = memcached.set_side_effect m = MemcachedBytecodeCache(memcached) - b = Bucket(None, 'key', '') - b.code = 'code' + b = Bucket(None, "key", "") + b.code = "code" m.dump_bytecode(b) m.load_bytecode(b) diff --git a/tests/test_core_tags.py b/tests/test_core_tags.py index 3b51e97..1bd96c4 100644 --- a/tests/test_core_tags.py +++ b/tests/test_core_tags.py @@ -1,16 +1,11 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.core_tags - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Test the core tags like for and if. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from jinja2 import Environment, TemplateSyntaxError, TemplateRuntimeError, \ - UndefinedError, DictLoader + +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import TemplateRuntimeError +from jinja2 import TemplateSyntaxError +from jinja2 import UndefinedError @pytest.fixture @@ -18,73 +13,90 @@ def env_trim(): return Environment(trim_blocks=True) -@pytest.mark.core_tags -@pytest.mark.for_loop class TestForLoop(object): - def test_simple(self, env): - tmpl = env.from_string('{% for item in seq %}{{ item }}{% endfor %}') - assert tmpl.render(seq=list(range(10))) == '0123456789' + tmpl = env.from_string("{% for item in seq %}{{ item }}{% endfor %}") + assert tmpl.render(seq=list(range(10))) == "0123456789" def test_else(self, env): - tmpl = env.from_string( - '{% for item in seq %}XXX{% else %}...{% endfor %}') - assert tmpl.render() == '...' + tmpl = env.from_string("{% for item in seq %}XXX{% else %}...{% endfor %}") + assert tmpl.render() == "..." def test_else_scoping_item(self, env): - tmpl = env.from_string( - '{% for item in [] %}{% else %}{{ item }}{% endfor %}') - assert tmpl.render(item=42) == '42' + tmpl = env.from_string("{% for item in [] %}{% else %}{{ item }}{% endfor %}") + assert tmpl.render(item=42) == "42" def test_empty_blocks(self, env): - tmpl = env.from_string('<{% for item in seq %}{% else %}{% endfor %}>') - assert tmpl.render() == '<>' + tmpl = env.from_string("<{% for item in seq %}{% else %}{% endfor %}>") + assert tmpl.render() == "<>" def test_context_vars(self, env): slist = [42, 24] for seq in [slist, iter(slist), reversed(slist), (_ for _ in slist)]: - tmpl = env.from_string('''{% for item in seq -%} + tmpl = env.from_string( + """{% for item in seq -%} {{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{ - loop.length }}###{% endfor %}''') - one, two, _ = tmpl.render(seq=seq).split('###') - (one_index, one_index0, one_revindex, one_revindex0, one_first, - one_last, one_length) = one.split('|') - (two_index, two_index0, two_revindex, two_revindex0, two_first, - two_last, two_length) = two.split('|') + loop.length }}###{% endfor %}""" + ) + one, two, _ = tmpl.render(seq=seq).split("###") + ( + one_index, + one_index0, + one_revindex, + one_revindex0, + one_first, + one_last, + one_length, + ) = one.split("|") + ( + two_index, + two_index0, + two_revindex, + two_revindex0, + two_first, + two_last, + two_length, + ) = two.split("|") assert int(one_index) == 1 and int(two_index) == 2 assert int(one_index0) == 0 and int(two_index0) == 1 assert int(one_revindex) == 2 and int(two_revindex) == 1 assert int(one_revindex0) == 1 and int(two_revindex0) == 0 - assert one_first == 'True' and two_first == 'False' - assert one_last == 'False' and two_last == 'True' - assert one_length == two_length == '2' + assert one_first == "True" and two_first == "False" + assert one_last == "False" and two_last == "True" + assert one_length == two_length == "2" def test_cycling(self, env): - tmpl = env.from_string('''{% for item in seq %}{{ + tmpl = env.from_string( + """{% for item in seq %}{{ loop.cycle('<1>', '<2>') }}{% endfor %}{% - for item in seq %}{{ loop.cycle(*through) }}{% endfor %}''') - output = tmpl.render(seq=list(range(4)), through=('<1>', '<2>')) - assert output == '<1><2>' * 4 + for item in seq %}{{ loop.cycle(*through) }}{% endfor %}""" + ) + output = tmpl.render(seq=list(range(4)), through=("<1>", "<2>")) + assert output == "<1><2>" * 4 def test_lookaround(self, env): - tmpl = env.from_string('''{% for item in seq -%} + tmpl = env.from_string( + """{% for item in seq -%} {{ loop.previtem|default('x') }}-{{ item }}-{{ loop.nextitem|default('x') }}| - {%- endfor %}''') + {%- endfor %}""" + ) output = tmpl.render(seq=list(range(4))) - assert output == 'x-0-1|0-1-2|1-2-3|2-3-x|' + assert output == "x-0-1|0-1-2|1-2-3|2-3-x|" def test_changed(self, env): - tmpl = env.from_string('''{% for item in seq -%} + tmpl = env.from_string( + """{% for item in seq -%} {{ loop.changed(item) }}, - {%- endfor %}''') + {%- endfor %}""" + ) output = tmpl.render(seq=[None, None, 1, 2, 2, 3, 4, 4, 4]) - assert output == 'True,False,True,True,False,True,True,False,False,' + assert output == "True,False,True,True,False,True,True,False,False," def test_scope(self, env): - tmpl = env.from_string('{% for item in seq %}{% endfor %}{{ item }}') + tmpl = env.from_string("{% for item in seq %}{% endfor %}{{ item }}") output = tmpl.render(seq=list(range(10))) assert not output @@ -92,115 +104,163 @@ class TestForLoop(object): def inner(): for item in range(5): yield item - tmpl = env.from_string('{% for item in iter %}{{ item }}{% endfor %}') + + tmpl = env.from_string("{% for item in iter %}{{ item }}{% endfor %}") output = tmpl.render(iter=inner()) - assert output == '01234' + assert output == "01234" def test_noniter(self, env): - tmpl = env.from_string('{% for item in none %}...{% endfor %}') + tmpl = env.from_string("{% for item in none %}...{% endfor %}") pytest.raises(TypeError, tmpl.render) def test_recursive(self, env): - tmpl = env.from_string('''{% for item in seq recursive -%} + tmpl = env.from_string( + """{% for item in seq recursive -%} [{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]' + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[1<[1][2]>][2<[1][2]>][3<[a]>]" + ) def test_recursive_lookaround(self, env): - tmpl = env.from_string('''{% for item in seq recursive -%} + tmpl = env.from_string( + """{% for item in seq recursive -%} [{{ loop.previtem.a if loop.previtem is defined else 'x' }}.{{ item.a }}.{{ loop.nextitem.a if loop.nextitem is defined else 'x' }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]' + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]" + ) def test_recursive_depth0(self, env): - tmpl = env.from_string('''{% for item in seq recursive -%} - [{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[0:1<[1:1][1:2]>][0:2<[1:1][1:2]>][0:3<[1:a]>]' + tmpl = env.from_string( + """{% for item in seq recursive -%} + [{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[0:1<[1:1][1:2]>][0:2<[1:1][1:2]>][0:3<[1:a]>]" + ) def test_recursive_depth(self, env): - tmpl = env.from_string('''{% for item in seq recursive -%} - [{{ loop.depth }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[1:1<[2:1][2:2]>][1:2<[2:1][2:2]>][1:3<[2:a]>]' + tmpl = env.from_string( + """{% for item in seq recursive -%} + [{{ loop.depth }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] + {%- endfor %}""" + ) + assert ( + tmpl.render( + seq=[ + dict(a=1, b=[dict(a=1), dict(a=2)]), + dict(a=2, b=[dict(a=1), dict(a=2)]), + dict(a=3, b=[dict(a="a")]), + ] + ) + == "[1:1<[2:1][2:2]>][1:2<[2:1][2:2]>][1:3<[2:a]>]" + ) def test_looploop(self, env): - tmpl = env.from_string('''{% for row in table %} + tmpl = env.from_string( + """{% for row in table %} {%- set rowloop = loop -%} {% for cell in row -%} [{{ rowloop.index }}|{{ loop.index }}] {%- endfor %} - {%- endfor %}''') - assert tmpl.render(table=['ab', 'cd']) == '[1|1][1|2][2|1][2|2]' + {%- endfor %}""" + ) + assert tmpl.render(table=["ab", "cd"]) == "[1|1][1|2][2|1][2|2]" def test_reversed_bug(self, env): - tmpl = env.from_string('{% for i in items %}{{ i }}' - '{% if not loop.last %}' - ',{% endif %}{% endfor %}') - assert tmpl.render(items=reversed([3, 2, 1])) == '1,2,3' + tmpl = env.from_string( + "{% for i in items %}{{ i }}" + "{% if not loop.last %}" + ",{% endif %}{% endfor %}" + ) + assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3" def test_loop_errors(self, env): - tmpl = env.from_string('''{% for item in [1] if loop.index - == 0 %}...{% endfor %}''') + tmpl = env.from_string( + """{% for item in [1] if loop.index + == 0 %}...{% endfor %}""" + ) pytest.raises(UndefinedError, tmpl.render) - tmpl = env.from_string('''{% for item in [] %}...{% else - %}{{ loop }}{% endfor %}''') - assert tmpl.render() == '' + tmpl = env.from_string( + """{% for item in [] %}...{% else + %}{{ loop }}{% endfor %}""" + ) + assert tmpl.render() == "" def test_loop_filter(self, env): - tmpl = env.from_string('{% for item in range(10) if item ' - 'is even %}[{{ item }}]{% endfor %}') - assert tmpl.render() == '[0][2][4][6][8]' - tmpl = env.from_string(''' + tmpl = env.from_string( + "{% for item in range(10) if item is even %}[{{ item }}]{% endfor %}" + ) + assert tmpl.render() == "[0][2][4][6][8]" + tmpl = env.from_string( + """ {%- for item in range(10) if item is even %}[{{ - loop.index }}:{{ item }}]{% endfor %}''') - assert tmpl.render() == '[1:0][2:2][3:4][4:6][5:8]' + loop.index }}:{{ item }}]{% endfor %}""" + ) + assert tmpl.render() == "[1:0][2:2][3:4][4:6][5:8]" def test_loop_unassignable(self, env): - pytest.raises(TemplateSyntaxError, env.from_string, - '{% for loop in seq %}...{% endfor %}') + pytest.raises( + TemplateSyntaxError, env.from_string, "{% for loop in seq %}...{% endfor %}" + ) def test_scoped_special_var(self, env): t = env.from_string( - '{% for s in seq %}[{{ loop.first }}{% for c in s %}' - '|{{ loop.first }}{% endfor %}]{% endfor %}') - assert t.render(seq=('ab', 'cd')) \ - == '[True|True|False][False|True|False]' + "{% for s in seq %}[{{ loop.first }}{% for c in s %}" + "|{{ loop.first }}{% endfor %}]{% endfor %}" + ) + assert t.render(seq=("ab", "cd")) == "[True|True|False][False|True|False]" def test_scoped_loop_var(self, env): - t = env.from_string('{% for x in seq %}{{ loop.first }}' - '{% for y in seq %}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalse' - t = env.from_string('{% for x in seq %}{% for y in seq %}' - '{{ loop.first }}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalseTrueFalse' + t = env.from_string( + "{% for x in seq %}{{ loop.first }}" + "{% for y in seq %}{% endfor %}{% endfor %}" + ) + assert t.render(seq="ab") == "TrueFalse" + t = env.from_string( + "{% for x in seq %}{% for y in seq %}" + "{{ loop.first }}{% endfor %}{% endfor %}" + ) + assert t.render(seq="ab") == "TrueFalseTrueFalse" def test_recursive_empty_loop_iter(self, env): - t = env.from_string(''' + t = env.from_string( + """ {%- for item in foo recursive -%}{%- endfor -%} - ''') - assert t.render(dict(foo=[])) == '' + """ + ) + assert t.render(dict(foo=[])) == "" def test_call_in_loop(self, env): - t = env.from_string(''' + t = env.from_string( + """ {%- macro do_something() -%} [{{ caller() }}] {%- endmacro %} @@ -210,150 +270,175 @@ class TestForLoop(object): {{ i }} {%- endcall %} {%- endfor -%} - ''') - assert t.render() == '[1][2][3]' + """ + ) + assert t.render() == "[1][2][3]" def test_scoping_bug(self, env): - t = env.from_string(''' + t = env.from_string( + """ {%- for item in foo %}...{{ item }}...{% endfor %} {%- macro item(a) %}...{{ a }}...{% endmacro %} {{- item(2) -}} - ''') - assert t.render(foo=(1,)) == '...1......2...' + """ + ) + assert t.render(foo=(1,)) == "...1......2..." def test_unpacking(self, env): - tmpl = env.from_string('{% for a, b, c in [[1, 2, 3]] %}' - '{{ a }}|{{ b }}|{{ c }}{% endfor %}') - assert tmpl.render() == '1|2|3' + tmpl = env.from_string( + "{% for a, b, c in [[1, 2, 3]] %}{{ a }}|{{ b }}|{{ c }}{% endfor %}" + ) + assert tmpl.render() == "1|2|3" def test_intended_scoping_with_set(self, env): - tmpl = env.from_string('{% for item in seq %}{{ x }}' - '{% set x = item %}{{ x }}{% endfor %}') - assert tmpl.render(x=0, seq=[1, 2, 3]) == '010203' + tmpl = env.from_string( + "{% for item in seq %}{{ x }}{% set x = item %}{{ x }}{% endfor %}" + ) + assert tmpl.render(x=0, seq=[1, 2, 3]) == "010203" - tmpl = env.from_string('{% set x = 9 %}{% for item in seq %}{{ x }}' - '{% set x = item %}{{ x }}{% endfor %}') - assert tmpl.render(x=0, seq=[1, 2, 3]) == '919293' + tmpl = env.from_string( + "{% set x = 9 %}{% for item in seq %}{{ x }}" + "{% set x = item %}{{ x }}{% endfor %}" + ) + assert tmpl.render(x=0, seq=[1, 2, 3]) == "919293" -@pytest.mark.core_tags -@pytest.mark.if_condition class TestIfCondition(object): - def test_simple(self, env): - tmpl = env.from_string('''{% if true %}...{% endif %}''') - assert tmpl.render() == '...' + tmpl = env.from_string("""{% if true %}...{% endif %}""") + assert tmpl.render() == "..." def test_elif(self, env): - tmpl = env.from_string('''{% if false %}XXX{% elif true - %}...{% else %}XXX{% endif %}''') - assert tmpl.render() == '...' + tmpl = env.from_string( + """{% if false %}XXX{% elif true + %}...{% else %}XXX{% endif %}""" + ) + assert tmpl.render() == "..." def test_elif_deep(self, env): - elifs = '\n'.join('{{% elif a == {0} %}}{0}'.format(i) - for i in range(1, 1000)) - tmpl = env.from_string('{{% if a == 0 %}}0{0}{{% else %}}x{{% endif %}}' - .format(elifs)) + elifs = "\n".join("{{% elif a == {0} %}}{0}".format(i) for i in range(1, 1000)) + tmpl = env.from_string( + "{{% if a == 0 %}}0{0}{{% else %}}x{{% endif %}}".format(elifs) + ) for x in (0, 10, 999): assert tmpl.render(a=x).strip() == str(x) - assert tmpl.render(a=1000).strip() == 'x' + assert tmpl.render(a=1000).strip() == "x" def test_else(self, env): - tmpl = env.from_string('{% if false %}XXX{% else %}...{% endif %}') - assert tmpl.render() == '...' + tmpl = env.from_string("{% if false %}XXX{% else %}...{% endif %}") + assert tmpl.render() == "..." def test_empty(self, env): - tmpl = env.from_string('[{% if true %}{% else %}{% endif %}]') - assert tmpl.render() == '[]' + tmpl = env.from_string("[{% if true %}{% else %}{% endif %}]") + assert tmpl.render() == "[]" def test_complete(self, env): - tmpl = env.from_string('{% if a %}A{% elif b %}B{% elif c == d %}' - 'C{% else %}D{% endif %}') - assert tmpl.render(a=0, b=False, c=42, d=42.0) == 'C' + tmpl = env.from_string( + "{% if a %}A{% elif b %}B{% elif c == d %}C{% else %}D{% endif %}" + ) + assert tmpl.render(a=0, b=False, c=42, d=42.0) == "C" def test_no_scope(self, env): - tmpl = env.from_string( - '{% if a %}{% set foo = 1 %}{% endif %}{{ foo }}') - assert tmpl.render(a=True) == '1' - tmpl = env.from_string( - '{% if true %}{% set foo = 1 %}{% endif %}{{ foo }}') - assert tmpl.render() == '1' + tmpl = env.from_string("{% if a %}{% set foo = 1 %}{% endif %}{{ foo }}") + assert tmpl.render(a=True) == "1" + tmpl = env.from_string("{% if true %}{% set foo = 1 %}{% endif %}{{ foo }}") + assert tmpl.render() == "1" -@pytest.mark.core_tags -@pytest.mark.macros class TestMacros(object): def test_simple(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro say_hello(name) %}Hello {{ name }}!{% endmacro %} -{{ say_hello('Peter') }}''') - assert tmpl.render() == 'Hello Peter!' +{{ say_hello('Peter') }}""" + ) + assert tmpl.render() == "Hello Peter!" def test_scoping(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro level1(data1) %} {% macro level2(data2) %}{{ data1 }}|{{ data2 }}{% endmacro %} {{ level2('bar') }}{% endmacro %} -{{ level1('foo') }}''') - assert tmpl.render() == 'foo|bar' +{{ level1('foo') }}""" + ) + assert tmpl.render() == "foo|bar" def test_arguments(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro m(a, b, c='c', d='d') %}{{ a }}|{{ b }}|{{ c }}|{{ d }}{% endmacro %} -{{ m() }}|{{ m('a') }}|{{ m('a', 'b') }}|{{ m(1, 2, 3) }}''') - assert tmpl.render() == '||c|d|a||c|d|a|b|c|d|1|2|3|d' +{{ m() }}|{{ m('a') }}|{{ m('a', 'b') }}|{{ m(1, 2, 3) }}""" + ) + assert tmpl.render() == "||c|d|a||c|d|a|b|c|d|1|2|3|d" def test_arguments_defaults_nonsense(self, env_trim): - pytest.raises(TemplateSyntaxError, env_trim.from_string, '''\ -{% macro m(a, b=1, c) %}a={{ a }}, b={{ b }}, c={{ c }}{% endmacro %}''') + pytest.raises( + TemplateSyntaxError, + env_trim.from_string, + """\ +{% macro m(a, b=1, c) %}a={{ a }}, b={{ b }}, c={{ c }}{% endmacro %}""", + ) def test_caller_defaults_nonsense(self, env_trim): - pytest.raises(TemplateSyntaxError, env_trim.from_string, '''\ + pytest.raises( + TemplateSyntaxError, + env_trim.from_string, + """\ {% macro a() %}{{ caller() }}{% endmacro %} -{% call(x, y=1, z) a() %}{% endcall %}''') +{% call(x, y=1, z) a() %}{% endcall %}""", + ) def test_varargs(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro test() %}{{ varargs|join('|') }}{% endmacro %}\ -{{ test(1, 2, 3) }}''') - assert tmpl.render() == '1|2|3' +{{ test(1, 2, 3) }}""" + ) + assert tmpl.render() == "1|2|3" def test_simple_call(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro test() %}[[{{ caller() }}]]{% endmacro %}\ -{% call test() %}data{% endcall %}''') - assert tmpl.render() == '[[data]]' +{% call test() %}data{% endcall %}""" + ) + assert tmpl.render() == "[[data]]" def test_complex_call(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% macro test() %}[[{{ caller('data') }}]]{% endmacro %}\ -{% call(data) test() %}{{ data }}{% endcall %}''') - assert tmpl.render() == '[[data]]' +{% call(data) test() %}{{ data }}{% endcall %}""" + ) + assert tmpl.render() == "[[data]]" def test_caller_undefined(self, env_trim): - tmpl = env_trim.from_string('''\ + tmpl = env_trim.from_string( + """\ {% set caller = 42 %}\ {% macro test() %}{{ caller is not defined }}{% endmacro %}\ -{{ test() }}''') - assert tmpl.render() == 'True' +{{ test() }}""" + ) + assert tmpl.render() == "True" def test_include(self, env_trim): env_trim = Environment( - loader=DictLoader({ - 'include': '{% macro test(foo) %}[{{ foo }}]{% endmacro %}' - }) + loader=DictLoader( + {"include": "{% macro test(foo) %}[{{ foo }}]{% endmacro %}"} + ) ) - tmpl = env_trim.from_string( - '{% from "include" import test %}{{ test("foo") }}') - assert tmpl.render() == '[foo]' + tmpl = env_trim.from_string('{% from "include" import test %}{{ test("foo") }}') + assert tmpl.render() == "[foo]" def test_macro_api(self, env_trim): tmpl = env_trim.from_string( - '{% macro foo(a, b) %}{% endmacro %}' - '{% macro bar() %}{{ varargs }}{{ kwargs }}{% endmacro %}' - '{% macro baz() %}{{ caller() }}{% endmacro %}') - assert tmpl.module.foo.arguments == ('a', 'b') - assert tmpl.module.foo.name == 'foo' + "{% macro foo(a, b) %}{% endmacro %}" + "{% macro bar() %}{{ varargs }}{{ kwargs }}{% endmacro %}" + "{% macro baz() %}{{ caller() }}{% endmacro %}" + ) + assert tmpl.module.foo.arguments == ("a", "b") + assert tmpl.module.foo.name == "foo" assert not tmpl.module.foo.caller assert not tmpl.module.foo.catch_kwargs assert not tmpl.module.foo.catch_varargs @@ -364,141 +449,154 @@ class TestMacros(object): assert tmpl.module.baz.caller def test_callself(self, env_trim): - tmpl = env_trim.from_string('{% macro foo(x) %}{{ x }}{% if x > 1 %}|' - '{{ foo(x - 1) }}{% endif %}{% endmacro %}' - '{{ foo(5) }}') - assert tmpl.render() == '5|4|3|2|1' + tmpl = env_trim.from_string( + "{% macro foo(x) %}{{ x }}{% if x > 1 %}|" + "{{ foo(x - 1) }}{% endif %}{% endmacro %}" + "{{ foo(5) }}" + ) + assert tmpl.render() == "5|4|3|2|1" def test_macro_defaults_self_ref(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- set x = 42 %} {%- macro m(a, b=x, x=23) %}{{ a }}|{{ b }}|{{ x }}{% endmacro -%} - ''') - assert tmpl.module.m(1) == '1||23' - assert tmpl.module.m(1, 2) == '1|2|23' - assert tmpl.module.m(1, 2, 3) == '1|2|3' - assert tmpl.module.m(1, x=7) == '1|7|7' + """ + ) + assert tmpl.module.m(1) == "1||23" + assert tmpl.module.m(1, 2) == "1|2|23" + assert tmpl.module.m(1, 2, 3) == "1|2|3" + assert tmpl.module.m(1, x=7) == "1|7|7" -@pytest.mark.core_tags -@pytest.mark.set class TestSet(object): - def test_normal(self, env_trim): - tmpl = env_trim.from_string('{% set foo = 1 %}{{ foo }}') - assert tmpl.render() == '1' + tmpl = env_trim.from_string("{% set foo = 1 %}{{ foo }}") + assert tmpl.render() == "1" assert tmpl.module.foo == 1 def test_block(self, env_trim): - tmpl = env_trim.from_string('{% set foo %}42{% endset %}{{ foo }}') - assert tmpl.render() == '42' - assert tmpl.module.foo == u'42' + tmpl = env_trim.from_string("{% set foo %}42{% endset %}{{ foo }}") + assert tmpl.render() == "42" + assert tmpl.module.foo == u"42" def test_block_escaping(self): env = Environment(autoescape=True) - tmpl = env.from_string('{% set foo %}<em>{{ test }}</em>' - '{% endset %}foo: {{ foo }}') - assert tmpl.render(test='<unsafe>') == 'foo: <em><unsafe></em>' + tmpl = env.from_string( + "{% set foo %}<em>{{ test }}</em>{% endset %}foo: {{ foo }}" + ) + assert tmpl.render(test="<unsafe>") == "foo: <em><unsafe></em>" def test_set_invalid(self, env_trim): - pytest.raises(TemplateSyntaxError, env_trim.from_string, - "{% set foo['bar'] = 1 %}") - tmpl = env_trim.from_string('{% set foo.bar = 1 %}') + pytest.raises( + TemplateSyntaxError, env_trim.from_string, "{% set foo['bar'] = 1 %}" + ) + tmpl = env_trim.from_string("{% set foo.bar = 1 %}") exc_info = pytest.raises(TemplateRuntimeError, tmpl.render, foo={}) - assert 'non-namespace object' in exc_info.value.message + assert "non-namespace object" in exc_info.value.message def test_namespace_redefined(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace() %}' - '{% set ns.bar = "hi" %}') - exc_info = pytest.raises(TemplateRuntimeError, tmpl.render, - namespace=dict) - assert 'non-namespace object' in exc_info.value.message + tmpl = env_trim.from_string("{% set ns = namespace() %}{% set ns.bar = 'hi' %}") + exc_info = pytest.raises(TemplateRuntimeError, tmpl.render, namespace=dict) + assert "non-namespace object" in exc_info.value.message def test_namespace(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace() %}' - '{% set ns.bar = "42" %}' - '{{ ns.bar }}') - assert tmpl.render() == '42' + tmpl = env_trim.from_string( + "{% set ns = namespace() %}{% set ns.bar = '42' %}{{ ns.bar }}" + ) + assert tmpl.render() == "42" def test_namespace_block(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace() %}' - '{% set ns.bar %}42{% endset %}' - '{{ ns.bar }}') - assert tmpl.render() == '42' + tmpl = env_trim.from_string( + "{% set ns = namespace() %}{% set ns.bar %}42{% endset %}{{ ns.bar }}" + ) + assert tmpl.render() == "42" def test_init_namespace(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace(d, self=37) %}' - '{% set ns.b = 42 %}' - '{{ ns.a }}|{{ ns.self }}|{{ ns.b }}') - assert tmpl.render(d={'a': 13}) == '13|37|42' + tmpl = env_trim.from_string( + "{% set ns = namespace(d, self=37) %}" + "{% set ns.b = 42 %}" + "{{ ns.a }}|{{ ns.self }}|{{ ns.b }}" + ) + assert tmpl.render(d={"a": 13}) == "13|37|42" def test_namespace_loop(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace(found=false) %}' - '{% for x in range(4) %}' - '{% if x == v %}' - '{% set ns.found = true %}' - '{% endif %}' - '{% endfor %}' - '{{ ns.found }}') - assert tmpl.render(v=3) == 'True' - assert tmpl.render(v=4) == 'False' + tmpl = env_trim.from_string( + "{% set ns = namespace(found=false) %}" + "{% for x in range(4) %}" + "{% if x == v %}" + "{% set ns.found = true %}" + "{% endif %}" + "{% endfor %}" + "{{ ns.found }}" + ) + assert tmpl.render(v=3) == "True" + assert tmpl.render(v=4) == "False" def test_namespace_macro(self, env_trim): - tmpl = env_trim.from_string('{% set ns = namespace() %}' - '{% set ns.a = 13 %}' - '{% macro magic(x) %}' - '{% set x.b = 37 %}' - '{% endmacro %}' - '{{ magic(ns) }}' - '{{ ns.a }}|{{ ns.b }}') - assert tmpl.render() == '13|37' + tmpl = env_trim.from_string( + "{% set ns = namespace() %}" + "{% set ns.a = 13 %}" + "{% macro magic(x) %}" + "{% set x.b = 37 %}" + "{% endmacro %}" + "{{ magic(ns) }}" + "{{ ns.a }}|{{ ns.b }}" + ) + assert tmpl.render() == "13|37" def test_block_escaping_filtered(self): env = Environment(autoescape=True) - tmpl = env.from_string('{% set foo | trim %}<em>{{ test }}</em> ' - '{% endset %}foo: {{ foo }}') - assert tmpl.render(test='<unsafe>') == 'foo: <em><unsafe></em>' + tmpl = env.from_string( + "{% set foo | trim %}<em>{{ test }}</em> {% endset %}foo: {{ foo }}" + ) + assert tmpl.render(test="<unsafe>") == "foo: <em><unsafe></em>" def test_block_filtered(self, env_trim): tmpl = env_trim.from_string( - '{% set foo | trim | length | string %} 42 {% endset %}' - '{{ foo }}') - assert tmpl.render() == '2' - assert tmpl.module.foo == u'2' + "{% set foo | trim | length | string %} 42 {% endset %}{{ foo }}" + ) + assert tmpl.render() == "2" + assert tmpl.module.foo == u"2" def test_block_filtered_set(self, env_trim): def _myfilter(val, arg): - assert arg == ' xxx ' + assert arg == " xxx " return val - env_trim.filters['myfilter'] = _myfilter + + env_trim.filters["myfilter"] = _myfilter tmpl = env_trim.from_string( '{% set a = " xxx " %}' - '{% set foo | myfilter(a) | trim | length | string %}' + "{% set foo | myfilter(a) | trim | length | string %}" ' {% set b = " yy " %} 42 {{ a }}{{ b }} ' - '{% endset %}' - '{{ foo }}') - assert tmpl.render() == '11' - assert tmpl.module.foo == u'11' + "{% endset %}" + "{{ foo }}" + ) + assert tmpl.render() == "11" + assert tmpl.module.foo == u"11" -@pytest.mark.core_tags -@pytest.mark.with_ class TestWith(object): - def test_with(self, env): - tmpl = env.from_string('''\ + tmpl = env.from_string( + """\ {% with a=42, b=23 -%} {{ a }} = {{ b }} {% endwith -%} {{ a }} = {{ b }}\ - ''') - assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \ - == ['42 = 23', '1 = 2'] + """ + ) + assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] == [ + "42 = 23", + "1 = 2", + ] def test_with_argument_scoping(self, env): - tmpl = env.from_string('''\ + tmpl = env.from_string( + """\ {%- with a=1, b=2, c=b, d=e, e=5 -%} {{ a }}|{{ b }}|{{ c }}|{{ d }}|{{ e }} {%- endwith -%} - ''') - assert tmpl.render(b=3, e=4) == '1|2|3|4|5' + """ + ) + assert tmpl.render(b=3, e=4) == "1|2|3|4|5" diff --git a/tests/test_debug.py b/tests/test_debug.py index a158524..284b9e9 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -1,84 +1,116 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.debug - ~~~~~~~~~~~~~~~~~~~~~~ - - Tests the debug system. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import pytest - +import pickle import re -import sys from traceback import format_exception -from jinja2 import Environment, TemplateSyntaxError +import pytest + +from jinja2 import ChoiceLoader +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import TemplateSyntaxError @pytest.fixture def fs_env(filesystem_loader): - '''returns a new environment. - ''' + """returns a new environment.""" return Environment(loader=filesystem_loader) -@pytest.mark.debug class TestDebug(object): - def assert_traceback_matches(self, callback, expected_tb): - try: + with pytest.raises(Exception) as exc_info: callback() - except Exception as e: - tb = format_exception(*sys.exc_info()) - if re.search(expected_tb.strip(), ''.join(tb)) is None: - assert False, ('Traceback did not match:\n\n%s\nexpected:\n%s' % - (''.join(tb), expected_tb)) - else: - assert False, 'Expected exception' + + tb = format_exception(exc_info.type, exc_info.value, exc_info.tb) + m = re.search(expected_tb.strip(), "".join(tb)) + assert m is not None, "Traceback did not match:\n\n%s\nexpected:\n%s" % ( + "".join(tb), + expected_tb, + ) def test_runtime_error(self, fs_env): def test(): tmpl.render(fail=lambda: 1 / 0) - tmpl = fs_env.get_template('broken.html') - self.assert_traceback_matches(test, r''' + + tmpl = fs_env.get_template("broken.html") + self.assert_traceback_matches( + test, + r""" File ".*?broken.html", line 2, in (top-level template code|<module>) \{\{ fail\(\) \}\} File ".*debug?.pyc?", line \d+, in <lambda> tmpl\.render\(fail=lambda: 1 / 0\) ZeroDivisionError: (int(eger)? )?division (or modulo )?by zero -''') +""", + ) def test_syntax_error(self, fs_env): - # XXX: the .*? is necessary for python3 which does not hide - # some of the stack frames we don't want to show. Not sure - # what's up with that, but that is not that critical. Should - # be fixed though. - self.assert_traceback_matches(lambda: fs_env.get_template('syntaxerror.html'), r'''(?sm) + # The trailing .*? is for PyPy 2 and 3, which don't seem to + # clear the exception's original traceback, leaving the syntax + # error in the middle of other compiler frames. + self.assert_traceback_matches( + lambda: fs_env.get_template("syntaxerror.html"), + """(?sm) File ".*?syntaxerror.html", line 4, in (template|<module>) - \{% endif %\}.*? -(jinja2\.exceptions\.)?TemplateSyntaxError: Encountered unknown tag 'endif'. Jinja was looking for the following tags: 'endfor' or 'else'. The innermost block that needs to be closed is 'for'. - ''') + \\{% endif %\\}.*? +(jinja2\\.exceptions\\.)?TemplateSyntaxError: Encountered unknown tag 'endif'. Jinja \ +was looking for the following tags: 'endfor' or 'else'. The innermost block that needs \ +to be closed is 'for'. + """, + ) def test_regular_syntax_error(self, fs_env): def test(): - raise TemplateSyntaxError('wtf', 42) - self.assert_traceback_matches(test, r''' + raise TemplateSyntaxError("wtf", 42) + + self.assert_traceback_matches( + test, + r""" File ".*debug.pyc?", line \d+, in test - raise TemplateSyntaxError\('wtf', 42\) + raise TemplateSyntaxError\("wtf", 42\) (jinja2\.exceptions\.)?TemplateSyntaxError: wtf - line 42''') + line 42""", + ) + + def test_pickleable_syntax_error(self, fs_env): + original = TemplateSyntaxError("bad template", 42, "test", "test.txt") + unpickled = pickle.loads(pickle.dumps(original)) + assert str(original) == str(unpickled) + assert original.name == unpickled.name + + def test_include_syntax_error_source(self, filesystem_loader): + e = Environment( + loader=ChoiceLoader( + [ + filesystem_loader, + DictLoader({"inc": "a\n{% include 'syntaxerror.html' %}\nb"}), + ] + ) + ) + t = e.get_template("inc") + + with pytest.raises(TemplateSyntaxError) as exc_info: + t.render() + + assert exc_info.value.source is not None def test_local_extraction(self): - from jinja2.debug import get_jinja_locals + from jinja2.debug import get_template_locals from jinja2.runtime import missing - locals = get_jinja_locals({ - 'l_0_foo': 42, - 'l_1_foo': 23, - 'l_2_foo': 13, - 'l_0_bar': 99, - 'l_1_bar': missing, - 'l_0_baz': missing, - }) - assert locals == {'foo': 13, 'bar': 99} + + locals = get_template_locals( + { + "l_0_foo": 42, + "l_1_foo": 23, + "l_2_foo": 13, + "l_0_bar": 99, + "l_1_bar": missing, + "l_0_baz": missing, + } + ) + assert locals == {"foo": 13, "bar": 99} + + def test_get_corresponding_lineno_traceback(self, fs_env): + tmpl = fs_env.get_template("test.html") + assert tmpl.get_corresponding_lineno(1) == 1 diff --git a/tests/test_ext.py b/tests/test_ext.py index c3b028f..8e4b411 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -1,143 +1,144 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.ext - ~~~~~~~~~~~~~~~~~~~~ - - Tests for the extensions. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import re + import pytest -from jinja2 import Environment, DictLoader, contextfunction, nodes +from jinja2 import contextfunction +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import nodes +from jinja2._compat import BytesIO +from jinja2._compat import itervalues +from jinja2._compat import text_type from jinja2.exceptions import TemplateAssertionError from jinja2.ext import Extension -from jinja2.lexer import Token, count_newlines -from jinja2._compat import BytesIO, itervalues, text_type +from jinja2.lexer import count_newlines +from jinja2.lexer import Token importable_object = 23 -_gettext_re = re.compile(r'_\((.*?)\)', re.DOTALL) +_gettext_re = re.compile(r"_\((.*?)\)", re.DOTALL) i18n_templates = { - 'master.html': '<title>{{ page_title|default(_("missing")) }}</title>' - '{% block body %}{% endblock %}', - 'child.html': '{% extends "master.html" %}{% block body %}' - '{% trans %}watch out{% endtrans %}{% endblock %}', - 'plural.html': '{% trans user_count %}One user online{% pluralize %}' - '{{ user_count }} users online{% endtrans %}', - 'plural2.html': '{% trans user_count=get_user_count() %}{{ user_count }}s' - '{% pluralize %}{{ user_count }}p{% endtrans %}', - 'stringformat.html': '{{ _("User: %(num)s")|format(num=user_count) }}' + "master.html": '<title>{{ page_title|default(_("missing")) }}</title>' + "{% block body %}{% endblock %}", + "child.html": '{% extends "master.html" %}{% block body %}' + "{% trans %}watch out{% endtrans %}{% endblock %}", + "plural.html": "{% trans user_count %}One user online{% pluralize %}" + "{{ user_count }} users online{% endtrans %}", + "plural2.html": "{% trans user_count=get_user_count() %}{{ user_count }}s" + "{% pluralize %}{{ user_count }}p{% endtrans %}", + "stringformat.html": '{{ _("User: %(num)s")|format(num=user_count) }}', } newstyle_i18n_templates = { - 'master.html': '<title>{{ page_title|default(_("missing")) }}</title>' - '{% block body %}{% endblock %}', - 'child.html': '{% extends "master.html" %}{% block body %}' - '{% trans %}watch out{% endtrans %}{% endblock %}', - 'plural.html': '{% trans user_count %}One user online{% pluralize %}' - '{{ user_count }} users online{% endtrans %}', - 'stringformat.html': '{{ _("User: %(num)s", num=user_count) }}', - 'ngettext.html': '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}', - 'ngettext_long.html': '{% trans 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 %}', - 'novars.html': '{% trans %}%(hello)s{% endtrans %}', - 'vars.html': '{% trans %}{{ foo }}%(foo)s{% endtrans %}', - 'explicitvars.html': '{% trans foo="42" %}%(foo)s{% endtrans %}' + "master.html": '<title>{{ page_title|default(_("missing")) }}</title>' + "{% block body %}{% endblock %}", + "child.html": '{% extends "master.html" %}{% block body %}' + "{% trans %}watch out{% endtrans %}{% endblock %}", + "plural.html": "{% trans user_count %}One user online{% pluralize %}" + "{{ user_count }} users online{% endtrans %}", + "stringformat.html": '{{ _("User: %(num)s", num=user_count) }}', + "ngettext.html": '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}', + "ngettext_long.html": "{% trans 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 %}", + "novars.html": "{% trans %}%(hello)s{% endtrans %}", + "vars.html": "{% trans %}{{ foo }}%(foo)s{% endtrans %}", + "explicitvars.html": '{% trans foo="42" %}%(foo)s{% endtrans %}', } languages = { - 'de': { - 'missing': u'fehlend', - 'watch out': u'pass auf', - 'One user online': u'Ein Benutzer online', - '%(user_count)s users online': u'%(user_count)s Benutzer online', - 'User: %(num)s': u'Benutzer: %(num)s', - 'User: %(count)s': u'Benutzer: %(count)s', - '%(num)s apple': u'%(num)s Apfel', - '%(num)s apples': u'%(num)s Äpfel' + "de": { + "missing": u"fehlend", + "watch out": u"pass auf", + "One user online": u"Ein Benutzer online", + "%(user_count)s users online": u"%(user_count)s Benutzer online", + "User: %(num)s": u"Benutzer: %(num)s", + "User: %(count)s": u"Benutzer: %(count)s", + "%(num)s apple": u"%(num)s Apfel", + "%(num)s apples": u"%(num)s Äpfel", } } @contextfunction def gettext(context, string): - language = context.get('LANGUAGE', 'en') + language = context.get("LANGUAGE", "en") return languages.get(language, {}).get(string, string) @contextfunction def ngettext(context, s, p, n): - language = context.get('LANGUAGE', 'en') + language = context.get("LANGUAGE", "en") if n != 1: return languages.get(language, {}).get(p, p) return languages.get(language, {}).get(s, s) i18n_env = Environment( - loader=DictLoader(i18n_templates), - extensions=['jinja2.ext.i18n'] + loader=DictLoader(i18n_templates), extensions=["jinja2.ext.i18n"] +) +i18n_env.globals.update({"_": gettext, "gettext": gettext, "ngettext": ngettext}) +i18n_env_trimmed = Environment(extensions=["jinja2.ext.i18n"]) +i18n_env_trimmed.policies["ext.i18n.trimmed"] = True +i18n_env_trimmed.globals.update( + {"_": gettext, "gettext": gettext, "ngettext": ngettext} ) -i18n_env.globals.update({ - '_': gettext, - 'gettext': gettext, - 'ngettext': ngettext -}) -i18n_env_trimmed = Environment(extensions=['jinja2.ext.i18n']) -i18n_env_trimmed.policies['ext.i18n.trimmed'] = True -i18n_env_trimmed.globals.update({ - '_': gettext, - 'gettext': gettext, - 'ngettext': ngettext -}) newstyle_i18n_env = Environment( - loader=DictLoader(newstyle_i18n_templates), - extensions=['jinja2.ext.i18n'] + loader=DictLoader(newstyle_i18n_templates), extensions=["jinja2.ext.i18n"] ) newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True) class ExampleExtension(Extension): - tags = set(['test']) + tags = set(["test"]) ext_attr = 42 + context_reference_node_cls = nodes.ContextReference def parse(self, parser): - return nodes.Output([self.call_method('_dump', [ - nodes.EnvironmentAttribute('sandboxed'), - self.attr('ext_attr'), - nodes.ImportedName(__name__ + '.importable_object'), - nodes.ContextReference() - ])]).set_lineno(next(parser.stream).lineno) + return nodes.Output( + [ + self.call_method( + "_dump", + [ + nodes.EnvironmentAttribute("sandboxed"), + self.attr("ext_attr"), + nodes.ImportedName(__name__ + ".importable_object"), + self.context_reference_node_cls(), + ], + ) + ] + ).set_lineno(next(parser.stream).lineno) def _dump(self, sandboxed, ext_attr, imported_object, context): - return '%s|%s|%s|%s' % ( + return "%s|%s|%s|%s|%s" % ( sandboxed, ext_attr, imported_object, - context.blocks + context.blocks, + context.get("test_var"), ) -class PreprocessorExtension(Extension): +class DerivedExampleExtension(ExampleExtension): + context_reference_node_cls = nodes.DerivedContextReference + +class PreprocessorExtension(Extension): def preprocess(self, source, name, filename=None): - return source.replace('[[TEST]]', '({{ foo }})') + return source.replace("[[TEST]]", "({{ foo }})") class StreamFilterExtension(Extension): - def filter_stream(self, stream): for token in stream: - if token.type == 'data': + if token.type == "data": for t in self.interpolate(token): yield t else: @@ -151,64 +152,79 @@ class StreamFilterExtension(Extension): match = _gettext_re.search(token.value, pos) if match is None: break - value = token.value[pos:match.start()] + value = token.value[pos : match.start()] if value: - yield Token(lineno, 'data', value) + yield Token(lineno, "data", value) lineno += count_newlines(token.value) - yield Token(lineno, 'variable_begin', None) - yield Token(lineno, 'name', 'gettext') - yield Token(lineno, 'lparen', None) - yield Token(lineno, 'string', match.group(1)) - yield Token(lineno, 'rparen', None) - yield Token(lineno, 'variable_end', None) + yield Token(lineno, "variable_begin", None) + yield Token(lineno, "name", "gettext") + yield Token(lineno, "lparen", None) + yield Token(lineno, "string", match.group(1)) + yield Token(lineno, "rparen", None) + yield Token(lineno, "variable_end", None) pos = match.end() if pos < end: - yield Token(lineno, 'data', token.value[pos:]) + yield Token(lineno, "data", token.value[pos:]) -@pytest.mark.ext class TestExtensions(object): - def test_extend_late(self): env = Environment() - env.add_extension('jinja2.ext.autoescape') - t = env.from_string( - '{% autoescape true %}{{ "<test>" }}{% endautoescape %}') - assert t.render() == '<test>' + env.add_extension("jinja2.ext.autoescape") + t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}') + assert t.render() == "<test>" def test_loop_controls(self): - env = Environment(extensions=['jinja2.ext.loopcontrols']) + env = Environment(extensions=["jinja2.ext.loopcontrols"]) - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- for item in [1, 2, 3, 4] %} {%- if item % 2 == 0 %}{% continue %}{% endif -%} {{ item }} - {%- endfor %}''') - assert tmpl.render() == '13' + {%- endfor %}""" + ) + assert tmpl.render() == "13" - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- for item in [1, 2, 3, 4] %} {%- if item > 2 %}{% break %}{% endif -%} {{ item }} - {%- endfor %}''') - assert tmpl.render() == '12' + {%- endfor %}""" + ) + assert tmpl.render() == "12" def test_do(self): - env = Environment(extensions=['jinja2.ext.do']) - tmpl = env.from_string(''' + env = Environment(extensions=["jinja2.ext.do"]) + tmpl = env.from_string( + """ {%- set items = [] %} {%- for char in "foo" %} {%- do items.append(loop.index0 ~ char) %} - {%- endfor %}{{ items|join(', ') }}''') - assert tmpl.render() == '0f, 1o, 2o' + {%- endfor %}{{ items|join(', ') }}""" + ) + assert tmpl.render() == "0f, 1o, 2o" def test_extension_nodes(self): env = Environment(extensions=[ExampleExtension]) - tmpl = env.from_string('{% test %}') - assert tmpl.render() == 'False|42|23|{}' + tmpl = env.from_string("{% test %}") + assert tmpl.render() == "False|42|23|{}|None" + + def test_contextreference_node_passes_context(self): + env = Environment(extensions=[ExampleExtension]) + tmpl = env.from_string('{% set test_var="test_content" %}{% test %}') + assert tmpl.render() == "False|42|23|{}|test_content" + + def test_contextreference_node_can_pass_locals(self): + env = Environment(extensions=[DerivedExampleExtension]) + tmpl = env.from_string( + '{% for test_var in ["test_content"] %}{% test %}{% endfor %}' + ) + assert tmpl.render() == "False|42|23|{}|test_content" def test_identifier(self): - assert ExampleExtension.identifier == __name__ + '.ExampleExtension' + assert ExampleExtension.identifier == __name__ + ".ExampleExtension" def test_rebinding(self): original = Environment(extensions=[ExampleExtension]) @@ -219,15 +235,15 @@ class TestExtensions(object): def test_preprocessor_extension(self): env = Environment(extensions=[PreprocessorExtension]) - tmpl = env.from_string('{[[TEST]]}') - assert tmpl.render(foo=42) == '{(42)}' + tmpl = env.from_string("{[[TEST]]}") + assert tmpl.render(foo=42) == "{(42)}" def test_streamfilter_extension(self): env = Environment(extensions=[StreamFilterExtension]) - env.globals['gettext'] = lambda x: x.upper() - tmpl = env.from_string('Foo _(bar) Baz') + env.globals["gettext"] = lambda x: x.upper() + tmpl = env.from_string("Foo _(bar) Baz") out = tmpl.render() - assert out == 'Foo BAR Baz' + assert out == "Foo BAR Baz" def test_extension_ordering(self): class T1(Extension): @@ -235,312 +251,359 @@ class TestExtensions(object): class T2(Extension): priority = 2 + env = Environment(extensions=[T1, T2]) ext = list(env.iter_extensions()) assert ext[0].__class__ is T1 assert ext[1].__class__ is T2 + def test_debug(self): + env = Environment(extensions=["jinja2.ext.debug"]) + t = env.from_string("Hello\n{% debug %}\nGoodbye") + out = t.render() + + for value in ("context", "cycler", "filters", "abs", "tests", "!="): + assert "'{}'".format(value) in out -@pytest.mark.ext -class TestInternationalization(object): +class TestInternationalization(object): def test_trans(self): - tmpl = i18n_env.get_template('child.html') - assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf' + tmpl = i18n_env.get_template("child.html") + assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf" def test_trans_plural(self): - tmpl = i18n_env.get_template('plural.html') - assert tmpl.render(LANGUAGE='de', user_count=1) \ - == 'Ein Benutzer online' - assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' + tmpl = i18n_env.get_template("plural.html") + assert tmpl.render(LANGUAGE="de", user_count=1) == "Ein Benutzer online" + assert tmpl.render(LANGUAGE="de", user_count=2) == "2 Benutzer online" def test_trans_plural_with_functions(self): - tmpl = i18n_env.get_template('plural2.html') + tmpl = i18n_env.get_template("plural2.html") def get_user_count(): get_user_count.called += 1 return 1 + get_user_count.called = 0 - assert tmpl.render(LANGUAGE='de', get_user_count=get_user_count) \ - == '1s' + assert tmpl.render(LANGUAGE="de", get_user_count=get_user_count) == "1s" assert get_user_count.called == 1 def test_complex_plural(self): tmpl = i18n_env.from_string( - '{% trans foo=42, count=2 %}{{ count }} item{% ' - 'pluralize count %}{{ count }} items{% endtrans %}') - assert tmpl.render() == '2 items' - pytest.raises(TemplateAssertionError, i18n_env.from_string, - '{% trans foo %}...{% pluralize bar %}...{% endtrans %}') + "{% trans foo=42, count=2 %}{{ count }} item{% " + "pluralize count %}{{ count }} items{% endtrans %}" + ) + assert tmpl.render() == "2 items" + pytest.raises( + TemplateAssertionError, + i18n_env.from_string, + "{% trans foo %}...{% pluralize bar %}...{% endtrans %}", + ) def test_trans_stringformatting(self): - tmpl = i18n_env.get_template('stringformat.html') - assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' + tmpl = i18n_env.get_template("stringformat.html") + assert tmpl.render(LANGUAGE="de", user_count=5) == "Benutzer: 5" def test_trimmed(self): tmpl = i18n_env.from_string( - '{%- trans trimmed %} hello\n world {% endtrans -%}') - assert tmpl.render() == 'hello world' + "{%- trans trimmed %} hello\n world {% endtrans -%}" + ) + assert tmpl.render() == "hello world" def test_trimmed_policy(self): - s = '{%- trans %} hello\n world {% endtrans -%}' + s = "{%- trans %} hello\n world {% endtrans -%}" tmpl = i18n_env.from_string(s) trimmed_tmpl = i18n_env_trimmed.from_string(s) - assert tmpl.render() == ' hello\n world ' - assert trimmed_tmpl.render() == 'hello world' + assert tmpl.render() == " hello\n world " + assert trimmed_tmpl.render() == "hello world" def test_trimmed_policy_override(self): tmpl = i18n_env_trimmed.from_string( - '{%- trans notrimmed %} hello\n world {% endtrans -%}') - assert tmpl.render() == ' hello\n world ' + "{%- trans notrimmed %} hello\n world {% endtrans -%}" + ) + assert tmpl.render() == " hello\n world " def test_trimmed_vars(self): tmpl = i18n_env.from_string( - '{%- trans trimmed x="world" %} hello\n {{ x }} {% endtrans -%}') - assert tmpl.render() == 'hello world' + '{%- trans trimmed x="world" %} hello\n {{ x }} {% endtrans -%}' + ) + assert tmpl.render() == "hello world" def test_trimmed_varname_trimmed(self): # unlikely variable name, but when used as a variable # it should not enable trimming tmpl = i18n_env.from_string( - '{%- trans trimmed = "world" %} hello\n {{ trimmed }} ' - '{% endtrans -%}') - assert tmpl.render() == ' hello\n world ' + "{%- trans trimmed = 'world' %} hello\n {{ trimmed }} {% endtrans -%}" + ) + assert tmpl.render() == " hello\n world " def test_extract(self): from jinja2.ext import babel_extract - source = BytesIO(''' + + source = BytesIO( + """ {{ gettext('Hello World') }} {% trans %}Hello World{% endtrans %} {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} - '''.encode('ascii')) # make python 3 happy - assert list(babel_extract(source, - ('gettext', 'ngettext', '_'), [], {})) == [ - (2, 'gettext', u'Hello World', []), - (3, 'gettext', u'Hello World', []), - (4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), []) + """.encode( + "ascii" + ) + ) # make python 3 happy + assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [ + (2, "gettext", u"Hello World", []), + (3, "gettext", u"Hello World", []), + (4, "ngettext", (u"%(users)s user", u"%(users)s users", None), []), ] def test_extract_trimmed(self): from jinja2.ext import babel_extract - source = BytesIO(''' + + source = BytesIO( + """ {{ gettext(' Hello \n World') }} {% trans trimmed %} Hello \n World{% endtrans %} {% trans trimmed %}{{ users }} \n user {%- pluralize %}{{ users }} \n users{% endtrans %} - '''.encode('ascii')) # make python 3 happy - assert list(babel_extract(source, - ('gettext', 'ngettext', '_'), [], {})) == [ - (2, 'gettext', u' Hello \n World', []), - (4, 'gettext', u'Hello World', []), - (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), []) + """.encode( + "ascii" + ) + ) # make python 3 happy + assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [ + (2, "gettext", u" Hello \n World", []), + (4, "gettext", u"Hello World", []), + (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), []), ] def test_extract_trimmed_option(self): from jinja2.ext import babel_extract - source = BytesIO(''' + + source = BytesIO( + """ {{ gettext(' Hello \n World') }} {% trans %} Hello \n World{% endtrans %} {% trans %}{{ users }} \n user {%- pluralize %}{{ users }} \n users{% endtrans %} - '''.encode('ascii')) # make python 3 happy - opts = {'trimmed': 'true'} - assert list(babel_extract(source, - ('gettext', 'ngettext', '_'), [], opts)) == [ - (2, 'gettext', u' Hello \n World', []), - (4, 'gettext', u'Hello World', []), - (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), []) + """.encode( + "ascii" + ) + ) # make python 3 happy + opts = {"trimmed": "true"} + assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], opts)) == [ + (2, "gettext", u" Hello \n World", []), + (4, "gettext", u"Hello World", []), + (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), []), ] def test_comment_extract(self): from jinja2.ext import babel_extract - source = BytesIO(''' + + source = BytesIO( + """ {# trans first #} {{ gettext('Hello World') }} {% trans %}Hello World{% endtrans %}{# trans second #} {#: third #} {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} - '''.encode('utf-8')) # make python 3 happy - assert list(babel_extract(source, - ('gettext', 'ngettext', '_'), - ['trans', ':'], {})) == [ - (3, 'gettext', u'Hello World', ['first']), - (4, 'gettext', u'Hello World', ['second']), - (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), - ['third']) + """.encode( + "utf-8" + ) + ) # make python 3 happy + assert list( + babel_extract(source, ("gettext", "ngettext", "_"), ["trans", ":"], {}) + ) == [ + (3, "gettext", u"Hello World", ["first"]), + (4, "gettext", u"Hello World", ["second"]), + (6, "ngettext", (u"%(users)s user", u"%(users)s users", None), ["third"]), ] -@pytest.mark.ext class TestScope(object): - def test_basic_scope_behavior(self): # This is what the old with statement compiled down to class ScopeExt(Extension): - tags = set(['scope']) + tags = set(["scope"]) def parse(self, parser): node = nodes.Scope(lineno=next(parser.stream).lineno) assignments = [] - while parser.stream.current.type != 'block_end': + while parser.stream.current.type != "block_end": lineno = parser.stream.current.lineno if assignments: - parser.stream.expect('comma') + parser.stream.expect("comma") target = parser.parse_assign_target() - parser.stream.expect('assign') + parser.stream.expect("assign") expr = parser.parse_expression() assignments.append(nodes.Assign(target, expr, lineno=lineno)) - node.body = assignments + \ - list(parser.parse_statements(('name:endscope',), - drop_needle=True)) + node.body = assignments + list( + parser.parse_statements(("name:endscope",), drop_needle=True) + ) return node env = Environment(extensions=[ScopeExt]) - tmpl = env.from_string('''\ + tmpl = env.from_string( + """\ {%- scope a=1, b=2, c=b, d=e, e=5 -%} {{ a }}|{{ b }}|{{ c }}|{{ d }}|{{ e }} {%- endscope -%} - ''') - assert tmpl.render(b=3, e=4) == '1|2|2|4|5' + """ + ) + assert tmpl.render(b=3, e=4) == "1|2|2|4|5" -@pytest.mark.ext class TestNewstyleInternationalization(object): - def test_trans(self): - tmpl = newstyle_i18n_env.get_template('child.html') - assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf' + tmpl = newstyle_i18n_env.get_template("child.html") + assert tmpl.render(LANGUAGE="de") == "<title>fehlend</title>pass auf" def test_trans_plural(self): - tmpl = newstyle_i18n_env.get_template('plural.html') - assert tmpl.render(LANGUAGE='de', user_count=1) \ - == 'Ein Benutzer online' - assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' + tmpl = newstyle_i18n_env.get_template("plural.html") + assert tmpl.render(LANGUAGE="de", user_count=1) == "Ein Benutzer online" + assert tmpl.render(LANGUAGE="de", user_count=2) == "2 Benutzer online" def test_complex_plural(self): tmpl = newstyle_i18n_env.from_string( - '{% trans foo=42, count=2 %}{{ count }} item{% ' - 'pluralize count %}{{ count }} items{% endtrans %}') - assert tmpl.render() == '2 items' - pytest.raises(TemplateAssertionError, i18n_env.from_string, - '{% trans foo %}...{% pluralize bar %}...{% endtrans %}') + "{% trans foo=42, count=2 %}{{ count }} item{% " + "pluralize count %}{{ count }} items{% endtrans %}" + ) + assert tmpl.render() == "2 items" + pytest.raises( + TemplateAssertionError, + i18n_env.from_string, + "{% trans foo %}...{% pluralize bar %}...{% endtrans %}", + ) def test_trans_stringformatting(self): - tmpl = newstyle_i18n_env.get_template('stringformat.html') - assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' + tmpl = newstyle_i18n_env.get_template("stringformat.html") + assert tmpl.render(LANGUAGE="de", user_count=5) == "Benutzer: 5" def test_newstyle_plural(self): - tmpl = newstyle_i18n_env.get_template('ngettext.html') - assert tmpl.render(LANGUAGE='de', apples=1) == '1 Apfel' - assert tmpl.render(LANGUAGE='de', apples=5) == u'5 Äpfel' + tmpl = newstyle_i18n_env.get_template("ngettext.html") + assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apfel" + assert tmpl.render(LANGUAGE="de", apples=5) == u"5 Äpfel" def test_autoescape_support(self): - env = Environment(extensions=['jinja2.ext.autoescape', - 'jinja2.ext.i18n']) + env = Environment(extensions=["jinja2.ext.autoescape", "jinja2.ext.i18n"]) env.install_gettext_callables( - lambda x: u'<strong>Wert: %(name)s</strong>', - lambda s, p, n: s, newstyle=True) - t = env.from_string('{% autoescape ae %}{{ gettext("foo", name=' - '"<test>") }}{% endautoescape %}') - assert t.render(ae=True) == '<strong>Wert: <test></strong>' - assert t.render(ae=False) == '<strong>Wert: <test></strong>' + lambda x: u"<strong>Wert: %(name)s</strong>", + lambda s, p, n: s, + newstyle=True, + ) + t = env.from_string( + '{% autoescape ae %}{{ gettext("foo", name=' + '"<test>") }}{% endautoescape %}' + ) + assert t.render(ae=True) == "<strong>Wert: <test></strong>" + assert t.render(ae=False) == "<strong>Wert: <test></strong>" def test_autoescape_macros(self): - env = Environment(autoescape=False, extensions=['jinja2.ext.autoescape']) + env = Environment(autoescape=False, extensions=["jinja2.ext.autoescape"]) template = ( - '{% macro m() %}<html>{% endmacro %}' - '{% autoescape true %}{{ m() }}{% endautoescape %}' + "{% macro m() %}<html>{% endmacro %}" + "{% autoescape true %}{{ m() }}{% endautoescape %}" ) - assert env.from_string(template).render() == '<html>' + assert env.from_string(template).render() == "<html>" def test_num_used_twice(self): - tmpl = newstyle_i18n_env.get_template('ngettext_long.html') - assert tmpl.render(apples=5, LANGUAGE='de') == u'5 Äpfel' + tmpl = newstyle_i18n_env.get_template("ngettext_long.html") + assert tmpl.render(apples=5, LANGUAGE="de") == u"5 Äpfel" def test_num_called_num(self): - source = newstyle_i18n_env.compile(''' + source = newstyle_i18n_env.compile( + """ {% trans num=3 %}{{ num }} apple{% pluralize %}{{ num }} apples{% endtrans %} - ''', raw=True) + """, + raw=True, + ) # quite hacky, but the only way to properly test that. The idea is # that the generated code does not pass num twice (although that # would work) for better performance. This only works on the # newstyle gettext of course - assert re.search(r"u?'\%\(num\)s apple', u?'\%\(num\)s " - r"apples', 3", source) is not None + assert ( + re.search(r"u?'\%\(num\)s apple', u?'\%\(num\)s " r"apples', 3", source) + is not None + ) def test_trans_vars(self): - t1 = newstyle_i18n_env.get_template('transvars1.html') - t2 = newstyle_i18n_env.get_template('transvars2.html') - t3 = newstyle_i18n_env.get_template('transvars3.html') - assert t1.render(num=1, LANGUAGE='de') == 'Benutzer: 1' - assert t2.render(count=23, LANGUAGE='de') == 'Benutzer: 23' - assert t3.render(num=42, LANGUAGE='de') == 'Benutzer: 42' + t1 = newstyle_i18n_env.get_template("transvars1.html") + t2 = newstyle_i18n_env.get_template("transvars2.html") + t3 = newstyle_i18n_env.get_template("transvars3.html") + assert t1.render(num=1, LANGUAGE="de") == "Benutzer: 1" + assert t2.render(count=23, LANGUAGE="de") == "Benutzer: 23" + assert t3.render(num=42, LANGUAGE="de") == "Benutzer: 42" def test_novars_vars_escaping(self): - t = newstyle_i18n_env.get_template('novars.html') - assert t.render() == '%(hello)s' - t = newstyle_i18n_env.get_template('vars.html') - assert t.render(foo='42') == '42%(foo)s' - t = newstyle_i18n_env.get_template('explicitvars.html') - assert t.render() == '%(foo)s' + t = newstyle_i18n_env.get_template("novars.html") + assert t.render() == "%(hello)s" + t = newstyle_i18n_env.get_template("vars.html") + assert t.render(foo="42") == "42%(foo)s" + t = newstyle_i18n_env.get_template("explicitvars.html") + assert t.render() == "%(foo)s" -@pytest.mark.ext class TestAutoEscape(object): - def test_scoped_setting(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - tmpl = env.from_string(''' + env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True) + tmpl = env.from_string( + """ {{ "<HelloWorld>" }} {% autoescape false %} {{ "<HelloWorld>" }} {% endautoescape %} {{ "<HelloWorld>" }} - ''') - assert tmpl.render().split() == \ - [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>'] + """ + ) + assert tmpl.render().split() == [ + u"<HelloWorld>", + u"<HelloWorld>", + u"<HelloWorld>", + ] - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=False) - tmpl = env.from_string(''' + env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=False) + tmpl = env.from_string( + """ {{ "<HelloWorld>" }} {% autoescape true %} {{ "<HelloWorld>" }} {% endautoescape %} {{ "<HelloWorld>" }} - ''') - assert tmpl.render().split() == \ - [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>'] + """ + ) + assert tmpl.render().split() == [ + u"<HelloWorld>", + u"<HelloWorld>", + u"<HelloWorld>", + ] def test_nonvolatile(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) + env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True) tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}') assert tmpl.render() == ' foo="<test>"' - tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}' - '|xmlattr|escape }}{% endautoescape %}') - assert tmpl.render() == ' foo="&lt;test&gt;"' + tmpl = env.from_string( + '{% autoescape false %}{{ {"foo": "<test>"}' + "|xmlattr|escape }}{% endautoescape %}" + ) + assert tmpl.render() == " foo="&lt;test&gt;"" def test_volatile(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}' - '|xmlattr|escape }}{% endautoescape %}') - assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"' + env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True) + tmpl = env.from_string( + '{% autoescape foo %}{{ {"foo": "<test>"}' + "|xmlattr|escape }}{% endautoescape %}" + ) + assert tmpl.render(foo=False) == " foo="&lt;test&gt;"" assert tmpl.render(foo=True) == ' foo="<test>"' def test_scoping(self): - env = Environment(extensions=['jinja2.ext.autoescape']) + env = Environment(extensions=["jinja2.ext.autoescape"]) tmpl = env.from_string( '{% autoescape true %}{% set x = "<x>" %}{{ x }}' - '{% endautoescape %}{{ x }}{{ "<y>" }}') - assert tmpl.render(x=1) == '<x>1<y>' + '{% endautoescape %}{{ x }}{{ "<y>" }}' + ) + assert tmpl.render(x=1) == "<x>1<y>" def test_volatile_scoping(self): - env = Environment(extensions=['jinja2.ext.autoescape']) - tmplsource = ''' + env = Environment(extensions=["jinja2.ext.autoescape"]) + tmplsource = """ {% autoescape val %} {% macro foo(x) %} [{{ x }}] @@ -548,41 +611,45 @@ class TestAutoEscape(object): {{ foo().__class__.__name__ }} {% endautoescape %} {{ '<testing>' }} - ''' + """ tmpl = env.from_string(tmplsource) - assert tmpl.render(val=True).split()[0] == 'Markup' + assert tmpl.render(val=True).split()[0] == "Markup" assert tmpl.render(val=False).split()[0] == text_type.__name__ # looking at the source we should see <testing> there in raw # (and then escaped as well) - env = Environment(extensions=['jinja2.ext.autoescape']) + env = Environment(extensions=["jinja2.ext.autoescape"]) pysource = env.compile(tmplsource, raw=True) - assert '<testing>\\n' in pysource + assert "<testing>\\n" in pysource - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) + env = Environment(extensions=["jinja2.ext.autoescape"], autoescape=True) pysource = env.compile(tmplsource, raw=True) - assert '<testing>\\n' in pysource + assert "<testing>\\n" in pysource def test_overlay_scopes(self): class MagicScopeExtension(Extension): - tags = set(['overlay']) + tags = set(["overlay"]) + def parse(self, parser): node = nodes.OverlayScope(lineno=next(parser.stream).lineno) - node.body = list(parser.parse_statements(('name:endoverlay',), - drop_needle=True)) - node.context = self.call_method('get_scope') + node.body = list( + parser.parse_statements(("name:endoverlay",), drop_needle=True) + ) + node.context = self.call_method("get_scope") return node + def get_scope(self): - return {'x': [1, 2, 3]} + return {"x": [1, 2, 3]} env = Environment(extensions=[MagicScopeExtension]) - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {{- x }}|{% set z = 99 %} {%- overlay %} {{- y }}|{{ z }}|{% for item in x %}[{{ item }}]{% endfor %} {%- endoverlay %}| {{- x -}} - ''') - assert tmpl.render(x=42, y=23) == '42|23|99|[1][2][3]|42' + """ + ) + assert tmpl.render(x=42, y=23) == "42|23|99|[1][2][3]|42" diff --git a/tests/test_features.py b/tests/test_features.py index 3187890..34b6f20 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,40 +1,42 @@ import sys + import pytest -from jinja2 import Template, Environment, contextfilter +from jinja2 import contextfilter +from jinja2 import Environment +from jinja2 import Template +from jinja2._compat import text_type -@pytest.mark.skipif(sys.version_info < (3, 5), - reason='Requires 3.5 or later') +@pytest.mark.skipif(sys.version_info < (3, 5), reason="Requires 3.5 or later") def test_generator_stop(): class X(object): def __getattr__(self, name): raise StopIteration() - t = Template('a{{ bad.bar() }}b') + t = Template("a{{ bad.bar() }}b") with pytest.raises(RuntimeError): t.render(bad=X()) -@pytest.mark.skipif(sys.version_info[0] > 2, - reason='Feature only supported on 2.x') +@pytest.mark.skipif(sys.version_info[0] > 2, reason="Feature only supported on 2.x") def test_ascii_str(): @contextfilter def assert_func(context, value): - assert type(value) is context['expected_type'] + assert type(value) is context["expected_type"] env = Environment() - env.filters['assert'] = assert_func + env.filters["assert"] = assert_func - env.policies['compiler.ascii_str'] = False + env.policies["compiler.ascii_str"] = False t = env.from_string('{{ "foo"|assert }}') - t.render(expected_type=unicode) + t.render(expected_type=text_type) - env.policies['compiler.ascii_str'] = True + env.policies["compiler.ascii_str"] = True t = env.from_string('{{ "foo"|assert }}') t.render(expected_type=str) for val in True, False: - env.policies['compiler.ascii_str'] = val + env.policies["compiler.ascii_str"] = val t = env.from_string(u'{{ "\N{SNOWMAN}"|assert }}') - t.render(expected_type=unicode) + t.render(expected_type=text_type) diff --git a/tests/test_filters.py b/tests/test_filters.py index 2769e9c..388c346 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,20 +1,15 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.filters - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests for the jinja filters. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import random -import pytest +from collections import namedtuple -from markupsafe import Markup +import pytest from jinja2 import Environment -from jinja2._compat import text_type, implements_to_string +from jinja2 import Markup +from jinja2 import StrictUndefined +from jinja2 import UndefinedError +from jinja2._compat import implements_to_string +from jinja2._compat import text_type @implements_to_string @@ -26,187 +21,239 @@ class Magic(object): return text_type(self.value) -@pytest.mark.filter -class TestFilter(object): +@implements_to_string +class Magic2(object): + def __init__(self, value1, value2): + self.value1 = value1 + self.value2 = value2 + + def __str__(self): + return u"(%s,%s)" % (text_type(self.value1), text_type(self.value2)) + +class TestFilter(object): def test_filter_calling(self, env): - rv = env.call_filter('sum', [1, 2, 3]) + rv = env.call_filter("sum", [1, 2, 3]) assert rv == 6 def test_capitalize(self, env): tmpl = env.from_string('{{ "foo bar"|capitalize }}') - assert tmpl.render() == 'Foo bar' + assert tmpl.render() == "Foo bar" def test_center(self, env): tmpl = env.from_string('{{ "foo"|center(9) }}') - assert tmpl.render() == ' foo ' + assert tmpl.render() == " foo " def test_default(self, env): tmpl = env.from_string( "{{ missing|default('no') }}|{{ false|default('no') }}|" "{{ false|default('no', true) }}|{{ given|default('no') }}" ) - assert tmpl.render(given='yes') == 'no|False|no|yes' - - @pytest.mark.parametrize('args,expect', ( - ('', "[('aa', 0), ('AB', 3), ('b', 1), ('c', 2)]"), - ('true', "[('AB', 3), ('aa', 0), ('b', 1), ('c', 2)]"), - ('by="value"', "[('aa', 0), ('b', 1), ('c', 2), ('AB', 3)]"), - ('reverse=true', "[('c', 2), ('b', 1), ('AB', 3), ('aa', 0)]") - )) + assert tmpl.render(given="yes") == "no|False|no|yes" + + @pytest.mark.parametrize( + "args,expect", + ( + ("", "[('aa', 0), ('AB', 3), ('b', 1), ('c', 2)]"), + ("true", "[('AB', 3), ('aa', 0), ('b', 1), ('c', 2)]"), + ('by="value"', "[('aa', 0), ('b', 1), ('c', 2), ('AB', 3)]"), + ("reverse=true", "[('c', 2), ('b', 1), ('AB', 3), ('aa', 0)]"), + ), + ) def test_dictsort(self, env, args, expect): - t = env.from_string('{{{{ foo|dictsort({args}) }}}}'.format(args=args)) + t = env.from_string("{{{{ foo|dictsort({args}) }}}}".format(args=args)) out = t.render(foo={"aa": 0, "b": 1, "c": 2, "AB": 3}) assert out == expect def test_batch(self, env): - tmpl = env.from_string("{{ foo|batch(3)|list }}|" - "{{ foo|batch(3, 'X')|list }}") + tmpl = env.from_string("{{ foo|batch(3)|list }}|{{ foo|batch(3, 'X')|list }}") out = tmpl.render(foo=list(range(10))) - assert out == ("[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]|" - "[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 'X', 'X']]") + assert out == ( + "[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]|" + "[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 'X', 'X']]" + ) def test_slice(self, env): - tmpl = env.from_string('{{ foo|slice(3)|list }}|' - '{{ foo|slice(3, "X")|list }}') + tmpl = env.from_string("{{ foo|slice(3)|list }}|{{ foo|slice(3, 'X')|list }}") out = tmpl.render(foo=list(range(10))) - assert out == ("[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" - "[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]") + assert out == ( + "[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" + "[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]" + ) def test_escape(self, env): - tmpl = env.from_string('''{{ '<">&'|escape }}''') + tmpl = env.from_string("""{{ '<">&'|escape }}""") out = tmpl.render() - assert out == '<">&' + assert out == "<">&" + + @pytest.mark.parametrize( + ("chars", "expect"), [(None, "..stays.."), (".", " ..stays"), (" .", "stays")] + ) + def test_trim(self, env, chars, expect): + tmpl = env.from_string("{{ foo|trim(chars) }}") + out = tmpl.render(foo=" ..stays..", chars=chars) + assert out == expect def test_striptags(self, env): - tmpl = env.from_string('''{{ foo|striptags }}''') - out = tmpl.render(foo=' <p>just a small \n <a href="#">' - 'example</a> link</p>\n<p>to a webpage</p> ' - '<!-- <p>and some commented stuff</p> -->') - assert out == 'just a small example link to a webpage' + tmpl = env.from_string("""{{ foo|striptags }}""") + out = tmpl.render( + foo=' <p>just a small \n <a href="#">' + "example</a> link</p>\n<p>to a webpage</p> " + "<!-- <p>and some commented stuff</p> -->" + ) + assert out == "just a small example link to a webpage" def test_filesizeformat(self, env): tmpl = env.from_string( - '{{ 100|filesizeformat }}|' - '{{ 1000|filesizeformat }}|' - '{{ 1000000|filesizeformat }}|' - '{{ 1000000000|filesizeformat }}|' - '{{ 1000000000000|filesizeformat }}|' - '{{ 100|filesizeformat(true) }}|' - '{{ 1000|filesizeformat(true) }}|' - '{{ 1000000|filesizeformat(true) }}|' - '{{ 1000000000|filesizeformat(true) }}|' - '{{ 1000000000000|filesizeformat(true) }}' + "{{ 100|filesizeformat }}|" + "{{ 1000|filesizeformat }}|" + "{{ 1000000|filesizeformat }}|" + "{{ 1000000000|filesizeformat }}|" + "{{ 1000000000000|filesizeformat }}|" + "{{ 100|filesizeformat(true) }}|" + "{{ 1000|filesizeformat(true) }}|" + "{{ 1000000|filesizeformat(true) }}|" + "{{ 1000000000|filesizeformat(true) }}|" + "{{ 1000000000000|filesizeformat(true) }}" ) out = tmpl.render() assert out == ( - '100 Bytes|1.0 kB|1.0 MB|1.0 GB|1.0 TB|100 Bytes|' - '1000 Bytes|976.6 KiB|953.7 MiB|931.3 GiB' + "100 Bytes|1.0 kB|1.0 MB|1.0 GB|1.0 TB|100 Bytes|" + "1000 Bytes|976.6 KiB|953.7 MiB|931.3 GiB" ) def test_filesizeformat_issue59(self, env): tmpl = env.from_string( - '{{ 300|filesizeformat }}|' - '{{ 3000|filesizeformat }}|' - '{{ 3000000|filesizeformat }}|' - '{{ 3000000000|filesizeformat }}|' - '{{ 3000000000000|filesizeformat }}|' - '{{ 300|filesizeformat(true) }}|' - '{{ 3000|filesizeformat(true) }}|' - '{{ 3000000|filesizeformat(true) }}' + "{{ 300|filesizeformat }}|" + "{{ 3000|filesizeformat }}|" + "{{ 3000000|filesizeformat }}|" + "{{ 3000000000|filesizeformat }}|" + "{{ 3000000000000|filesizeformat }}|" + "{{ 300|filesizeformat(true) }}|" + "{{ 3000|filesizeformat(true) }}|" + "{{ 3000000|filesizeformat(true) }}" ) out = tmpl.render() assert out == ( - '300 Bytes|3.0 kB|3.0 MB|3.0 GB|3.0 TB|300 Bytes|' - '2.9 KiB|2.9 MiB' + "300 Bytes|3.0 kB|3.0 MB|3.0 GB|3.0 TB|300 Bytes|2.9 KiB|2.9 MiB" ) def test_first(self, env): - tmpl = env.from_string('{{ foo|first }}') + tmpl = env.from_string("{{ foo|first }}") out = tmpl.render(foo=list(range(10))) - assert out == '0' + assert out == "0" - def test_float(self, env): - tmpl = env.from_string('{{ "42"|float }}|' - '{{ "ajsghasjgd"|float }}|' - '{{ "32.32"|float }}') - out = tmpl.render() - assert out == '42.0|0.0|32.32' + @pytest.mark.parametrize( + ("value", "expect"), (("42", "42.0"), ("abc", "0.0"), ("32.32", "32.32"),) + ) + def test_float(self, env, value, expect): + t = env.from_string("{{ '%s'|float }}" % value) + assert t.render() == expect + + def test_float_default(self, env): + t = env.from_string("{{ value|float(default=1.0) }}") + assert t.render(value="abc") == "1.0" def test_format(self, env): - tmpl = env.from_string('''{{ "%s|%s"|format("a", "b") }}''') + tmpl = env.from_string("""{{ "%s|%s"|format("a", "b") }}""") out = tmpl.render() - assert out == 'a|b' + assert out == "a|b" + + @staticmethod + def _test_indent_multiline_template(env, markup=False): + text = "\n".join(["", "foo bar", '"baz"', ""]) + if markup: + text = Markup(text) + t = env.from_string("{{ foo|indent(2, false, false) }}") + assert t.render(foo=text) == '\n foo bar\n "baz"\n' + t = env.from_string("{{ foo|indent(2, false, true) }}") + assert t.render(foo=text) == '\n foo bar\n "baz"\n ' + t = env.from_string("{{ foo|indent(2, true, false) }}") + assert t.render(foo=text) == ' \n foo bar\n "baz"\n' + t = env.from_string("{{ foo|indent(2, true, true) }}") + assert t.render(foo=text) == ' \n foo bar\n "baz"\n ' def test_indent(self, env): - text = '\n'.join(['', 'foo bar', '']) - t = env.from_string('{{ foo|indent(2, false, false) }}') - assert t.render(foo=text) == '\n foo bar\n' - t = env.from_string('{{ foo|indent(2, false, true) }}') - assert t.render(foo=text) == '\n foo bar\n ' - t = env.from_string('{{ foo|indent(2, true, false) }}') - assert t.render(foo=text) == ' \n foo bar\n' - t = env.from_string('{{ foo|indent(2, true, true) }}') - assert t.render(foo=text) == ' \n foo bar\n ' - + self._test_indent_multiline_template(env) t = env.from_string('{{ "jinja"|indent }}') - assert t.render() == 'jinja' + assert t.render() == "jinja" t = env.from_string('{{ "jinja"|indent(first=true) }}') - assert t.render() == ' jinja' + assert t.render() == " jinja" t = env.from_string('{{ "jinja"|indent(blank=true) }}') - assert t.render() == 'jinja' + assert t.render() == "jinja" + + def test_indent_markup_input(self, env): + """ + Tests cases where the filter input is a Markup type + """ + self._test_indent_multiline_template(env, markup=True) + + @pytest.mark.parametrize( + ("value", "expect"), + ( + ("42", "42"), + ("abc", "0"), + ("32.32", "32"), + ("12345678901234567890", "12345678901234567890"), + ), + ) + def test_int(self, env, value, expect): + t = env.from_string("{{ '%s'|int }}" % value) + assert t.render() == expect - def test_indentfirst_deprecated(self, env): - with pytest.warns(DeprecationWarning): - env.from_string('{{ "jinja"|indent(indentfirst=true) }}').render() + @pytest.mark.parametrize( + ("value", "base", "expect"), + (("0x4d32", 16, "19762"), ("011", 8, "9"), ("0x33Z", 16, "0"),), + ) + def test_int_base(self, env, value, base, expect): + t = env.from_string("{{ '%s'|int(base=%d) }}" % (value, base)) + assert t.render() == expect - def test_int(self, env): + def test_int_default(self, env): + t = env.from_string("{{ value|int(default=1) }}") + assert t.render(value="abc") == "1" + + def test_int_special_method(self, env): class IntIsh(object): def __int__(self): return 42 - tmpl = env.from_string('{{ "42"|int }}|{{ "ajsghasjgd"|int }}|' - '{{ "32.32"|int }}|{{ "0x4d32"|int(0, 16) }}|' - '{{ "011"|int(0, 8)}}|{{ "0x33FU"|int(0, 16) }}|' - '{{ obj|int }}') - out = tmpl.render(obj=IntIsh()) - assert out == '42|0|32|19762|9|0|42' + t = env.from_string("{{ value|int }}") + assert t.render(value=IntIsh()) == "42" def test_join(self, env): tmpl = env.from_string('{{ [1, 2, 3]|join("|") }}') out = tmpl.render() - assert out == '1|2|3' + assert out == "1|2|3" env2 = Environment(autoescape=True) - tmpl = env2.from_string( - '{{ ["<foo>", "<span>foo</span>"|safe]|join }}') - assert tmpl.render() == '<foo><span>foo</span>' + tmpl = env2.from_string('{{ ["<foo>", "<span>foo</span>"|safe]|join }}') + assert tmpl.render() == "<foo><span>foo</span>" def test_join_attribute(self, env): - class User(object): - def __init__(self, username): - self.username = username - tmpl = env.from_string('''{{ users|join(', ', 'username') }}''') - assert tmpl.render(users=map(User, ['foo', 'bar'])) == 'foo, bar' + User = namedtuple("User", "username") + tmpl = env.from_string("""{{ users|join(', ', 'username') }}""") + assert tmpl.render(users=map(User, ["foo", "bar"])) == "foo, bar" def test_last(self, env): - tmpl = env.from_string('''{{ foo|last }}''') + tmpl = env.from_string("""{{ foo|last }}""") out = tmpl.render(foo=list(range(10))) - assert out == '9' + assert out == "9" def test_length(self, env): - tmpl = env.from_string('''{{ "hello world"|length }}''') + tmpl = env.from_string("""{{ "hello world"|length }}""") out = tmpl.render() - assert out == '11' + assert out == "11" def test_lower(self, env): - tmpl = env.from_string('''{{ "FOO"|lower }}''') + tmpl = env.from_string("""{{ "FOO"|lower }}""") out = tmpl.render() - assert out == 'foo' + assert out == "foo" def test_pprint(self, env): from pprint import pformat - tmpl = env.from_string('''{{ data|pprint }}''') + + tmpl = env.from_string("""{{ data|pprint }}""") data = list(range(1000)) assert tmpl.render(data=data) == pformat(data) @@ -215,177 +262,178 @@ class TestFilter(object): state = random.getstate() request.addfinalizer(lambda: random.setstate(state)) # generate the random values from a known seed - random.seed('jinja') - expected = [random.choice('1234567890') for _ in range(10)] + random.seed("jinja") + expected = [random.choice("1234567890") for _ in range(10)] # check that the random sequence is generated again by a template # ensures that filter result is not constant folded - random.seed('jinja') + random.seed("jinja") t = env.from_string('{{ "1234567890"|random }}') for value in expected: assert t.render() == value def test_reverse(self, env): - tmpl = env.from_string('{{ "foobar"|reverse|join }}|' - '{{ [1, 2, 3]|reverse|list }}') - assert tmpl.render() == 'raboof|[3, 2, 1]' + tmpl = env.from_string( + "{{ 'foobar'|reverse|join }}|{{ [1, 2, 3]|reverse|list }}" + ) + assert tmpl.render() == "raboof|[3, 2, 1]" def test_string(self, env): x = [1, 2, 3, 4, 5] - tmpl = env.from_string('''{{ obj|string }}''') + tmpl = env.from_string("""{{ obj|string }}""") assert tmpl.render(obj=x) == text_type(x) def test_title(self, env): - tmpl = env.from_string('''{{ "foo bar"|title }}''') + tmpl = env.from_string("""{{ "foo bar"|title }}""") assert tmpl.render() == "Foo Bar" - tmpl = env.from_string('''{{ "foo's bar"|title }}''') + tmpl = env.from_string("""{{ "foo's bar"|title }}""") assert tmpl.render() == "Foo's Bar" - tmpl = env.from_string('''{{ "foo bar"|title }}''') + tmpl = env.from_string("""{{ "foo bar"|title }}""") assert tmpl.render() == "Foo Bar" - tmpl = env.from_string('''{{ "f bar f"|title }}''') + tmpl = env.from_string("""{{ "f bar f"|title }}""") assert tmpl.render() == "F Bar F" - tmpl = env.from_string('''{{ "foo-bar"|title }}''') + tmpl = env.from_string("""{{ "foo-bar"|title }}""") assert tmpl.render() == "Foo-Bar" - tmpl = env.from_string('''{{ "foo\tbar"|title }}''') + tmpl = env.from_string("""{{ "foo\tbar"|title }}""") assert tmpl.render() == "Foo\tBar" - tmpl = env.from_string('''{{ "FOO\tBAR"|title }}''') + tmpl = env.from_string("""{{ "FOO\tBAR"|title }}""") assert tmpl.render() == "Foo\tBar" - tmpl = env.from_string('''{{ "foo (bar)"|title }}''') + tmpl = env.from_string("""{{ "foo (bar)"|title }}""") assert tmpl.render() == "Foo (Bar)" - tmpl = env.from_string('''{{ "foo {bar}"|title }}''') + tmpl = env.from_string("""{{ "foo {bar}"|title }}""") assert tmpl.render() == "Foo {Bar}" - tmpl = env.from_string('''{{ "foo [bar]"|title }}''') + tmpl = env.from_string("""{{ "foo [bar]"|title }}""") assert tmpl.render() == "Foo [Bar]" - tmpl = env.from_string('''{{ "foo <bar>"|title }}''') + tmpl = env.from_string("""{{ "foo <bar>"|title }}""") assert tmpl.render() == "Foo <Bar>" class Foo: def __str__(self): - return 'foo-bar' + return "foo-bar" - tmpl = env.from_string('''{{ data|title }}''') + tmpl = env.from_string("""{{ data|title }}""") out = tmpl.render(data=Foo()) - assert out == 'Foo-Bar' + assert out == "Foo-Bar" def test_truncate(self, env): tmpl = env.from_string( '{{ data|truncate(15, true, ">>>") }}|' '{{ data|truncate(15, false, ">>>") }}|' - '{{ smalldata|truncate(15) }}' + "{{ smalldata|truncate(15) }}" ) - out = tmpl.render(data='foobar baz bar' * 1000, - smalldata='foobar baz bar') - msg = 'Current output: %s' % out - assert out == 'foobar baz b>>>|foobar baz>>>|foobar baz bar', msg + out = tmpl.render(data="foobar baz bar" * 1000, smalldata="foobar baz bar") + msg = "Current output: %s" % out + assert out == "foobar baz b>>>|foobar baz>>>|foobar baz bar", msg def test_truncate_very_short(self, env): tmpl = env.from_string( - '{{ "foo bar baz"|truncate(9) }}|' - '{{ "foo bar baz"|truncate(9, true) }}' + '{{ "foo bar baz"|truncate(9) }}|{{ "foo bar baz"|truncate(9, true) }}' ) out = tmpl.render() - assert out == 'foo bar baz|foo bar baz', out + assert out == "foo bar baz|foo bar baz", out def test_truncate_end_length(self, env): tmpl = env.from_string('{{ "Joel is a slug"|truncate(7, true) }}') out = tmpl.render() - assert out == 'Joel...', 'Current output: %s' % out + assert out == "Joel...", "Current output: %s" % out def test_upper(self, env): tmpl = env.from_string('{{ "foo"|upper }}') - assert tmpl.render() == 'FOO' + assert tmpl.render() == "FOO" def test_urlize(self, env): - tmpl = env.from_string( - '{{ "foo http://www.example.com/ bar"|urlize }}') + tmpl = env.from_string('{{ "foo http://www.example.com/ bar"|urlize }}') assert tmpl.render() == ( 'foo <a href="http://www.example.com/" rel="noopener">' - 'http://www.example.com/</a> bar' + "http://www.example.com/</a> bar" ) def test_urlize_rel_policy(self): env = Environment() - env.policies['urlize.rel'] = None - tmpl = env.from_string( - '{{ "foo http://www.example.com/ bar"|urlize }}') + env.policies["urlize.rel"] = None + tmpl = env.from_string('{{ "foo http://www.example.com/ bar"|urlize }}') assert tmpl.render() == ( - 'foo <a href="http://www.example.com/">' - 'http://www.example.com/</a> bar' + 'foo <a href="http://www.example.com/">http://www.example.com/</a> bar' ) def test_urlize_target_parameter(self, env): tmpl = env.from_string( '{{ "foo http://www.example.com/ bar"|urlize(target="_blank") }}' ) - assert tmpl.render() \ - == 'foo <a href="http://www.example.com/" rel="noopener" target="_blank">'\ - 'http://www.example.com/</a> bar' + assert ( + tmpl.render() + == 'foo <a href="http://www.example.com/" rel="noopener" target="_blank">' + "http://www.example.com/</a> bar" + ) def test_wordcount(self, env): tmpl = env.from_string('{{ "foo bar baz"|wordcount }}') - assert tmpl.render() == '3' + assert tmpl.render() == "3" + + strict_env = Environment(undefined=StrictUndefined) + t = strict_env.from_string("{{ s|wordcount }}") + with pytest.raises(UndefinedError): + t.render() def test_block(self, env): - tmpl = env.from_string( - '{% filter lower|escape %}<HEHE>{% endfilter %}' - ) - assert tmpl.render() == '<hehe>' + tmpl = env.from_string("{% filter lower|escape %}<HEHE>{% endfilter %}") + assert tmpl.render() == "<hehe>" def test_chaining(self, env): - tmpl = env.from_string( - '''{{ ['<foo>', '<bar>']|first|upper|escape }}''' - ) - assert tmpl.render() == '<FOO>' + tmpl = env.from_string("""{{ ['<foo>', '<bar>']|first|upper|escape }}""") + assert tmpl.render() == "<FOO>" def test_sum(self, env): - tmpl = env.from_string('''{{ [1, 2, 3, 4, 5, 6]|sum }}''') - assert tmpl.render() == '21' + tmpl = env.from_string("""{{ [1, 2, 3, 4, 5, 6]|sum }}""") + assert tmpl.render() == "21" def test_sum_attributes(self, env): - tmpl = env.from_string('''{{ values|sum('value') }}''') - assert tmpl.render(values=[ - {'value': 23}, - {'value': 1}, - {'value': 18}, - ]) == '42' + tmpl = env.from_string("""{{ values|sum('value') }}""") + assert tmpl.render(values=[{"value": 23}, {"value": 1}, {"value": 18}]) == "42" def test_sum_attributes_nested(self, env): - tmpl = env.from_string('''{{ values|sum('real.value') }}''') - assert tmpl.render(values=[ - {'real': {'value': 23}}, - {'real': {'value': 1}}, - {'real': {'value': 18}}, - ]) == '42' + tmpl = env.from_string("""{{ values|sum('real.value') }}""") + assert ( + tmpl.render( + values=[ + {"real": {"value": 23}}, + {"real": {"value": 1}}, + {"real": {"value": 18}}, + ] + ) + == "42" + ) def test_sum_attributes_tuple(self, env): - tmpl = env.from_string('''{{ values.items()|sum('1') }}''') - assert tmpl.render(values={ - 'foo': 23, - 'bar': 1, - 'baz': 18, - }) == '42' + tmpl = env.from_string("""{{ values.items()|sum('1') }}""") + assert tmpl.render(values={"foo": 23, "bar": 1, "baz": 18}) == "42" def test_abs(self, env): - tmpl = env.from_string('''{{ -1|abs }}|{{ 1|abs }}''') - assert tmpl.render() == '1|1', tmpl.render() + tmpl = env.from_string("""{{ -1|abs }}|{{ 1|abs }}""") + assert tmpl.render() == "1|1", tmpl.render() def test_round_positive(self, env): - tmpl = env.from_string('{{ 2.7|round }}|{{ 2.1|round }}|' - "{{ 2.1234|round(3, 'floor') }}|" - "{{ 2.1|round(0, 'ceil') }}") - assert tmpl.render() == '3.0|2.0|2.123|3.0', tmpl.render() + tmpl = env.from_string( + "{{ 2.7|round }}|{{ 2.1|round }}|" + "{{ 2.1234|round(3, 'floor') }}|" + "{{ 2.1|round(0, 'ceil') }}" + ) + assert tmpl.render() == "3.0|2.0|2.123|3.0", tmpl.render() def test_round_negative(self, env): - tmpl = env.from_string('{{ 21.3|round(-1)}}|' - "{{ 21.3|round(-1, 'ceil')}}|" - "{{ 21.3|round(-1, 'floor')}}") - assert tmpl.render() == '20.0|30.0|20.0', tmpl.render() + tmpl = env.from_string( + "{{ 21.3|round(-1)}}|" + "{{ 21.3|round(-1, 'ceil')}}|" + "{{ 21.3|round(-1, 'floor')}}" + ) + assert tmpl.render() == "20.0|30.0|20.0", tmpl.render() def test_xmlattr(self, env): tmpl = env.from_string( "{{ {'foo': 42, 'bar': 23, 'fish': none, " - "'spam': missing, 'blub:blub': '<?>'}|xmlattr }}") + "'spam': missing, 'blub:blub': '<?>'}|xmlattr }}" + ) out = tmpl.render().split() assert len(out) == 3 assert 'foo="42"' in out @@ -393,21 +441,60 @@ class TestFilter(object): assert 'blub:blub="<?>"' in out def test_sort1(self, env): - tmpl = env.from_string( - '{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}') - assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' + tmpl = env.from_string("{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}") + assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]" def test_sort2(self, env): tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort) }}') - assert tmpl.render() == 'AbcD' + assert tmpl.render() == "AbcD" def test_sort3(self, env): - tmpl = env.from_string('''{{ ['foo', 'Bar', 'blah']|sort }}''') + tmpl = env.from_string("""{{ ['foo', 'Bar', 'blah']|sort }}""") assert tmpl.render() == "['Bar', 'blah', 'foo']" def test_sort4(self, env): - tmpl = env.from_string('''{{ items|sort(attribute='value')|join }}''') - assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == '1234' + tmpl = env.from_string("""{{ items|sort(attribute='value')|join }}""") + assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == "1234" + + def test_sort5(self, env): + tmpl = env.from_string("""{{ items|sort(attribute='value.0')|join }}""") + assert tmpl.render(items=map(Magic, [[3], [2], [4], [1]])) == "[1][2][3][4]" + + def test_sort6(self, env): + tmpl = env.from_string("""{{ items|sort(attribute='value1,value2')|join }}""") + assert ( + tmpl.render( + items=map( + lambda x: Magic2(x[0], x[1]), [(3, 1), (2, 2), (2, 1), (2, 5)] + ) + ) + == "(2,1)(2,2)(2,5)(3,1)" + ) + + def test_sort7(self, env): + tmpl = env.from_string("""{{ items|sort(attribute='value2,value1')|join }}""") + assert ( + tmpl.render( + items=map( + lambda x: Magic2(x[0], x[1]), [(3, 1), (2, 2), (2, 1), (2, 5)] + ) + ) + == "(2,1)(3,1)(2,2)(2,5)" + ) + + def test_sort8(self, env): + tmpl = env.from_string( + """{{ items|sort(attribute='value1.0,value2.0')|join }}""" + ) + assert ( + tmpl.render( + items=map( + lambda x: Magic2(x[0], x[1]), + [([3], [1]), ([2], [2]), ([2], [1]), ([2], [5])], + ) + ) + == "([2],[1])([2],[2])([2],[5])([3],[1])" + ) def test_unique(self, env): t = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique) }}') @@ -419,237 +506,246 @@ class TestFilter(object): def test_unique_attribute(self, env): t = env.from_string("{{ items|unique(attribute='value')|join }}") - assert t.render(items=map(Magic, [3, 2, 4, 1, 2])) == '3241' - - @pytest.mark.parametrize('source,expect', ( - ('{{ ["a", "B"]|min }}', 'a'), - ('{{ ["a", "B"]|min(case_sensitive=true) }}', 'B'), - ('{{ []|min }}', ''), - ('{{ ["a", "B"]|max }}', 'B'), - ('{{ ["a", "B"]|max(case_sensitive=true) }}', 'a'), - ('{{ []|max }}', ''), - )) + assert t.render(items=map(Magic, [3, 2, 4, 1, 2])) == "3241" + + @pytest.mark.parametrize( + "source,expect", + ( + ('{{ ["a", "B"]|min }}', "a"), + ('{{ ["a", "B"]|min(case_sensitive=true) }}', "B"), + ("{{ []|min }}", ""), + ('{{ ["a", "B"]|max }}', "B"), + ('{{ ["a", "B"]|max(case_sensitive=true) }}', "a"), + ("{{ []|max }}", ""), + ), + ) def test_min_max(self, env, source, expect): t = env.from_string(source) assert t.render() == expect - @pytest.mark.parametrize('name,expect', ( - ('min', '1'), - ('max', '9'), - )) + @pytest.mark.parametrize("name,expect", (("min", "1"), ("max", "9"),)) def test_min_max_attribute(self, env, name, expect): - t = env.from_string('{{ items|' + name + '(attribute="value") }}') + t = env.from_string("{{ items|" + name + '(attribute="value") }}') assert t.render(items=map(Magic, [5, 1, 9])) == expect def test_groupby(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- for grouper, list in [{'foo': 1, 'bar': 2}, {'foo': 2, 'bar': 3}, {'foo': 1, 'bar': 1}, {'foo': 3, 'bar': 4}]|groupby('foo') -%} {{ grouper }}{% for x in list %}: {{ x.foo }}, {{ x.bar }}{% endfor %}| - {%- endfor %}''') - assert tmpl.render().split('|') == [ - "1: 1, 2: 1, 1", - "2: 2, 3", - "3: 3, 4", - "" - ] + {%- endfor %}""" + ) + assert tmpl.render().split("|") == ["1: 1, 2: 1, 1", "2: 2, 3", "3: 3, 4", ""] def test_groupby_tuple_index(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- for grouper, list in [('a', 1), ('a', 2), ('b', 1)]|groupby(0) -%} {{ grouper }}{% for x in list %}:{{ x.1 }}{% endfor %}| - {%- endfor %}''') - assert tmpl.render() == 'a:1:2|b:1|' + {%- endfor %}""" + ) + assert tmpl.render() == "a:1:2|b:1|" def test_groupby_multidot(self, env): - class Date(object): - def __init__(self, day, month, year): - self.day = day - self.month = month - self.year = year - - class Article(object): - def __init__(self, title, *date): - self.date = Date(*date) - self.title = title + Date = namedtuple("Date", "day,month,year") + Article = namedtuple("Article", "title,date") articles = [ - Article('aha', 1, 1, 1970), - Article('interesting', 2, 1, 1970), - Article('really?', 3, 1, 1970), - Article('totally not', 1, 1, 1971) + Article("aha", Date(1, 1, 1970)), + Article("interesting", Date(2, 1, 1970)), + Article("really?", Date(3, 1, 1970)), + Article("totally not", Date(1, 1, 1971)), ] - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {%- for year, list in articles|groupby('date.year') -%} {{ year }}{% for x in list %}[{{ x.title }}]{% endfor %}| - {%- endfor %}''') - assert tmpl.render(articles=articles).split('|') == [ - '1970[aha][interesting][really?]', - '1971[totally not]', - '' + {%- endfor %}""" + ) + assert tmpl.render(articles=articles).split("|") == [ + "1970[aha][interesting][really?]", + "1971[totally not]", + "", ] def test_filtertag(self, env): - tmpl = env.from_string("{% filter upper|replace('FOO', 'foo') %}" - "foobar{% endfilter %}") - assert tmpl.render() == 'fooBAR' + tmpl = env.from_string( + "{% filter upper|replace('FOO', 'foo') %}foobar{% endfilter %}" + ) + assert tmpl.render() == "fooBAR" def test_replace(self, env): env = Environment() tmpl = env.from_string('{{ string|replace("o", 42) }}') - assert tmpl.render(string='<foo>') == '<f4242>' + assert tmpl.render(string="<foo>") == "<f4242>" env = Environment(autoescape=True) tmpl = env.from_string('{{ string|replace("o", 42) }}') - assert tmpl.render(string='<foo>') == '<f4242>' + assert tmpl.render(string="<foo>") == "<f4242>" tmpl = env.from_string('{{ string|replace("<", 42) }}') - assert tmpl.render(string='<foo>') == '42foo>' + assert tmpl.render(string="<foo>") == "42foo>" tmpl = env.from_string('{{ string|replace("o", ">x<") }}') - assert tmpl.render(string=Markup('foo')) == 'f>x<>x<' + assert tmpl.render(string=Markup("foo")) == "f>x<>x<" def test_forceescape(self, env): - tmpl = env.from_string('{{ x|forceescape }}') - assert tmpl.render(x=Markup('<div />')) == u'<div />' + tmpl = env.from_string("{{ x|forceescape }}") + assert tmpl.render(x=Markup("<div />")) == u"<div />" def test_safe(self, env): env = Environment(autoescape=True) tmpl = env.from_string('{{ "<div>foo</div>"|safe }}') - assert tmpl.render() == '<div>foo</div>' + assert tmpl.render() == "<div>foo</div>" tmpl = env.from_string('{{ "<div>foo</div>" }}') - assert tmpl.render() == '<div>foo</div>' - - def test_urlencode(self, env): - env = Environment(autoescape=True) - tmpl = env.from_string('{{ "Hello, world!"|urlencode }}') - assert tmpl.render() == 'Hello%2C%20world%21' - tmpl = env.from_string('{{ o|urlencode }}') - assert tmpl.render(o=u"Hello, world\u203d") \ - == "Hello%2C%20world%E2%80%BD" - assert tmpl.render(o=(("f", 1),)) == "f=1" - assert tmpl.render(o=(('f', 1), ("z", 2))) == "f=1&z=2" - assert tmpl.render(o=((u"\u203d", 1),)) == "%E2%80%BD=1" - assert tmpl.render(o={u"\u203d": 1}) == "%E2%80%BD=1" - assert tmpl.render(o={0: 1}) == "0=1" + assert tmpl.render() == "<div>foo</div>" + + @pytest.mark.parametrize( + ("value", "expect"), + [ + ("Hello, world!", "Hello%2C%20world%21"), + (u"Hello, world\u203d", "Hello%2C%20world%E2%80%BD"), + ({"f": 1}, "f=1"), + ([("f", 1), ("z", 2)], "f=1&z=2"), + ({u"\u203d": 1}, "%E2%80%BD=1"), + ({0: 1}, "0=1"), + ([("a b/c", "a b/c")], "a+b%2Fc=a+b%2Fc"), + ("a b/c", "a%20b/c"), + ], + ) + def test_urlencode(self, value, expect): + e = Environment(autoescape=True) + t = e.from_string("{{ value|urlencode }}") + assert t.render(value=value) == expect def test_simple_map(self, env): env = Environment() tmpl = env.from_string('{{ ["1", "2", "3"]|map("int")|sum }}') - assert tmpl.render() == '6' + assert tmpl.render() == "6" + + def test_map_sum(self, env): + tmpl = env.from_string('{{ [[1,2], [3], [4,5,6]]|map("sum")|list }}') + assert tmpl.render() == "[3, 3, 15]" def test_attribute_map(self, env): - class User(object): - def __init__(self, name): - self.name = name + User = namedtuple("User", "name") env = Environment() users = [ - User('john'), - User('jane'), - User('mike'), + User("john"), + User("jane"), + User("mike"), ] tmpl = env.from_string('{{ users|map(attribute="name")|join("|") }}') - assert tmpl.render(users=users) == 'john|jane|mike' + assert tmpl.render(users=users) == "john|jane|mike" def test_empty_map(self, env): env = Environment() tmpl = env.from_string('{{ none|map("upper")|list }}') - assert tmpl.render() == '[]' + assert tmpl.render() == "[]" + + def test_map_default(self, env): + Fullname = namedtuple("Fullname", "firstname,lastname") + Firstname = namedtuple("Firstname", "firstname") + env = Environment() + tmpl = env.from_string( + '{{ users|map(attribute="lastname", default="smith")|join(", ") }}' + ) + users = [ + Fullname("john", "lennon"), + Fullname("jane", "edwards"), + Fullname("jon", None), + Firstname("mike"), + ] + assert tmpl.render(users=users) == "lennon, edwards, None, smith" def test_simple_select(self, env): env = Environment() tmpl = env.from_string('{{ [1, 2, 3, 4, 5]|select("odd")|join("|") }}') - assert tmpl.render() == '1|3|5' + assert tmpl.render() == "1|3|5" def test_bool_select(self, env): env = Environment() - tmpl = env.from_string( - '{{ [none, false, 0, 1, 2, 3, 4, 5]|select|join("|") }}' - ) - assert tmpl.render() == '1|2|3|4|5' + tmpl = env.from_string('{{ [none, false, 0, 1, 2, 3, 4, 5]|select|join("|") }}') + assert tmpl.render() == "1|2|3|4|5" def test_simple_reject(self, env): env = Environment() tmpl = env.from_string('{{ [1, 2, 3, 4, 5]|reject("odd")|join("|") }}') - assert tmpl.render() == '2|4' + assert tmpl.render() == "2|4" def test_bool_reject(self, env): env = Environment() - tmpl = env.from_string( - '{{ [none, false, 0, 1, 2, 3, 4, 5]|reject|join("|") }}' - ) - assert tmpl.render() == 'None|False|0' + tmpl = env.from_string('{{ [none, false, 0, 1, 2, 3, 4, 5]|reject|join("|") }}') + assert tmpl.render() == "None|False|0" def test_simple_select_attr(self, env): - class User(object): - def __init__(self, name, is_active): - self.name = name - self.is_active = is_active + User = namedtuple("User", "name,is_active") env = Environment() users = [ - User('john', True), - User('jane', True), - User('mike', False), + User("john", True), + User("jane", True), + User("mike", False), ] tmpl = env.from_string( - '{{ users|selectattr("is_active")|' - 'map(attribute="name")|join("|") }}' + '{{ users|selectattr("is_active")|map(attribute="name")|join("|") }}' ) - assert tmpl.render(users=users) == 'john|jane' + assert tmpl.render(users=users) == "john|jane" def test_simple_reject_attr(self, env): - class User(object): - def __init__(self, name, is_active): - self.name = name - self.is_active = is_active + User = namedtuple("User", "name,is_active") env = Environment() users = [ - User('john', True), - User('jane', True), - User('mike', False), + User("john", True), + User("jane", True), + User("mike", False), ] - tmpl = env.from_string('{{ users|rejectattr("is_active")|' - 'map(attribute="name")|join("|") }}') - assert tmpl.render(users=users) == 'mike' + tmpl = env.from_string( + '{{ users|rejectattr("is_active")|map(attribute="name")|join("|") }}' + ) + assert tmpl.render(users=users) == "mike" def test_func_select_attr(self, env): - class User(object): - def __init__(self, id, name): - self.id = id - self.name = name + User = namedtuple("User", "id,name") env = Environment() users = [ - User(1, 'john'), - User(2, 'jane'), - User(3, 'mike'), + User(1, "john"), + User(2, "jane"), + User(3, "mike"), ] - tmpl = env.from_string('{{ users|selectattr("id", "odd")|' - 'map(attribute="name")|join("|") }}') - assert tmpl.render(users=users) == 'john|mike' + tmpl = env.from_string( + '{{ users|selectattr("id", "odd")|map(attribute="name")|join("|") }}' + ) + assert tmpl.render(users=users) == "john|mike" def test_func_reject_attr(self, env): - class User(object): - def __init__(self, id, name): - self.id = id - self.name = name + User = namedtuple("User", "id,name") env = Environment() users = [ - User(1, 'john'), - User(2, 'jane'), - User(3, 'mike'), + User(1, "john"), + User(2, "jane"), + User(3, "mike"), ] - tmpl = env.from_string('{{ users|rejectattr("id", "odd")|' - 'map(attribute="name")|join("|") }}') - assert tmpl.render(users=users) == 'jane' + tmpl = env.from_string( + '{{ users|rejectattr("id", "odd")|map(attribute="name")|join("|") }}' + ) + assert tmpl.render(users=users) == "jane" def test_json_dump(self): env = Environment(autoescape=True) - t = env.from_string('{{ x|tojson }}') - assert t.render(x={'foo': 'bar'}) == '{"foo": "bar"}' - assert t.render(x='"ba&r\'') == r'"\"ba\u0026r\u0027"' - assert t.render(x='<bar>') == r'"\u003cbar\u003e"' + t = env.from_string("{{ x|tojson }}") + assert t.render(x={"foo": "bar"}) == '{"foo": "bar"}' + assert t.render(x="\"ba&r'") == r'"\"ba\u0026r\u0027"' + assert t.render(x="<bar>") == r'"\u003cbar\u003e"' def my_dumps(value, **options): - assert options == {'foo': 'bar'} - return '42' - env.policies['json.dumps_function'] = my_dumps - env.policies['json.dumps_kwargs'] = {'foo': 'bar'} - assert t.render(x=23) == '42' + assert options == {"foo": "bar"} + return "42" + + env.policies["json.dumps_function"] = my_dumps + env.policies["json.dumps_kwargs"] = {"foo": "bar"} + assert t.render(x=23) == "42" + + def test_wordwrap(self, env): + env.newline_sequence = "\n" + t = env.from_string("{{ s|wordwrap(20) }}") + result = t.render(s="Hello!\nThis is Jinja saying something.") + assert result == "Hello!\nThis is Jinja saying\nsomething." diff --git a/tests/test_idtracking.py b/tests/test_idtracking.py index 29312d3..4c79ee6 100644 --- a/tests/test_idtracking.py +++ b/tests/test_idtracking.py @@ -4,213 +4,286 @@ from jinja2.idtracking import symbols_for_node def test_basics(): for_loop = nodes.For( - nodes.Name('foo', 'store'), - nodes.Name('seq', 'load'), - [nodes.Output([nodes.Name('foo', 'load')])], - [], None, False) - tmpl = nodes.Template([ - nodes.Assign( - nodes.Name('foo', 'store'), - nodes.Name('bar', 'load')), - for_loop]) + nodes.Name("foo", "store"), + nodes.Name("seq", "load"), + [nodes.Output([nodes.Name("foo", "load")])], + [], + None, + False, + ) + tmpl = nodes.Template( + [nodes.Assign(nodes.Name("foo", "store"), nodes.Name("bar", "load")), for_loop] + ) sym = symbols_for_node(tmpl) assert sym.refs == { - 'foo': 'l_0_foo', - 'bar': 'l_0_bar', - 'seq': 'l_0_seq', + "foo": "l_0_foo", + "bar": "l_0_bar", + "seq": "l_0_seq", } assert sym.loads == { - 'l_0_foo': ('undefined', None), - 'l_0_bar': ('resolve', 'bar'), - 'l_0_seq': ('resolve', 'seq'), + "l_0_foo": ("undefined", None), + "l_0_bar": ("resolve", "bar"), + "l_0_seq": ("resolve", "seq"), } sym = symbols_for_node(for_loop, sym) assert sym.refs == { - 'foo': 'l_1_foo', + "foo": "l_1_foo", } assert sym.loads == { - 'l_1_foo': ('param', None), + "l_1_foo": ("param", None), } def test_complex(): - title_block = nodes.Block('title', [ - nodes.Output([nodes.TemplateData(u'Page Title')]) - ], False) - - render_title_macro = nodes.Macro('render_title', [nodes.Name('title', 'param')], [], [ - nodes.Output([ - nodes.TemplateData(u'\n <div class="title">\n <h1>'), - nodes.Name('title', 'load'), - nodes.TemplateData(u'</h1>\n <p>'), - nodes.Name('subtitle', 'load'), - nodes.TemplateData(u'</p>\n ')]), - nodes.Assign( - nodes.Name('subtitle', 'store'), nodes.Const('something else')), - nodes.Output([ - nodes.TemplateData(u'\n <p>'), - nodes.Name('subtitle', 'load'), - nodes.TemplateData(u'</p>\n </div>\n'), - nodes.If( - nodes.Name('something', 'load'), [ - nodes.Assign(nodes.Name('title_upper', 'store'), - nodes.Filter(nodes.Name('title', 'load'), - 'upper', [], [], None, None)), - nodes.Output([ - nodes.Name('title_upper', 'load'), - nodes.Call(nodes.Name('render_title', 'load'), [ - nodes.Const('Aha')], [], None, None)])], [], [])])]) + title_block = nodes.Block( + "title", [nodes.Output([nodes.TemplateData(u"Page Title")])], False + ) + + render_title_macro = nodes.Macro( + "render_title", + [nodes.Name("title", "param")], + [], + [ + nodes.Output( + [ + nodes.TemplateData(u'\n <div class="title">\n <h1>'), + nodes.Name("title", "load"), + nodes.TemplateData(u"</h1>\n <p>"), + nodes.Name("subtitle", "load"), + nodes.TemplateData(u"</p>\n "), + ] + ), + nodes.Assign( + nodes.Name("subtitle", "store"), nodes.Const("something else") + ), + nodes.Output( + [ + nodes.TemplateData(u"\n <p>"), + nodes.Name("subtitle", "load"), + nodes.TemplateData(u"</p>\n </div>\n"), + nodes.If( + nodes.Name("something", "load"), + [ + nodes.Assign( + nodes.Name("title_upper", "store"), + nodes.Filter( + nodes.Name("title", "load"), + "upper", + [], + [], + None, + None, + ), + ), + nodes.Output( + [ + nodes.Name("title_upper", "load"), + nodes.Call( + nodes.Name("render_title", "load"), + [nodes.Const("Aha")], + [], + None, + None, + ), + ] + ), + ], + [], + [], + ), + ] + ), + ], + ) for_loop = nodes.For( - nodes.Name('item', 'store'), - nodes.Name('seq', 'load'), [ - nodes.Output([ - nodes.TemplateData(u'\n <li>'), - nodes.Name('item', 'load'), - nodes.TemplateData(u'</li>\n <span>')]), - nodes.Include(nodes.Const('helper.html'), True, False), - nodes.Output([ - nodes.TemplateData(u'</span>\n ')])], [], None, False) - - body_block = nodes.Block('body', [ - nodes.Output([ - nodes.TemplateData(u'\n '), - nodes.Call(nodes.Name('render_title', 'load'), [ - nodes.Name('item', 'load')], [], None, None), - nodes.TemplateData(u'\n <ul>\n ')]), - for_loop, - nodes.Output([nodes.TemplateData(u'\n </ul>\n')])], - False) - - tmpl = nodes.Template([ - nodes.Extends(nodes.Const('layout.html')), - title_block, - render_title_macro, - body_block, - ]) + nodes.Name("item", "store"), + nodes.Name("seq", "load"), + [ + nodes.Output( + [ + nodes.TemplateData(u"\n <li>"), + nodes.Name("item", "load"), + nodes.TemplateData(u"</li>\n <span>"), + ] + ), + nodes.Include(nodes.Const("helper.html"), True, False), + nodes.Output([nodes.TemplateData(u"</span>\n ")]), + ], + [], + None, + False, + ) + + body_block = nodes.Block( + "body", + [ + nodes.Output( + [ + nodes.TemplateData(u"\n "), + nodes.Call( + nodes.Name("render_title", "load"), + [nodes.Name("item", "load")], + [], + None, + None, + ), + nodes.TemplateData(u"\n <ul>\n "), + ] + ), + for_loop, + nodes.Output([nodes.TemplateData(u"\n </ul>\n")]), + ], + False, + ) + + tmpl = nodes.Template( + [ + nodes.Extends(nodes.Const("layout.html")), + title_block, + render_title_macro, + body_block, + ] + ) tmpl_sym = symbols_for_node(tmpl) assert tmpl_sym.refs == { - 'render_title': 'l_0_render_title', + "render_title": "l_0_render_title", } assert tmpl_sym.loads == { - 'l_0_render_title': ('undefined', None), + "l_0_render_title": ("undefined", None), } - assert tmpl_sym.stores == set(['render_title']) + assert tmpl_sym.stores == set(["render_title"]) assert tmpl_sym.dump_stores() == { - 'render_title': 'l_0_render_title', + "render_title": "l_0_render_title", } macro_sym = symbols_for_node(render_title_macro, tmpl_sym) assert macro_sym.refs == { - 'subtitle': 'l_1_subtitle', - 'something': 'l_1_something', - 'title': 'l_1_title', - 'title_upper': 'l_1_title_upper', + "subtitle": "l_1_subtitle", + "something": "l_1_something", + "title": "l_1_title", + "title_upper": "l_1_title_upper", } assert macro_sym.loads == { - 'l_1_subtitle': ('resolve', 'subtitle'), - 'l_1_something': ('resolve','something'), - 'l_1_title': ('param', None), - 'l_1_title_upper': ('resolve', 'title_upper'), + "l_1_subtitle": ("resolve", "subtitle"), + "l_1_something": ("resolve", "something"), + "l_1_title": ("param", None), + "l_1_title_upper": ("resolve", "title_upper"), } - assert macro_sym.stores == set(['title', 'title_upper', 'subtitle']) - assert macro_sym.find_ref('render_title') == 'l_0_render_title' + assert macro_sym.stores == set(["title", "title_upper", "subtitle"]) + assert macro_sym.find_ref("render_title") == "l_0_render_title" assert macro_sym.dump_stores() == { - 'title': 'l_1_title', - 'title_upper': 'l_1_title_upper', - 'subtitle': 'l_1_subtitle', - 'render_title': 'l_0_render_title', + "title": "l_1_title", + "title_upper": "l_1_title_upper", + "subtitle": "l_1_subtitle", + "render_title": "l_0_render_title", } body_sym = symbols_for_node(body_block) assert body_sym.refs == { - 'item': 'l_0_item', - 'seq': 'l_0_seq', - 'render_title': 'l_0_render_title', + "item": "l_0_item", + "seq": "l_0_seq", + "render_title": "l_0_render_title", } assert body_sym.loads == { - 'l_0_item': ('resolve', 'item'), - 'l_0_seq': ('resolve', 'seq'), - 'l_0_render_title': ('resolve', 'render_title'), + "l_0_item": ("resolve", "item"), + "l_0_seq": ("resolve", "seq"), + "l_0_render_title": ("resolve", "render_title"), } assert body_sym.stores == set([]) for_sym = symbols_for_node(for_loop, body_sym) assert for_sym.refs == { - 'item': 'l_1_item', + "item": "l_1_item", } assert for_sym.loads == { - 'l_1_item': ('param', None), + "l_1_item": ("param", None), } - assert for_sym.stores == set(['item']) + assert for_sym.stores == set(["item"]) assert for_sym.dump_stores() == { - 'item': 'l_1_item', + "item": "l_1_item", } def test_if_branching_stores(): - tmpl = nodes.Template([ - nodes.If(nodes.Name('expression', 'load'), [ - nodes.Assign(nodes.Name('variable', 'store'), - nodes.Const(42))], [], [])]) + tmpl = nodes.Template( + [ + nodes.If( + nodes.Name("expression", "load"), + [nodes.Assign(nodes.Name("variable", "store"), nodes.Const(42))], + [], + [], + ) + ] + ) sym = symbols_for_node(tmpl) - assert sym.refs == { - 'variable': 'l_0_variable', - 'expression': 'l_0_expression' - } - assert sym.stores == set(['variable']) + assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"} + assert sym.stores == set(["variable"]) assert sym.loads == { - 'l_0_variable': ('resolve', 'variable'), - 'l_0_expression': ('resolve', 'expression') + "l_0_variable": ("resolve", "variable"), + "l_0_expression": ("resolve", "expression"), } assert sym.dump_stores() == { - 'variable': 'l_0_variable', + "variable": "l_0_variable", } def test_if_branching_stores_undefined(): - tmpl = nodes.Template([ - nodes.Assign(nodes.Name('variable', 'store'), nodes.Const(23)), - nodes.If(nodes.Name('expression', 'load'), [ - nodes.Assign(nodes.Name('variable', 'store'), - nodes.Const(42))], [], [])]) + tmpl = nodes.Template( + [ + nodes.Assign(nodes.Name("variable", "store"), nodes.Const(23)), + nodes.If( + nodes.Name("expression", "load"), + [nodes.Assign(nodes.Name("variable", "store"), nodes.Const(42))], + [], + [], + ), + ] + ) sym = symbols_for_node(tmpl) - assert sym.refs == { - 'variable': 'l_0_variable', - 'expression': 'l_0_expression' - } - assert sym.stores == set(['variable']) + assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"} + assert sym.stores == set(["variable"]) assert sym.loads == { - 'l_0_variable': ('undefined', None), - 'l_0_expression': ('resolve', 'expression') + "l_0_variable": ("undefined", None), + "l_0_expression": ("resolve", "expression"), } assert sym.dump_stores() == { - 'variable': 'l_0_variable', + "variable": "l_0_variable", } def test_if_branching_multi_scope(): - for_loop = nodes.For(nodes.Name('item', 'store'), nodes.Name('seq', 'load'), [ - nodes.If(nodes.Name('expression', 'load'), [ - nodes.Assign(nodes.Name('x', 'store'), nodes.Const(42))], [], []), - nodes.Include(nodes.Const('helper.html'), True, False) - ], [], None, False) + for_loop = nodes.For( + nodes.Name("item", "store"), + nodes.Name("seq", "load"), + [ + nodes.If( + nodes.Name("expression", "load"), + [nodes.Assign(nodes.Name("x", "store"), nodes.Const(42))], + [], + [], + ), + nodes.Include(nodes.Const("helper.html"), True, False), + ], + [], + None, + False, + ) - tmpl = nodes.Template([ - nodes.Assign(nodes.Name('x', 'store'), nodes.Const(23)), - for_loop - ]) + tmpl = nodes.Template( + [nodes.Assign(nodes.Name("x", "store"), nodes.Const(23)), for_loop] + ) tmpl_sym = symbols_for_node(tmpl) for_sym = symbols_for_node(for_loop, tmpl_sym) - assert for_sym.stores == set(['item', 'x']) + assert for_sym.stores == set(["item", "x"]) assert for_sym.loads == { - 'l_1_x': ('alias', 'l_0_x'), - 'l_1_item': ('param', None), - 'l_1_expression': ('resolve', 'expression'), + "l_1_x": ("alias", "l_0_x"), + "l_1_item": ("param", None), + "l_1_expression": ("resolve", "expression"), } diff --git a/tests/test_imports.py b/tests/test_imports.py index 65aae43..fad2eda 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -1,55 +1,50 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.imports - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests the import features (with includes). - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from jinja2 import Environment, DictLoader -from jinja2.exceptions import TemplateNotFound, TemplatesNotFound, \ - TemplateSyntaxError +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2.exceptions import TemplateNotFound +from jinja2.exceptions import TemplatesNotFound +from jinja2.exceptions import TemplateSyntaxError @pytest.fixture def test_env(): - env = Environment(loader=DictLoader(dict( - module='{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}', - header='[{{ foo }}|{{ 23 }}]', - o_printer='({{ o }})' - ))) - env.globals['bar'] = 23 + env = Environment( + loader=DictLoader( + dict( + module="{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}", + header="[{{ foo }}|{{ 23 }}]", + o_printer="({{ o }})", + ) + ) + ) + env.globals["bar"] = 23 return env -@pytest.mark.imports class TestImports(object): - def test_context_imports(self, test_env): t = test_env.from_string('{% import "module" as m %}{{ m.test() }}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env.from_string( '{% import "module" as m without context %}{{ m.test() }}' ) - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env.from_string( '{% import "module" as m with context %}{{ m.test() }}' ) - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env.from_string('{% from "module" import test %}{{ test() }}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env.from_string( '{% from "module" import test without context %}{{ test() }}' ) - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" t = test_env.from_string( '{% from "module" import test with context %}{{ test() }}' ) - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" def test_import_needs_name(self, test_env): test_env.from_string('{% from "foo" import bar %}') @@ -82,85 +77,84 @@ class TestImports(object): test_env.from_string('{% from "foo" import bar with context, %}') def test_exports(self, test_env): - m = test_env.from_string(''' + m = test_env.from_string( + """ {% macro toplevel() %}...{% endmacro %} {% macro __private() %}...{% endmacro %} {% set variable = 42 %} {% for item in [1] %} {% macro notthere() %}{% endmacro %} {% endfor %} - ''').module - assert m.toplevel() == '...' - assert not hasattr(m, '__missing') + """ + ).module + assert m.toplevel() == "..." + assert not hasattr(m, "__missing") assert m.variable == 42 - assert not hasattr(m, 'notthere') + assert not hasattr(m, "notthere") -@pytest.mark.imports -@pytest.mark.includes class TestIncludes(object): - def test_context_include(self, test_env): t = test_env.from_string('{% include "header" %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env.from_string('{% include "header" with context %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" t = test_env.from_string('{% include "header" without context %}') - assert t.render(foo=42) == '[|23]' + assert t.render(foo=42) == "[|23]" def test_choice_includes(self, test_env): t = test_env.from_string('{% include ["missing", "header"] %}') - assert t.render(foo=42) == '[42|23]' + assert t.render(foo=42) == "[42|23]" - t = test_env.from_string( - '{% include ["missing", "missing2"] ignore missing %}' - ) - assert t.render(foo=42) == '' + t = test_env.from_string('{% include ["missing", "missing2"] ignore missing %}') + assert t.render(foo=42) == "" t = test_env.from_string('{% include ["missing", "missing2"] %}') pytest.raises(TemplateNotFound, t.render) - try: + with pytest.raises(TemplatesNotFound) as e: t.render() - except TemplatesNotFound as e: - assert e.templates == ['missing', 'missing2'] - assert e.name == 'missing2' - else: - assert False, 'thou shalt raise' + + assert e.value.templates == ["missing", "missing2"] + assert e.value.name == "missing2" def test_includes(t, **ctx): - ctx['foo'] = 42 - assert t.render(ctx) == '[42|23]' + ctx["foo"] = 42 + assert t.render(ctx) == "[42|23]" t = test_env.from_string('{% include ["missing", "header"] %}') test_includes(t) - t = test_env.from_string('{% include x %}') - test_includes(t, x=['missing', 'header']) + t = test_env.from_string("{% include x %}") + test_includes(t, x=["missing", "header"]) t = test_env.from_string('{% include [x, "header"] %}') - test_includes(t, x='missing') - t = test_env.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env.from_string('{% include [x] %}') - test_includes(t, x='header') + test_includes(t, x="missing") + t = test_env.from_string("{% include x %}") + test_includes(t, x="header") + t = test_env.from_string("{% include [x] %}") + test_includes(t, x="header") def test_include_ignoring_missing(self, test_env): t = test_env.from_string('{% include "missing" %}') pytest.raises(TemplateNotFound, t.render) - for extra in '', 'with context', 'without context': - t = test_env.from_string('{% include "missing" ignore missing ' + - extra + ' %}') - assert t.render() == '' + for extra in "", "with context", "without context": + t = test_env.from_string( + '{% include "missing" ignore missing ' + extra + " %}" + ) + assert t.render() == "" def test_context_include_with_overrides(self, test_env): - env = Environment(loader=DictLoader(dict( - main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", - item="{{ item }}" - ))) + env = Environment( + loader=DictLoader( + dict( + main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", + item="{{ item }}", + ) + ) + ) assert env.get_template("main").render() == "123" def test_unoptimized_scopes(self, test_env): - t = test_env.from_string(""" + t = test_env.from_string( + """ {% macro outer(o) %} {% macro inner() %} {% include "o_printer" %} @@ -168,13 +162,15 @@ class TestIncludes(object): {{ inner() }} {% endmacro %} {{ outer("FOO") }} - """) - assert t.render().strip() == '(FOO)' + """ + ) + assert t.render().strip() == "(FOO)" def test_import_from_with_context(self): - env = Environment(loader=DictLoader({ - 'a': '{% macro x() %}{{ foobar }}{% endmacro %}', - })) - t = env.from_string('{% set foobar = 42 %}{% from "a" ' - 'import x with context %}{{ x() }}') - assert t.render() == '42' + env = Environment( + loader=DictLoader({"a": "{% macro x() %}{{ foobar }}{% endmacro %}"}) + ) + t = env.from_string( + "{% set foobar = 42 %}{% from 'a' import x with context %}{{ x() }}" + ) + assert t.render() == "42" diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index 7746c2d..e513d2e 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -1,46 +1,38 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.inheritance - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests the template inheritance feature. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from jinja2 import Environment, DictLoader, TemplateError +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import TemplateRuntimeError - -LAYOUTTEMPLATE = '''\ +LAYOUTTEMPLATE = """\ |{% block block1 %}block 1 from layout{% endblock %} |{% block block2 %}block 2 from layout{% endblock %} |{% block block3 %} {% block block4 %}nested block 4 from layout{% endblock %} -{% endblock %}|''' +{% endblock %}|""" -LEVEL1TEMPLATE = '''\ +LEVEL1TEMPLATE = """\ {% extends "layout" %} -{% block block1 %}block 1 from level1{% endblock %}''' +{% block block1 %}block 1 from level1{% endblock %}""" -LEVEL2TEMPLATE = '''\ +LEVEL2TEMPLATE = """\ {% extends "level1" %} {% block block2 %}{% block block5 %}nested block 5 from level2{% -endblock %}{% endblock %}''' +endblock %}{% endblock %}""" -LEVEL3TEMPLATE = '''\ +LEVEL3TEMPLATE = """\ {% extends "level2" %} {% block block5 %}block 5 from level3{% endblock %} {% block block4 %}block 4 from level3{% endblock %} -''' +""" -LEVEL4TEMPLATE = '''\ +LEVEL4TEMPLATE = """\ {% extends "level3" %} {% block block3 %}block 3 from level4{% endblock %} -''' +""" -WORKINGTEMPLATE = '''\ +WORKINGTEMPLATE = """\ {% extends "layout" %} {% block block1 %} {% if false %} @@ -49,9 +41,9 @@ WORKINGTEMPLATE = '''\ {% endblock %} {% endif %} {% endblock %} -''' +""" -DOUBLEEXTENDS = '''\ +DOUBLEEXTENDS = """\ {% extends "layout" %} {% extends "layout" %} {% block block1 %} @@ -61,127 +53,165 @@ DOUBLEEXTENDS = '''\ {% endblock %} {% endif %} {% endblock %} -''' +""" @pytest.fixture def env(): - return Environment(loader=DictLoader({ - 'layout': LAYOUTTEMPLATE, - 'level1': LEVEL1TEMPLATE, - 'level2': LEVEL2TEMPLATE, - 'level3': LEVEL3TEMPLATE, - 'level4': LEVEL4TEMPLATE, - 'working': WORKINGTEMPLATE, - 'doublee': DOUBLEEXTENDS, - }), trim_blocks=True) - - -@pytest.mark.inheritance -class TestInheritance(object): + return Environment( + loader=DictLoader( + { + "layout": LAYOUTTEMPLATE, + "level1": LEVEL1TEMPLATE, + "level2": LEVEL2TEMPLATE, + "level3": LEVEL3TEMPLATE, + "level4": LEVEL4TEMPLATE, + "working": WORKINGTEMPLATE, + "doublee": DOUBLEEXTENDS, + } + ), + trim_blocks=True, + ) + +class TestInheritance(object): def test_layout(self, env): - tmpl = env.get_template('layout') - assert tmpl.render() == ('|block 1 from layout|block 2 from ' - 'layout|nested block 4 from layout|') + tmpl = env.get_template("layout") + assert tmpl.render() == ( + "|block 1 from layout|block 2 from layout|nested block 4 from layout|" + ) def test_level1(self, env): - tmpl = env.get_template('level1') - assert tmpl.render() == ('|block 1 from level1|block 2 from ' - 'layout|nested block 4 from layout|') + tmpl = env.get_template("level1") + assert tmpl.render() == ( + "|block 1 from level1|block 2 from layout|nested block 4 from layout|" + ) def test_level2(self, env): - tmpl = env.get_template('level2') - assert tmpl.render() == ('|block 1 from level1|nested block 5 from ' - 'level2|nested block 4 from layout|') + tmpl = env.get_template("level2") + assert tmpl.render() == ( + "|block 1 from level1|nested block 5 from " + "level2|nested block 4 from layout|" + ) def test_level3(self, env): - tmpl = env.get_template('level3') - assert tmpl.render() == ('|block 1 from level1|block 5 from level3|' - 'block 4 from level3|') + tmpl = env.get_template("level3") + assert tmpl.render() == ( + "|block 1 from level1|block 5 from level3|block 4 from level3|" + ) def test_level4(self, env): - tmpl = env.get_template('level4') - assert tmpl.render() == ('|block 1 from level1|block 5 from ' - 'level3|block 3 from level4|') + tmpl = env.get_template("level4") + assert tmpl.render() == ( + "|block 1 from level1|block 5 from level3|block 3 from level4|" + ) def test_super(self, env): - env = Environment(loader=DictLoader({ - 'a': '{% block intro %}INTRO{% endblock %}|' - 'BEFORE|{% block data %}INNER{% endblock %}|AFTER', - 'b': '{% extends "a" %}{% block data %}({{ ' - 'super() }}){% endblock %}', - 'c': '{% extends "b" %}{% block intro %}--{{ ' - 'super() }}--{% endblock %}\n{% block data ' - '%}[{{ super() }}]{% endblock %}' - })) - tmpl = env.get_template('c') - assert tmpl.render() == '--INTRO--|BEFORE|[(INNER)]|AFTER' + env = Environment( + loader=DictLoader( + { + "a": "{% block intro %}INTRO{% endblock %}|" + "BEFORE|{% block data %}INNER{% endblock %}|AFTER", + "b": '{% extends "a" %}{% block data %}({{ ' + "super() }}){% endblock %}", + "c": '{% extends "b" %}{% block intro %}--{{ ' + "super() }}--{% endblock %}\n{% block data " + "%}[{{ super() }}]{% endblock %}", + } + ) + ) + tmpl = env.get_template("c") + assert tmpl.render() == "--INTRO--|BEFORE|[(INNER)]|AFTER" def test_working(self, env): - tmpl = env.get_template('working') + env.get_template("working") def test_reuse_blocks(self, env): - tmpl = env.from_string('{{ self.foo() }}|{% block foo %}42' - '{% endblock %}|{{ self.foo() }}') - assert tmpl.render() == '42|42|42' + tmpl = env.from_string( + "{{ self.foo() }}|{% block foo %}42{% endblock %}|{{ self.foo() }}" + ) + assert tmpl.render() == "42|42|42" def test_preserve_blocks(self, env): - env = Environment(loader=DictLoader({ - 'a': '{% if false %}{% block x %}A{% endblock %}' - '{% endif %}{{ self.x() }}', - 'b': '{% extends "a" %}{% block x %}B{{ super() }}{% endblock %}' - })) - tmpl = env.get_template('b') - assert tmpl.render() == 'BA' + env = Environment( + loader=DictLoader( + { + "a": "{% if false %}{% block x %}A{% endblock %}" + "{% endif %}{{ self.x() }}", + "b": '{% extends "a" %}{% block x %}B{{ super() }}{% endblock %}', + } + ) + ) + tmpl = env.get_template("b") + assert tmpl.render() == "BA" def test_dynamic_inheritance(self, env): - env = Environment(loader=DictLoader({ - 'master1': 'MASTER1{% block x %}{% endblock %}', - 'master2': 'MASTER2{% block x %}{% endblock %}', - 'child': '{% extends master %}{% block x %}CHILD{% endblock %}' - })) - tmpl = env.get_template('child') + env = Environment( + loader=DictLoader( + { + "master1": "MASTER1{% block x %}{% endblock %}", + "master2": "MASTER2{% block x %}{% endblock %}", + "child": "{% extends master %}{% block x %}CHILD{% endblock %}", + } + ) + ) + tmpl = env.get_template("child") for m in range(1, 3): - assert tmpl.render(master='master%d' % m) == 'MASTER%dCHILD' % m + assert tmpl.render(master="master%d" % m) == "MASTER%dCHILD" % m def test_multi_inheritance(self, env): - env = Environment(loader=DictLoader({ - 'master1': 'MASTER1{% block x %}{% endblock %}', - 'master2': 'MASTER2{% block x %}{% endblock %}', - 'child': - '''{% if master %}{% extends master %}{% else %}{% extends - 'master1' %}{% endif %}{% block x %}CHILD{% endblock %}''' - })) - tmpl = env.get_template('child') - assert tmpl.render(master='master2') == 'MASTER2CHILD' - assert tmpl.render(master='master1') == 'MASTER1CHILD' - assert tmpl.render() == 'MASTER1CHILD' + env = Environment( + loader=DictLoader( + { + "master1": "MASTER1{% block x %}{% endblock %}", + "master2": "MASTER2{% block x %}{% endblock %}", + "child": """{% if master %}{% extends master %}{% else %}{% extends + 'master1' %}{% endif %}{% block x %}CHILD{% endblock %}""", + } + ) + ) + tmpl = env.get_template("child") + assert tmpl.render(master="master2") == "MASTER2CHILD" + assert tmpl.render(master="master1") == "MASTER1CHILD" + assert tmpl.render() == "MASTER1CHILD" def test_scoped_block(self, env): - env = Environment(loader=DictLoader({ - 'master.html': '{% for item in seq %}[{% block item scoped %}' - '{% endblock %}]{% endfor %}' - })) - t = env.from_string('{% extends "master.html" %}{% block item %}' - '{{ item }}{% endblock %}') - assert t.render(seq=list(range(5))) == '[0][1][2][3][4]' + env = Environment( + loader=DictLoader( + { + "master.html": "{% for item in seq %}[{% block item scoped %}" + "{% endblock %}]{% endfor %}" + } + ) + ) + t = env.from_string( + "{% extends 'master.html' %}{% block item %}{{ item }}{% endblock %}" + ) + assert t.render(seq=list(range(5))) == "[0][1][2][3][4]" def test_super_in_scoped_block(self, env): - env = Environment(loader=DictLoader({ - 'master.html': '{% for item in seq %}[{% block item scoped %}' - '{{ item }}{% endblock %}]{% endfor %}' - })) - t = env.from_string('{% extends "master.html" %}{% block item %}' - '{{ super() }}|{{ item * 2 }}{% endblock %}') - assert t.render(seq=list(range(5))) == '[0|0][1|2][2|4][3|6][4|8]' + env = Environment( + loader=DictLoader( + { + "master.html": "{% for item in seq %}[{% block item scoped %}" + "{{ item }}{% endblock %}]{% endfor %}" + } + ) + ) + t = env.from_string( + '{% extends "master.html" %}{% block item %}' + "{{ super() }}|{{ item * 2 }}{% endblock %}" + ) + assert t.render(seq=list(range(5))) == "[0|0][1|2][2|4][3|6][4|8]" def test_scoped_block_after_inheritance(self, env): - env = Environment(loader=DictLoader({ - 'layout.html': ''' + env = Environment( + loader=DictLoader( + { + "layout.html": """ {% block useless %}{% endblock %} - ''', - 'index.html': ''' + """, + "index.html": """ {%- extends 'layout.html' %} {% from 'helpers.html' import foo with context %} {% block useless %} @@ -191,21 +221,24 @@ class TestInheritance(object): {% endblock %} {% endfor %} {% endblock %} - ''', - 'helpers.html': ''' + """, + "helpers.html": """ {% macro foo(x) %}{{ the_foo + x }}{% endmacro %} - ''' - })) - rv = env.get_template('index.html').render(the_foo=42).split() - assert rv == ['43', '44', '45'] + """, + } + ) + ) + rv = env.get_template("index.html").render(the_foo=42).split() + assert rv == ["43", "44", "45"] -@pytest.mark.inheritance class TestBugFix(object): - def test_fixed_macro_scoping_bug(self, env): - assert Environment(loader=DictLoader({ - 'test.html': '''\ + assert ( + Environment( + loader=DictLoader( + { + "test.html": """\ {% extends 'details.html' %} {% macro my_macro() %} @@ -215,8 +248,8 @@ class TestBugFix(object): {% block inner_box %} {{ my_macro() }} {% endblock %} - ''', - 'details.html': '''\ + """, + "details.html": """\ {% extends 'standard.html' %} {% macro my_macro() %} @@ -231,18 +264,22 @@ class TestBugFix(object): {% endblock %} {% endblock %} {% endblock %} - ''', - 'standard.html': ''' + """, + "standard.html": """ {% block content %} {% endblock %} - ''' - })).get_template("test.html").render().split() \ - == [u'outer_box', u'my_macro'] + """, + } + ) + ) + .get_template("test.html") + .render() + .split() + == [u"outer_box", u"my_macro"] + ) def test_double_extends(self, env): """Ensures that a template with more than 1 {% extends ... %} usage raises a ``TemplateError``. """ - try: - tmpl = env.get_template('doublee') - except Exception as e: - assert isinstance(e, TemplateError) + with pytest.raises(TemplateRuntimeError, match="extended multiple times"): + env.get_template("doublee").render() diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py index 7da72c0..83ae75e 100644 --- a/tests/test_lexnparse.py +++ b/tests/test_lexnparse.py @@ -1,36 +1,37 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.lexnparse - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - All the unittests regarding lexing, parsing and syntax. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from jinja2 import Environment, Template, TemplateSyntaxError, \ - UndefinedError, nodes -from jinja2._compat import iteritems, text_type, PY2 -from jinja2.lexer import Token, TokenStream, TOKEN_EOF, \ - TOKEN_BLOCK_BEGIN, TOKEN_BLOCK_END +from jinja2 import Environment +from jinja2 import nodes +from jinja2 import Template +from jinja2 import TemplateSyntaxError +from jinja2 import UndefinedError +from jinja2._compat import iteritems +from jinja2._compat import PY2 +from jinja2._compat import text_type +from jinja2.lexer import Token +from jinja2.lexer import TOKEN_BLOCK_BEGIN +from jinja2.lexer import TOKEN_BLOCK_END +from jinja2.lexer import TOKEN_EOF +from jinja2.lexer import TokenStream # how does a string look like in jinja syntax? if PY2: + def jinja_string_repr(string): return repr(string)[1:] + + else: jinja_string_repr = repr -@pytest.mark.lexnparse -@pytest.mark.tokenstream class TestTokenStream(object): - test_tokens = [Token(1, TOKEN_BLOCK_BEGIN, ''), - Token(2, TOKEN_BLOCK_END, ''), - ] + test_tokens = [ + Token(1, TOKEN_BLOCK_BEGIN, ""), + Token(2, TOKEN_BLOCK_END, ""), + ] def test_simple(self, env): ts = TokenStream(self.test_tokens, "foo", "bar") @@ -47,105 +48,129 @@ class TestTokenStream(object): assert bool(ts.eos) def test_iter(self, env): - token_types = [ - t.type for t in TokenStream(self.test_tokens, "foo", "bar") + token_types = [t.type for t in TokenStream(self.test_tokens, "foo", "bar")] + assert token_types == [ + "block_begin", + "block_end", ] - assert token_types == ['block_begin', 'block_end', ] -@pytest.mark.lexnparse -@pytest.mark.lexer class TestLexer(object): - def test_raw1(self, env): tmpl = env.from_string( - '{% raw %}foo{% endraw %}|' - '{%raw%}{{ bar }}|{% baz %}{% endraw %}') - assert tmpl.render() == 'foo|{{ bar }}|{% baz %}' + "{% raw %}foo{% endraw %}|" + "{%raw%}{{ bar }}|{% baz %}{% endraw %}" + ) + assert tmpl.render() == "foo|{{ bar }}|{% baz %}" def test_raw2(self, env): - tmpl = env.from_string('1 {%- raw -%} 2 {%- endraw -%} 3') - assert tmpl.render() == '123' + tmpl = env.from_string("1 {%- raw -%} 2 {%- endraw -%} 3") + assert tmpl.render() == "123" + + def test_raw3(self, env): + # The second newline after baz exists because it is AFTER the + # {% raw %} and is ignored. + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string("bar\n{% raw %}\n {{baz}}2 spaces\n{% endraw %}\nfoo") + assert tmpl.render(baz="test") == "bar\n\n {{baz}}2 spaces\nfoo" + + def test_raw4(self, env): + # The trailing dash of the {% raw -%} cleans both the spaces and + # newlines up to the first character of data. + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + "bar\n{%- raw -%}\n\n \n 2 spaces\n space{%- endraw -%}\nfoo" + ) + assert tmpl.render() == "bar2 spaces\n spacefoo" def test_balancing(self, env): - env = Environment('{%', '%}', '${', '}') - tmpl = env.from_string('''{% for item in seq - %}${{'foo': item}|upper}{% endfor %}''') - assert tmpl.render(seq=list(range(3))) \ - == "{'FOO': 0}{'FOO': 1}{'FOO': 2}" + env = Environment("{%", "%}", "${", "}") + tmpl = env.from_string( + """{% for item in seq + %}${{'foo': item}|upper}{% endfor %}""" + ) + assert tmpl.render(seq=list(range(3))) == "{'FOO': 0}{'FOO': 1}{'FOO': 2}" def test_comments(self, env): - env = Environment('<!--', '-->', '{', '}') - tmpl = env.from_string('''\ + env = Environment("<!--", "-->", "{", "}") + tmpl = env.from_string( + """\ <ul> <!--- for item in seq --> <li>{item}</li> <!--- endfor --> -</ul>''') - assert tmpl.render(seq=list(range(3))) \ - == ("<ul>\n <li>0</li>\n ""<li>1</li>\n <li>2</li>\n</ul>") +</ul>""" + ) + assert tmpl.render(seq=list(range(3))) == ( + "<ul>\n <li>0</li>\n <li>1</li>\n <li>2</li>\n</ul>" + ) def test_string_escapes(self, env): - for char in u'\0', u'\u2668', u'\xe4', u'\t', u'\r', u'\n': - tmpl = env.from_string('{{ %s }}' % jinja_string_repr(char)) + for char in u"\0", u"\u2668", u"\xe4", u"\t", u"\r", u"\n": + tmpl = env.from_string("{{ %s }}" % jinja_string_repr(char)) assert tmpl.render() == char - assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == u'\u2668' + assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == u"\u2668" def test_bytefallback(self, env): from pprint import pformat - tmpl = env.from_string(u'''{{ 'foo'|pprint }}|{{ 'bär'|pprint }}''') - assert tmpl.render() == pformat('foo') + '|' + pformat(u'bär') + + tmpl = env.from_string(u"""{{ 'foo'|pprint }}|{{ 'bär'|pprint }}""") + assert tmpl.render() == pformat("foo") + "|" + pformat(u"bär") def test_operators(self, env): from jinja2.lexer import operators + for test, expect in iteritems(operators): - if test in '([{}])': + if test in "([{}])": continue - stream = env.lexer.tokenize('{{ %s }}' % test) + stream = env.lexer.tokenize("{{ %s }}" % test) next(stream) assert stream.current.type == expect def test_normalizing(self, env): - for seq in '\r', '\r\n', '\n': + for seq in "\r", "\r\n", "\n": env = Environment(newline_sequence=seq) - tmpl = env.from_string('1\n2\r\n3\n4\n') + tmpl = env.from_string("1\n2\r\n3\n4\n") result = tmpl.render() - assert result.replace(seq, 'X') == '1X2X3X4' + assert result.replace(seq, "X") == "1X2X3X4" def test_trailing_newline(self, env): for keep in [True, False]: env = Environment(keep_trailing_newline=keep) for template, expected in [ - ('', {}), - ('no\nnewline', {}), - ('with\nnewline\n', {False: 'with\nnewline'}), - ('with\nseveral\n\n\n', {False: 'with\nseveral\n\n'}), - ]: + ("", {}), + ("no\nnewline", {}), + ("with\nnewline\n", {False: "with\nnewline"}), + ("with\nseveral\n\n\n", {False: "with\nseveral\n\n"}), + ]: tmpl = env.from_string(template) expect = expected.get(keep, template) result = tmpl.render() assert result == expect, (keep, template, result, expect) - @pytest.mark.parametrize('name,valid2,valid3', ( - (u'foo', True, True), - (u'föö', False, True), - (u'き', False, True), - (u'_', True, True), - (u'1a', False, False), # invalid ascii start - (u'a-', False, False), # invalid ascii continue - (u'🐍', False, False), # invalid unicode start - (u'a🐍', False, False), # invalid unicode continue - # start characters not matched by \w - (u'\u1885', False, True), - (u'\u1886', False, True), - (u'\u2118', False, True), - (u'\u212e', False, True), - # continue character not matched by \w - (u'\xb7', False, False), - (u'a\xb7', False, True), - )) + @pytest.mark.parametrize( + "name,valid2,valid3", + ( + (u"foo", True, True), + (u"föö", False, True), + (u"き", False, True), + (u"_", True, True), + (u"1a", False, False), # invalid ascii start + (u"a-", False, False), # invalid ascii continue + (u"🐍", False, False), # invalid unicode start + (u"a🐍", False, False), # invalid unicode continue + # start characters not matched by \w + (u"\u1885", False, True), + (u"\u1886", False, True), + (u"\u2118", False, True), + (u"\u212e", False, True), + # continue character not matched by \w + (u"\xb7", False, False), + (u"a\xb7", False, True), + ), + ) def test_name(self, env, name, valid2, valid3): - t = u'{{ ' + name + u' }}' + t = u"{{ " + name + u" }}" if (valid2 and PY2) or (valid3 and not PY2): # valid for version being tested, shouldn't raise @@ -153,488 +178,753 @@ class TestLexer(object): else: pytest.raises(TemplateSyntaxError, env.from_string, t) + def test_lineno_with_strip(self, env): + tokens = env.lex( + """\ +<html> + <body> + {%- block content -%} + <hr> + {{ item }} + {% endblock %} + </body> +</html>""" + ) + for tok in tokens: + lineno, token_type, value = tok + if token_type == "name" and value == "item": + assert lineno == 5 + break -@pytest.mark.lexnparse -@pytest.mark.parser -class TestParser(object): +class TestParser(object): def test_php_syntax(self, env): - env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->') - tmpl = env.from_string('''\ + env = Environment("<?", "?>", "<?=", "?>", "<!--", "-->") + tmpl = env.from_string( + """\ <!-- I'm a comment, I'm not interesting -->\ <? for item in seq -?> <?= item ?> -<?- endfor ?>''') - assert tmpl.render(seq=list(range(5))) == '01234' +<?- endfor ?>""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" def test_erb_syntax(self, env): - env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>') - tmpl = env.from_string('''\ + env = Environment("<%", "%>", "<%=", "%>", "<%#", "%>") + tmpl = env.from_string( + """\ <%# I'm a comment, I'm not interesting %>\ <% for item in seq -%> <%= item %> -<%- endfor %>''') - assert tmpl.render(seq=list(range(5))) == '01234' +<%- endfor %>""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" def test_comment_syntax(self, env): - env = Environment('<!--', '-->', '${', '}', '<!--#', '-->') - tmpl = env.from_string('''\ + env = Environment("<!--", "-->", "${", "}", "<!--#", "-->") + tmpl = env.from_string( + """\ <!--# I'm a comment, I'm not interesting -->\ <!-- for item in seq ---> ${item} -<!--- endfor -->''') - assert tmpl.render(seq=list(range(5))) == '01234' +<!--- endfor -->""" + ) + assert tmpl.render(seq=list(range(5))) == "01234" def test_balancing(self, env): - tmpl = env.from_string('''{{{'foo':'bar'}.foo}}''') - assert tmpl.render() == 'bar' + tmpl = env.from_string("""{{{'foo':'bar'}.foo}}""") + assert tmpl.render() == "bar" def test_start_comment(self, env): - tmpl = env.from_string('''{# foo comment + tmpl = env.from_string( + """{# foo comment and bar comment #} {% macro blub() %}foo{% endmacro %} -{{ blub() }}''') - assert tmpl.render().strip() == 'foo' +{{ blub() }}""" + ) + assert tmpl.render().strip() == "foo" def test_line_syntax(self, env): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%') - tmpl = env.from_string('''\ + env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%") + tmpl = env.from_string( + """\ <%# regular comment %> % for item in seq: ${item} -% endfor''') +% endfor""" + ) assert [ int(x.strip()) for x in tmpl.render(seq=list(range(5))).split() ] == list(range(5)) - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##') - tmpl = env.from_string('''\ + env = Environment("<%", "%>", "${", "}", "<%#", "%>", "%", "##") + tmpl = env.from_string( + """\ <%# regular comment %> % for item in seq: ${item} ## the rest of the stuff -% endfor''') +% endfor""" + ) assert [ int(x.strip()) for x in tmpl.render(seq=list(range(5))).split() ] == list(range(5)) def test_line_syntax_priority(self, env): # XXX: why is the whitespace there in front of the newline? - env = Environment('{%', '%}', '${', '}', '/*', '*/', '##', '#') - tmpl = env.from_string('''\ + env = Environment("{%", "%}", "${", "}", "/*", "*/", "##", "#") + tmpl = env.from_string( + """\ /* ignore me. I'm a multiline comment */ ## for item in seq: * ${item} # this is just extra stuff -## endfor''') - assert tmpl.render(seq=[1, 2]).strip() == '* 1\n* 2' - env = Environment('{%', '%}', '${', '}', '/*', '*/', '#', '##') - tmpl = env.from_string('''\ +## endfor""" + ) + assert tmpl.render(seq=[1, 2]).strip() == "* 1\n* 2" + env = Environment("{%", "%}", "${", "}", "/*", "*/", "#", "##") + tmpl = env.from_string( + """\ /* ignore me. I'm a multiline comment */ # for item in seq: * ${item} ## this is just extra stuff ## extra stuff i just want to ignore -# endfor''') - assert tmpl.render(seq=[1, 2]).strip() == '* 1\n\n* 2' +# endfor""" + ) + assert tmpl.render(seq=[1, 2]).strip() == "* 1\n\n* 2" def test_error_messages(self, env): def assert_error(code, expected): - try: + with pytest.raises(TemplateSyntaxError, match=expected): Template(code) - except TemplateSyntaxError as e: - assert str(e) == expected, 'unexpected error message' - else: - assert False, 'that was supposed to be an error' - assert_error('{% for item in seq %}...{% endif %}', - "Encountered unknown tag 'endif'. Jinja was looking " - "for the following tags: 'endfor' or 'else'. The " - "innermost block that needs to be closed is 'for'.") assert_error( - '{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}', + "{% for item in seq %}...{% endif %}", + "Encountered unknown tag 'endif'. Jinja was looking " + "for the following tags: 'endfor' or 'else'. The " + "innermost block that needs to be closed is 'for'.", + ) + assert_error( + "{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}", "Encountered unknown tag 'endfor'. Jinja was looking for " "the following tags: 'elif' or 'else' or 'endif'. The " - "innermost block that needs to be closed is 'if'.") - assert_error('{% if foo %}', - "Unexpected end of template. Jinja was looking for the " - "following tags: 'elif' or 'else' or 'endif'. The " - "innermost block that needs to be closed is 'if'.") - assert_error('{% for item in seq %}', - "Unexpected end of template. Jinja was looking for the " - "following tags: 'endfor' or 'else'. The innermost block " - "that needs to be closed is 'for'.") + "innermost block that needs to be closed is 'if'.", + ) assert_error( - '{% block foo-bar-baz %}', + "{% if foo %}", + "Unexpected end of template. Jinja was looking for the " + "following tags: 'elif' or 'else' or 'endif'. The " + "innermost block that needs to be closed is 'if'.", + ) + assert_error( + "{% for item in seq %}", + "Unexpected end of template. Jinja was looking for the " + "following tags: 'endfor' or 'else'. The innermost block " + "that needs to be closed is 'for'.", + ) + assert_error( + "{% block foo-bar-baz %}", "Block names in Jinja have to be valid Python identifiers " - "and may not contain hyphens, use an underscore instead.") - assert_error('{% unknown_tag %}', - "Encountered unknown tag 'unknown_tag'.") + "and may not contain hyphens, use an underscore instead.", + ) + assert_error("{% unknown_tag %}", "Encountered unknown tag 'unknown_tag'.") -@pytest.mark.lexnparse -@pytest.mark.syntax class TestSyntax(object): - def test_call(self, env): env = Environment() - env.globals['foo'] = lambda a, b, c, e, g: a + b + c + e + g - tmpl = env.from_string( - "{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}" - ) - assert tmpl.render() == 'abdfh' + env.globals["foo"] = lambda a, b, c, e, g: a + b + c + e + g + tmpl = env.from_string("{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}") + assert tmpl.render() == "abdfh" def test_slicing(self, env): - tmpl = env.from_string('{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}') - assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' + tmpl = env.from_string("{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}") + assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]" def test_attr(self, env): tmpl = env.from_string("{{ foo.bar }}|{{ foo['bar'] }}") - assert tmpl.render(foo={'bar': 42}) == '42|42' + assert tmpl.render(foo={"bar": 42}) == "42|42" def test_subscript(self, env): tmpl = env.from_string("{{ foo[0] }}|{{ foo[-1] }}") - assert tmpl.render(foo=[0, 1, 2]) == '0|2' + assert tmpl.render(foo=[0, 1, 2]) == "0|2" def test_tuple(self, env): - tmpl = env.from_string('{{ () }}|{{ (1,) }}|{{ (1, 2) }}') - assert tmpl.render() == '()|(1,)|(1, 2)' + tmpl = env.from_string("{{ () }}|{{ (1,) }}|{{ (1, 2) }}") + assert tmpl.render() == "()|(1,)|(1, 2)" def test_math(self, env): - tmpl = env.from_string('{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}') - assert tmpl.render() == '1.5|8' + tmpl = env.from_string("{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}") + assert tmpl.render() == "1.5|8" def test_div(self, env): - tmpl = env.from_string('{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}') - assert tmpl.render() == '1|1.5|1' + tmpl = env.from_string("{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}") + assert tmpl.render() == "1|1.5|1" def test_unary(self, env): - tmpl = env.from_string('{{ +3 }}|{{ -3 }}') - assert tmpl.render() == '3|-3' + tmpl = env.from_string("{{ +3 }}|{{ -3 }}") + assert tmpl.render() == "3|-3" def test_concat(self, env): tmpl = env.from_string("{{ [1, 2] ~ 'foo' }}") - assert tmpl.render() == '[1, 2]foo' - - def test_compare(self, env): - tmpl = env.from_string('{{ 1 > 0 }}|{{ 1 >= 1 }}|{{ 2 < 3 }}|' - '{{ 2 == 2 }}|{{ 1 <= 1 }}') - assert tmpl.render() == 'True|True|True|True|True' + assert tmpl.render() == "[1, 2]foo" + + @pytest.mark.parametrize( + ("a", "op", "b"), + [ + (1, ">", 0), + (1, ">=", 1), + (2, "<", 3), + (3, "<=", 4), + (4, "==", 4), + (4, "!=", 5), + ], + ) + def test_compare(self, env, a, op, b): + t = env.from_string("{{ %d %s %d }}" % (a, op, b)) + assert t.render() == "True" + + def test_compare_parens(self, env): + t = env.from_string("{{ i * (j < 5) }}") + assert t.render(i=2, j=3) == "2" + + @pytest.mark.parametrize( + ("src", "expect"), + [ + ("{{ 4 < 2 < 3 }}", "False"), + ("{{ a < b < c }}", "False"), + ("{{ 4 > 2 > 3 }}", "False"), + ("{{ a > b > c }}", "False"), + ("{{ 4 > 2 < 3 }}", "True"), + ("{{ a > b < c }}", "True"), + ], + ) + def test_compare_compound(self, env, src, expect): + t = env.from_string(src) + assert t.render(a=4, b=2, c=3) == expect def test_inop(self, env): - tmpl = env.from_string('{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}') - assert tmpl.render() == 'True|False' - - def test_literals(self, env): - tmpl = env.from_string('{{ [] }}|{{ {} }}|{{ () }}') - assert tmpl.render().lower() == '[]|{}|()' + tmpl = env.from_string("{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}") + assert tmpl.render() == "True|False" + + @pytest.mark.parametrize("value", ("[]", "{}", "()")) + def test_collection_literal(self, env, value): + t = env.from_string("{{ %s }}" % value) + assert t.render() == value + + @pytest.mark.parametrize( + ("value", "expect"), + ( + ("1", "1"), + ("123", "123"), + ("12_34_56", "123456"), + ("1.2", "1.2"), + ("34.56", "34.56"), + ("3_4.5_6", "34.56"), + ("1e0", "1.0"), + ("10e1", "100.0"), + ("2.5e100", "2.5e+100"), + ("2.5e+100", "2.5e+100"), + ("25.6e-10", "2.56e-09"), + ("1_2.3_4e5_6", "1.234e+57"), + ), + ) + def test_numeric_literal(self, env, value, expect): + t = env.from_string("{{ %s }}" % value) + assert t.render() == expect def test_bool(self, env): - tmpl = env.from_string('{{ true and false }}|{{ false ' - 'or true }}|{{ not false }}') - assert tmpl.render() == 'False|True|True' + tmpl = env.from_string( + "{{ true and false }}|{{ false or true }}|{{ not false }}" + ) + assert tmpl.render() == "False|True|True" def test_grouping(self, env): tmpl = env.from_string( - '{{ (true and false) or (false and true) and not false }}') - assert tmpl.render() == 'False' + "{{ (true and false) or (false and true) and not false }}" + ) + assert tmpl.render() == "False" def test_django_attr(self, env): - tmpl = env.from_string('{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}') - assert tmpl.render() == '1|1' + tmpl = env.from_string("{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}") + assert tmpl.render() == "1|1" def test_conditional_expression(self, env): - tmpl = env.from_string('''{{ 0 if true else 1 }}''') - assert tmpl.render() == '0' + tmpl = env.from_string("""{{ 0 if true else 1 }}""") + assert tmpl.render() == "0" def test_short_conditional_expression(self, env): - tmpl = env.from_string('<{{ 1 if false }}>') - assert tmpl.render() == '<>' + tmpl = env.from_string("<{{ 1 if false }}>") + assert tmpl.render() == "<>" - tmpl = env.from_string('<{{ (1 if false).bar }}>') + tmpl = env.from_string("<{{ (1 if false).bar }}>") pytest.raises(UndefinedError, tmpl.render) def test_filter_priority(self, env): tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}') - assert tmpl.render() == 'FOOBAR' + assert tmpl.render() == "FOOBAR" def test_function_calls(self, env): tests = [ - (True, '*foo, bar'), - (True, '*foo, *bar'), - (True, '*foo, bar=42'), - (True, '**foo, *bar'), - (True, '**foo, bar'), - (False, 'foo, bar'), - (False, 'foo, bar=42'), - (False, 'foo, bar=23, *args'), - (False, 'a, b=c, *d, **e'), - (False, '*foo, **bar') + (True, "*foo, bar"), + (True, "*foo, *bar"), + (True, "**foo, *bar"), + (True, "**foo, bar"), + (True, "**foo, **bar"), + (True, "**foo, bar=42"), + (False, "foo, bar"), + (False, "foo, bar=42"), + (False, "foo, bar=23, *args"), + (False, "foo, *args, bar=23"), + (False, "a, b=c, *d, **e"), + (False, "*foo, bar=42"), + (False, "*foo, **bar"), + (False, "*foo, bar=42, **baz"), + (False, "foo, *args, bar=23, **baz"), ] for should_fail, sig in tests: if should_fail: - pytest.raises(TemplateSyntaxError, - env.from_string, '{{ foo(%s) }}' % sig) + pytest.raises( + TemplateSyntaxError, env.from_string, "{{ foo(%s) }}" % sig + ) else: - env.from_string('foo(%s)' % sig) + env.from_string("foo(%s)" % sig) def test_tuple_expr(self, env): for tmpl in [ - '{{ () }}', - '{{ (1, 2) }}', - '{{ (1, 2,) }}', - '{{ 1, }}', - '{{ 1, 2 }}', - '{% for foo, bar in seq %}...{% endfor %}', - '{% for x in foo, bar %}...{% endfor %}', - '{% for x in foo, %}...{% endfor %}' + "{{ () }}", + "{{ (1, 2) }}", + "{{ (1, 2,) }}", + "{{ 1, }}", + "{{ 1, 2 }}", + "{% for foo, bar in seq %}...{% endfor %}", + "{% for x in foo, bar %}...{% endfor %}", + "{% for x in foo, %}...{% endfor %}", ]: assert env.from_string(tmpl) def test_trailing_comma(self, env): - tmpl = env.from_string('{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}') - assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}' + tmpl = env.from_string("{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}") + assert tmpl.render().lower() == "(1, 2)|[1, 2]|{1: 2}" def test_block_end_name(self, env): - env.from_string('{% block foo %}...{% endblock foo %}') - pytest.raises(TemplateSyntaxError, env.from_string, - '{% block x %}{% endblock y %}') + env.from_string("{% block foo %}...{% endblock foo %}") + pytest.raises( + TemplateSyntaxError, env.from_string, "{% block x %}{% endblock y %}" + ) def test_constant_casing(self, env): for const in True, False, None: - tmpl = env.from_string('{{ %s }}|{{ %s }}|{{ %s }}' % ( - str(const), str(const).lower(), str(const).upper() - )) - assert tmpl.render() == '%s|%s|' % (const, const) + tmpl = env.from_string( + "{{ %s }}|{{ %s }}|{{ %s }}" + % (str(const), str(const).lower(), str(const).upper()) + ) + assert tmpl.render() == "%s|%s|" % (const, const) def test_test_chaining(self, env): - pytest.raises(TemplateSyntaxError, env.from_string, - '{{ foo is string is sequence }}') - assert env.from_string( - '{{ 42 is string or 42 is number }}' - ).render() == 'True' + pytest.raises( + TemplateSyntaxError, env.from_string, "{{ foo is string is sequence }}" + ) + assert env.from_string("{{ 42 is string or 42 is number }}").render() == "True" def test_string_concatenation(self, env): tmpl = env.from_string('{{ "foo" "bar" "baz" }}') - assert tmpl.render() == 'foobarbaz' + assert tmpl.render() == "foobarbaz" def test_notin(self, env): bar = range(100) - tmpl = env.from_string('''{{ not 42 in bar }}''') - assert tmpl.render(bar=bar) == text_type(not 42 in bar) + tmpl = env.from_string("""{{ not 42 in bar }}""") + assert tmpl.render(bar=bar) == "False" def test_operator_precedence(self, env): - tmpl = env.from_string('''{{ 2 * 3 + 4 % 2 + 1 - 2 }}''') + tmpl = env.from_string("""{{ 2 * 3 + 4 % 2 + 1 - 2 }}""") assert tmpl.render() == text_type(2 * 3 + 4 % 2 + 1 - 2) def test_implicit_subscribed_tuple(self, env): class Foo(object): def __getitem__(self, x): return x - t = env.from_string('{{ foo[1, 2] }}') - assert t.render(foo=Foo()) == u'(1, 2)' + + t = env.from_string("{{ foo[1, 2] }}") + assert t.render(foo=Foo()) == u"(1, 2)" def test_raw2(self, env): - tmpl = env.from_string('{% raw %}{{ FOO }} and {% BAR %}{% endraw %}') - assert tmpl.render() == '{{ FOO }} and {% BAR %}' + tmpl = env.from_string("{% raw %}{{ FOO }} and {% BAR %}{% endraw %}") + assert tmpl.render() == "{{ FOO }} and {% BAR %}" def test_const(self, env): tmpl = env.from_string( - '{{ true }}|{{ false }}|{{ none }}|' - '{{ none is defined }}|{{ missing is defined }}') - assert tmpl.render() == 'True|False|None|True|False' + "{{ true }}|{{ false }}|{{ none }}|" + "{{ none is defined }}|{{ missing is defined }}" + ) + assert tmpl.render() == "True|False|None|True|False" def test_neg_filter_priority(self, env): - node = env.parse('{{ -1|foo }}') + node = env.parse("{{ -1|foo }}") assert isinstance(node.body[0].nodes[0], nodes.Filter) assert isinstance(node.body[0].nodes[0].node, nodes.Neg) def test_const_assign(self, env): - constass1 = '''{% set true = 42 %}''' - constass2 = '''{% for none in seq %}{% endfor %}''' + constass1 = """{% set true = 42 %}""" + constass2 = """{% for none in seq %}{% endfor %}""" for tmpl in constass1, constass2: pytest.raises(TemplateSyntaxError, env.from_string, tmpl) def test_localset(self, env): - tmpl = env.from_string('''{% set foo = 0 %}\ + tmpl = env.from_string( + """{% set foo = 0 %}\ {% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\ -{{ foo }}''') - assert tmpl.render() == '0' +{{ foo }}""" + ) + assert tmpl.render() == "0" def test_parse_unary(self, env): tmpl = env.from_string('{{ -foo["bar"] }}') - assert tmpl.render(foo={'bar': 42}) == '-42' + assert tmpl.render(foo={"bar": 42}) == "-42" tmpl = env.from_string('{{ -foo["bar"]|abs }}') - assert tmpl.render(foo={'bar': 42}) == '42' + assert tmpl.render(foo={"bar": 42}) == "42" -@pytest.mark.lexnparse -@pytest.mark.lstripblocks class TestLstripBlocks(object): - def test_lstrip(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' {% if True %}\n {% endif %}''') + tmpl = env.from_string(""" {% if True %}\n {% endif %}""") assert tmpl.render() == "\n" def test_lstrip_trim(self, env): env = Environment(lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string(''' {% if True %}\n {% endif %}''') + tmpl = env.from_string(""" {% if True %}\n {% endif %}""") assert tmpl.render() == "" def test_no_lstrip(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' {%+ if True %}\n {%+ endif %}''') + tmpl = env.from_string(""" {%+ if True %}\n {%+ endif %}""") + assert tmpl.render() == " \n " + + def test_lstrip_blocks_false_with_no_lstrip(self, env): + # Test that + is a NOP (but does not cause an error) if lstrip_blocks=False + env = Environment(lstrip_blocks=False, trim_blocks=False) + tmpl = env.from_string(""" {% if True %}\n {% endif %}""") + assert tmpl.render() == " \n " + tmpl = env.from_string(""" {%+ if True %}\n {%+ endif %}""") assert tmpl.render() == " \n " def test_lstrip_endline(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string( - ''' hello{% if True %}\n goodbye{% endif %}''') + tmpl = env.from_string(""" hello{% if True %}\n goodbye{% endif %}""") assert tmpl.render() == " hello\n goodbye" def test_lstrip_inline(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' {% if True %}hello {% endif %}''') - assert tmpl.render() == 'hello ' + tmpl = env.from_string(""" {% if True %}hello {% endif %}""") + assert tmpl.render() == "hello " def test_lstrip_nested(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) tmpl = env.from_string( - ''' {% if True %}a {% if True %}b {% endif %}c {% endif %}''') - assert tmpl.render() == 'a b c ' + """ {% if True %}a {% if True %}b {% endif %}c {% endif %}""" + ) + assert tmpl.render() == "a b c " def test_lstrip_left_chars(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' abc {% if True %} - hello{% endif %}''') - assert tmpl.render() == ' abc \n hello' + tmpl = env.from_string( + """ abc {% if True %} + hello{% endif %}""" + ) + assert tmpl.render() == " abc \n hello" def test_lstrip_embeded_strings(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' {% set x = " {% str %} " %}{{ x }}''') - assert tmpl.render() == ' {% str %} ' + tmpl = env.from_string(""" {% set x = " {% str %} " %}{{ x }}""") + assert tmpl.render() == " {% str %} " def test_lstrip_preserve_leading_newlines(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string('''\n\n\n{% set hello = 1 %}''') - assert tmpl.render() == '\n\n\n' + tmpl = env.from_string("""\n\n\n{% set hello = 1 %}""") + assert tmpl.render() == "\n\n\n" def test_lstrip_comment(self, env): env = Environment(lstrip_blocks=True, trim_blocks=False) - tmpl = env.from_string(''' {# if True #} + tmpl = env.from_string( + """ {# if True #} hello - {#endif#}''') - assert tmpl.render() == '\nhello\n' + {#endif#}""" + ) + assert tmpl.render() == "\nhello\n" def test_lstrip_angle_bracket_simple(self, env): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string(''' <% if True %>hello <% endif %>''') - assert tmpl.render() == 'hello ' + env = Environment( + "<%", + "%>", + "${", + "}", + "<%#", + "%>", + "%", + "##", + lstrip_blocks=True, + trim_blocks=True, + ) + tmpl = env.from_string(""" <% if True %>hello <% endif %>""") + assert tmpl.render() == "hello " def test_lstrip_angle_bracket_comment(self, env): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string(''' <%# if True %>hello <%# endif %>''') - assert tmpl.render() == 'hello ' + env = Environment( + "<%", + "%>", + "${", + "}", + "<%#", + "%>", + "%", + "##", + lstrip_blocks=True, + trim_blocks=True, + ) + tmpl = env.from_string(""" <%# if True %>hello <%# endif %>""") + assert tmpl.render() == "hello " def test_lstrip_angle_bracket(self, env): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<%", + "%>", + "${", + "}", + "<%#", + "%>", + "%", + "##", + lstrip_blocks=True, + trim_blocks=True, + ) + tmpl = env.from_string( + """\ <%# regular comment %> <% for item in seq %> ${item} ## the rest of the stuff - <% endfor %>''') - assert tmpl.render(seq=range(5)) == \ - ''.join('%s\n' % x for x in range(5)) + <% endfor %>""" + ) + assert tmpl.render(seq=range(5)) == "".join("%s\n" % x for x in range(5)) def test_lstrip_angle_bracket_compact(self, env): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<%", + "%>", + "${", + "}", + "<%#", + "%>", + "%", + "##", + lstrip_blocks=True, + trim_blocks=True, + ) + tmpl = env.from_string( + """\ <%#regular comment%> <%for item in seq%> ${item} ## the rest of the stuff - <%endfor%>''') - assert tmpl.render(seq=range(5)) == \ - ''.join('%s\n' % x for x in range(5)) + <%endfor%>""" + ) + assert tmpl.render(seq=range(5)) == "".join("%s\n" % x for x in range(5)) + + def test_lstrip_blocks_outside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs %}(\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " ){% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "(\na=1 b=2 \n )" + + def test_lstrip_trim_blocks_outside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " {% if kvs %}(\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " ){% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "(\na=1 b=2 )" + + def test_lstrip_blocks_inside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " ({% if kvs %}\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " {% endif %})" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " (\na=1 b=2 \n)" + + def test_lstrip_trim_blocks_inside_with_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " ({% if kvs %}\n" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}\n" + " {% endif %})" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " (a=1 b=2 )" + + def test_lstrip_blocks_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs %}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}" + " {% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " a=1 b=2 " + + def test_lstrip_trim_blocks_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string( + " {% if kvs %}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor %}" + " {% endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == " a=1 b=2 " + + def test_lstrip_blocks_consume_after_without_new_line(self): + env = Environment(lstrip_blocks=True, trim_blocks=False) + tmpl = env.from_string( + " {% if kvs -%}" + " {% for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}" + " {% endif -%}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "a=1 b=2 " + + def test_lstrip_trim_blocks_consume_before_without_new_line(self): + env = Environment(lstrip_blocks=False, trim_blocks=False) + tmpl = env.from_string( + " {%- if kvs %}" + " {%- for k, v in kvs %}{{ k }}={{ v }} {% endfor -%}" + " {%- endif %}" + ) + out = tmpl.render(kvs=[("a", 1), ("b", 2)]) + assert out == "a=1 b=2 " + + def test_lstrip_trim_blocks_comment(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string(" {# 1 space #}\n {# 2 spaces #} {# 4 spaces #}") + out = tmpl.render() + assert out == " " * 4 + + def test_lstrip_trim_blocks_raw(self): + env = Environment(lstrip_blocks=True, trim_blocks=True) + tmpl = env.from_string("{{x}}\n{%- raw %} {% endraw -%}\n{{ y }}") + out = tmpl.render(x=1, y=2) + assert out == "1 2" def test_php_syntax_with_manual(self, env): - env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string( + """\ <!-- I'm a comment, I'm not interesting --> <? for item in seq -?> <?= item ?> - <?- endfor ?>''') - assert tmpl.render(seq=range(5)) == '01234' + <?- endfor ?>""" + ) + assert tmpl.render(seq=range(5)) == "01234" def test_php_syntax(self, env): - env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string( + """\ <!-- I'm a comment, I'm not interesting --> <? for item in seq ?> <?= item ?> - <? endfor ?>''') - assert tmpl.render(seq=range(5)) \ - == ''.join(' %s\n' % x for x in range(5)) + <? endfor ?>""" + ) + assert tmpl.render(seq=range(5)) == "".join( + " %s\n" % x for x in range(5) + ) def test_php_syntax_compact(self, env): - env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<?", "?>", "<?=", "?>", "<!--", "-->", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string( + """\ <!-- I'm a comment, I'm not interesting --> <?for item in seq?> <?=item?> - <?endfor?>''') - assert tmpl.render(seq=range(5)) \ - == ''.join(' %s\n' % x for x in range(5)) + <?endfor?>""" + ) + assert tmpl.render(seq=range(5)) == "".join( + " %s\n" % x for x in range(5) + ) def test_erb_syntax(self, env): - env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>', - lstrip_blocks=True, trim_blocks=True) + env = Environment( + "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True + ) # env.from_string('') # for n,r in env.lexer.rules.iteritems(): # print n # print env.lexer.rules['root'][0][0].pattern # print "'%s'" % tmpl.render(seq=range(5)) - tmpl = env.from_string('''\ + tmpl = env.from_string( + """\ <%# I'm a comment, I'm not interesting %> <% for item in seq %> <%= item %> <% endfor %> -''') - assert tmpl.render(seq=range(5)) \ - == ''.join(' %s\n' % x for x in range(5)) +""" + ) + assert tmpl.render(seq=range(5)) == "".join(" %s\n" % x for x in range(5)) def test_erb_syntax_with_manual(self, env): - env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string( + """\ <%# I'm a comment, I'm not interesting %> <% for item in seq -%> <%= item %> - <%- endfor %>''') - assert tmpl.render(seq=range(5)) == '01234' + <%- endfor %>""" + ) + assert tmpl.render(seq=range(5)) == "01234" def test_erb_syntax_no_lstrip(self, env): - env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<%", "%>", "<%=", "%>", "<%#", "%>", lstrip_blocks=True, trim_blocks=True + ) + tmpl = env.from_string( + """\ <%# I'm a comment, I'm not interesting %> <%+ for item in seq -%> <%= item %> - <%- endfor %>''') - assert tmpl.render(seq=range(5)) == ' 01234' + <%- endfor %>""" + ) + assert tmpl.render(seq=range(5)) == " 01234" def test_comment_syntax(self, env): - env = Environment('<!--', '-->', '${', '}', '<!--#', '-->', - lstrip_blocks=True, trim_blocks=True) - tmpl = env.from_string('''\ + env = Environment( + "<!--", + "-->", + "${", + "}", + "<!--#", + "-->", + lstrip_blocks=True, + trim_blocks=True, + ) + tmpl = env.from_string( + """\ <!--# I'm a comment, I'm not interesting -->\ <!-- for item in seq ---> ${item} -<!--- endfor -->''') - assert tmpl.render(seq=range(5)) == '01234' +<!--- endfor -->""" + ) + assert tmpl.render(seq=range(5)) == "01234" diff --git a/tests/test_loader.py b/tests/test_loader.py index 7e12628..f10f756 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,145 +1,206 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.loader - ~~~~~~~~~~~~~~~~~~~~~~~ - - Test the loaders. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import os +import shutil import sys import tempfile -import shutil -import pytest +import time import weakref -from jinja2 import Environment, loaders -from jinja2._compat import PYPY, PY2 -from jinja2.loaders import split_template_path +import pytest + +from jinja2 import Environment +from jinja2 import loaders +from jinja2._compat import PY2 +from jinja2._compat import PYPY from jinja2.exceptions import TemplateNotFound +from jinja2.loaders import split_template_path -@pytest.mark.loaders class TestLoaders(object): - def test_dict_loader(self, dict_loader): env = Environment(loader=dict_loader) - tmpl = env.get_template('justdict.html') - assert tmpl.render().strip() == 'FOO' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') + tmpl = env.get_template("justdict.html") + assert tmpl.render().strip() == "FOO" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") def test_package_loader(self, package_loader): env = Environment(loader=package_loader) - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_filesystem_loader(self, filesystem_loader): - env = Environment(loader=filesystem_loader) - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - tmpl = env.get_template('foo/test.html') - assert tmpl.render().strip() == 'FOO' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') + tmpl = env.get_template("test.html") + assert tmpl.render().strip() == "BAR" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") + + def test_filesystem_loader_overlapping_names(self, filesystem_loader): + res = os.path.dirname(filesystem_loader.searchpath[0]) + t2_dir = os.path.join(res, "templates2") + # Make "foo" show up before "foo/test.html". + filesystem_loader.searchpath.insert(0, t2_dir) + e = Environment(loader=filesystem_loader) + e.get_template("foo") + # This would raise NotADirectoryError if "t2/foo" wasn't skipped. + e.get_template("foo/test.html") def test_choice_loader(self, choice_loader): env = Environment(loader=choice_loader) - tmpl = env.get_template('justdict.html') - assert tmpl.render().strip() == 'FOO' - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') + tmpl = env.get_template("justdict.html") + assert tmpl.render().strip() == "FOO" + tmpl = env.get_template("test.html") + assert tmpl.render().strip() == "BAR" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") def test_function_loader(self, function_loader): env = Environment(loader=function_loader) - tmpl = env.get_template('justfunction.html') - assert tmpl.render().strip() == 'FOO' - pytest.raises(TemplateNotFound, env.get_template, 'missing.html') + tmpl = env.get_template("justfunction.html") + assert tmpl.render().strip() == "FOO" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") def test_prefix_loader(self, prefix_loader): env = Environment(loader=prefix_loader) - tmpl = env.get_template('a/test.html') - assert tmpl.render().strip() == 'BAR' - tmpl = env.get_template('b/justdict.html') - assert tmpl.render().strip() == 'FOO' - pytest.raises(TemplateNotFound, env.get_template, 'missing') + tmpl = env.get_template("a/test.html") + assert tmpl.render().strip() == "BAR" + tmpl = env.get_template("b/justdict.html") + assert tmpl.render().strip() == "FOO" + pytest.raises(TemplateNotFound, env.get_template, "missing") def test_caching(self): changed = False class TestLoader(loaders.BaseLoader): def get_source(self, environment, template): - return u'foo', None, lambda: not changed + return u"foo", None, lambda: not changed + env = Environment(loader=TestLoader(), cache_size=-1) - tmpl = env.get_template('template') - assert tmpl is env.get_template('template') + tmpl = env.get_template("template") + assert tmpl is env.get_template("template") changed = True - assert tmpl is not env.get_template('template') + assert tmpl is not env.get_template("template") changed = False def test_no_cache(self): - mapping = {'foo': 'one'} + mapping = {"foo": "one"} env = Environment(loader=loaders.DictLoader(mapping), cache_size=0) - assert env.get_template('foo') is not env.get_template('foo') + assert env.get_template("foo") is not env.get_template("foo") def test_limited_size_cache(self): - mapping = {'one': 'foo', 'two': 'bar', 'three': 'baz'} + mapping = {"one": "foo", "two": "bar", "three": "baz"} loader = loaders.DictLoader(mapping) env = Environment(loader=loader, cache_size=2) - t1 = env.get_template('one') - t2 = env.get_template('two') - assert t2 is env.get_template('two') - assert t1 is env.get_template('one') - t3 = env.get_template('three') + t1 = env.get_template("one") + t2 = env.get_template("two") + assert t2 is env.get_template("two") + assert t1 is env.get_template("one") + env.get_template("three") loader_ref = weakref.ref(loader) - assert (loader_ref, 'one') in env.cache - assert (loader_ref, 'two') not in env.cache - assert (loader_ref, 'three') in env.cache + assert (loader_ref, "one") in env.cache + assert (loader_ref, "two") not in env.cache + assert (loader_ref, "three") in env.cache def test_cache_loader_change(self): - loader1 = loaders.DictLoader({'foo': 'one'}) - loader2 = loaders.DictLoader({'foo': 'two'}) + loader1 = loaders.DictLoader({"foo": "one"}) + loader2 = loaders.DictLoader({"foo": "two"}) env = Environment(loader=loader1, cache_size=2) - assert env.get_template('foo').render() == 'one' + assert env.get_template("foo").render() == "one" env.loader = loader2 - assert env.get_template('foo').render() == 'two' + assert env.get_template("foo").render() == "two" def test_dict_loader_cache_invalidates(self): - mapping = {'foo': "one"} + mapping = {"foo": "one"} env = Environment(loader=loaders.DictLoader(mapping)) - assert env.get_template('foo').render() == "one" - mapping['foo'] = "two" - assert env.get_template('foo').render() == "two" + assert env.get_template("foo").render() == "one" + mapping["foo"] = "two" + assert env.get_template("foo").render() == "two" def test_split_template_path(self): - assert split_template_path('foo/bar') == ['foo', 'bar'] - assert split_template_path('./foo/bar') == ['foo', 'bar'] - pytest.raises(TemplateNotFound, split_template_path, '../foo') + assert split_template_path("foo/bar") == ["foo", "bar"] + assert split_template_path("./foo/bar") == ["foo", "bar"] + pytest.raises(TemplateNotFound, split_template_path, "../foo") + + +class TestFileSystemLoader(object): + searchpath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "res", "templates" + ) + + @staticmethod + def _test_common(env): + tmpl = env.get_template("test.html") + assert tmpl.render().strip() == "BAR" + tmpl = env.get_template("foo/test.html") + assert tmpl.render().strip() == "FOO" + pytest.raises(TemplateNotFound, env.get_template, "missing.html") + + def test_searchpath_as_str(self): + filesystem_loader = loaders.FileSystemLoader(self.searchpath) + + env = Environment(loader=filesystem_loader) + self._test_common(env) + + @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2") + def test_searchpath_as_pathlib(self): + import pathlib + + searchpath = pathlib.Path(self.searchpath) + + filesystem_loader = loaders.FileSystemLoader(searchpath) + + env = Environment(loader=filesystem_loader) + self._test_common(env) + + @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2") + def test_searchpath_as_list_including_pathlib(self): + import pathlib + + searchpath = pathlib.Path(self.searchpath) + + filesystem_loader = loaders.FileSystemLoader(["/tmp/templates", searchpath]) + + env = Environment(loader=filesystem_loader) + self._test_common(env) + + def test_caches_template_based_on_mtime(self): + filesystem_loader = loaders.FileSystemLoader(self.searchpath) + + env = Environment(loader=filesystem_loader) + tmpl1 = env.get_template("test.html") + tmpl2 = env.get_template("test.html") + assert tmpl1 is tmpl2 + + os.utime(os.path.join(self.searchpath, "test.html"), (time.time(), time.time())) + tmpl3 = env.get_template("test.html") + assert tmpl1 is not tmpl3 + + @pytest.mark.parametrize( + ("encoding", "expect"), + [ + ("utf-8", u"文字化け"), + ("iso-8859-1", u"æ\x96\x87\xe5\xad\x97\xe5\x8c\x96\xe3\x81\x91"), + ], + ) + def test_uses_specified_encoding(self, encoding, expect): + loader = loaders.FileSystemLoader(self.searchpath, encoding=encoding) + e = Environment(loader=loader) + t = e.get_template("mojibake.txt") + assert t.render() == expect -@pytest.mark.loaders -@pytest.mark.moduleloader class TestModuleLoader(object): archive = None - def compile_down(self, prefix_loader, zip='deflated', py_compile=False): + def compile_down(self, prefix_loader, zip="deflated", py_compile=False): log = [] self.reg_env = Environment(loader=prefix_loader) if zip is not None: - fd, self.archive = tempfile.mkstemp(suffix='.zip') + fd, self.archive = tempfile.mkstemp(suffix=".zip") os.close(fd) else: self.archive = tempfile.mkdtemp() - self.reg_env.compile_templates(self.archive, zip=zip, - log_function=log.append, - py_compile=py_compile) + self.reg_env.compile_templates( + self.archive, zip=zip, log_function=log.append, py_compile=py_compile + ) self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive)) - return ''.join(log) + return "".join(log) def teardown(self): - if hasattr(self, 'mod_env'): + if hasattr(self, "mod_env"): if os.path.isfile(self.archive): os.remove(self.archive) else: @@ -148,27 +209,31 @@ class TestModuleLoader(object): def test_log(self, prefix_loader): log = self.compile_down(prefix_loader) - assert 'Compiled "a/foo/test.html" as ' \ - 'tmpl_a790caf9d669e39ea4d280d597ec891c4ef0404a' in log - assert 'Finished compiling templates' in log - assert 'Could not compile "a/syntaxerror.html": ' \ - 'Encountered unknown tag \'endif\'' in log + assert ( + 'Compiled "a/foo/test.html" as ' + "tmpl_a790caf9d669e39ea4d280d597ec891c4ef0404a" in log + ) + assert "Finished compiling templates" in log + assert ( + 'Could not compile "a/syntaxerror.html": ' + "Encountered unknown tag 'endif'" in log + ) def _test_common(self): - tmpl1 = self.reg_env.get_template('a/test.html') - tmpl2 = self.mod_env.get_template('a/test.html') + tmpl1 = self.reg_env.get_template("a/test.html") + tmpl2 = self.mod_env.get_template("a/test.html") assert tmpl1.render() == tmpl2.render() - tmpl1 = self.reg_env.get_template('b/justdict.html') - tmpl2 = self.mod_env.get_template('b/justdict.html') + tmpl1 = self.reg_env.get_template("b/justdict.html") + tmpl2 = self.mod_env.get_template("b/justdict.html") assert tmpl1.render() == tmpl2.render() def test_deflated_zip_compile(self, prefix_loader): - self.compile_down(prefix_loader, zip='deflated') + self.compile_down(prefix_loader, zip="deflated") self._test_common() def test_stored_zip_compile(self, prefix_loader): - self.compile_down(prefix_loader, zip='stored') + self.compile_down(prefix_loader, zip="stored") self._test_common() def test_filesystem_compile(self, prefix_loader): @@ -177,58 +242,81 @@ class TestModuleLoader(object): def test_weak_references(self, prefix_loader): self.compile_down(prefix_loader) - tmpl = self.mod_env.get_template('a/test.html') - key = loaders.ModuleLoader.get_template_key('a/test.html') + self.mod_env.get_template("a/test.html") + key = loaders.ModuleLoader.get_template_key("a/test.html") name = self.mod_env.loader.module.__name__ assert hasattr(self.mod_env.loader.module, key) assert name in sys.modules # unset all, ensure the module is gone from sys.modules - self.mod_env = tmpl = None + self.mod_env = None try: import gc + gc.collect() - except: + except BaseException: pass assert name not in sys.modules # This test only makes sense on non-pypy python 2 @pytest.mark.skipif( - not (PY2 and not PYPY), - reason='This test only makes sense on non-pypy python 2') + not (PY2 and not PYPY), reason="This test only makes sense on non-pypy python 2" + ) def test_byte_compilation(self, prefix_loader): log = self.compile_down(prefix_loader, py_compile=True) assert 'Byte-compiled "a/test.html"' in log - tmpl1 = self.mod_env.get_template('a/test.html') - mod = self.mod_env.loader.module. \ - tmpl_3c4ddf650c1a73df961a6d3d2ce2752f1b8fd490 - assert mod.__file__.endswith('.pyc') + self.mod_env.get_template("a/test.html") + mod = self.mod_env.loader.module.tmpl_3c4ddf650c1a73df961a6d3d2ce2752f1b8fd490 + assert mod.__file__.endswith(".pyc") def test_choice_loader(self, prefix_loader): - log = self.compile_down(prefix_loader) + self.compile_down(prefix_loader) + self.mod_env.loader = loaders.ChoiceLoader( + [self.mod_env.loader, loaders.DictLoader({"DICT_SOURCE": "DICT_TEMPLATE"})] + ) + tmpl1 = self.mod_env.get_template("a/test.html") + assert tmpl1.render() == "BAR" + tmpl2 = self.mod_env.get_template("DICT_SOURCE") + assert tmpl2.render() == "DICT_TEMPLATE" + + def test_prefix_loader(self, prefix_loader): + self.compile_down(prefix_loader) + self.mod_env.loader = loaders.PrefixLoader( + { + "MOD": self.mod_env.loader, + "DICT": loaders.DictLoader({"test.html": "DICT_TEMPLATE"}), + } + ) + tmpl1 = self.mod_env.get_template("MOD/a/test.html") + assert tmpl1.render() == "BAR" + tmpl2 = self.mod_env.get_template("DICT/test.html") + assert tmpl2.render() == "DICT_TEMPLATE" + + @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2") + def test_path_as_pathlib(self, prefix_loader): + self.compile_down(prefix_loader) - self.mod_env.loader = loaders.ChoiceLoader([ - self.mod_env.loader, - loaders.DictLoader({'DICT_SOURCE': 'DICT_TEMPLATE'}) - ]) + mod_path = self.mod_env.loader.module.__path__[0] - tmpl1 = self.mod_env.get_template('a/test.html') - assert tmpl1.render() == 'BAR' - tmpl2 = self.mod_env.get_template('DICT_SOURCE') - assert tmpl2.render() == 'DICT_TEMPLATE' + import pathlib - def test_prefix_loader(self, prefix_loader): - log = self.compile_down(prefix_loader) + mod_loader = loaders.ModuleLoader(pathlib.Path(mod_path)) + self.mod_env = Environment(loader=mod_loader) + + self._test_common() + + @pytest.mark.skipif(PY2, reason="pathlib is not available in Python 2") + def test_supports_pathlib_in_list_of_paths(self, prefix_loader): + self.compile_down(prefix_loader) - self.mod_env.loader = loaders.PrefixLoader({ - 'MOD': self.mod_env.loader, - 'DICT': loaders.DictLoader({'test.html': 'DICT_TEMPLATE'}) - }) + mod_path = self.mod_env.loader.module.__path__[0] - tmpl1 = self.mod_env.get_template('MOD/a/test.html') - assert tmpl1.render() == 'BAR' - tmpl2 = self.mod_env.get_template('DICT/test.html') - assert tmpl2.render() == 'DICT_TEMPLATE' + import pathlib + + mod_loader = loaders.ModuleLoader([pathlib.Path(mod_path), "/tmp/templates"]) + self.mod_env = Environment(loader=mod_loader) + + self._test_common() diff --git a/tests/test_nativetypes.py b/tests/test_nativetypes.py index aec1a3b..76871ab 100644 --- a/tests/test_nativetypes.py +++ b/tests/test_nativetypes.py @@ -3,6 +3,7 @@ import pytest from jinja2._compat import text_type from jinja2.exceptions import UndefinedError from jinja2.nativetypes import NativeEnvironment +from jinja2.nativetypes import NativeTemplate from jinja2.runtime import Undefined @@ -11,100 +12,137 @@ def env(): return NativeEnvironment() -class TestNativeEnvironment(object): - def test_is_defined_native_return(self, env): - t = env.from_string('{{ missing is defined }}') - assert not t.render() - - def test_undefined_native_return(self, env): - t = env.from_string('{{ missing }}') - assert isinstance(t.render(), Undefined) - - def test_adding_undefined_native_return(self, env): - t = env.from_string('{{ 3 + missing }}') - - with pytest.raises(UndefinedError): - t.render() - - def test_cast_int(self, env): - t = env.from_string("{{ anumber|int }}") - result = t.render(anumber='3') - assert isinstance(result, int) - assert result == 3 - - def test_list_add(self, env): - t = env.from_string("{{ listone + listtwo }}") - result = t.render(listone=['a', 'b'], listtwo=['c', 'd']) - assert isinstance(result, list) - assert result == ['a', 'b', 'c', 'd'] - - def test_multi_expression_add(self, env): - t = env.from_string("{{ listone }} + {{ listtwo }}") - result = t.render(listone=['a', 'b'], listtwo=['c', 'd']) - assert not isinstance(result, list) - assert result == "['a', 'b'] + ['c', 'd']" - - def test_loops(self, env): - t = env.from_string("{% for x in listone %}{{ x }}{% endfor %}") - result = t.render(listone=['a', 'b', 'c', 'd']) - assert isinstance(result, text_type) - assert result == 'abcd' - - def test_loops_with_ints(self, env): - t = env.from_string("{% for x in listone %}{{ x }}{% endfor %}") - result = t.render(listone=[1, 2, 3, 4]) - assert isinstance(result, int) - assert result == 1234 - - def test_loop_look_alike(self, env): - t = env.from_string("{% for x in listone %}{{ x }}{% endfor %}") - result = t.render(listone=[1]) - assert isinstance(result, int) - assert result == 1 - - def test_booleans(self, env): - t = env.from_string("{{ boolval }}") - result = t.render(boolval=True) - assert isinstance(result, bool) - assert result is True - - t = env.from_string("{{ boolval }}") - result = t.render(boolval=False) - assert isinstance(result, bool) - assert result is False - - t = env.from_string("{{ 1 == 1 }}") - result = t.render() - assert isinstance(result, bool) - assert result is True - - t = env.from_string("{{ 2 + 2 == 5 }}") - result = t.render() - assert isinstance(result, bool) - assert result is False - - t = env.from_string("{{ None == None }}") - result = t.render() - assert isinstance(result, bool) - assert result is True - - t = env.from_string("{{ '' == None }}") - result = t.render() - assert isinstance(result, bool) - assert result is False - - def test_variable_dunder(self, env): - t = env.from_string("{{ x.__class__ }}") - result = t.render(x=True) - assert isinstance(result, type) - - def test_constant_dunder(self, env): - t = env.from_string("{{ true.__class__ }}") - result = t.render() - assert isinstance(result, type) - - def test_constant_dunder_to_string(self, env): - t = env.from_string("{{ true.__class__|string }}") - result = t.render() - assert not isinstance(result, type) - assert result in ["<type 'bool'>", "<class 'bool'>"] +def test_is_defined_native_return(env): + t = env.from_string("{{ missing is defined }}") + assert not t.render() + + +def test_undefined_native_return(env): + t = env.from_string("{{ missing }}") + assert isinstance(t.render(), Undefined) + + +def test_adding_undefined_native_return(env): + t = env.from_string("{{ 3 + missing }}") + + with pytest.raises(UndefinedError): + t.render() + + +def test_cast_int(env): + t = env.from_string("{{ value|int }}") + result = t.render(value="3") + assert isinstance(result, int) + assert result == 3 + + +def test_list_add(env): + t = env.from_string("{{ a + b }}") + result = t.render(a=["a", "b"], b=["c", "d"]) + assert isinstance(result, list) + assert result == ["a", "b", "c", "d"] + + +def test_multi_expression_add(env): + t = env.from_string("{{ a }} + {{ b }}") + result = t.render(a=["a", "b"], b=["c", "d"]) + assert not isinstance(result, list) + assert result == "['a', 'b'] + ['c', 'd']" + + +def test_loops(env): + t = env.from_string("{% for x in value %}{{ x }}{% endfor %}") + result = t.render(value=["a", "b", "c", "d"]) + assert isinstance(result, text_type) + assert result == "abcd" + + +def test_loops_with_ints(env): + t = env.from_string("{% for x in value %}{{ x }}{% endfor %}") + result = t.render(value=[1, 2, 3, 4]) + assert isinstance(result, int) + assert result == 1234 + + +def test_loop_look_alike(env): + t = env.from_string("{% for x in value %}{{ x }}{% endfor %}") + result = t.render(value=[1]) + assert isinstance(result, int) + assert result == 1 + + +@pytest.mark.parametrize( + ("source", "expect"), + ( + ("{{ value }}", True), + ("{{ value }}", False), + ("{{ 1 == 1 }}", True), + ("{{ 2 + 2 == 5 }}", False), + ("{{ None is none }}", True), + ("{{ '' == None }}", False), + ), +) +def test_booleans(env, source, expect): + t = env.from_string(source) + result = t.render(value=expect) + assert isinstance(result, bool) + assert result is expect + + +def test_variable_dunder(env): + t = env.from_string("{{ x.__class__ }}") + result = t.render(x=True) + assert isinstance(result, type) + + +def test_constant_dunder(env): + t = env.from_string("{{ true.__class__ }}") + result = t.render() + assert isinstance(result, type) + + +def test_constant_dunder_to_string(env): + t = env.from_string("{{ true.__class__|string }}") + result = t.render() + assert not isinstance(result, type) + assert result in {"<type 'bool'>", "<class 'bool'>"} + + +def test_string_literal_var(env): + t = env.from_string("[{{ 'all' }}]") + result = t.render() + assert isinstance(result, text_type) + assert result == "[all]" + + +def test_string_top_level(env): + t = env.from_string("'Jinja'") + result = t.render() + assert result == "Jinja" + + +def test_tuple_of_variable_strings(env): + t = env.from_string("'{{ a }}', 'data', '{{ b }}', b'{{ c }}'") + result = t.render(a=1, b=2, c="bytes") + assert isinstance(result, tuple) + assert result == ("1", "data", "2", b"bytes") + + +def test_concat_strings_with_quotes(env): + t = env.from_string("--host='{{ host }}' --user \"{{ user }}\"") + result = t.render(host="localhost", user="Jinja") + assert result == "--host='localhost' --user \"Jinja\"" + + +def test_no_intermediate_eval(env): + t = env.from_string("0.000{{ a }}") + result = t.render(a=7) + assert isinstance(result, float) + # If intermediate eval happened, 0.000 would render 0.0, then 7 + # would be appended, resulting in 0.07. + assert result < 0.007 # TODO use math.isclose in Python 3 + + +def test_spontaneous_env(): + t = NativeTemplate("{{ true }}") + assert isinstance(t.environment, NativeEnvironment) diff --git a/tests/test_regression.py b/tests/test_regression.py index 876b41c..c5a0d68 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -1,111 +1,121 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.regression - ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests corner cases and bugs. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import sys + import pytest -from jinja2 import Template, Environment, DictLoader, TemplateSyntaxError, \ - TemplateAssertionError, TemplateNotFound, PrefixLoader +from jinja2 import DictLoader +from jinja2 import Environment +from jinja2 import PrefixLoader +from jinja2 import Template +from jinja2 import TemplateAssertionError +from jinja2 import TemplateNotFound +from jinja2 import TemplateSyntaxError from jinja2._compat import text_type -@pytest.mark.regression class TestCorner(object): - def test_assigned_scoping(self, env): - t = env.from_string(''' + t = env.from_string( + """ {%- for item in (1, 2, 3, 4) -%} [{{ item }}] {%- endfor %} {{- item -}} - ''') - assert t.render(item=42) == '[1][2][3][4]42' + """ + ) + assert t.render(item=42) == "[1][2][3][4]42" - t = env.from_string(''' + t = env.from_string( + """ {%- for item in (1, 2, 3, 4) -%} [{{ item }}] {%- endfor %} {%- set item = 42 %} {{- item -}} - ''') - assert t.render() == '[1][2][3][4]42' + """ + ) + assert t.render() == "[1][2][3][4]42" - t = env.from_string(''' + t = env.from_string( + """ {%- set item = 42 %} {%- for item in (1, 2, 3, 4) -%} [{{ item }}] {%- endfor %} {{- item -}} - ''') - assert t.render() == '[1][2][3][4]42' + """ + ) + assert t.render() == "[1][2][3][4]42" def test_closure_scoping(self, env): - t = env.from_string(''' + t = env.from_string( + """ {%- set wrapper = "<FOO>" %} {%- for item in (1, 2, 3, 4) %} {%- macro wrapper() %}[{{ item }}]{% endmacro %} {{- wrapper() }} {%- endfor %} {{- wrapper -}} - ''') - assert t.render() == '[1][2][3][4]<FOO>' + """ + ) + assert t.render() == "[1][2][3][4]<FOO>" - t = env.from_string(''' + t = env.from_string( + """ {%- for item in (1, 2, 3, 4) %} {%- macro wrapper() %}[{{ item }}]{% endmacro %} {{- wrapper() }} {%- endfor %} {%- set wrapper = "<FOO>" %} {{- wrapper -}} - ''') - assert t.render() == '[1][2][3][4]<FOO>' + """ + ) + assert t.render() == "[1][2][3][4]<FOO>" - t = env.from_string(''' + t = env.from_string( + """ {%- for item in (1, 2, 3, 4) %} {%- macro wrapper() %}[{{ item }}]{% endmacro %} {{- wrapper() }} {%- endfor %} {{- wrapper -}} - ''') - assert t.render(wrapper=23) == '[1][2][3][4]23' + """ + ) + assert t.render(wrapper=23) == "[1][2][3][4]23" -@pytest.mark.regression class TestBug(object): - def test_keyword_folding(self, env): env = Environment() - env.filters['testing'] = lambda value, some: value + some - assert env.from_string("{{ 'test'|testing(some='stuff') }}") \ - .render() == 'teststuff' + env.filters["testing"] = lambda value, some: value + some + assert ( + env.from_string("{{ 'test'|testing(some='stuff') }}").render() + == "teststuff" + ) def test_extends_output_bugs(self, env): - env = Environment(loader=DictLoader({ - 'parent.html': '(({% block title %}{% endblock %}))' - })) + env = Environment( + loader=DictLoader({"parent.html": "(({% block title %}{% endblock %}))"}) + ) t = env.from_string( '{% if expr %}{% extends "parent.html" %}{% endif %}' - '[[{% block title %}title{% endblock %}]]' - '{% for item in [1, 2, 3] %}({{ item }}){% endfor %}' + "[[{% block title %}title{% endblock %}]]" + "{% for item in [1, 2, 3] %}({{ item }}){% endfor %}" ) - assert t.render(expr=False) == '[[title]](1)(2)(3)' - assert t.render(expr=True) == '((title))' + assert t.render(expr=False) == "[[title]](1)(2)(3)" + assert t.render(expr=True) == "((title))" def test_urlize_filter_escaping(self, env): tmpl = env.from_string('{{ "http://www.example.org/<foo"|urlize }}') - assert tmpl.render() == '<a href="http://www.example.org/<foo" rel="noopener">'\ - 'http://www.example.org/<foo</a>' + assert ( + tmpl.render() == '<a href="http://www.example.org/<foo" rel="noopener">' + "http://www.example.org/<foo</a>" + ) def test_loop_call_loop(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {% macro test() %} {{ caller() }} @@ -119,29 +129,35 @@ class TestBug(object): {% endcall %} {% endfor %} - ''') + """ + ) - assert tmpl.render().split() \ - == [text_type(x) for x in range(1, 11)] * 5 + assert tmpl.render().split() == [text_type(x) for x in range(1, 11)] * 5 def test_weird_inline_comment(self, env): - env = Environment(line_statement_prefix='%') - pytest.raises(TemplateSyntaxError, env.from_string, - '% for item in seq {# missing #}\n...% endfor') + env = Environment(line_statement_prefix="%") + pytest.raises( + TemplateSyntaxError, + env.from_string, + "% for item in seq {# missing #}\n...% endfor", + ) def test_old_macro_loop_scoping_bug(self, env): - tmpl = env.from_string('{% for i in (1, 2) %}{{ i }}{% endfor %}' - '{% macro i() %}3{% endmacro %}{{ i() }}') - assert tmpl.render() == '123' + tmpl = env.from_string( + "{% for i in (1, 2) %}{{ i }}{% endfor %}" + "{% macro i() %}3{% endmacro %}{{ i() }}" + ) + assert tmpl.render() == "123" def test_partial_conditional_assignments(self, env): - tmpl = env.from_string('{% if b %}{% set a = 42 %}{% endif %}{{ a }}') - assert tmpl.render(a=23) == '23' - assert tmpl.render(b=True) == '42' + tmpl = env.from_string("{% if b %}{% set a = 42 %}{% endif %}{{ a }}") + assert tmpl.render(a=23) == "23" + assert tmpl.render(b=True) == "42" def test_stacked_locals_scoping_bug(self, env): - env = Environment(line_statement_prefix='#') - t = env.from_string('''\ + env = Environment(line_statement_prefix="#") + t = env.from_string( + """\ # for j in [1, 2]: # set x = 1 # for i in [1, 2]: @@ -160,11 +176,13 @@ class TestBug(object): # else # print 'D' # endif - ''') - assert t.render(a=0, b=False, c=42, d=42.0) == '1111C' + """ + ) + assert t.render(a=0, b=False, c=42, d=42.0) == "1111C" def test_stacked_locals_scoping_bug_twoframe(self, env): - t = Template(''' + t = Template( + """ {% set x = 1 %} {% for item in foo %} {% if item == 1 %} @@ -172,12 +190,14 @@ class TestBug(object): {% endif %} {% endfor %} {{ x }} - ''') + """ + ) rv = t.render(foo=[1]).strip() - assert rv == u'1' + assert rv == u"1" def test_call_with_args(self, env): - t = Template("""{% macro dump_users(users) -%} + t = Template( + """{% macro dump_users(users) -%} <ul> {%- for user in users -%} <li><p>{{ user.username|e }}</p>{{ caller(user) }}</li> @@ -192,77 +212,85 @@ class TestBug(object): <dl>Description</dl> <dd>{{ user.description }}</dd> </dl> - {% endcall %}""") - - assert [x.strip() for x in t.render(list_of_user=[{ - 'username': 'apo', - 'realname': 'something else', - 'description': 'test' - }]).splitlines()] == [ - u'<ul><li><p>apo</p><dl>', - u'<dl>Realname</dl>', - u'<dd>something else</dd>', - u'<dl>Description</dl>', - u'<dd>test</dd>', - u'</dl>', - u'</li></ul>' + {% endcall %}""" + ) + + assert [ + x.strip() + for x in t.render( + list_of_user=[ + { + "username": "apo", + "realname": "something else", + "description": "test", + } + ] + ).splitlines() + ] == [ + u"<ul><li><p>apo</p><dl>", + u"<dl>Realname</dl>", + u"<dd>something else</dd>", + u"<dl>Description</dl>", + u"<dd>test</dd>", + u"</dl>", + u"</li></ul>", ] def test_empty_if_condition_fails(self, env): - pytest.raises(TemplateSyntaxError, - Template, '{% if %}....{% endif %}') - pytest.raises(TemplateSyntaxError, - Template, '{% if foo %}...{% elif %}...{% endif %}') - pytest.raises(TemplateSyntaxError, - Template, '{% for x in %}..{% endfor %}') + pytest.raises(TemplateSyntaxError, Template, "{% if %}....{% endif %}") + pytest.raises( + TemplateSyntaxError, Template, "{% if foo %}...{% elif %}...{% endif %}" + ) + pytest.raises(TemplateSyntaxError, Template, "{% for x in %}..{% endfor %}") - def test_recursive_loop_bug(self, env): - tpl1 = Template(""" - {% for p in foo recursive%} - {{p.bar}} - {% for f in p.fields recursive%} - {{f.baz}} + def test_recursive_loop_compile(self, env): + Template( + """ + {% for p in foo recursive%} {{p.bar}} - {% if f.rec %} - {{ loop(f.sub) }} - {% endif %} + {% for f in p.fields recursive%} + {{f.baz}} + {{p.bar}} + {% if f.rec %} + {{ loop(f.sub) }} + {% endif %} + {% endfor %} {% endfor %} - {% endfor %} - """) - - tpl2 = Template(""" - {% for p in foo%} - {{p.bar}} - {% for f in p.fields recursive%} - {{f.baz}} + """ + ) + Template( + """ + {% for p in foo%} {{p.bar}} - {% if f.rec %} - {{ loop(f.sub) }} - {% endif %} + {% for f in p.fields recursive%} + {{f.baz}} + {{p.bar}} + {% if f.rec %} + {{ loop(f.sub) }} + {% endif %} + {% endfor %} {% endfor %} - {% endfor %} - """) + """ + ) def test_else_loop_bug(self, env): - t = Template(''' + t = Template( + """ {% for x in y %} {{ loop.index0 }} {% else %} {% for i in range(3) %}{{ i }}{% endfor %} {% endfor %} - ''') - assert t.render(y=[]).strip() == '012' + """ + ) + assert t.render(y=[]).strip() == "012" def test_correct_prefix_loader_name(self, env): - env = Environment(loader=PrefixLoader({ - 'foo': DictLoader({}) - })) - try: - env.get_template('foo/bar.html') - except TemplateNotFound as e: - assert e.name == 'foo/bar.html' - else: - assert False, 'expected error here' + env = Environment(loader=PrefixLoader({"foo": DictLoader({})})) + with pytest.raises(TemplateNotFound) as e: + env.get_template("foo/bar.html") + + assert e.value.name == "foo/bar.html" def test_contextfunction_callable_classes(self, env): from jinja2.utils import contextfunction @@ -270,74 +298,84 @@ class TestBug(object): class CallableClass(object): @contextfunction def __call__(self, ctx): - return ctx.resolve('hello') + return ctx.resolve("hello") tpl = Template("""{{ callableclass() }}""") - output = tpl.render(callableclass=CallableClass(), hello='TEST') - expected = 'TEST' + output = tpl.render(callableclass=CallableClass(), hello="TEST") + expected = "TEST" assert output == expected - @pytest.mark.skipif(sys.version_info[0] > 2, - reason='This only works on 2.x') + @pytest.mark.skipif(sys.version_info[0] > 2, reason="This only works on 2.x") def test_old_style_attribute(self, env): class Foo: x = 42 - assert env.getitem(Foo(), 'x') == 42 + + assert env.getitem(Foo(), "x") == 42 def test_block_set_with_extends(self): - env = Environment(loader=DictLoader({ - 'main': '{% block body %}[{{ x }}]{% endblock %}', - })) + env = Environment( + loader=DictLoader({"main": "{% block body %}[{{ x }}]{% endblock %}"}) + ) t = env.from_string('{% extends "main" %}{% set x %}42{% endset %}') - assert t.render() == '[42]' + assert t.render() == "[42]" def test_nested_for_else(self, env): - tmpl = env.from_string('{% for x in y %}{{ loop.index0 }}{% else %}' - '{% for i in range(3) %}{{ i }}{% endfor %}' - '{% endfor %}') - assert tmpl.render() == '012' + tmpl = env.from_string( + "{% for x in y %}{{ loop.index0 }}{% else %}" + "{% for i in range(3) %}{{ i }}{% endfor %}" + "{% endfor %}" + ) + assert tmpl.render() == "012" def test_macro_var_bug(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {% set i = 1 %} {% macro test() %} {% for i in range(0, 10) %}{{ i }}{% endfor %} {% endmacro %}{{ test() }} - ''') - assert tmpl.render().strip() == '0123456789' + """ + ) + assert tmpl.render().strip() == "0123456789" def test_macro_var_bug_advanced(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {% macro outer() %} {% set i = 1 %} {% macro test() %} {% for i in range(0, 10) %}{{ i }}{% endfor %} {% endmacro %}{{ test() }} {% endmacro %}{{ outer() }} - ''') - assert tmpl.render().strip() == '0123456789' + """ + ) + assert tmpl.render().strip() == "0123456789" def test_callable_defaults(self): env = Environment() - env.globals['get_int'] = lambda: 42 - t = env.from_string(''' + env.globals["get_int"] = lambda: 42 + t = env.from_string( + """ {% macro test(a, b, c=get_int()) -%} {{ a + b + c }} {%- endmacro %} {{ test(1, 2) }}|{{ test(1, 2, 3) }} - ''') - assert t.render().strip() == '45|6' + """ + ) + assert t.render().strip() == "45|6" def test_macro_escaping(self): env = Environment( - autoescape=lambda x: False, extensions=['jinja2.ext.autoescape']) + autoescape=lambda x: False, extensions=["jinja2.ext.autoescape"] + ) template = "{% macro m() %}<html>{% endmacro %}" template += "{% autoescape true %}{{ m() }}{% endautoescape %}" assert env.from_string(template).render() def test_macro_scoping(self, env): - tmpl = env.from_string(''' + tmpl = env.from_string( + """ {% set n=[1,2,3,4,5] %} {% for n in [[1,2,3], [3,4,5], [5,6,7]] %} @@ -349,47 +387,56 @@ class TestBug(object): {{ x(n) }} {% endfor %} - ''') - assert list(map(int, tmpl.render().split())) == \ - [3, 2, 1, 5, 4, 3, 7, 6, 5] + """ + ) + assert list(map(int, tmpl.render().split())) == [3, 2, 1, 5, 4, 3, 7, 6, 5] def test_scopes_and_blocks(self): - env = Environment(loader=DictLoader({ - 'a.html': ''' + env = Environment( + loader=DictLoader( + { + "a.html": """ {%- set foo = 'bar' -%} {% include 'x.html' -%} - ''', - 'b.html': ''' + """, + "b.html": """ {%- set foo = 'bar' -%} {% block test %}{% include 'x.html' %}{% endblock -%} - ''', - 'c.html': ''' + """, + "c.html": """ {%- set foo = 'bar' -%} {% block test %}{% set foo = foo %}{% include 'x.html' %}{% endblock -%} - ''', - 'x.html': '''{{ foo }}|{{ test }}''' - })) + """, + "x.html": """{{ foo }}|{{ test }}""", + } + ) + ) - a = env.get_template('a.html') - b = env.get_template('b.html') - c = env.get_template('c.html') + a = env.get_template("a.html") + b = env.get_template("b.html") + c = env.get_template("c.html") - assert a.render(test='x').strip() == 'bar|x' - assert b.render(test='x').strip() == 'bar|x' - assert c.render(test='x').strip() == 'bar|x' + assert a.render(test="x").strip() == "bar|x" + assert b.render(test="x").strip() == "bar|x" + assert c.render(test="x").strip() == "bar|x" def test_scopes_and_include(self): - env = Environment(loader=DictLoader({ - 'include.html': '{{ var }}', - 'base.html': '{% include "include.html" %}', - 'child.html': '{% extends "base.html" %}{% set var = 42 %}', - })) - t = env.get_template('child.html') - assert t.render() == '42' + env = Environment( + loader=DictLoader( + { + "include.html": "{{ var }}", + "base.html": '{% include "include.html" %}', + "child.html": '{% extends "base.html" %}{% set var = 42 %}', + } + ) + ) + t = env.get_template("child.html") + assert t.render() == "42" def test_caller_scoping(self, env): - t = env.from_string(''' + t = env.from_string( + """ {% macro detail(icon, value) -%} {% if value -%} <p><span class="fa fa-fw fa-{{ icon }}"></span> @@ -407,54 +454,67 @@ class TestBug(object): <a href="{{ href }}">{{ value }}</a> {%- endcall %} {%- endmacro %} - ''') + """ + ) - assert t.module.link_detail('circle', 'Index', '/') == ( - '<p><span class="fa fa-fw fa-circle">' - '</span><a href="/">Index</a></p>') + assert t.module.link_detail("circle", "Index", "/") == ( + '<p><span class="fa fa-fw fa-circle"></span><a href="/">Index</a></p>' + ) def test_variable_reuse(self, env): - t = env.from_string('{% for x in x.y %}{{ x }}{% endfor %}') - assert t.render(x={'y': [0, 1, 2]}) == '012' + t = env.from_string("{% for x in x.y %}{{ x }}{% endfor %}") + assert t.render(x={"y": [0, 1, 2]}) == "012" - t = env.from_string('{% for x in x.y %}{{ loop.index0 }}|{{ x }}{% endfor %}') - assert t.render(x={'y': [0, 1, 2]}) == '0|01|12|2' + t = env.from_string("{% for x in x.y %}{{ loop.index0 }}|{{ x }}{% endfor %}") + assert t.render(x={"y": [0, 1, 2]}) == "0|01|12|2" - t = env.from_string('{% for x in x.y recursive %}{{ x }}{% endfor %}') - assert t.render(x={'y': [0, 1, 2]}) == '012' + t = env.from_string("{% for x in x.y recursive %}{{ x }}{% endfor %}") + assert t.render(x={"y": [0, 1, 2]}) == "012" def test_double_caller(self, env): - t = env.from_string('{% macro x(caller=none) %}[{% if caller %}' - '{{ caller() }}{% endif %}]{% endmacro %}' - '{{ x() }}{% call x() %}aha!{% endcall %}') - assert t.render() == '[][aha!]' + t = env.from_string( + "{% macro x(caller=none) %}[{% if caller %}" + "{{ caller() }}{% endif %}]{% endmacro %}" + "{{ x() }}{% call x() %}aha!{% endcall %}" + ) + assert t.render() == "[][aha!]" def test_double_caller_no_default(self, env): with pytest.raises(TemplateAssertionError) as exc_info: - env.from_string('{% macro x(caller) %}[{% if caller %}' - '{{ caller() }}{% endif %}]{% endmacro %}') - assert exc_info.match(r'"caller" argument must be omitted or ' - r'be given a default') + env.from_string( + "{% macro x(caller) %}[{% if caller %}" + "{{ caller() }}{% endif %}]{% endmacro %}" + ) + assert exc_info.match( + r'"caller" argument must be omitted or ' r"be given a default" + ) - t = env.from_string('{% macro x(caller=none) %}[{% if caller %}' - '{{ caller() }}{% endif %}]{% endmacro %}') + t = env.from_string( + "{% macro x(caller=none) %}[{% if caller %}" + "{{ caller() }}{% endif %}]{% endmacro %}" + ) with pytest.raises(TypeError) as exc_info: t.module.x(None, caller=lambda: 42) - assert exc_info.match(r'\'x\' was invoked with two values for the ' - r'special caller argument') + assert exc_info.match( + r"\'x\' was invoked with two values for the " r"special caller argument" + ) def test_macro_blocks(self, env): - t = env.from_string('{% macro x() %}{% block foo %}x{% ' - 'endblock %}{% endmacro %}{{ x() }}') - assert t.render() == 'x' + t = env.from_string( + "{% macro x() %}{% block foo %}x{% endblock %}{% endmacro %}{{ x() }}" + ) + assert t.render() == "x" def test_scoped_block(self, env): - t = env.from_string('{% set x = 1 %}{% with x = 2 %}{% block y scoped %}' - '{{ x }}{% endblock %}{% endwith %}') - assert t.render() == '2' + t = env.from_string( + "{% set x = 1 %}{% with x = 2 %}{% block y scoped %}" + "{{ x }}{% endblock %}{% endwith %}" + ) + assert t.render() == "2" def test_recursive_loop_filter(self, env): - t = env.from_string(''' + t = env.from_string( + """ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> {%- for page in [site.root] if page.url != this recursive %} @@ -462,45 +522,63 @@ class TestBug(object): {{- loop(page.children) }} {%- endfor %} </urlset> - ''') - sm =t.render(this='/foo', site={'root': { - 'url': '/', - 'children': [ - {'url': '/foo'}, - {'url': '/bar'}, - ] - }}) + """ + ) + sm = t.render( + this="/foo", + site={"root": {"url": "/", "children": [{"url": "/foo"}, {"url": "/bar"}]}}, + ) lines = [x.strip() for x in sm.splitlines() if x.strip()] assert lines == [ '<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', - '<url><loc>/</loc></url>', - '<url><loc>/bar</loc></url>', - '</urlset>', + "<url><loc>/</loc></url>", + "<url><loc>/bar</loc></url>", + "</urlset>", ] def test_empty_if(self, env): - t = env.from_string('{% if foo %}{% else %}42{% endif %}') - assert t.render(foo=False) == '42' + t = env.from_string("{% if foo %}{% else %}42{% endif %}") + assert t.render(foo=False) == "42" + + def test_subproperty_if(self, env): + t = env.from_string( + "{% if object1.subproperty1 is eq object2.subproperty2 %}42{% endif %}" + ) + assert ( + t.render( + object1={"subproperty1": "value"}, object2={"subproperty2": "value"} + ) + == "42" + ) def test_set_and_include(self): - env = Environment(loader=DictLoader({ - 'inc': 'bar', - 'main': '{% set foo = "foo" %}{{ foo }}{% include "inc" %}' - })) - assert env.get_template('main').render() == 'foobar' + env = Environment( + loader=DictLoader( + { + "inc": "bar", + "main": '{% set foo = "foo" %}{{ foo }}{% include "inc" %}', + } + ) + ) + assert env.get_template("main").render() == "foobar" def test_loop_include(self): - env = Environment(loader=DictLoader({ - 'inc': '{{ item }}', - 'main': '{% for item in [1, 2, 3] %}{% include "inc" %}{% endfor %}', - })) - assert env.get_template('main').render() == '123' + env = Environment( + loader=DictLoader( + { + "inc": "{{ i }}", + "main": '{% for i in [1, 2, 3] %}{% include "inc" %}{% endfor %}', + } + ) + ) + assert env.get_template("main").render() == "123" def test_grouper_repr(self): from jinja2.filters import _GroupTuple - t = _GroupTuple('foo', [1, 2]) - assert t.grouper == 'foo' + + t = _GroupTuple("foo", [1, 2]) + assert t.grouper == "foo" assert t.list == [1, 2] assert repr(t) == "('foo', [1, 2])" assert str(t) == "('foo', [1, 2])" @@ -514,28 +592,33 @@ class TestBug(object): class MyEnvironment(Environment): context_class = MyContext - loader = DictLoader({'base': '{{ foobar }}', - 'test': '{% extends "base" %}'}) + loader = DictLoader({"base": "{{ foobar }}", "test": '{% extends "base" %}'}) env = MyEnvironment(loader=loader) - assert env.get_template('test').render(foobar='test') == 'test' + assert env.get_template("test").render(foobar="test") == "test" def test_legacy_custom_context(self, env): - from jinja2.runtime import Context, Undefined, missing + from jinja2.runtime import Context, missing class MyContext(Context): def resolve(self, name): - if name == 'foo': + if name == "foo": return 42 return super(MyContext, self).resolve(name) - x = MyContext(env, parent={'bar': 23}, name='foo', blocks={}) + 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 + 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 -%} - ''') - assert tmpl.render(values=[]) == '0' + tmpl = env.from_string( + "{%- for value in values recursive %}1{% else %}0{% endfor -%}" + ) + assert tmpl.render(values=[]) == "0" + + def test_markup_and_chainable_undefined(self): + from jinja2 import Markup + from jinja2.runtime import ChainableUndefined + + assert str(Markup(ChainableUndefined())) == "" diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..5e4686c --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,75 @@ +import itertools + +from jinja2 import Template +from jinja2.runtime import LoopContext + +TEST_IDX_TEMPLATE_STR_1 = ( + "[{% for i in lst|reverse %}(len={{ loop.length }}," + " revindex={{ loop.revindex }}, index={{ loop.index }}, val={{ i }}){% endfor %}]" +) +TEST_IDX0_TEMPLATE_STR_1 = ( + "[{% for i in lst|reverse %}(len={{ loop.length }}," + " revindex0={{ loop.revindex0 }}, index0={{ loop.index0 }}, val={{ i }})" + "{% endfor %}]" +) + + +def test_loop_idx(): + t = Template(TEST_IDX_TEMPLATE_STR_1) + lst = [10] + excepted_render = "[(len=1, revindex=1, index=1, val=10)]" + assert excepted_render == t.render(lst=lst) + + +def test_loop_idx0(): + t = Template(TEST_IDX0_TEMPLATE_STR_1) + lst = [10] + excepted_render = "[(len=1, revindex0=0, index0=0, val=10)]" + assert excepted_render == t.render(lst=lst) + + +def test_loopcontext0(): + in_lst = [] + lc = LoopContext(reversed(in_lst), None) + assert lc.length == len(in_lst) + + +def test_loopcontext1(): + in_lst = [10] + lc = LoopContext(reversed(in_lst), None) + assert lc.length == len(in_lst) + + +def test_loopcontext2(): + in_lst = [10, 11] + lc = LoopContext(reversed(in_lst), None) + assert lc.length == len(in_lst) + + +def test_iterator_not_advanced_early(): + t = Template("{% for _, g in gs %}{{ loop.index }} {{ g|list }}\n{% endfor %}") + out = t.render( + gs=itertools.groupby([(1, "a"), (1, "b"), (2, "c"), (3, "d")], lambda x: x[0]) + ) + # groupby groups depend on the current position of the iterator. If + # it was advanced early, the lists would appear empty. + assert out == "1 [(1, 'a'), (1, 'b')]\n2 [(2, 'c')]\n3 [(3, 'd')]\n" + + +def test_mock_not_contextfunction(): + """If a callable class has a ``__getattr__`` that returns True-like + values for arbitrary attrs, it should not be incorrectly identified + as a ``contextfunction``. + """ + + class Calc(object): + def __getattr__(self, item): + return object() + + def __call__(self, *args, **kwargs): + return len(args) + len(kwargs) + + t = Template("{{ calc() }}") + out = t.render(calc=Calc()) + # Would be "1" if context argument was passed. + assert out == "0" diff --git a/tests/test_security.py b/tests/test_security.py index 6e6b2f2..7e8974c 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1,29 +1,20 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.security - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Checks the sandbox and other security features. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from markupsafe import Markup - from jinja2 import Environment -from jinja2.sandbox import SandboxedEnvironment, \ - ImmutableSandboxedEnvironment, unsafe from jinja2 import escape -from jinja2.exceptions import SecurityError, TemplateSyntaxError, \ - TemplateRuntimeError -from jinja2.nodes import EvalContext +from jinja2 import Markup from jinja2._compat import text_type +from jinja2.exceptions import SecurityError +from jinja2.exceptions import TemplateRuntimeError +from jinja2.exceptions import TemplateSyntaxError +from jinja2.nodes import EvalContext +from jinja2.sandbox import ImmutableSandboxedEnvironment +from jinja2.sandbox import SandboxedEnvironment +from jinja2.sandbox import unsafe class PrivateStuff(object): - def bar(self): return 23 @@ -32,65 +23,74 @@ class PrivateStuff(object): return 42 def __repr__(self): - return 'PrivateStuff' + return "PrivateStuff" class PublicStuff(object): - bar = lambda self: 23 - _foo = lambda self: 42 + def bar(self): + return 23 + + def _foo(self): + return 42 def __repr__(self): - return 'PublicStuff' + return "PublicStuff" -@pytest.mark.sandbox class TestSandbox(object): - def test_unsafe(self, env): env = SandboxedEnvironment() - pytest.raises(SecurityError, env.from_string("{{ foo.foo() }}").render, - foo=PrivateStuff()) - assert env.from_string("{{ foo.bar() }}").render(foo=PrivateStuff()) == '23' - - pytest.raises(SecurityError, - env.from_string("{{ foo._foo() }}").render, - foo=PublicStuff()) - assert env.from_string("{{ foo.bar() }}").render(foo=PublicStuff()) == '23' - assert env.from_string("{{ foo.__class__ }}").render(foo=42) == '' - assert env.from_string("{{ foo.func_code }}").render(foo=lambda:None) == '' + pytest.raises( + SecurityError, env.from_string("{{ foo.foo() }}").render, foo=PrivateStuff() + ) + assert env.from_string("{{ foo.bar() }}").render(foo=PrivateStuff()) == "23" + + pytest.raises( + SecurityError, env.from_string("{{ foo._foo() }}").render, foo=PublicStuff() + ) + assert env.from_string("{{ foo.bar() }}").render(foo=PublicStuff()) == "23" + assert env.from_string("{{ foo.__class__ }}").render(foo=42) == "" + assert env.from_string("{{ foo.func_code }}").render(foo=lambda: None) == "" # security error comes from __class__ already. - pytest.raises(SecurityError, env.from_string( - "{{ foo.__class__.__subclasses__() }}").render, foo=42) + pytest.raises( + SecurityError, + env.from_string("{{ foo.__class__.__subclasses__() }}").render, + foo=42, + ) def test_immutable_environment(self, env): env = ImmutableSandboxedEnvironment() - pytest.raises(SecurityError, env.from_string( - '{{ [].append(23) }}').render) - pytest.raises(SecurityError, env.from_string( - '{{ {1:2}.clear() }}').render) + pytest.raises(SecurityError, env.from_string("{{ [].append(23) }}").render) + pytest.raises(SecurityError, env.from_string("{{ {1:2}.clear() }}").render) def test_restricted(self, env): env = SandboxedEnvironment() - pytest.raises(TemplateSyntaxError, env.from_string, - "{% for item.attribute in seq %}...{% endfor %}") - pytest.raises(TemplateSyntaxError, env.from_string, - "{% for foo, bar.baz in seq %}...{% endfor %}") + pytest.raises( + TemplateSyntaxError, + env.from_string, + "{% for item.attribute in seq %}...{% endfor %}", + ) + pytest.raises( + TemplateSyntaxError, + env.from_string, + "{% for foo, bar.baz in seq %}...{% endfor %}", + ) def test_markup_operations(self, env): # adding two strings should escape the unsafe one unsafe = '<script type="application/x-some-script">alert("foo");</script>' - safe = Markup('<em>username</em>') + safe = Markup("<em>username</em>") assert unsafe + safe == text_type(escape(unsafe)) + text_type(safe) # string interpolations are safe to use too - assert Markup('<em>%s</em>') % '<bad user>' == \ - '<em><bad user></em>' - assert Markup('<em>%(username)s</em>') % { - 'username': '<bad user>' - } == '<em><bad user></em>' + assert Markup("<em>%s</em>") % "<bad user>" == "<em><bad user></em>" + assert ( + Markup("<em>%(username)s</em>") % {"username": "<bad user>"} + == "<em><bad user></em>" + ) # an escaped object is markup too - assert type(Markup('foo') + 'bar') is Markup + assert type(Markup("foo") + "bar") is Markup # and it implements __html__ by returning itself x = Markup("foo") @@ -99,33 +99,38 @@ class TestSandbox(object): # it also knows how to treat __html__ objects class Foo(object): def __html__(self): - return '<em>awesome</em>' + return "<em>awesome</em>" def __unicode__(self): - return 'awesome' - assert Markup(Foo()) == '<em>awesome</em>' - assert Markup('<strong>%s</strong>') % Foo() == \ - '<strong><em>awesome</em></strong>' + return "awesome" + + assert Markup(Foo()) == "<em>awesome</em>" + assert ( + Markup("<strong>%s</strong>") % Foo() == "<strong><em>awesome</em></strong>" + ) # escaping and unescaping - assert escape('"<>&\'') == '"<>&'' + assert escape("\"<>&'") == ""<>&'" assert Markup("<em>Foo & Bar</em>").striptags() == "Foo & Bar" assert Markup("<test>").unescape() == "<test>" def test_template_data(self, env): env = Environment(autoescape=True) - t = env.from_string('{% macro say_hello(name) %}' - '<p>Hello {{ name }}!</p>{% endmacro %}' - '{{ say_hello("<blink>foo</blink>") }}') - escaped_out = '<p>Hello <blink>foo</blink>!</p>' + t = env.from_string( + "{% macro say_hello(name) %}" + "<p>Hello {{ name }}!</p>{% endmacro %}" + '{{ say_hello("<blink>foo</blink>") }}' + ) + escaped_out = "<p>Hello <blink>foo</blink>!</p>" assert t.render() == escaped_out assert text_type(t.module) == escaped_out assert escape(t.module) == escaped_out - assert t.module.say_hello('<blink>foo</blink>') == escaped_out - assert escape(t.module.say_hello( - EvalContext(env), '<blink>foo</blink>')) == escaped_out - assert escape(t.module.say_hello( - '<blink>foo</blink>')) == escaped_out + assert t.module.say_hello("<blink>foo</blink>") == escaped_out + assert ( + escape(t.module.say_hello(EvalContext(env), "<blink>foo</blink>")) + == escaped_out + ) + assert escape(t.module.say_hello("<blink>foo</blink>")) == escaped_out def test_attr_filter(self, env): env = SandboxedEnvironment() @@ -134,77 +139,72 @@ class TestSandbox(object): def test_binary_operator_intercepting(self, env): def disable_op(left, right): - raise TemplateRuntimeError('that operator so does not work') - for expr, ctx, rv in ('1 + 2', {}, '3'), ('a + 2', {'a': 2}, '4'): + raise TemplateRuntimeError("that operator so does not work") + + for expr, ctx, rv in ("1 + 2", {}, "3"), ("a + 2", {"a": 2}, "4"): env = SandboxedEnvironment() - env.binop_table['+'] = disable_op - t = env.from_string('{{ %s }}' % expr) + env.binop_table["+"] = disable_op + t = env.from_string("{{ %s }}" % expr) assert t.render(ctx) == rv - env.intercepted_binops = frozenset(['+']) - t = env.from_string('{{ %s }}' % expr) - try: + env.intercepted_binops = frozenset(["+"]) + t = env.from_string("{{ %s }}" % expr) + with pytest.raises(TemplateRuntimeError): t.render(ctx) - except TemplateRuntimeError as e: - pass - else: - assert False, 'expected runtime error' def test_unary_operator_intercepting(self, env): def disable_op(arg): - raise TemplateRuntimeError('that operator so does not work') - for expr, ctx, rv in ('-1', {}, '-1'), ('-a', {'a': 2}, '-2'): + raise TemplateRuntimeError("that operator so does not work") + + for expr, ctx, rv in ("-1", {}, "-1"), ("-a", {"a": 2}, "-2"): env = SandboxedEnvironment() - env.unop_table['-'] = disable_op - t = env.from_string('{{ %s }}' % expr) + env.unop_table["-"] = disable_op + t = env.from_string("{{ %s }}" % expr) assert t.render(ctx) == rv - env.intercepted_unops = frozenset(['-']) - t = env.from_string('{{ %s }}' % expr) - try: + env.intercepted_unops = frozenset(["-"]) + t = env.from_string("{{ %s }}" % expr) + with pytest.raises(TemplateRuntimeError): t.render(ctx) - except TemplateRuntimeError as e: - pass - else: - assert False, 'expected runtime error' -@pytest.mark.sandbox class TestStringFormat(object): - def test_basic_format_safety(self): env = SandboxedEnvironment() t = env.from_string('{{ "a{0.__class__}b".format(42) }}') - assert t.render() == 'ab' + assert t.render() == "ab" def test_basic_format_all_okay(self): env = SandboxedEnvironment() t = env.from_string('{{ "a{0.foo}b".format({"foo": 42}) }}') - assert t.render() == 'a42b' + assert t.render() == "a42b" def test_safe_format_safety(self): env = SandboxedEnvironment() t = env.from_string('{{ ("a{0.__class__}b{1}"|safe).format(42, "<foo>") }}') - assert t.render() == 'ab<foo>' + assert t.render() == "ab<foo>" def test_safe_format_all_okay(self): env = SandboxedEnvironment() t = env.from_string('{{ ("a{0.foo}b{1}"|safe).format({"foo": 42}, "<foo>") }}') - assert t.render() == 'a42b<foo>' + assert t.render() == "a42b<foo>" -@pytest.mark.sandbox -@pytest.mark.skipif(not hasattr(str, 'format_map'), reason='requires str.format_map method') +@pytest.mark.skipif( + not hasattr(str, "format_map"), reason="requires str.format_map method" +) class TestStringFormatMap(object): def test_basic_format_safety(self): env = SandboxedEnvironment() t = env.from_string('{{ "a{x.__class__}b".format_map({"x":42}) }}') - assert t.render() == 'ab' + assert t.render() == "ab" def test_basic_format_all_okay(self): env = SandboxedEnvironment() t = env.from_string('{{ "a{x.foo}b".format_map({"x":{"foo": 42}}) }}') - assert t.render() == 'a42b' + assert t.render() == "a42b" def test_safe_format_all_okay(self): env = SandboxedEnvironment() - t = env.from_string('{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}') - assert t.render() == 'a42b<foo>' + t = env.from_string( + '{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}' + ) + assert t.render() == "a42b<foo>" diff --git a/tests/test_tests.py b/tests/test_tests.py index 1198516..b903e3b 100644 --- a/tests/test_tests.py +++ b/tests/test_tests.py @@ -1,163 +1,209 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.tests - ~~~~~~~~~~~~~~~~~~~~~~ - - Who tests the tests? - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" import pytest -from markupsafe import Markup - from jinja2 import Environment +from jinja2 import Markup -@pytest.mark.test_tests -class TestTestsCase(object): +class MyDict(dict): + pass + +class TestTestsCase(object): def test_defined(self, env): - tmpl = env.from_string('{{ missing is defined }}|' - '{{ true is defined }}') - assert tmpl.render() == 'False|True' + tmpl = env.from_string("{{ missing is defined }}|{{ true is defined }}") + assert tmpl.render() == "False|True" def test_even(self, env): - tmpl = env.from_string('''{{ 1 is even }}|{{ 2 is even }}''') - assert tmpl.render() == 'False|True' + tmpl = env.from_string("""{{ 1 is even }}|{{ 2 is even }}""") + assert tmpl.render() == "False|True" def test_odd(self, env): - tmpl = env.from_string('''{{ 1 is odd }}|{{ 2 is odd }}''') - assert tmpl.render() == 'True|False' + tmpl = env.from_string("""{{ 1 is odd }}|{{ 2 is odd }}""") + assert tmpl.render() == "True|False" def test_lower(self, env): - tmpl = env.from_string('''{{ "foo" is lower }}|{{ "FOO" is lower }}''') - assert tmpl.render() == 'True|False' - - def test_typechecks(self, env): - tmpl = env.from_string(''' - {{ 42 is undefined }} - {{ 42 is defined }} - {{ 42 is none }} - {{ none is none }} - {{ 42 is number }} - {{ 42 is string }} - {{ "foo" is string }} - {{ "foo" is sequence }} - {{ [1] is sequence }} - {{ range is callable }} - {{ 42 is callable }} - {{ range(5) is iterable }} - {{ {} is mapping }} - {{ mydict is mapping }} - {{ [] is mapping }} - {{ 10 is number }} - {{ (10 ** 100) is number }} - {{ 3.14159 is number }} - {{ complex is number }} - ''') - - class MyDict(dict): - pass - - assert tmpl.render(mydict=MyDict(), complex=complex(1, 2)).split() == [ - 'False', 'True', 'False', 'True', 'True', 'False', - 'True', 'True', 'True', 'True', 'False', 'True', - 'True', 'True', 'False', 'True', 'True', 'True', 'True' - ] - - def test_sequence(self, env): - tmpl = env.from_string( - '{{ [1, 2, 3] is sequence }}|' - '{{ "foo" is sequence }}|' - '{{ 42 is sequence }}' - ) - assert tmpl.render() == 'True|True|False' + tmpl = env.from_string("""{{ "foo" is lower }}|{{ "FOO" is lower }}""") + assert tmpl.render() == "True|False" + + # Test type checks + @pytest.mark.parametrize( + "op,expect", + ( + ("none is none", True), + ("false is none", False), + ("true is none", False), + ("42 is none", False), + ("none is true", False), + ("false is true", False), + ("true is true", True), + ("0 is true", False), + ("1 is true", False), + ("42 is true", False), + ("none is false", False), + ("false is false", True), + ("true is false", False), + ("0 is false", False), + ("1 is false", False), + ("42 is false", False), + ("none is boolean", False), + ("false is boolean", True), + ("true is boolean", True), + ("0 is boolean", False), + ("1 is boolean", False), + ("42 is boolean", False), + ("0.0 is boolean", False), + ("1.0 is boolean", False), + ("3.14159 is boolean", False), + ("none is integer", False), + ("false is integer", False), + ("true is integer", False), + ("42 is integer", True), + ("3.14159 is integer", False), + ("(10 ** 100) is integer", True), + ("none is float", False), + ("false is float", False), + ("true is float", False), + ("42 is float", False), + ("4.2 is float", True), + ("(10 ** 100) is float", False), + ("none is number", False), + ("false is number", True), + ("true is number", True), + ("42 is number", True), + ("3.14159 is number", True), + ("complex is number", True), + ("(10 ** 100) is number", True), + ("none is string", False), + ("false is string", False), + ("true is string", False), + ("42 is string", False), + ('"foo" is string', True), + ("none is sequence", False), + ("false is sequence", False), + ("42 is sequence", False), + ('"foo" is sequence', True), + ("[] is sequence", True), + ("[1, 2, 3] is sequence", True), + ("{} is sequence", True), + ("none is mapping", False), + ("false is mapping", False), + ("42 is mapping", False), + ('"foo" is mapping', False), + ("[] is mapping", False), + ("{} is mapping", True), + ("mydict is mapping", True), + ("none is iterable", False), + ("false is iterable", False), + ("42 is iterable", False), + ('"foo" is iterable', True), + ("[] is iterable", True), + ("{} is iterable", True), + ("range(5) is iterable", True), + ("none is callable", False), + ("false is callable", False), + ("42 is callable", False), + ('"foo" is callable', False), + ("[] is callable", False), + ("{} is callable", False), + ("range is callable", True), + ), + ) + def test_types(self, env, op, expect): + t = env.from_string("{{{{ {op} }}}}".format(op=op)) + assert t.render(mydict=MyDict(), complex=complex(1, 2)) == str(expect) def test_upper(self, env): tmpl = env.from_string('{{ "FOO" is upper }}|{{ "foo" is upper }}') - assert tmpl.render() == 'True|False' + assert tmpl.render() == "True|False" def test_equalto(self, env): tmpl = env.from_string( - '{{ foo is eq 12 }}|' - '{{ foo is eq 0 }}|' - '{{ foo is eq (3 * 4) }}|' + "{{ foo is eq 12 }}|" + "{{ foo is eq 0 }}|" + "{{ foo is eq (3 * 4) }}|" '{{ bar is eq "baz" }}|' '{{ bar is eq "zab" }}|' '{{ bar is eq ("ba" + "z") }}|' - '{{ bar is eq bar }}|' - '{{ bar is eq foo }}' + "{{ bar is eq bar }}|" + "{{ bar is eq foo }}" ) - assert tmpl.render(foo=12, bar="baz") \ - == 'True|False|True|True|False|True|True|False' - - @pytest.mark.parametrize('op,expect', ( - ('eq 2', True), - ('eq 3', False), - ('ne 3', True), - ('ne 2', False), - ('lt 3', True), - ('lt 2', False), - ('le 2', True), - ('le 1', False), - ('gt 1', True), - ('gt 2', False), - ('ge 2', True), - ('ge 3', False), - )) + assert ( + tmpl.render(foo=12, bar="baz") + == "True|False|True|True|False|True|True|False" + ) + + @pytest.mark.parametrize( + "op,expect", + ( + ("eq 2", True), + ("eq 3", False), + ("ne 3", True), + ("ne 2", False), + ("lt 3", True), + ("lt 2", False), + ("le 2", True), + ("le 1", False), + ("gt 1", True), + ("gt 2", False), + ("ge 2", True), + ("ge 3", False), + ), + ) def test_compare_aliases(self, env, op, expect): - t = env.from_string('{{{{ 2 is {op} }}}}'.format(op=op)) + t = env.from_string("{{{{ 2 is {op} }}}}".format(op=op)) assert t.render() == str(expect) def test_sameas(self, env): - tmpl = env.from_string('{{ foo is sameas false }}|' - '{{ 0 is sameas false }}') - assert tmpl.render(foo=False) == 'True|False' + tmpl = env.from_string("{{ foo is sameas false }}|{{ 0 is sameas false }}") + assert tmpl.render(foo=False) == "True|False" def test_no_paren_for_arg1(self, env): - tmpl = env.from_string('{{ foo is sameas none }}') - assert tmpl.render(foo=None) == 'True' + tmpl = env.from_string("{{ foo is sameas none }}") + assert tmpl.render(foo=None) == "True" def test_escaped(self, env): env = Environment(autoescape=True) - tmpl = env.from_string('{{ x is escaped }}|{{ y is escaped }}') - assert tmpl.render(x='foo', y=Markup('foo')) == 'False|True' + tmpl = env.from_string("{{ x is escaped }}|{{ y is escaped }}") + assert tmpl.render(x="foo", y=Markup("foo")) == "False|True" def test_greaterthan(self, env): - tmpl = env.from_string('{{ 1 is greaterthan 0 }}|' - '{{ 0 is greaterthan 1 }}') - assert tmpl.render() == 'True|False' + tmpl = env.from_string("{{ 1 is greaterthan 0 }}|{{ 0 is greaterthan 1 }}") + assert tmpl.render() == "True|False" def test_lessthan(self, env): - tmpl = env.from_string('{{ 0 is lessthan 1 }}|' - '{{ 1 is lessthan 0 }}') - assert tmpl.render() == 'True|False' + tmpl = env.from_string("{{ 0 is lessthan 1 }}|{{ 1 is lessthan 0 }}") + assert tmpl.render() == "True|False" def test_multiple_tests(self): items = [] + def matching(x, y): items.append((x, y)) return False + env = Environment() - env.tests['matching'] = matching - tmpl = env.from_string("{{ 'us-west-1' is matching " - "'(us-east-1|ap-northeast-1)' " - "or 'stage' is matching '(dev|stage)' }}") - assert tmpl.render() == 'False' - assert items == [('us-west-1', '(us-east-1|ap-northeast-1)'), - ('stage', '(dev|stage)')] + env.tests["matching"] = matching + tmpl = env.from_string( + "{{ 'us-west-1' is matching '(us-east-1|ap-northeast-1)'" + " or 'stage' is matching '(dev|stage)' }}" + ) + assert tmpl.render() == "False" + assert items == [ + ("us-west-1", "(us-east-1|ap-northeast-1)"), + ("stage", "(dev|stage)"), + ] def test_in(self, env): - tmpl = env.from_string('{{ "o" is in "foo" }}|' - '{{ "foo" is in "foo" }}|' - '{{ "b" is in "foo" }}|' - '{{ 1 is in ((1, 2)) }}|' - '{{ 3 is in ((1, 2)) }}|' - '{{ 1 is in [1, 2] }}|' - '{{ 3 is in [1, 2] }}|' - '{{ "foo" is in {"foo": 1}}}|' - '{{ "baz" is in {"bar": 1}}}') - assert tmpl.render() \ - == 'True|True|False|True|False|True|False|True|False' + tmpl = env.from_string( + '{{ "o" is in "foo" }}|' + '{{ "foo" is in "foo" }}|' + '{{ "b" is in "foo" }}|' + "{{ 1 is in ((1, 2)) }}|" + "{{ 3 is in ((1, 2)) }}|" + "{{ 1 is in [1, 2] }}|" + "{{ 3 is in [1, 2] }}|" + '{{ "foo" is in {"foo": 1}}}|' + '{{ "baz" is in {"bar": 1}}}' + ) + assert tmpl.render() == "True|True|False|True|False|True|False|True|False" diff --git a/tests/test_utils.py b/tests/test_utils.py index f71c608..b379a97 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,27 +1,24 @@ # -*- coding: utf-8 -*- -""" - jinja2.testsuite.utils - ~~~~~~~~~~~~~~~~~~~~~~ - - Tests utilities jinja uses. - - :copyright: (c) 2017 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import gc +import pickle +import random +from collections import deque +from copy import copy as shallow_copy import pytest +from markupsafe import Markup -import pickle - -from jinja2.utils import LRUCache, escape, object_type_repr, urlize, \ - select_autoescape +from jinja2._compat import range_type +from jinja2._compat import string_types +from jinja2.utils import consume +from jinja2.utils import generate_lorem_ipsum +from jinja2.utils import LRUCache +from jinja2.utils import missing +from jinja2.utils import object_type_repr +from jinja2.utils import select_autoescape +from jinja2.utils import urlize -@pytest.mark.utils -@pytest.mark.lrucache class TestLRUCache(object): - def test_simple(self): d = LRUCache(3) d["a"] = 1 @@ -30,7 +27,28 @@ class TestLRUCache(object): d["a"] d["d"] = 4 assert len(d) == 3 - assert 'a' in d and 'c' in d and 'd' in d and 'b' not in d + assert "a" in d and "c" in d and "d" in d and "b" not in d + + def test_itervalue_deprecated(self): + cache = LRUCache(3) + cache["a"] = 1 + cache["b"] = 2 + with pytest.deprecated_call(): + cache.itervalue() + + def test_itervalues(self): + cache = LRUCache(3) + cache["b"] = 1 + cache["a"] = 2 + values = [v for v in cache.values()] + assert len(values) == 2 + assert 1 in values + assert 2 in values + + def test_itervalues_empty(self): + cache = LRUCache(2) + values = [v for v in cache.values()] + assert len(values) == 0 def test_pickleable(self): cache = LRUCache(2) @@ -44,43 +62,139 @@ class TestLRUCache(object): assert copy._mapping == cache._mapping assert copy._queue == cache._queue + @pytest.mark.parametrize("copy_func", [LRUCache.copy, shallow_copy]) + def test_copy(self, copy_func): + cache = LRUCache(2) + cache["a"] = 1 + cache["b"] = 2 + copy = copy_func(cache) + assert copy._queue == cache._queue + copy["c"] = 3 + assert copy._queue != cache._queue + assert "a" not in copy and "b" in copy and "c" in copy + + def test_clear(self): + d = LRUCache(3) + d["a"] = 1 + d["b"] = 2 + d["c"] = 3 + d.clear() + assert d.__getstate__() == {"capacity": 3, "_mapping": {}, "_queue": deque([])} -@pytest.mark.utils -@pytest.mark.helpers -class TestHelpers(object): + def test_repr(self): + d = LRUCache(3) + d["a"] = 1 + d["b"] = 2 + d["c"] = 3 + # Sort the strings - mapping is unordered + assert sorted(repr(d)) == sorted(u"<LRUCache {'a': 1, 'b': 2, 'c': 3}>") + def test_items(self): + """Test various items, keys, values and iterators of LRUCache.""" + d = LRUCache(3) + d["a"] = 1 + d["b"] = 2 + d["c"] = 3 + assert d.items() == [("c", 3), ("b", 2), ("a", 1)] + assert d.keys() == ["c", "b", "a"] + assert d.values() == [3, 2, 1] + assert list(reversed(d)) == ["a", "b", "c"] + + # Change the cache a little + d["b"] + d["a"] = 4 + assert d.items() == [("a", 4), ("b", 2), ("c", 3)] + assert d.keys() == ["a", "b", "c"] + assert d.values() == [4, 2, 3] + assert list(reversed(d)) == ["c", "b", "a"] + + def test_setdefault(self): + d = LRUCache(3) + assert len(d) == 0 + assert d.setdefault("a") is None + assert d.setdefault("a", 1) is None + assert len(d) == 1 + assert d.setdefault("b", 2) == 2 + assert len(d) == 2 + + +class TestHelpers(object): def test_object_type_repr(self): class X(object): pass - assert object_type_repr(42) == 'int object' - assert object_type_repr([]) == 'list object' - assert object_type_repr(X()) == 'test_utils.X object' - assert object_type_repr(None) == 'None' - assert object_type_repr(Ellipsis) == 'Ellipsis' + + assert object_type_repr(42) == "int object" + assert object_type_repr([]) == "list object" + assert object_type_repr(X()) == "test_utils.X object" + assert object_type_repr(None) == "None" + assert object_type_repr(Ellipsis) == "Ellipsis" def test_autoescape_select(self): func = select_autoescape( - enabled_extensions=('html', '.htm'), - disabled_extensions=('txt',), - default_for_string='STRING', - default='NONE', + enabled_extensions=("html", ".htm"), + disabled_extensions=("txt",), + default_for_string="STRING", + default="NONE", ) - assert func(None) == 'STRING' - assert func('unknown.foo') == 'NONE' - assert func('foo.html') == True - assert func('foo.htm') == True - assert func('foo.txt') == False - assert func('FOO.HTML') == True - assert func('FOO.TXT') == False + assert func(None) == "STRING" + assert func("unknown.foo") == "NONE" + assert func("foo.html") + assert func("foo.htm") + assert not func("foo.txt") + assert func("FOO.HTML") + assert not func("FOO.TXT") -@pytest.mark.utils -@pytest.mark.escapeUrlizeTarget class TestEscapeUrlizeTarget(object): def test_escape_urlize_target(self): url = "http://example.org" target = "<script>" - assert urlize(url, target=target) == ('<a href="http://example.org"' - ' target="<script>">' - 'http://example.org</a>') + assert urlize(url, target=target) == ( + '<a href="http://example.org"' + ' target="<script>">' + "http://example.org</a>" + ) + + +class TestLoremIpsum(object): + def test_lorem_ipsum_markup(self): + """Test that output of lorem_ipsum is Markup by default.""" + assert isinstance(generate_lorem_ipsum(), Markup) + + def test_lorem_ipsum_html(self): + """Test that output of lorem_ipsum is a string_type when not html.""" + assert isinstance(generate_lorem_ipsum(html=False), string_types) + + def test_lorem_ipsum_n(self): + """Test that the n (number of lines) works as expected.""" + assert generate_lorem_ipsum(n=0, html=False) == u"" + for n in range_type(1, 50): + assert generate_lorem_ipsum(n=n, html=False).count("\n") == (n - 1) * 2 + + def test_lorem_ipsum_min(self): + """Test that at least min words are in the output of each line""" + for _ in range_type(5): + m = random.randrange(20, 99) + for _ in range_type(10): + assert generate_lorem_ipsum(n=1, min=m, html=False).count(" ") >= m - 1 + + def test_lorem_ipsum_max(self): + """Test that at least max words are in the output of each line""" + for _ in range_type(5): + m = random.randrange(21, 100) + for _ in range_type(10): + assert generate_lorem_ipsum(n=1, max=m, html=False).count(" ") < m - 1 + + +def test_missing(): + """Test the repr of missing.""" + assert repr(missing) == u"missing" + + +def test_consume(): + """Test that consume consumes an iterator.""" + x = iter([1, 2, 3, 4, 5]) + consume(x) + with pytest.raises(StopIteration): + next(x) @@ -1,36 +1,20 @@ [tox] envlist = - py{37,36,35,27,py3,py} - docs-html - coverage + py{38,37,36,35,27,py3,py} + style + docs skip_missing_interpreters = true [testenv] deps = - coverage pytest -commands = coverage run -p -m pytest --tb=short -Werror --basetemp={envtmpdir} {posargs} +commands = pytest --tb=short --basetemp={envtmpdir} {posargs} -[testenv:docs-html] -deps = - Sphinx - Pallets-Sphinx-Themes - sphinxcontrib-log-cabinet - sphinx-issues -commands = sphinx-build -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html - -[testenv:coverage] -deps = coverage +[testenv:style] +deps = pre-commit skip_install = true -commands = - coverage combine - coverage html - coverage report +commands = pre-commit run --all-files --show-diff-on-failure -[testenv:coverage-ci] -deps = coverage -skip_install = true -commands = - coverage combine - # Ignoring errors because 2.7.15 and 3.5.5 on Azure can't parse async files. - coverage xml --ignore-errors +[testenv:docs] +deps = -r docs/requirements.txt +commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html |