mirror of
https://github.com/openharmony/third_party_jinja2.git
synced 2026-07-01 10:05:25 -04:00
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,19 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
-922
@@ -1,922 +0,0 @@
|
||||
API
|
||||
===
|
||||
|
||||
.. module:: jinja2
|
||||
:noindex:
|
||||
:synopsis: public Jinja API
|
||||
|
||||
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
|
||||
------
|
||||
|
||||
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
|
||||
:class:`Template` class, an environment is created automatically for you,
|
||||
albeit a shared one.
|
||||
|
||||
Most applications will create one :class:`Environment` object on application
|
||||
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 Jinja to load templates for your
|
||||
application is to use :class:`~loaders.PackageLoader`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
env = Environment(
|
||||
loader=PackageLoader("yourapp"),
|
||||
autoescape=select_autoescape()
|
||||
)
|
||||
|
||||
This will create a template environment with a loader that looks up
|
||||
templates in the ``templates`` folder inside the ``yourapp`` Python
|
||||
package (or next to the ``yourapp.py`` Python module). It also enables
|
||||
autoescaping for HTML files. This loader only requires that ``yourapp``
|
||||
is importable, it figures out the absolute path to the folder for you.
|
||||
|
||||
Different loaders are available to load templates in other ways or from
|
||||
other locations. They're listed in the `Loaders`_ section below. You can
|
||||
also write your own if you want to load templates from a source that's
|
||||
more specialized to your project.
|
||||
|
||||
To load a template from this environment, call the :meth:`get_template`
|
||||
method, which returns the loaded :class:`Template`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
template = env.get_template("mytemplate.html")
|
||||
|
||||
To render it with some variables, call the :meth:`render` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(template.render(the="variables", go="here"))
|
||||
|
||||
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.
|
||||
|
||||
|
||||
High Level API
|
||||
--------------
|
||||
|
||||
The high-level API is the API you will use in the application to load and
|
||||
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])
|
||||
:members: from_string, get_template, select_template,
|
||||
get_or_select_template, join_path, extend, compile_expression,
|
||||
compile_templates, list_templates, add_extension
|
||||
|
||||
.. attribute:: shared
|
||||
|
||||
If a template was created by using the :class:`Template` constructor
|
||||
an environment is created automatically. These environments are
|
||||
created as shared environments which means that multiple templates
|
||||
may have the same anonymous environment. For all shared environments
|
||||
this attribute is `True`, else `False`.
|
||||
|
||||
.. attribute:: sandboxed
|
||||
|
||||
If the environment is sandboxed this attribute is `True`. For the
|
||||
sandbox mode have a look at the documentation for the
|
||||
:class:`~jinja2.sandbox.SandboxedEnvironment`.
|
||||
|
||||
.. attribute:: filters
|
||||
|
||||
A dict of filters for this environment. As long as no template was
|
||||
loaded it's safe to add new filters or remove old. For custom filters
|
||||
see :ref:`writing-filters`. For valid filter names have a look at
|
||||
:ref:`identifier-naming`.
|
||||
|
||||
.. attribute:: tests
|
||||
|
||||
A dict of test functions for this environment. As long as no
|
||||
template was loaded it's safe to modify this dict. For custom tests
|
||||
see :ref:`writing-tests`. For valid test names have a look at
|
||||
:ref:`identifier-naming`.
|
||||
|
||||
.. attribute:: globals
|
||||
|
||||
A dict of variables that are available in every template loaded
|
||||
by the environment. As long as no template was loaded it's safe
|
||||
to modify this. For more details see :ref:`global-namespace`.
|
||||
For valid object names see :ref:`identifier-naming`.
|
||||
|
||||
.. attribute:: policies
|
||||
|
||||
A dictionary with :ref:`policies`. These can be reconfigured to
|
||||
change the runtime behavior or certain template features. Usually
|
||||
these are security related.
|
||||
|
||||
.. attribute:: code_generator_class
|
||||
|
||||
The class used for code generation. This should not be changed
|
||||
in most cases, unless you need to modify the Python code a
|
||||
template compiles to.
|
||||
|
||||
.. attribute:: context_class
|
||||
|
||||
The context used for templates. This should not be changed
|
||||
in most cases, unless you need to modify internals of how
|
||||
template variables are handled. For details, see
|
||||
:class:`~jinja2.runtime.Context`.
|
||||
|
||||
.. automethod:: overlay([options])
|
||||
|
||||
.. method:: undefined([hint, obj, name, exc])
|
||||
|
||||
Creates a new :class:`Undefined` object for `name`. This is useful
|
||||
for filters or functions that may return undefined objects for
|
||||
some operations. All parameters except of `hint` should be provided
|
||||
as keyword parameters for better readability. The `hint` is used as
|
||||
error message for the exception if provided, otherwise the error
|
||||
message will be generated from `obj` and `name` automatically. The exception
|
||||
provided as `exc` is raised if something with the generated undefined
|
||||
object is done that the undefined object does not allow. The default
|
||||
exception is :exc:`UndefinedError`. If a `hint` is provided the
|
||||
`name` may be omitted.
|
||||
|
||||
The most common way to create an undefined object is by providing
|
||||
a name only::
|
||||
|
||||
return environment.undefined(name='some_name')
|
||||
|
||||
This means that the name `some_name` is not defined. If the name
|
||||
was from an attribute of an object it makes sense to tell the
|
||||
undefined object the holder object to improve the error message::
|
||||
|
||||
if not hasattr(obj, 'attr'):
|
||||
return environment.undefined(obj=obj, name='attr')
|
||||
|
||||
For a more complex example you can provide a hint. For example
|
||||
the :func:`first` filter creates an undefined object that way::
|
||||
|
||||
return environment.undefined('no first item, sequence was empty')
|
||||
|
||||
If it the `name` or `obj` is known (for example because an attribute
|
||||
was accessed) it should be passed to the undefined object, even if
|
||||
a custom `hint` is provided. This gives undefined objects the
|
||||
possibility to enhance the error message.
|
||||
|
||||
.. autoclass:: Template
|
||||
:members: module, make_module
|
||||
|
||||
.. attribute:: globals
|
||||
|
||||
A dict of variables that are available every time the template
|
||||
is rendered, without needing to pass them during render. This
|
||||
should not be modified, as depending on how the template was
|
||||
loaded it may be shared with the environment and other
|
||||
templates.
|
||||
|
||||
Defaults to :attr:`Environment.globals` unless extra values are
|
||||
passed to :meth:`Environment.get_template`.
|
||||
|
||||
Globals are only intended for data that is common to every
|
||||
render of the template. Specific data should be passed to
|
||||
:meth:`render`.
|
||||
|
||||
See :ref:`global-namespace`.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The loading name of the template. If the template was loaded from a
|
||||
string this is `None`.
|
||||
|
||||
.. attribute:: filename
|
||||
|
||||
The filename of the template on the file system if it was loaded from
|
||||
there. Otherwise this is `None`.
|
||||
|
||||
.. automethod:: render([context])
|
||||
|
||||
.. automethod:: generate([context])
|
||||
|
||||
.. automethod:: stream([context])
|
||||
|
||||
.. automethod:: render_async([context])
|
||||
|
||||
.. automethod:: generate_async([context])
|
||||
|
||||
|
||||
.. autoclass:: jinja2.environment.TemplateStream()
|
||||
:members: disable_buffering, enable_buffering, dump
|
||||
|
||||
|
||||
Autoescaping
|
||||
------------
|
||||
|
||||
.. 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
|
||||
|
||||
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::
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
env = Environment(autoescape=select_autoescape(['html', 'htm', 'xml']),
|
||||
loader=PackageLoader('mypackage'))
|
||||
|
||||
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:
|
||||
|
||||
Notes on Identifiers
|
||||
--------------------
|
||||
|
||||
Jinja uses Python naming rules. Valid identifiers can be any combination
|
||||
of 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
|
||||
filters and tests by topic. For example it's perfectly valid to add a
|
||||
function into the filter dict and call it `to.str`. The regular
|
||||
expression for filter and test identifiers is
|
||||
``[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*``.
|
||||
|
||||
|
||||
Undefined Types
|
||||
---------------
|
||||
|
||||
These classes can be used as undefined types. The :class:`Environment`
|
||||
constructor takes an `undefined` parameter that can be one of those classes
|
||||
or a custom subclass of :class:`Undefined`. Whenever the template engine is
|
||||
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 :class:`StrictUndefined` which
|
||||
disallows all operations beside testing if it's an undefined object.
|
||||
|
||||
.. autoclass:: jinja2.Undefined()
|
||||
|
||||
.. attribute:: _undefined_hint
|
||||
|
||||
Either `None` or a string with the error message for the
|
||||
undefined object.
|
||||
|
||||
.. attribute:: _undefined_obj
|
||||
|
||||
Either `None` or the owner object that caused the undefined object
|
||||
to be created (for example because an attribute does not exist).
|
||||
|
||||
.. attribute:: _undefined_name
|
||||
|
||||
The name for the undefined variable / attribute or just `None`
|
||||
if no such information exists.
|
||||
|
||||
.. attribute:: _undefined_exception
|
||||
|
||||
The exception that the undefined object wants to raise. This
|
||||
is usually one of :exc:`UndefinedError` or :exc:`SecurityError`.
|
||||
|
||||
.. method:: _fail_with_undefined_error(\*args, \**kwargs)
|
||||
|
||||
When called with any arguments this method raises
|
||||
: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()
|
||||
|
||||
There is also a factory function that can decorate undefined objects to
|
||||
implement logging on failures:
|
||||
|
||||
.. autofunction:: jinja2.make_logging_undefined
|
||||
|
||||
Undefined objects are created by calling :attr:`undefined`.
|
||||
|
||||
.. admonition:: Implementation
|
||||
|
||||
:class:`Undefined` is implemented by overriding the special
|
||||
``__underscore__`` methods. For example the default
|
||||
:class:`Undefined` class implements ``__str__`` to returns an empty
|
||||
string, while ``__int__`` and others fail with an exception. To
|
||||
allow conversion to int by returning ``0`` you can implement your
|
||||
own subclass.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class NullUndefined(Undefined):
|
||||
def __int__(self):
|
||||
return 0
|
||||
|
||||
def __float__(self):
|
||||
return 0.0
|
||||
|
||||
To disallow a method, override it and raise
|
||||
:attr:`~Undefined._undefined_exception`. Because this is very
|
||||
common there is the helper method
|
||||
:meth:`~Undefined._fail_with_undefined_error` that raises the error
|
||||
with the correct information. Here's a class that works like the
|
||||
regular :class:`Undefined` but fails on iteration::
|
||||
|
||||
class NonIterableUndefined(Undefined):
|
||||
def __iter__(self):
|
||||
self._fail_with_undefined_error()
|
||||
|
||||
|
||||
The Context
|
||||
-----------
|
||||
|
||||
.. autoclass:: jinja2.runtime.Context()
|
||||
:members: get, resolve, resolve_or_missing, get_exported, get_all
|
||||
|
||||
.. attribute:: parent
|
||||
|
||||
A dict of read only, global variables the template looks up. These
|
||||
can either come from another :class:`Context`, from the
|
||||
:attr:`Environment.globals` or :attr:`Template.globals` or points
|
||||
to a dict created by combining the globals with the variables
|
||||
passed to the render function. It must not be altered.
|
||||
|
||||
.. attribute:: vars
|
||||
|
||||
The template local variables. This list contains environment and
|
||||
context functions from the :attr:`parent` scope as well as local
|
||||
modifications and exported variables from the template. The template
|
||||
will modify this dict during template evaluation but filters and
|
||||
context functions are not allowed to modify it.
|
||||
|
||||
.. attribute:: environment
|
||||
|
||||
The environment that loaded the template.
|
||||
|
||||
.. attribute:: exported_vars
|
||||
|
||||
This set contains all the names the template exports. The values for
|
||||
the names are in the :attr:`vars` dict. In order to get a copy of the
|
||||
exported variables as dict, :meth:`get_exported` can be used.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The load name of the template owning this context.
|
||||
|
||||
.. attribute:: blocks
|
||||
|
||||
A dict with the current mapping of blocks in the template. The keys
|
||||
in this dict are the names of the blocks, and the values a list of
|
||||
blocks registered. The last item in each list is the current active
|
||||
block (latest in the inheritance chain).
|
||||
|
||||
.. attribute:: eval_ctx
|
||||
|
||||
The current :ref:`eval-context`.
|
||||
|
||||
.. automethod:: jinja2.runtime.Context.call(callable, \*args, \**kwargs)
|
||||
|
||||
|
||||
The context is immutable, it prevents modifications, and if it is
|
||||
modified somehow despite that those changes may not show up. For
|
||||
performance, Jinja does not use the context as data storage for, only as
|
||||
a primary data source. Variables that the template does not define are
|
||||
looked up in the context, but variables the template does define are
|
||||
stored locally.
|
||||
|
||||
Instead of modifying the context directly, a function should return
|
||||
a value that can be assigned to a variable within the template itself.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% set comments = get_latest_comments() %}
|
||||
|
||||
|
||||
.. _loaders:
|
||||
|
||||
Loaders
|
||||
-------
|
||||
|
||||
Loaders are responsible for loading templates from a resource such as the
|
||||
file system. The environment will keep the compiled modules in memory like
|
||||
Python's `sys.modules`. Unlike `sys.modules` however this cache is limited in
|
||||
size by default and templates are automatically reloaded.
|
||||
All loaders are subclasses of :class:`BaseLoader`. If you want to create your
|
||||
own loader, subclass :class:`BaseLoader` and override `get_source`.
|
||||
|
||||
.. autoclass:: jinja2.BaseLoader
|
||||
:members: get_source, load
|
||||
|
||||
Here a list of the builtin loaders Jinja provides:
|
||||
|
||||
.. autoclass:: jinja2.FileSystemLoader
|
||||
|
||||
.. autoclass:: jinja2.PackageLoader
|
||||
|
||||
.. autoclass:: jinja2.DictLoader
|
||||
|
||||
.. autoclass:: jinja2.FunctionLoader
|
||||
|
||||
.. autoclass:: jinja2.PrefixLoader
|
||||
|
||||
.. autoclass:: jinja2.ChoiceLoader
|
||||
|
||||
.. autoclass:: jinja2.ModuleLoader
|
||||
|
||||
|
||||
.. _bytecode-cache:
|
||||
|
||||
Bytecode Cache
|
||||
--------------
|
||||
|
||||
Jinja 2.1 and higher support external bytecode caching. Bytecode caches make
|
||||
it possible to store the generated bytecode on the file system or a different
|
||||
location to avoid parsing the templates on first use.
|
||||
|
||||
This is especially useful if you have a web application that is initialized on
|
||||
the first request and Jinja compiles many templates at once which slows down
|
||||
the application.
|
||||
|
||||
To use a bytecode cache, instantiate it and pass it to the :class:`Environment`.
|
||||
|
||||
.. autoclass:: jinja2.BytecodeCache
|
||||
:members: load_bytecode, dump_bytecode, clear
|
||||
|
||||
.. autoclass:: jinja2.bccache.Bucket
|
||||
:members: write_bytecode, load_bytecode, bytecode_from_string,
|
||||
bytecode_to_string, reset
|
||||
|
||||
.. attribute:: environment
|
||||
|
||||
The :class:`Environment` that created the bucket.
|
||||
|
||||
.. attribute:: key
|
||||
|
||||
The unique cache key for this bucket
|
||||
|
||||
.. attribute:: code
|
||||
|
||||
The bytecode if it's loaded, otherwise `None`.
|
||||
|
||||
|
||||
Builtin bytecode caches:
|
||||
|
||||
.. autoclass:: jinja2.FileSystemBytecodeCache
|
||||
|
||||
.. autoclass:: jinja2.MemcachedBytecodeCache
|
||||
|
||||
|
||||
Async Support
|
||||
-------------
|
||||
|
||||
.. 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_running_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 adds 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. 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.
|
||||
|
||||
|
||||
.. _policies:
|
||||
|
||||
Policies
|
||||
--------
|
||||
|
||||
Starting with Jinja 2.9 policies can be configured on the environment
|
||||
which can slightly influence how filters and other template constructs
|
||||
behave. They can be configured with the
|
||||
:attr:`~jinja2.Environment.policies` attribute.
|
||||
|
||||
Example::
|
||||
|
||||
env.policies['urlize.rel'] = 'nofollow noopener'
|
||||
|
||||
``truncate.leeway``:
|
||||
Configures the leeway default for the `truncate` filter. Leeway as
|
||||
introduced in 2.9 but to restore compatibility with older templates
|
||||
it can be configured to `0` to get the old behavior back. The default
|
||||
is `5`.
|
||||
|
||||
``urlize.rel``:
|
||||
A string that defines the items for the `rel` attribute of generated
|
||||
links with the `urlize` filter. These items are always added. The
|
||||
default is `noopener`.
|
||||
|
||||
``urlize.target``:
|
||||
The default target that is issued for links from the `urlize` filter
|
||||
if no other target is defined by the call explicitly.
|
||||
|
||||
``urlize.extra_schemes``:
|
||||
Recognize URLs that start with these schemes in addition to the
|
||||
default ``http://``, ``https://``, and ``mailto:``.
|
||||
|
||||
``json.dumps_function``:
|
||||
If this is set to a value other than `None` then the `tojson` filter
|
||||
will dump with this function instead of the default one. Note that
|
||||
this function should accept arbitrary extra arguments which might be
|
||||
passed in the future from the filter. Currently the only argument
|
||||
that might be passed is `indent`. The default dump function is
|
||||
``json.dumps``.
|
||||
|
||||
``json.dumps_kwargs``:
|
||||
Keyword arguments to be passed to the dump function. The default is
|
||||
``{'sort_keys': True}``.
|
||||
|
||||
.. _ext-i18n-trimmed:
|
||||
|
||||
``ext.i18n.trimmed``:
|
||||
If this is set to `True`, ``{% trans %}`` blocks of the
|
||||
:ref:`i18n-extension` will always unify linebreaks and surrounding
|
||||
whitespace as if the `trimmed` modifier was used.
|
||||
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
These helper functions and classes are useful if you add custom filters or
|
||||
functions to a Jinja environment.
|
||||
|
||||
.. autofunction:: jinja2.pass_context
|
||||
|
||||
.. autofunction:: jinja2.pass_eval_context
|
||||
|
||||
.. autofunction:: jinja2.pass_environment
|
||||
|
||||
.. autofunction:: jinja2.clear_caches
|
||||
|
||||
.. autofunction:: jinja2.is_undefined
|
||||
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. autoexception:: jinja2.TemplateError
|
||||
|
||||
.. autoexception:: jinja2.UndefinedError
|
||||
|
||||
.. autoexception:: jinja2.TemplateNotFound
|
||||
|
||||
.. autoexception:: jinja2.TemplatesNotFound
|
||||
|
||||
.. autoexception:: jinja2.TemplateSyntaxError
|
||||
|
||||
.. attribute:: message
|
||||
|
||||
The error message.
|
||||
|
||||
.. attribute:: lineno
|
||||
|
||||
The line number where the error occurred.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The load name for the template.
|
||||
|
||||
.. attribute:: filename
|
||||
|
||||
The filename that loaded the template in the encoding of the
|
||||
file system (most likely utf-8, or mbcs on Windows systems).
|
||||
|
||||
.. autoexception:: jinja2.TemplateRuntimeError
|
||||
|
||||
.. autoexception:: jinja2.TemplateAssertionError
|
||||
|
||||
|
||||
.. _writing-filters:
|
||||
|
||||
Custom Filters
|
||||
--------------
|
||||
|
||||
Filters are Python functions that take the value to the left of the
|
||||
filter as the first argument and produce a new value. Arguments passed
|
||||
to the filter are passed after the value.
|
||||
|
||||
For example, the filter ``{{ 42|myfilter(23) }}`` is called behind the
|
||||
scenes as ``myfilter(42, 23)``.
|
||||
|
||||
Jinja comes with some :ref:`built-in filters <builtin-filters>`. To use
|
||||
a custom filter, write a function that takes at least a ``value``
|
||||
argument, then register it in :attr:`Environment.filters`.
|
||||
|
||||
Here's a filter that formats datetime objects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def datetime_format(value, format="%H:%M %d-%m-%y"):
|
||||
return value.strftime(format)
|
||||
|
||||
environment.filters["datetime_format"] = datetime_format
|
||||
|
||||
Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ article.pub_date|datetimeformat }}
|
||||
{{ article.pub_date|datetimeformat("%B %Y") }}
|
||||
|
||||
Some decorators are available to tell Jinja to pass extra information to
|
||||
the filter. The object is passed as the first argument, making the value
|
||||
being filtered the second argument.
|
||||
|
||||
- :func:`pass_environment` passes the :class:`Environment`.
|
||||
- :func:`pass_eval_context` passes the :ref:`eval-context`.
|
||||
- :func:`pass_context` passes the current
|
||||
:class:`~jinja2.runtime.Context`.
|
||||
|
||||
Here's a filter that converts line breaks into HTML ``<br>`` and ``<p>``
|
||||
tags. It uses the eval context to check if autoescape is currently
|
||||
enabled before escaping the input and marking the output safe.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import re
|
||||
from jinja2 import pass_eval_context
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
@pass_eval_context
|
||||
def nl2br(eval_ctx, value):
|
||||
br = "<br>\n"
|
||||
|
||||
if eval_ctx.autoescape:
|
||||
value = escape(value)
|
||||
br = Markup(br)
|
||||
|
||||
result = "\n\n".join(
|
||||
f"<p>{br.join(p.splitlines())}<\p>"
|
||||
for p in re.split(r"(?:\r\n|\r(?!\n)|\n){2,}", value)
|
||||
)
|
||||
return Markup(result) if autoescape else result
|
||||
|
||||
|
||||
.. _writing-tests:
|
||||
|
||||
Custom Tests
|
||||
------------
|
||||
|
||||
Test are Python functions that take the value to the left of the test as
|
||||
the first argument, and return ``True`` or ``False``. Arguments passed
|
||||
to the test are passed after the value.
|
||||
|
||||
For example, the test ``{{ 42 is even }}`` is called behind the scenes
|
||||
as ``is_even(42)``.
|
||||
|
||||
Jinja comes with some :ref:`built-in tests <builtin-tests>`. To use a
|
||||
custom tests, write a function that takes at least a ``value`` argument,
|
||||
then register it in :attr:`Environment.tests`.
|
||||
|
||||
Here's a test that checks if a value is a prime number:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import math
|
||||
|
||||
def is_prime(n):
|
||||
if n == 2:
|
||||
return True
|
||||
|
||||
for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
|
||||
if n % i == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
environment.tests["prime"] = is_prime
|
||||
|
||||
Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{% if value is prime %}
|
||||
{{ value }} is a prime number
|
||||
{% else %}
|
||||
{{ value }} is not a prime number
|
||||
{% endif %}
|
||||
|
||||
Some decorators are available to tell Jinja to pass extra information to
|
||||
the filter. The object is passed as the first argument, making the value
|
||||
being filtered the second argument.
|
||||
|
||||
- :func:`pass_environment` passes the :class:`Environment`.
|
||||
- :func:`pass_eval_context` passes the :ref:`eval-context`.
|
||||
- :func:`pass_context` passes the current
|
||||
:class:`~jinja2.runtime.Context`.
|
||||
|
||||
|
||||
.. _eval-context:
|
||||
|
||||
Evaluation Context
|
||||
------------------
|
||||
|
||||
The evaluation context (short eval context or eval ctx) makes it
|
||||
possible to activate and deactivate compiled features at runtime.
|
||||
|
||||
Currently it is only used to enable and disable automatic escaping, but
|
||||
it can be used by extensions as well.
|
||||
|
||||
The ``autoescape`` setting should be checked on the evaluation context,
|
||||
not the environment. The evaluation context will have the computed value
|
||||
for the current template.
|
||||
|
||||
Instead of ``pass_environment``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pass_environment
|
||||
def filter(env, value):
|
||||
result = do_something(value)
|
||||
|
||||
if env.autoescape:
|
||||
result = Markup(result)
|
||||
|
||||
return result
|
||||
|
||||
Use ``pass_eval_context`` if you only need the setting:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pass_eval_context
|
||||
def filter(eval_ctx, value):
|
||||
result = do_something(value)
|
||||
|
||||
if eval_ctx.autoescape:
|
||||
result = Markup(result)
|
||||
|
||||
return result
|
||||
|
||||
Or use ``pass_context`` if you need other context behavior as well:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pass_context
|
||||
def filter(context, value):
|
||||
result = do_something(value)
|
||||
|
||||
if context.eval_ctx.autoescape:
|
||||
result = Markup(result)
|
||||
|
||||
return result
|
||||
|
||||
The evaluation context must not be modified at runtime. Modifications
|
||||
must only happen with a :class:`nodes.EvalContextModifier` and
|
||||
:class:`nodes.ScopedEvalContextModifier` from an extension, not on the
|
||||
eval context object itself.
|
||||
|
||||
.. autoclass:: jinja2.nodes.EvalContext
|
||||
|
||||
.. attribute:: autoescape
|
||||
|
||||
`True` or `False` depending on if autoescaping is active or not.
|
||||
|
||||
.. attribute:: volatile
|
||||
|
||||
`True` if the compiler cannot evaluate some expressions at compile
|
||||
time. At runtime this should always be `False`.
|
||||
|
||||
|
||||
.. _global-namespace:
|
||||
|
||||
The Global Namespace
|
||||
--------------------
|
||||
|
||||
The global namespace stores variables and functions that should be
|
||||
available without needing to pass them to :meth:`Template.render`. They
|
||||
are also available to templates that are imported or included without
|
||||
context. Most applications should only use :attr:`Environment.globals`.
|
||||
|
||||
:attr:`Environment.globals` are intended for data that is common to all
|
||||
templates loaded by that environment. :attr:`Template.globals` are
|
||||
intended for data that is common to all renders of that template, and
|
||||
default to :attr:`Environment.globals` unless they're given in
|
||||
:meth:`Environment.get_template`, etc. Data that is specific to a
|
||||
render should be passed as context to :meth:`Template.render`.
|
||||
|
||||
Only one set of globals is used during any specific rendering. If
|
||||
templates A and B both have template globals, and B extends A, then
|
||||
only B's globals are used for both when using ``b.render()``.
|
||||
|
||||
Environment globals should not be changed after loading any templates,
|
||||
and template globals should not be changed at any time after loading the
|
||||
template. Changing globals after loading a template will result in
|
||||
unexpected behavior as they may be shared between the environment and
|
||||
other templates.
|
||||
|
||||
|
||||
.. _low-level-api:
|
||||
|
||||
Low Level API
|
||||
-------------
|
||||
|
||||
The low level API exposes functionality that can be useful to understand some
|
||||
implementation details, debugging purposes or advanced :ref:`extension
|
||||
<jinja-extensions>` techniques. Unless you know exactly what you are doing we
|
||||
don't recommend using any of those.
|
||||
|
||||
.. automethod:: Environment.lex
|
||||
|
||||
.. automethod:: Environment.parse
|
||||
|
||||
.. automethod:: Environment.preprocess
|
||||
|
||||
.. automethod:: Template.new_context
|
||||
|
||||
.. method:: Template.root_render_func(context)
|
||||
|
||||
This is the low level render function. It's passed a :class:`Context`
|
||||
that has to be created by :meth:`new_context` of the same template or
|
||||
a compatible template. This render function is generated by the
|
||||
compiler from the template code and returns a generator that yields
|
||||
strings.
|
||||
|
||||
If an exception in the template code happens the template engine will
|
||||
not rewrite the exception but pass through the original one. As a
|
||||
matter of fact this function should only be called from within a
|
||||
:meth:`render` / :meth:`generate` / :meth:`stream` call.
|
||||
|
||||
.. attribute:: Template.blocks
|
||||
|
||||
A dict of block render functions. Each of these functions works exactly
|
||||
like the :meth:`root_render_func` with the same limitations.
|
||||
|
||||
.. attribute:: Template.is_up_to_date
|
||||
|
||||
This attribute is `False` if there is a newer version of the template
|
||||
available, otherwise `True`.
|
||||
|
||||
.. admonition:: Note
|
||||
|
||||
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
|
||||
------------
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
The meta API returns some information about abstract syntax trees that
|
||||
could help applications to implement more advanced template concepts. All
|
||||
the functions of the meta API operate on an abstract syntax tree as
|
||||
returned by the :meth:`Environment.parse` method.
|
||||
|
||||
.. autofunction:: jinja2.meta.find_undeclared_variables
|
||||
|
||||
.. autofunction:: jinja2.meta.find_referenced_templates
|
||||
@@ -1,4 +0,0 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
.. include:: ../CHANGES.rst
|
||||
@@ -1,53 +0,0 @@
|
||||
from pallets_sphinx_themes import get_version
|
||||
from pallets_sphinx_themes import ProjectLink
|
||||
|
||||
# Project --------------------------------------------------------------
|
||||
|
||||
project = "Jinja"
|
||||
copyright = "2007 Pallets"
|
||||
author = "Pallets"
|
||||
release, version = get_version("Jinja2")
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
master_doc = "index"
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"pallets_sphinx_themes",
|
||||
"sphinxcontrib.log_cabinet",
|
||||
"sphinx_issues",
|
||||
]
|
||||
autodoc_typehints = "description"
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
|
||||
issues_github_path = "pallets/jinja"
|
||||
|
||||
# HTML -----------------------------------------------------------------
|
||||
|
||||
html_theme = "jinja"
|
||||
html_theme_options = {"index_sidebar_logo": False}
|
||||
html_context = {
|
||||
"project_links": [
|
||||
ProjectLink("Donate", "https://palletsprojects.com/donate"),
|
||||
ProjectLink("PyPI Releases", "https://pypi.org/project/Jinja2/"),
|
||||
ProjectLink("Source Code", "https://github.com/pallets/jinja/"),
|
||||
ProjectLink("Issue Tracker", "https://github.com/pallets/jinja/issues/"),
|
||||
ProjectLink("Website", "https://palletsprojects.com/p/jinja/"),
|
||||
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
|
||||
ProjectLink("Chat", "https://discord.gg/pallets"),
|
||||
]
|
||||
}
|
||||
html_sidebars = {
|
||||
"index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
|
||||
"**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
|
||||
}
|
||||
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
|
||||
html_static_path = ["_static"]
|
||||
html_favicon = "_static/jinja-logo-sidebar.png"
|
||||
html_logo = "_static/jinja-logo-sidebar.png"
|
||||
html_title = f"Jinja Documentation ({version})"
|
||||
html_show_sourcelink = False
|
||||
|
||||
# LaTeX ----------------------------------------------------------------
|
||||
|
||||
latex_documents = [(master_doc, f"Jinja-{version}.tex", html_title, author, "manual")]
|
||||
@@ -1,54 +0,0 @@
|
||||
from jinja2 import nodes
|
||||
from jinja2.ext import Extension
|
||||
|
||||
|
||||
class FragmentCacheExtension(Extension):
|
||||
# a set of names that trigger the extension.
|
||||
tags = {"cache"}
|
||||
|
||||
def __init__(self, environment):
|
||||
super().__init__(environment)
|
||||
|
||||
# add the defaults to the environment
|
||||
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
|
||||
# we only listen to ``'cache'`` so this will be a name token with
|
||||
# `cache` as value. We get the line number so that we can give
|
||||
# that line number to the nodes we create by hand.
|
||||
lineno = next(parser.stream).lineno
|
||||
|
||||
# now we parse a single expression that is used as cache key.
|
||||
args = [parser.parse_expression()]
|
||||
|
||||
# if there is a comma, the user provided a timeout. If not use
|
||||
# None as second parameter.
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
def _cache_support(self, name, timeout, caller):
|
||||
"""Helper callback."""
|
||||
key = self.environment.fragment_cache_prefix + name
|
||||
|
||||
# try to load the block from the cache
|
||||
# if there is no fragment in the cache, render it and store
|
||||
# it in the cache.
|
||||
rv = self.environment.fragment_cache.get(key)
|
||||
if rv is not None:
|
||||
return rv
|
||||
rv = caller()
|
||||
self.environment.fragment_cache.add(key, rv, timeout)
|
||||
return rv
|
||||
@@ -1,72 +0,0 @@
|
||||
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 True:
|
||||
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 += -1 if gtok == ")" else 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,
|
||||
)
|
||||
@@ -1,423 +0,0 @@
|
||||
.. _jinja-extensions:
|
||||
|
||||
Extensions
|
||||
==========
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Adding Extensions
|
||||
-----------------
|
||||
|
||||
Extensions are added to the Jinja environment at creation time. To add an
|
||||
extension pass a list of extension classes or import paths to the
|
||||
``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'])
|
||||
|
||||
To add extensions after creation time, use the :meth:`~jinja2.Environment.add_extension` method::
|
||||
|
||||
jinja_env.add_extension('jinja2.ext.debug')
|
||||
|
||||
|
||||
.. _i18n-extension:
|
||||
|
||||
i18n Extension
|
||||
--------------
|
||||
|
||||
**Import name:** ``jinja2.ext.i18n``
|
||||
|
||||
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, an application has to provide functions for ``gettext``,
|
||||
``ngettext``, and optionally ``pgettext`` and ``npgettext``, either
|
||||
globally or when rendering. A ``_()`` function is added as an alias to
|
||||
the ``gettext`` function.
|
||||
|
||||
|
||||
Environment Methods
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
After enabling the extension, the environment provides the following
|
||||
additional methods:
|
||||
|
||||
.. method:: jinja2.Environment.install_gettext_translations(translations, newstyle=False)
|
||||
|
||||
Installs a translation globally for the environment. The
|
||||
``translations`` object must implement ``gettext``, ``ngettext``,
|
||||
and optionally ``pgettext`` and ``npgettext``.
|
||||
:class:`gettext.NullTranslations`, :class:`gettext.GNUTranslations`,
|
||||
and `Babel`_\s ``Translations`` are supported.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Added ``pgettext`` and ``npgettext``.
|
||||
|
||||
.. versionchanged:: 2.5
|
||||
Added new-style gettext support.
|
||||
|
||||
.. method:: jinja2.Environment.install_null_translations(newstyle=False)
|
||||
|
||||
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 Added new-style gettext support.
|
||||
|
||||
.. method:: jinja2.Environment.install_gettext_callables(gettext, ngettext, newstyle=False, pgettext=None, npgettext=None)
|
||||
|
||||
Install the given ``gettext``, ``ngettext``, ``pgettext``, and
|
||||
``npgettext`` callables into the environment. They should behave
|
||||
exactly like :func:`gettext.gettext`, :func:`gettext.ngettext`,
|
||||
:func:`gettext.pgettext` and :func:`gettext.npgettext`.
|
||||
|
||||
If ``newstyle`` is activated, the callables are wrapped to work like
|
||||
newstyle callables. See :ref:`newstyle-gettext` for more information.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Added ``pgettext`` and ``npgettext``.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
Added new-style gettext support.
|
||||
|
||||
.. method:: jinja2.Environment.uninstall_gettext_translations()
|
||||
|
||||
Uninstall the environment's globally installed translation.
|
||||
|
||||
.. method:: jinja2.Environment.extract_translations(source)
|
||||
|
||||
Extract localizable strings from the given template node or source.
|
||||
|
||||
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).
|
||||
- ``message`` is the string itself, or a tuple of strings for
|
||||
functions with multiple arguments.
|
||||
|
||||
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, 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.install_gettext_translations(translations)
|
||||
|
||||
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 in
|
||||
:ref:`the template documentation <i18n-in-templates>`.
|
||||
|
||||
.. _gettext: https://docs.python.org/3/library/gettext.html
|
||||
.. _Babel: https://babel.pocoo.org/
|
||||
|
||||
|
||||
Whitespace Trimming
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.10
|
||||
|
||||
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:
|
||||
|
||||
New Style Gettext
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
New style gettext calls are less to type, less error prone, and support
|
||||
autoescaping better.
|
||||
|
||||
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.
|
||||
|
||||
With standard ``gettext`` calls, string formatting is a separate step
|
||||
done with the ``|format`` filter. This requires duplicating work for
|
||||
``ngettext`` calls.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ gettext("Hello, World!") }}
|
||||
{{ gettext("Hello, %(name)s!")|format(name=name) }}
|
||||
{{ ngettext(
|
||||
"%(num)d apple", "%(num)d apples", apples|count
|
||||
)|format(num=apples|count) }}
|
||||
{{ pgettext("greeting", "Hello, World!") }}
|
||||
{{ npgettext(
|
||||
"fruit", "%(num)d apple", "%(num)d apples", apples|count
|
||||
)|format(num=apples|count) }}
|
||||
|
||||
New style ``gettext`` make formatting part of the call, and behind the
|
||||
scenes enforce more consistency.
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ gettext("Hello, World!") }}
|
||||
{{ gettext("Hello, %(name)s!", name=name) }}
|
||||
{{ ngettext("%(num)d apple", "%(num)d apples", apples|count) }}
|
||||
{{ pgettext("greeting", "Hello, World!") }}
|
||||
{{ npgettext("fruit", "%(num)d apple", "%(num)d apples", apples|count) }}
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Expression Statement
|
||||
--------------------
|
||||
|
||||
**Import name:** ``jinja2.ext.do``
|
||||
|
||||
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.
|
||||
|
||||
.. _loopcontrols-extension:
|
||||
|
||||
Loop Controls
|
||||
-------------
|
||||
|
||||
**Import name:** ``jinja2.ext.loopcontrols``
|
||||
|
||||
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:
|
||||
|
||||
With Statement
|
||||
--------------
|
||||
|
||||
**Import name:** ``jinja2.ext.with_``
|
||||
|
||||
.. versionchanged:: 2.9
|
||||
|
||||
This extension is now built-in and no longer does anything.
|
||||
|
||||
.. _autoescape-extension:
|
||||
|
||||
Autoescape Extension
|
||||
--------------------
|
||||
|
||||
**Import name:** ``jinja2.ext.autoescape``
|
||||
|
||||
.. versionchanged:: 2.9
|
||||
|
||||
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:
|
||||
|
||||
Writing Extensions
|
||||
------------------
|
||||
|
||||
.. module:: jinja2.ext
|
||||
|
||||
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
|
||||
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 Extensions
|
||||
------------------
|
||||
|
||||
Cache
|
||||
~~~~~
|
||||
|
||||
The following example implements a ``cache`` tag for Jinja by using the
|
||||
`cachelib`_ library:
|
||||
|
||||
.. literalinclude:: examples/cache_extension.py
|
||||
:language: python
|
||||
|
||||
And here is how you use it in an environment::
|
||||
|
||||
from jinja2 import Environment
|
||||
from cachelib import SimpleCache
|
||||
|
||||
env = Environment(extensions=[FragmentCacheExtension])
|
||||
env.fragment_cache = SimpleCache()
|
||||
|
||||
Inside the template it's then possible to mark blocks as cacheable. The
|
||||
following example caches a sidebar for 300 seconds:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
{% cache 'sidebar', 300 %}
|
||||
<div class="sidebar">
|
||||
...
|
||||
</div>
|
||||
{% endcache %}
|
||||
|
||||
.. _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:
|
||||
|
||||
.. autoclass:: Extension
|
||||
:members: preprocess, filter_stream, parse, attr, call_method
|
||||
|
||||
.. attribute:: identifier
|
||||
|
||||
The identifier of the extension. This is always the true import name
|
||||
of the extension class and must not be changed.
|
||||
|
||||
.. attribute:: tags
|
||||
|
||||
If the extension implements custom tags this is a set of tag names
|
||||
the extension is listening for.
|
||||
|
||||
|
||||
Parser
|
||||
~~~~~~
|
||||
|
||||
The parser passed to :meth:`Extension.parse` provides ways to parse
|
||||
expressions of different types. The following methods may be used by
|
||||
extensions:
|
||||
|
||||
.. autoclass:: jinja2.parser.Parser
|
||||
:members: parse_expression, parse_tuple, parse_assign_target,
|
||||
parse_statements, free_identifier, fail
|
||||
|
||||
.. attribute:: filename
|
||||
|
||||
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``.
|
||||
|
||||
.. attribute:: name
|
||||
|
||||
The load name of the template.
|
||||
|
||||
.. attribute:: stream
|
||||
|
||||
The current :class:`~jinja2.lexer.TokenStream`
|
||||
|
||||
.. autoclass:: jinja2.lexer.TokenStream
|
||||
:members: push, look, eos, skip, __next__, next_if, skip_if, expect
|
||||
|
||||
.. attribute:: current
|
||||
|
||||
The current :class:`~jinja2.lexer.Token`.
|
||||
|
||||
.. autoclass:: jinja2.lexer.Token
|
||||
:members: test, test_any
|
||||
|
||||
.. attribute:: lineno
|
||||
|
||||
The line number of the token
|
||||
|
||||
.. attribute:: type
|
||||
|
||||
The type of the token. This string is interned so you may compare
|
||||
it with arbitrary strings using the ``is`` operator.
|
||||
|
||||
.. attribute:: value
|
||||
|
||||
The value of the token.
|
||||
|
||||
There is also a utility function in the lexer module that can count newline
|
||||
characters in strings:
|
||||
|
||||
.. autofunction:: jinja2.lexer.count_newlines
|
||||
|
||||
|
||||
AST
|
||||
~~~
|
||||
|
||||
The AST (Abstract Syntax Tree) is used to represent a template after parsing.
|
||||
It's build of nodes that the compiler then converts into executable Python
|
||||
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 Jinja versions but will stay backwards compatible.
|
||||
|
||||
For more information have a look at the repr of :meth:`jinja2.Environment.parse`.
|
||||
|
||||
.. module:: jinja2.nodes
|
||||
|
||||
.. jinja:nodes:: jinja2.nodes.Node
|
||||
|
||||
.. autoexception:: Impossible
|
||||
@@ -1,75 +0,0 @@
|
||||
Frequently Asked Questions
|
||||
==========================
|
||||
|
||||
|
||||
Why is it called Jinja?
|
||||
-----------------------
|
||||
|
||||
"Jinja" is a Japanese `Shinto shrine`_, or temple, and temple and
|
||||
template share a similar English pronunciation. It is not named after
|
||||
the `city in Uganda`_.
|
||||
|
||||
.. _Shinto shrine: https://en.wikipedia.org/wiki/Shinto_shrine
|
||||
.. _city in Uganda: https://en.wikipedia.org/wiki/Jinja%2C_Uganda
|
||||
|
||||
|
||||
How fast is Jinja?
|
||||
------------------
|
||||
|
||||
Jinja is relatively fast among template engines because it compiles and
|
||||
caches template code to Python code, so that the template does not need
|
||||
to be parsed and interpreted each time. Rendering a template becomes as
|
||||
close to executing a Python function as possible.
|
||||
|
||||
Jinja also makes extensive use of caching. Templates are cached by name
|
||||
after loading, so future uses of the template avoid loading. The
|
||||
template loading itself uses a bytecode cache to avoid repeated
|
||||
compiling. The caches can be external to persist across restarts.
|
||||
Templates can also be precompiled and loaded as fast Python imports.
|
||||
|
||||
We dislike benchmarks because they don't reflect real use. Performance
|
||||
depends on many factors. Different engines have different default
|
||||
configurations and tradeoffs that make it unclear how to set up a useful
|
||||
comparison. Often, database access, API calls, and data processing have
|
||||
a much larger effect on performance than the template engine.
|
||||
|
||||
|
||||
Isn't it a bad idea to put logic in templates?
|
||||
----------------------------------------------
|
||||
|
||||
Without a doubt you should try to remove as much logic from templates as
|
||||
possible. With less logic, the template is easier to understand, has
|
||||
fewer potential side effects, and is faster to compile and render. But a
|
||||
template without any logic means processing must be done in code before
|
||||
rendering. A template engine that does that is shipped with Python,
|
||||
called :class:`string.Template`, and while it's definitely fast it's not
|
||||
convenient.
|
||||
|
||||
Jinja's features such as blocks, statements, filters, and function calls
|
||||
make it much easier to write expressive templates, with very few
|
||||
restrictions. Jinja doesn't allow arbitrary Python code in templates, or
|
||||
every feature available in the Python language. This keeps the engine
|
||||
easier to maintain, and keeps templates more readable.
|
||||
|
||||
Some amount of logic is required in templates to keep everyone happy.
|
||||
Too much logic in the template can make it complex to reason about and
|
||||
maintain. It's up to you to decide how your application will work and
|
||||
balance how much logic you want to put in the template.
|
||||
|
||||
|
||||
Why is HTML escaping not the default?
|
||||
-------------------------------------
|
||||
|
||||
Jinja provides a feature that can be enabled to escape HTML syntax in
|
||||
rendered templates. However, it is disabled by default.
|
||||
|
||||
Jinja is a general purpose template engine, it is not only used for HTML
|
||||
documents. You can generate plain text, LaTeX, emails, CSS, JavaScript,
|
||||
configuration files, etc. HTML escaping wouldn't make sense for any of
|
||||
these document types.
|
||||
|
||||
While automatic escaping means that you are less likely have an XSS
|
||||
problem, it also requires significant extra processing during compiling
|
||||
and rendering, which can reduce performance. Jinja uses MarkupSafe for
|
||||
escaping, which provides optimized C code for speed, but it still
|
||||
introduces overhead to track escaping across methods and formatting.
|
||||
@@ -1,29 +0,0 @@
|
||||
.. rst-class:: hide-header
|
||||
|
||||
Jinja
|
||||
=====
|
||||
|
||||
.. image:: _static/jinja-logo.png
|
||||
:align: center
|
||||
:target: https://palletsprojects.com/p/jinja/
|
||||
|
||||
Jinja is a fast, expressive, extensible templating engine. Special
|
||||
placeholders in the template allow writing code similar to Python
|
||||
syntax. Then the template is passed data to render the final document.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
intro
|
||||
api
|
||||
sandbox
|
||||
nativetypes
|
||||
templates
|
||||
extensions
|
||||
integration
|
||||
switching
|
||||
tricks
|
||||
faq
|
||||
license
|
||||
changes
|
||||
@@ -1,94 +0,0 @@
|
||||
Integration
|
||||
===========
|
||||
|
||||
|
||||
Flask
|
||||
-----
|
||||
|
||||
The `Flask`_ web application framework, also maintained by Pallets, uses
|
||||
Jinja templates by default. Flask sets up a Jinja environment and
|
||||
template loader for you, and provides functions to easily render
|
||||
templates from view functions.
|
||||
|
||||
.. _Flask: https://flask.palletsprojects.com
|
||||
|
||||
|
||||
Django
|
||||
------
|
||||
|
||||
Django supports using Jinja as its template engine, see
|
||||
https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines.
|
||||
|
||||
|
||||
.. _babel-integration:
|
||||
|
||||
Babel
|
||||
-----
|
||||
|
||||
Jinja provides support for extracting gettext messages from templates
|
||||
via a `Babel`_ extractor entry point called
|
||||
``jinja2.ext.babel_extract``. The support is implemented as part of the
|
||||
:ref:`i18n-extension` extension.
|
||||
|
||||
Gettext messages are extracted from both ``trans`` tags and code
|
||||
expressions.
|
||||
|
||||
To extract gettext messages from templates, the project needs a Jinja
|
||||
section in its Babel extraction method `mapping file`_:
|
||||
|
||||
.. sourcecode:: ini
|
||||
|
||||
[jinja2: **/templates/**.html]
|
||||
encoding = utf-8
|
||||
|
||||
The syntax related options of the :class:`Environment` are also
|
||||
available as configuration values in the mapping file. For example, to
|
||||
tell the extractor that templates use ``%`` as
|
||||
``line_statement_prefix`` you can use this code:
|
||||
|
||||
.. sourcecode:: ini
|
||||
|
||||
[jinja2: **/templates/**.html]
|
||||
encoding = utf-8
|
||||
line_statement_prefix = %
|
||||
|
||||
:ref:`jinja-extensions` may also be defined by passing a comma separated
|
||||
list of import paths as the ``extensions`` value. The i18n extension is
|
||||
added automatically.
|
||||
|
||||
Template syntax errors are ignored by default. The assumption is that
|
||||
tests will catch syntax errors in templates. If you don't want to ignore
|
||||
errors, add ``silent = false`` to the settings.
|
||||
|
||||
.. _Babel: https://babel.readthedocs.io/
|
||||
.. _mapping file: https://babel.readthedocs.io/en/latest/messages.html#extraction-method-mapping-and-configuration
|
||||
|
||||
|
||||
Pylons
|
||||
------
|
||||
|
||||
It's easy to integrate Jinja into a `Pylons`_ application.
|
||||
|
||||
The template engine is configured in ``config/environment.py``. The
|
||||
configuration for Jinja looks something like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from jinja2 import Environment, PackageLoader
|
||||
config['pylons.app_globals'].jinja_env = Environment(
|
||||
loader=PackageLoader('yourapplication', 'templates')
|
||||
)
|
||||
|
||||
After that you can render Jinja templates by using the ``render_jinja``
|
||||
function from the ``pylons.templating`` module.
|
||||
|
||||
Additionally it's a good idea to set the Pylons ``c`` object to strict
|
||||
mode. By default attribute access on missing attributes on the ``c``
|
||||
object returns an empty string and not an undefined object. To change
|
||||
this add this to ``config/environment.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
config['pylons.strict_c'] = True
|
||||
|
||||
.. _Pylons: https://pylonshq.com/
|
||||
@@ -1,63 +0,0 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
Jinja is a fast, expressive, extensible templating engine. Special
|
||||
placeholders in the template allow writing code similar to Python
|
||||
syntax. Then the template is passed data to render the final document.
|
||||
|
||||
It includes:
|
||||
|
||||
- Template inheritance and inclusion.
|
||||
- Define and import macros within templates.
|
||||
- HTML templates can use autoescaping to prevent XSS from untrusted
|
||||
user input.
|
||||
- A sandboxed environment can safely render untrusted templates.
|
||||
- Async support for generating templates that automatically handle
|
||||
sync and async functions without extra syntax.
|
||||
- I18N support with Babel.
|
||||
- Templates are compiled to optimized Python code just-in-time and
|
||||
cached, or can be compiled ahead-of-time.
|
||||
- Exceptions point to the correct line in templates to make debugging
|
||||
easier.
|
||||
- Extensible filters, tests, functions, and even syntax.
|
||||
|
||||
Jinja's philosophy is that while application logic belongs in Python if
|
||||
possible, it shouldn't make the template designer's job difficult by
|
||||
restricting functionality too much.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
We recommend using the latest version of Python. Jinja supports Python
|
||||
3.7 and newer. We also recommend using a `virtual environment`_ in order
|
||||
to isolate your project dependencies from other projects and the system.
|
||||
|
||||
.. _virtual environment: https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments
|
||||
|
||||
Install the most recent Jinja version using pip:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install Jinja2
|
||||
|
||||
|
||||
Dependencies
|
||||
~~~~~~~~~~~~
|
||||
|
||||
These will be installed automatically when installing Jinja.
|
||||
|
||||
- `MarkupSafe`_ escapes untrusted input when rendering templates to
|
||||
avoid injection attacks.
|
||||
|
||||
.. _MarkupSafe: https://markupsafe.palletsprojects.com/
|
||||
|
||||
|
||||
Optional Dependencies
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These distributions will not be installed automatically.
|
||||
|
||||
- `Babel`_ provides translation support in templates.
|
||||
|
||||
.. _Babel: https://babel.pocoo.org/
|
||||
@@ -1,4 +0,0 @@
|
||||
BSD-3-Clause License
|
||||
====================
|
||||
|
||||
.. include:: ../LICENSE.rst
|
||||
@@ -1,35 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
@@ -1,64 +0,0 @@
|
||||
.. module:: jinja2.nativetypes
|
||||
|
||||
.. _nativetypes:
|
||||
|
||||
Native Python Types
|
||||
===================
|
||||
|
||||
The default :class:`~jinja2.Environment` renders templates to strings. With
|
||||
:class:`NativeEnvironment`, rendering a template produces a native Python type.
|
||||
This is useful if you are using Jinja outside the context of creating text
|
||||
files. For example, your code may have an intermediate step where users may use
|
||||
templates to define values that will then be passed to a traditional string
|
||||
environment.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Adding two values results in an integer, not a string with a number:
|
||||
|
||||
>>> env = NativeEnvironment()
|
||||
>>> t = env.from_string('{{ x + y }}')
|
||||
>>> result = t.render(x=4, y=2)
|
||||
>>> print(result)
|
||||
6
|
||||
>>> print(type(result))
|
||||
int
|
||||
|
||||
Rendering list syntax produces a list:
|
||||
|
||||
>>> t = env.from_string('[{% for item in data %}{{ item + 1 }},{% endfor %}]')
|
||||
>>> result = t.render(data=range(5))
|
||||
>>> print(result)
|
||||
[1, 2, 3, 4, 5]
|
||||
>>> print(type(result))
|
||||
list
|
||||
|
||||
Rendering something that doesn't look like a Python literal produces a string:
|
||||
|
||||
>>> t = env.from_string('{{ x }} * {{ y }}')
|
||||
>>> result = t.render(x=4, y=2)
|
||||
>>> print(result)
|
||||
4 * 2
|
||||
>>> print(type(result))
|
||||
str
|
||||
|
||||
Rendering a Python object produces that object as long as it is the only node:
|
||||
|
||||
>>> class Foo:
|
||||
... def __init__(self, value):
|
||||
... self.value = value
|
||||
...
|
||||
>>> result = env.from_string('{{ x }}').render(x=Foo(15))
|
||||
>>> print(type(result).__name__)
|
||||
Foo
|
||||
>>> print(result.value)
|
||||
15
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. autoclass:: NativeEnvironment([options])
|
||||
|
||||
.. autoclass:: NativeTemplate([options])
|
||||
:members: render
|
||||
@@ -1,111 +0,0 @@
|
||||
Sandbox
|
||||
=======
|
||||
|
||||
The Jinja sandbox can be used to render untrusted templates. Access to
|
||||
attributes, method calls, operators, mutating data structures, and
|
||||
string formatting can be intercepted and prohibited.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from jinja2.sandbox import SandboxedEnvironment
|
||||
>>> env = SandboxedEnvironment()
|
||||
>>> func = lambda: "Hello, Sandbox!"
|
||||
>>> env.from_string("{{ func() }}").render(func=func)
|
||||
'Hello, Sandbox!'
|
||||
>>> env.from_string("{{ func.__code__.co_code }}").render(func=func)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
SecurityError: access to attribute '__code__' of 'function' object is unsafe.
|
||||
|
||||
A sandboxed environment can be useful, for example, to allow users of an
|
||||
internal reporting system to create custom emails. You would document
|
||||
what data is available in the templates, then the user would write a
|
||||
template using that information. Your code would generate the report
|
||||
data and pass it to the user's sandboxed template to render.
|
||||
|
||||
|
||||
Security Considerations
|
||||
-----------------------
|
||||
|
||||
The sandbox alone is not a solution for perfect security. Keep these
|
||||
things in mind when using the sandbox.
|
||||
|
||||
Templates can still raise errors when compiled or rendered. Your code
|
||||
should attempt to catch errors instead of crashing.
|
||||
|
||||
It is possible to construct a relatively small template that renders to
|
||||
a very large amount of output, which could correspond to a high use of
|
||||
CPU or memory. You should run your application with limits on resources
|
||||
such as CPU and memory to mitigate this.
|
||||
|
||||
Jinja only renders text, it does not understand, for example, JavaScript
|
||||
code. Depending on how the rendered template will be used, you may need
|
||||
to do other postprocessing to restrict the output.
|
||||
|
||||
Pass only the data that is relevant to the template. Avoid passing
|
||||
global data, or objects with methods that have side effects. By default
|
||||
the sandbox prevents private and internal attribute access. You can
|
||||
override :meth:`~SandboxedEnvironment.is_safe_attribute` to further
|
||||
restrict attributes access. Decorate methods with :func:`unsafe` to
|
||||
prevent calling them from templates when passing objects as data. Use
|
||||
:class:`ImmutableSandboxedEnvironment` to prevent modifying lists and
|
||||
dictionaries.
|
||||
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
.. module:: jinja2.sandbox
|
||||
|
||||
.. autoclass:: SandboxedEnvironment([options])
|
||||
:members: is_safe_attribute, is_safe_callable, default_binop_table,
|
||||
default_unop_table, intercepted_binops, intercepted_unops,
|
||||
call_binop, call_unop
|
||||
|
||||
.. autoclass:: ImmutableSandboxedEnvironment([options])
|
||||
|
||||
.. autoexception:: SecurityError
|
||||
|
||||
.. autofunction:: unsafe
|
||||
|
||||
.. autofunction:: is_internal_attribute
|
||||
|
||||
.. autofunction:: modifies_known_mutable
|
||||
|
||||
|
||||
Operator Intercepting
|
||||
---------------------
|
||||
|
||||
For performance, Jinja outputs operators directly when compiling. This
|
||||
means it's not possible to intercept operator behavior by overriding
|
||||
:meth:`SandboxEnvironment.call <Environment.call>` by default, because
|
||||
operator special methods are handled by the Python interpreter, and
|
||||
might not correspond with exactly one method depending on the operator's
|
||||
use.
|
||||
|
||||
The sandbox can instruct the compiler to output a function to intercept
|
||||
certain operators instead. Override
|
||||
:attr:`SandboxedEnvironment.intercepted_binops` and
|
||||
:attr:`SandboxedEnvironment.intercepted_unops` with the operator symbols
|
||||
you want to intercept. The compiler will replace the symbols with calls
|
||||
to :meth:`SandboxedEnvironment.call_binop` and
|
||||
:meth:`SandboxedEnvironment.call_unop` instead. The default
|
||||
implementation of those methods will use
|
||||
:attr:`SandboxedEnvironment.binop_table` and
|
||||
:attr:`SandboxedEnvironment.unop_table` to translate operator symbols
|
||||
into :mod:`operator` functions.
|
||||
|
||||
For example, the power (``**``) operator can be disabled:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
class MyEnvironment(SandboxedEnvironment):
|
||||
intercepted_binops = frozenset(["**"])
|
||||
|
||||
def call_binop(self, context, operator, left, right):
|
||||
if operator == "**":
|
||||
return self.undefined("The power (**) operator is unavailable.")
|
||||
|
||||
return super().call_binop(self, context, operator, left, right)
|
||||
@@ -1,181 +0,0 @@
|
||||
Switching From Other Template Engines
|
||||
=====================================
|
||||
|
||||
This is a brief guide on some of the differences between Jinja syntax
|
||||
and other template languages. See :doc:`/templates` for a comprehensive
|
||||
guide to Jinja syntax and features.
|
||||
|
||||
|
||||
Django
|
||||
------
|
||||
|
||||
If you have previously worked with Django templates, you should find
|
||||
Jinja very familiar. Many of the syntax elements look and work the same.
|
||||
However, Jinja provides some more syntax elements, and some work a bit
|
||||
differently.
|
||||
|
||||
This section covers the template changes. The API, including extension
|
||||
support, is fundamentally different so it won't be covered here.
|
||||
|
||||
Django supports using Jinja as its template engine, see
|
||||
https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines.
|
||||
|
||||
|
||||
Method Calls
|
||||
~~~~~~~~~~~~
|
||||
|
||||
In Django, methods are called implicitly, without parentheses.
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% for page in user.get_created_pages %}
|
||||
...
|
||||
{% endfor %}
|
||||
|
||||
In Jinja, using parentheses is required for calls, like in Python. This
|
||||
allows you to pass variables to the method, which is not possible
|
||||
in Django. This syntax is also used for calling macros.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% for page in user.get_created_pages() %}
|
||||
...
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Filter Arguments
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
In Django, one literal value can be passed to a filter after a colon.
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{{ items|join:", " }}
|
||||
|
||||
In Jinja, filters can take any number of positional and keyword
|
||||
arguments in parentheses, like function calls. Arguments can also be
|
||||
variables instead of literal values.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{{ items|join(", ") }}
|
||||
|
||||
|
||||
Tests
|
||||
~~~~~
|
||||
|
||||
In addition to filters, Jinja also has "tests" used with the ``is``
|
||||
operator. This operator is not the same as the Python operator.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if user.user_id is odd %}
|
||||
{{ user.username|e }} is odd
|
||||
{% else %}
|
||||
hmm. {{ user.username|e }} looks pretty normal
|
||||
{% endif %}
|
||||
|
||||
Loops
|
||||
~~~~~
|
||||
|
||||
In Django, the special variable for the loop context is called
|
||||
``forloop``, and the ``empty`` is used for no loop items.
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% empty %}
|
||||
No items!
|
||||
{% endfor %}
|
||||
|
||||
In Jinja, the special variable for the loop context is called ``loop``,
|
||||
and the ``else`` block is used for no loop items.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% for item in items %}
|
||||
{{ loop.index}}. {{ item }}
|
||||
{% else %}
|
||||
No items!
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Cycle
|
||||
~~~~~
|
||||
|
||||
In Django, the ``{% cycle %}`` can be used in a for loop to alternate
|
||||
between values per loop.
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% for user in users %}
|
||||
<li class="{% cycle 'odd' 'even' %}">{{ user }}</li>
|
||||
{% endfor %}
|
||||
|
||||
In Jinja, the ``loop`` context has a ``cycle`` method.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% for user in users %}
|
||||
<li class="{{ loop.cycle('odd', 'even') }}">{{ user }}</li>
|
||||
{% endfor %}
|
||||
|
||||
A cycler can also be assigned to a variable and used outside or across
|
||||
loops with the ``cycle()`` global function.
|
||||
|
||||
|
||||
Mako
|
||||
----
|
||||
|
||||
You can configure Jinja to look more like Mako:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
env = Environment(
|
||||
block_start_string="<%",
|
||||
block_end_string="%>",
|
||||
variable_start_string="${",
|
||||
variable_end_string="}",
|
||||
comment_start_string="<%doc>",
|
||||
commend_end_string="</%doc>",
|
||||
line_statement_prefix="%",
|
||||
line_comment_prefix="##",
|
||||
)
|
||||
|
||||
With an environment configured like that, Jinja should be able to
|
||||
interpret a small subset of Mako templates without any changes.
|
||||
|
||||
Jinja does not support embedded Python code, so you would have to move
|
||||
that out of the template. You could either process the data with the
|
||||
same code before rendering, or add a global function or filter to the
|
||||
Jinja environment.
|
||||
|
||||
The syntax for defs (which are called macros in Jinja) and template
|
||||
inheritance is different too.
|
||||
|
||||
The following Mako template:
|
||||
|
||||
.. code-block:: mako
|
||||
|
||||
<%inherit file="layout.html" />
|
||||
<%def name="title()">Page Title</%def>
|
||||
<ul>
|
||||
% for item in list:
|
||||
<li>${item}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
Looks like this in Jinja with the above configuration:
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<% extends "layout.html" %>
|
||||
<% block title %>Page Title<% endblock %>
|
||||
<% block body %>
|
||||
<ul>
|
||||
% for item in list:
|
||||
<li>${item}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
<% endblock %>
|
||||
-1949
File diff suppressed because it is too large
Load Diff
-100
@@ -1,100 +0,0 @@
|
||||
Tips and Tricks
|
||||
===============
|
||||
|
||||
.. highlight:: html+jinja
|
||||
|
||||
This part of the documentation shows some tips and tricks for Jinja
|
||||
templates.
|
||||
|
||||
|
||||
.. _null-default-fallback:
|
||||
|
||||
Null-Default Fallback
|
||||
---------------------
|
||||
|
||||
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
|
||||
for a neat trick.
|
||||
|
||||
Usually child templates extend from one template that adds a basic HTML
|
||||
skeleton. However it's possible to put the `extends` tag into an `if` tag to
|
||||
only extend from the layout template if the `standalone` variable evaluates
|
||||
to false which it does per default if it's not defined. Additionally a very
|
||||
basic skeleton is added to the file so that if it's indeed rendered with
|
||||
`standalone` set to `True` a very basic HTML skeleton is added::
|
||||
|
||||
{% if not standalone %}{% extends 'default.html' %}{% endif -%}
|
||||
<!DOCTYPE html>
|
||||
<title>{% block title %}The Page Title{% endblock %}</title>
|
||||
<link rel="stylesheet" href="style.css" type="text/css">
|
||||
{% block body %}
|
||||
<p>This is the page body.</p>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
Alternating Rows
|
||||
----------------
|
||||
|
||||
If you want to have different styles for each row of a table or
|
||||
list you can use the `cycle` method on the `loop` object::
|
||||
|
||||
<ul>
|
||||
{% for row in rows %}
|
||||
<li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
`cycle` can take an unlimited number of strings. Each time this
|
||||
tag is encountered the next item from the list is rendered.
|
||||
|
||||
|
||||
Highlighting Active Menu Items
|
||||
------------------------------
|
||||
|
||||
Often you want to have a navigation bar with an active navigation
|
||||
item. This is really simple to achieve. Because assignments outside
|
||||
of `block`\s in child templates are global and executed before the layout
|
||||
template is evaluated it's possible to define the active menu item in the
|
||||
child template::
|
||||
|
||||
{% extends "layout.html" %}
|
||||
{% set active_page = "index" %}
|
||||
|
||||
The layout template can then access `active_page`. Additionally it makes
|
||||
sense to define a default for that variable::
|
||||
|
||||
{% set navigation_bar = [
|
||||
('/', 'index', 'Index'),
|
||||
('/downloads/', 'downloads', 'Downloads'),
|
||||
('/about/', 'about', 'About')
|
||||
] -%}
|
||||
{% set active_page = active_page|default('index') -%}
|
||||
...
|
||||
<ul id="navigation">
|
||||
{% for href, id, caption in navigation_bar %}
|
||||
<li{% if id == active_page %} class="active"{% endif %}>
|
||||
<a href="{{ href|e }}">{{ caption|e }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
...
|
||||
|
||||
.. _accessing-the-parent-loop:
|
||||
|
||||
Accessing the parent Loop
|
||||
-------------------------
|
||||
|
||||
The special `loop` variable always points to the innermost loop. If it's
|
||||
desired to have access to an outer loop it's possible to alias it::
|
||||
|
||||
<table>
|
||||
{% for row in table %}
|
||||
<tr>
|
||||
{% set rowloop = loop %}
|
||||
{% for cell in row %}
|
||||
<td id="cell-{{ rowloop.index }}-{{ loop.index }}">{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -1,16 +0,0 @@
|
||||
from jinja2 import Environment
|
||||
|
||||
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()
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
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]))
|
||||
@@ -1,13 +0,0 @@
|
||||
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())
|
||||
@@ -1,6 +0,0 @@
|
||||
{% from 'subbroken.html' import may_break %}
|
||||
<ul>
|
||||
{% for item in seq %}
|
||||
<li>{{ may_break(item) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -1,3 +0,0 @@
|
||||
{% macro may_break(item) -%}
|
||||
[{{ item / 0 }}]
|
||||
{%- endmacro %}
|
||||
@@ -1,29 +0,0 @@
|
||||
from jinja2 import Environment
|
||||
from jinja2.loaders import DictLoader
|
||||
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"child.html": """\
|
||||
{% extends default_layout or 'default.html' %}
|
||||
{% include helpers = 'helpers.html' %}
|
||||
{% macro get_the_answer() %}42{% endmacro %}
|
||||
{% title = 'Hello World' %}
|
||||
{% block body %}
|
||||
{{ get_the_answer() }}
|
||||
{{ helpers.conspirate() }}
|
||||
{% endblock %}
|
||||
""",
|
||||
"default.html": """\
|
||||
<!doctype html>
|
||||
<title>{{ title }}</title>
|
||||
{% block body %}{% endblock %}
|
||||
""",
|
||||
"helpers.html": """\
|
||||
{% macro conspirate() %}23{% endmacro %}
|
||||
""",
|
||||
}
|
||||
)
|
||||
)
|
||||
tmpl = env.get_template("child.html")
|
||||
print(tmpl.render())
|
||||
@@ -1,27 +0,0 @@
|
||||
from jinja2 import Environment
|
||||
|
||||
env = Environment(
|
||||
line_statement_prefix="%", variable_start_string="${", variable_end_string="}"
|
||||
)
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
% macro foo()
|
||||
${caller(42)}
|
||||
% endmacro
|
||||
<ul>
|
||||
% for item in seq
|
||||
<li>${item}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% call(var) foo()
|
||||
[${var}]
|
||||
% endcall
|
||||
% filter escape
|
||||
<hello world>
|
||||
% for item in [1, 2, 3]
|
||||
- ${item}
|
||||
% endfor
|
||||
% endfilter
|
||||
"""
|
||||
)
|
||||
print(tmpl.render(seq=range(10)))
|
||||
@@ -1,13 +0,0 @@
|
||||
from jinja2 import Environment
|
||||
|
||||
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))
|
||||
@@ -1,18 +0,0 @@
|
||||
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",
|
||||
}[s if n == 1 else p]
|
||||
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])
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
-r docs.in
|
||||
-r tests.in
|
||||
-r typing.in
|
||||
pip-compile-multi
|
||||
pre-commit
|
||||
tox
|
||||
@@ -1,60 +0,0 @@
|
||||
# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
-r docs.txt
|
||||
-r tests.txt
|
||||
-r typing.txt
|
||||
cfgv==3.3.1
|
||||
# via pre-commit
|
||||
click==8.1.2
|
||||
# via
|
||||
# pip-compile-multi
|
||||
# pip-tools
|
||||
distlib==0.3.4
|
||||
# via virtualenv
|
||||
filelock==3.6.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
identify==2.5.0
|
||||
# via pre-commit
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
pep517==0.12.0
|
||||
# via pip-tools
|
||||
pip-compile-multi==2.4.5
|
||||
# via -r requirements/dev.in
|
||||
pip-tools==6.6.0
|
||||
# via pip-compile-multi
|
||||
platformdirs==2.5.2
|
||||
# via virtualenv
|
||||
pre-commit==2.18.1
|
||||
# via -r requirements/dev.in
|
||||
pyyaml==6.0
|
||||
# via pre-commit
|
||||
six==1.16.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
toposort==1.7
|
||||
# via pip-compile-multi
|
||||
tox==3.25.0
|
||||
# via -r requirements/dev.in
|
||||
virtualenv==20.14.1
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.37.1
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
@@ -1,4 +0,0 @@
|
||||
Pallets-Sphinx-Themes
|
||||
Sphinx
|
||||
sphinx-issues
|
||||
sphinxcontrib-log-cabinet
|
||||
@@ -1,65 +0,0 @@
|
||||
# SHA1:45c590f97fe95b8bdc755eef796e91adf5fbe4ea
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
babel==2.10.1
|
||||
# via sphinx
|
||||
certifi==2021.10.8
|
||||
# via requests
|
||||
charset-normalizer==2.0.12
|
||||
# via requests
|
||||
docutils==0.17.1
|
||||
# via sphinx
|
||||
idna==3.3
|
||||
# via requests
|
||||
imagesize==1.3.0
|
||||
# via sphinx
|
||||
jinja2==3.1.1
|
||||
# via sphinx
|
||||
markupsafe==2.1.1
|
||||
# via jinja2
|
||||
packaging==21.3
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.0.2
|
||||
# via -r requirements/docs.in
|
||||
pygments==2.12.0
|
||||
# via sphinx
|
||||
pyparsing==3.0.8
|
||||
# via packaging
|
||||
pytz==2022.1
|
||||
# via babel
|
||||
requests==2.27.1
|
||||
# via sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==4.5.0
|
||||
# via
|
||||
# -r requirements/docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==3.0.1
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
urllib3==1.26.9
|
||||
# via requests
|
||||
@@ -1 +0,0 @@
|
||||
pytest
|
||||
@@ -1,23 +0,0 @@
|
||||
# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
attrs==21.4.0
|
||||
# via pytest
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
packaging==21.3
|
||||
# via pytest
|
||||
pluggy==1.0.0
|
||||
# via pytest
|
||||
py==1.11.0
|
||||
# via pytest
|
||||
pyparsing==3.0.8
|
||||
# via packaging
|
||||
pytest==7.1.2
|
||||
# via -r requirements/tests.in
|
||||
tomli==2.0.1
|
||||
# via pytest
|
||||
@@ -1 +0,0 @@
|
||||
mypy
|
||||
@@ -1,15 +0,0 @@
|
||||
# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
mypy==0.950
|
||||
# via -r requirements/typing.in
|
||||
mypy-extensions==0.4.3
|
||||
# via mypy
|
||||
tomli==2.0.1
|
||||
# via mypy
|
||||
typing-extensions==4.2.0
|
||||
# via mypy
|
||||
@@ -1,74 +0,0 @@
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def get_characters():
|
||||
"""Find every Unicode character that is valid in a Python `identifier`_ but
|
||||
is not matched by the regex ``\\w`` group.
|
||||
|
||||
``\\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
|
||||
continue characters.
|
||||
|
||||
_identifier: https://docs.python.org/3/reference/lexical_analysis.html#identifiers
|
||||
"""
|
||||
for cp in range(sys.maxunicode + 1):
|
||||
s = chr(cp)
|
||||
|
||||
if ("a" + s).isidentifier() and not re.match(r"\w", s):
|
||||
yield s
|
||||
|
||||
|
||||
def collapse_ranges(data):
|
||||
"""Given a sorted list of unique characters, generate ranges representing
|
||||
sequential code points.
|
||||
|
||||
Source: https://stackoverflow.com/a/4629241/400617
|
||||
"""
|
||||
for _, b in itertools.groupby(enumerate(data), lambda x: ord(x[1]) - x[0]):
|
||||
b = list(b)
|
||||
yield b[0][1], b[-1][1]
|
||||
|
||||
|
||||
def build_pattern(ranges):
|
||||
"""Output the regex pattern for ranges of characters.
|
||||
|
||||
One and two character ranges output the individual characters.
|
||||
"""
|
||||
out = []
|
||||
|
||||
for a, b in ranges:
|
||||
if a == b: # single char
|
||||
out.append(a)
|
||||
elif ord(b) - ord(a) == 1: # two chars, range is redundant
|
||||
out.append(a)
|
||||
out.append(b)
|
||||
else:
|
||||
out.append(f"{a}-{b}")
|
||||
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
"""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__), "..", "src", "jinja2", "_identifier.py")
|
||||
)
|
||||
|
||||
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(f' r"[\\w{pattern}]+" # noqa: B950\n')
|
||||
f.write(")\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,49 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import loaders
|
||||
from jinja2.environment import Environment
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
"""returns a new environment."""
|
||||
return Environment()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dict_loader():
|
||||
"""returns DictLoader"""
|
||||
return loaders.DictLoader({"justdict.html": "FOO"})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_loader():
|
||||
"""returns PackageLoader initialized from templates"""
|
||||
return loaders.PackageLoader("res", "templates")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def filesystem_loader():
|
||||
"""returns FileSystemLoader initialized to res/templates directory"""
|
||||
here = Path(__file__).parent.resolve()
|
||||
return loaders.FileSystemLoader(here / "res" / "templates")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def function_loader():
|
||||
"""returns a FunctionLoader"""
|
||||
return loaders.FunctionLoader({"justfunction.html": "FOO"}.get)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choice_loader(dict_loader, package_loader):
|
||||
"""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})
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
Before
|
||||
{{ fail() }}
|
||||
After
|
||||
@@ -1 +0,0 @@
|
||||
FOO
|
||||
@@ -1 +0,0 @@
|
||||
文字化け
|
||||
@@ -1,4 +0,0 @@
|
||||
Foo
|
||||
{% for item in broken %}
|
||||
...
|
||||
{% endif %}
|
||||
@@ -1 +0,0 @@
|
||||
BAR
|
||||
@@ -1,2 +0,0 @@
|
||||
Looks like the start of templates/foo/test.html
|
||||
Tested by test_filesystem_loader_overlapping_names
|
||||
@@ -1,434 +0,0 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
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 Cycler
|
||||
from jinja2.utils import pass_context
|
||||
from jinja2.utils import pass_environment
|
||||
from jinja2.utils import pass_eval_context
|
||||
|
||||
|
||||
class TestExtendedAPI:
|
||||
def test_item_and_attribute(self, env):
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
for env in Environment(), SandboxedEnvironment():
|
||||
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)]"
|
||||
tmpl = env.from_string('{{ foo["items"] }}')
|
||||
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):
|
||||
@pass_context
|
||||
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):
|
||||
@pass_eval_context
|
||||
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):
|
||||
@pass_environment
|
||||
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
|
||||
c = Cycler(*items)
|
||||
for item in items + items:
|
||||
assert c.current == item
|
||||
assert next(c) == item
|
||||
next(c)
|
||||
assert c.current == 2
|
||||
c.reset()
|
||||
assert c.current == 1
|
||||
|
||||
def test_expressions(self, env):
|
||||
expr = env.compile_expression("foo")
|
||||
assert expr() is None
|
||||
assert expr(foo=42) == 42
|
||||
expr2 = env.compile_expression("foo", undefined_to_none=False)
|
||||
assert is_undefined(expr2())
|
||||
|
||||
expr = env.compile_expression("42 + foo")
|
||||
assert expr(foo=42) == 84
|
||||
|
||||
def test_template_passthrough(self, env):
|
||||
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:
|
||||
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>"
|
||||
|
||||
def test_sandbox_max_range(self, env):
|
||||
from jinja2.sandbox import SandboxedEnvironment, MAX_RANGE
|
||||
|
||||
env = SandboxedEnvironment()
|
||||
t = env.from_string("{% for item in range(total) %}{{ item }}{% endfor %}")
|
||||
|
||||
with pytest.raises(OverflowError):
|
||||
t.render(total=MAX_RANGE + 1)
|
||||
|
||||
|
||||
class TestMeta:
|
||||
def test_find_undeclared_variables(self, env):
|
||||
ast = env.parse("{% set foo = 42 %}{{ bar + foo }}")
|
||||
x = meta.find_undeclared_variables(ast)
|
||||
assert x == {"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 == {"bar", "seq", "muh"}
|
||||
|
||||
ast = env.parse("{% for x in range(5) %}{{ x }}{% endfor %}{{ foo }}")
|
||||
x = meta.find_undeclared_variables(ast)
|
||||
assert x == {"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) 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" %}'
|
||||
)
|
||||
i = meta.find_referenced_templates(ast)
|
||||
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"]
|
||||
|
||||
ast = env.parse('{% include ("foo.html", "bar.html") %}')
|
||||
i = meta.find_referenced_templates(ast)
|
||||
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]
|
||||
|
||||
ast = env.parse('{% include ("foo.html", "bar.html", foo) %}')
|
||||
i = meta.find_referenced_templates(ast)
|
||||
assert list(i) == ["foo.html", "bar.html", None]
|
||||
|
||||
|
||||
class TestStreaming:
|
||||
def test_basic_streaming(self, env):
|
||||
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(3)))
|
||||
stream.enable_buffering(size=3)
|
||||
assert next(stream) == "<ul><li>1"
|
||||
assert next(stream) == " - 0</li>"
|
||||
|
||||
def test_streaming_behavior(self, env):
|
||||
tmpl = env.from_string("")
|
||||
stream = tmpl.stream()
|
||||
assert not stream.buffered
|
||||
stream.enable_buffering(20)
|
||||
assert stream.buffered
|
||||
stream.disable_buffering()
|
||||
assert not stream.buffered
|
||||
|
||||
def test_dump_stream(self, env):
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
tmpl = env.from_string("\u2713")
|
||||
stream = tmpl.stream()
|
||||
stream.dump(str(tmp / "dump.txt"), "utf-8")
|
||||
assert (tmp / "dump.txt").read_bytes() == b"\xe2\x9c\x93"
|
||||
finally:
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
|
||||
class TestUndefined:
|
||||
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")
|
||||
pytest.raises(UndefinedError, t.render, test=test)
|
||||
|
||||
def test_undefined_and_special_attributes(self):
|
||||
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:
|
||||
@property # type: ignore
|
||||
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:
|
||||
def warning(self, msg, *args):
|
||||
_messages.append("W:" + msg % args)
|
||||
|
||||
def error(self, msg, *args):
|
||||
_messages.append("E:" + msg % args)
|
||||
|
||||
logging_undefined = make_logging_undefined(DebugLogger())
|
||||
env = Environment(undefined=logging_undefined)
|
||||
assert env.from_string("{{ missing }}").render() == ""
|
||||
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",
|
||||
"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",
|
||||
]
|
||||
|
||||
def test_default_undefined(self):
|
||||
env = Environment(undefined=Undefined)
|
||||
assert env.from_string("{{ missing }}").render() == ""
|
||||
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("{{ 'foo' in missing }}").render() == "False"
|
||||
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() == ""
|
||||
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() == ""
|
||||
assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == "foo"
|
||||
assert (
|
||||
env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(foo=42)
|
||||
== "bar"
|
||||
)
|
||||
assert (
|
||||
env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(
|
||||
foo={"bar": 42}
|
||||
)
|
||||
== "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)
|
||||
== "{{ no such element: int object['missing'] }}"
|
||||
)
|
||||
assert env.from_string("{{ not missing }}").render() == "True"
|
||||
undefined_hint = "this is testing undefined hint of DebugUndefined"
|
||||
assert (
|
||||
str(DebugUndefined(hint=undefined_hint))
|
||||
== f"{{{{ undefined value printed: {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)
|
||||
pytest.raises(UndefinedError, env.from_string("{{ 'foo' in missing }}").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):
|
||||
with pytest.raises(UndefinedError, match="'None' has no attribute 'split'"):
|
||||
Environment().getattr(None, "split")()
|
||||
|
||||
def test_object_repr(self):
|
||||
with pytest.raises(
|
||||
UndefinedError, match="'int object' has no attribute 'upper'"
|
||||
):
|
||||
Undefined(obj=42, name="upper")()
|
||||
|
||||
|
||||
class TestLowLevel:
|
||||
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"))
|
||||
else:
|
||||
super().visit_Const(node, frame)
|
||||
|
||||
class CustomEnvironment(Environment):
|
||||
code_generator_class = CustomCodeGenerator
|
||||
|
||||
env = CustomEnvironment()
|
||||
tmpl = env.from_string('{% set foo = "foo" %}{{ foo }}')
|
||||
assert tmpl.render() == "bar"
|
||||
|
||||
def test_custom_context(self):
|
||||
class CustomContext(Context):
|
||||
def resolve_or_missing(self, key):
|
||||
return "resolve-" + key
|
||||
|
||||
class CustomEnvironment(Environment):
|
||||
context_class = CustomContext
|
||||
|
||||
env = CustomEnvironment()
|
||||
tmpl = env.from_string("{{ foo }}")
|
||||
assert tmpl.render() == "resolve-foo"
|
||||
@@ -1,660 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import ChainableUndefined
|
||||
from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import Template
|
||||
from jinja2.async_utils import auto_aiter
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from jinja2.exceptions import TemplatesNotFound
|
||||
from jinja2.exceptions import UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
|
||||
|
||||
def test_basic_async():
|
||||
t = Template(
|
||||
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
|
||||
)
|
||||
|
||||
async def func():
|
||||
return await t.render_async()
|
||||
|
||||
rv = asyncio.run(func())
|
||||
assert rv == "[1][2][3]"
|
||||
|
||||
|
||||
def test_await_on_calls():
|
||||
t = Template("{{ async_func() + normal_func() }}", enable_async=True)
|
||||
|
||||
async def async_func():
|
||||
return 42
|
||||
|
||||
def normal_func():
|
||||
return 23
|
||||
|
||||
async def func():
|
||||
return await t.render_async(async_func=async_func, normal_func=normal_func)
|
||||
|
||||
rv = asyncio.run(func())
|
||||
assert rv == "65"
|
||||
|
||||
|
||||
def test_await_on_calls_normal_render():
|
||||
t = Template("{{ async_func() + normal_func() }}", enable_async=True)
|
||||
|
||||
async def async_func():
|
||||
return 42
|
||||
|
||||
def normal_func():
|
||||
return 23
|
||||
|
||||
rv = t.render(async_func=async_func, normal_func=normal_func)
|
||||
assert rv == "65"
|
||||
|
||||
|
||||
def test_await_and_macros():
|
||||
t = Template(
|
||||
"{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}",
|
||||
enable_async=True,
|
||||
)
|
||||
|
||||
async def async_func():
|
||||
return 42
|
||||
|
||||
async def func():
|
||||
return await t.render_async(async_func=async_func)
|
||||
|
||||
rv = asyncio.run(func())
|
||||
assert rv == "[42][42]"
|
||||
|
||||
|
||||
def test_async_blocks():
|
||||
t = Template(
|
||||
"{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
|
||||
enable_async=True,
|
||||
autoescape=True,
|
||||
)
|
||||
|
||||
async def func():
|
||||
return await t.render_async()
|
||||
|
||||
rv = asyncio.run(func())
|
||||
assert rv == "<Test><Test>"
|
||||
|
||||
|
||||
def test_async_generate():
|
||||
t = Template("{% for x in [1, 2, 3] %}{{ x }}{% endfor %}", enable_async=True)
|
||||
rv = list(t.generate())
|
||||
assert rv == ["1", "2", "3"]
|
||||
|
||||
|
||||
def test_async_iteration_in_templates():
|
||||
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"]
|
||||
|
||||
|
||||
def test_async_iteration_in_templates_extended():
|
||||
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
|
||||
return env
|
||||
|
||||
|
||||
class TestAsyncImports:
|
||||
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]"
|
||||
t = test_env_async.from_string(
|
||||
'{% import "module" as m without context %}{{ m.test() }}'
|
||||
)
|
||||
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]"
|
||||
t = test_env_async.from_string('{% from "module" import test %}{{ test() }}')
|
||||
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]"
|
||||
t = test_env_async.from_string(
|
||||
'{% from "module" import test with context %}{{ test() }}'
|
||||
)
|
||||
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 %}')
|
||||
test_env_async.from_string('{% from "foo" import bar, baz, with context %}')
|
||||
test_env_async.from_string('{% from "foo" import bar, with context %}')
|
||||
test_env_async.from_string('{% from "foo" import bar, with, context %}')
|
||||
test_env_async.from_string('{% from "foo" import bar, with with context %}')
|
||||
|
||||
def test_exports(self, test_env_async):
|
||||
coro = test_env_async.from_string(
|
||||
"""
|
||||
{% macro toplevel() %}...{% endmacro %}
|
||||
{% macro __private() %}...{% endmacro %}
|
||||
{% set variable = 42 %}
|
||||
{% for item in [1] %}
|
||||
{% macro notthere() %}{% endmacro %}
|
||||
{% endfor %}
|
||||
"""
|
||||
)._get_default_module_async()
|
||||
m = asyncio.run(coro)
|
||||
assert asyncio.run(m.toplevel()) == "..."
|
||||
assert not hasattr(m, "__missing")
|
||||
assert m.variable == 42
|
||||
assert not hasattr(m, "notthere")
|
||||
|
||||
def test_import_with_globals(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
'{% import "module" as m %}{{ m.test() }}', globals={"foo": 42}
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
t = test_env_async.from_string('{% import "module" as m %}{{ m.test() }}')
|
||||
assert t.render() == "[|23]"
|
||||
|
||||
def test_import_with_globals_override(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
'{% set foo = 41 %}{% import "module" as m %}{{ m.test() }}',
|
||||
globals={"foo": 42},
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
def test_from_import_with_globals(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
'{% from "module" import test %}{{ test() }}',
|
||||
globals={"foo": 42},
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
|
||||
class TestAsyncIncludes:
|
||||
def test_context_include(self, test_env_async):
|
||||
t = test_env_async.from_string('{% include "header" %}')
|
||||
assert t.render(foo=42) == "[42|23]"
|
||||
t = test_env_async.from_string('{% include "header" with context %}')
|
||||
assert t.render(foo=42) == "[42|23]"
|
||||
t = test_env_async.from_string('{% include "header" without context %}')
|
||||
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]"
|
||||
|
||||
t = test_env_async.from_string(
|
||||
'{% include ["missing", "missing2"] ignore missing %}'
|
||||
)
|
||||
assert t.render(foo=42) == ""
|
||||
|
||||
t = test_env_async.from_string('{% include ["missing", "missing2"] %}')
|
||||
pytest.raises(TemplateNotFound, t.render)
|
||||
with pytest.raises(TemplatesNotFound) as e:
|
||||
t.render()
|
||||
|
||||
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]"
|
||||
|
||||
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, "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() == ""
|
||||
|
||||
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 }}",
|
||||
)
|
||||
)
|
||||
)
|
||||
assert env.get_template("main").render() == "123"
|
||||
|
||||
def test_unoptimized_scopes(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
"""
|
||||
{% macro outer(o) %}
|
||||
{% macro inner() %}
|
||||
{% include "o_printer" %}
|
||||
{% endmacro %}
|
||||
{{ inner() }}
|
||||
{% endmacro %}
|
||||
{{ outer("FOO") }}
|
||||
"""
|
||||
)
|
||||
assert t.render().strip() == "(FOO)"
|
||||
|
||||
def test_unoptimized_scopes_autoescape(self):
|
||||
env = Environment(
|
||||
loader=DictLoader({"o_printer": "({{ o }})"}),
|
||||
autoescape=True,
|
||||
enable_async=True,
|
||||
)
|
||||
t = env.from_string(
|
||||
"""
|
||||
{% macro outer(o) %}
|
||||
{% macro inner() %}
|
||||
{% include "o_printer" %}
|
||||
{% endmacro %}
|
||||
{{ inner() }}
|
||||
{% endmacro %}
|
||||
{{ outer("FOO") }}
|
||||
"""
|
||||
)
|
||||
assert t.render().strip() == "(FOO)"
|
||||
|
||||
|
||||
class TestAsyncForLoop:
|
||||
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"
|
||||
|
||||
def test_else(self, test_env_async):
|
||||
tmpl = test_env_async.from_string(
|
||||
"{% 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() == "<>"
|
||||
|
||||
@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 %}{{
|
||||
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
|
||||
|
||||
def test_lookaround(self, test_env_async):
|
||||
tmpl = test_env_async.from_string(
|
||||
"""{% for item in seq -%}
|
||||
{{ loop.previtem|default('x') }}-{{ item }}-{{
|
||||
loop.nextitem|default('x') }}|
|
||||
{%- endfor %}"""
|
||||
)
|
||||
output = tmpl.render(seq=list(range(4)))
|
||||
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 -%}
|
||||
{{ loop.changed(item) }},
|
||||
{%- 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,"
|
||||
|
||||
def test_scope(self, test_env_async):
|
||||
tmpl = test_env_async.from_string("{% for item in seq %}{% endfor %}{{ item }}")
|
||||
output = tmpl.render(seq=list(range(10)))
|
||||
assert not output
|
||||
|
||||
def test_varlen(self, test_env_async):
|
||||
def inner():
|
||||
yield from range(5)
|
||||
|
||||
tmpl = test_env_async.from_string(
|
||||
"{% for item in iter %}{{ item }}{% endfor %}"
|
||||
)
|
||||
output = tmpl.render(iter=inner())
|
||||
assert output == "01234"
|
||||
|
||||
def test_noniter(self, test_env_async):
|
||||
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 -%}
|
||||
[{{ 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]>]"
|
||||
)
|
||||
|
||||
def test_recursive_lookaround(self, test_env_async):
|
||||
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]>]"
|
||||
)
|
||||
|
||||
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]>]"
|
||||
)
|
||||
|
||||
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]>]"
|
||||
)
|
||||
|
||||
def test_looploop(self, test_env_async):
|
||||
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]"
|
||||
|
||||
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"
|
||||
|
||||
def test_loop_errors(self, test_env_async):
|
||||
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() == ""
|
||||
|
||||
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(
|
||||
"""
|
||||
{%- 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]"
|
||||
|
||||
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]"
|
||||
|
||||
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"
|
||||
|
||||
def test_recursive_empty_loop_iter(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
"""
|
||||
{%- for item in foo recursive -%}{%- endfor -%}
|
||||
"""
|
||||
)
|
||||
assert t.render(dict(foo=[])) == ""
|
||||
|
||||
def test_call_in_loop(self, test_env_async):
|
||||
t = test_env_async.from_string(
|
||||
"""
|
||||
{%- macro do_something() -%}
|
||||
[{{ caller() }}]
|
||||
{%- endmacro %}
|
||||
|
||||
{%- for i in [1, 2, 3] %}
|
||||
{%- call do_something() -%}
|
||||
{{ i }}
|
||||
{%- endcall %}
|
||||
{%- endfor -%}
|
||||
"""
|
||||
)
|
||||
assert t.render() == "[1][2][3]"
|
||||
|
||||
def test_scoping_bug(self, test_env_async):
|
||||
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..."
|
||||
|
||||
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"
|
||||
|
||||
def test_recursive_loop_filter(self, test_env_async):
|
||||
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 %}
|
||||
<url><loc>{{ page.url }}</loc></url>
|
||||
{{- loop(page.children) }}
|
||||
{%- endfor %}
|
||||
</urlset>
|
||||
"""
|
||||
)
|
||||
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>",
|
||||
]
|
||||
|
||||
def test_nonrecursive_loop_filter(self, test_env_async):
|
||||
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"}]
|
||||
)
|
||||
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>",
|
||||
]
|
||||
|
||||
def test_bare_async(self, test_env_async):
|
||||
t = test_env_async.from_string('{% extends "header" %}')
|
||||
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"
|
||||
|
||||
asyncio.run(_test())
|
||||
|
||||
|
||||
def test_chainable_undefined_aiter():
|
||||
async def _test():
|
||||
t = Template(
|
||||
"{% for x in a['b']['c'] %}{{ x }}{% endfor %}",
|
||||
enable_async=True,
|
||||
undefined=ChainableUndefined,
|
||||
)
|
||||
rv = await t.render_async(a={})
|
||||
assert rv == ""
|
||||
|
||||
asyncio.run(_test())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_native_env():
|
||||
return NativeEnvironment(enable_async=True)
|
||||
|
||||
|
||||
def test_native_async(async_native_env):
|
||||
async def _test():
|
||||
t = async_native_env.from_string("{{ x }}")
|
||||
rv = await t.render_async(x=23)
|
||||
assert rv == 23
|
||||
|
||||
asyncio.run(_test())
|
||||
|
||||
|
||||
def test_native_list_async(async_native_env):
|
||||
async def _test():
|
||||
t = async_native_env.from_string("{{ x }}")
|
||||
rv = await t.render_async(x=list(range(3)))
|
||||
assert rv == [0, 1, 2]
|
||||
|
||||
asyncio.run(_test())
|
||||
|
||||
|
||||
def test_getitem_after_filter():
|
||||
env = Environment(enable_async=True)
|
||||
env.filters["add_each"] = lambda v, x: [i + x for i in v]
|
||||
t = env.from_string("{{ (a|add_each(2))[1:] }}")
|
||||
out = t.render(a=range(3))
|
||||
assert out == "[3, 4]"
|
||||
|
||||
|
||||
def test_getitem_after_call():
|
||||
env = Environment(enable_async=True)
|
||||
env.globals["add_each"] = lambda v, x: [i + x for i in v]
|
||||
t = env.from_string("{{ add_each(a, 2)[1:] }}")
|
||||
out = t.render(a=range(3))
|
||||
assert out == "[3, 4]"
|
||||
@@ -1,273 +0,0 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from markupsafe import Markup
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2.async_utils import auto_aiter
|
||||
|
||||
|
||||
async def make_aiter(iter):
|
||||
for item in iter:
|
||||
yield item
|
||||
|
||||
|
||||
def mark_dualiter(parameter, factory):
|
||||
def decorator(f):
|
||||
return pytest.mark.parametrize(
|
||||
parameter, [lambda: factory(), lambda: make_aiter(factory())]
|
||||
)(f)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env_async():
|
||||
return Environment(enable_async=True)
|
||||
|
||||
|
||||
@mark_dualiter("foo", lambda: range(10))
|
||||
def test_first(env_async, foo):
|
||||
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},
|
||||
],
|
||||
)
|
||||
def test_groupby(env_async, items):
|
||||
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("|") == [
|
||||
"1: 1, 2: 1, 1",
|
||||
"2: 2, 3",
|
||||
"3: 3, 4",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("case_sensitive", "expect"),
|
||||
[
|
||||
(False, "a: 1, 3\nb: 2\n"),
|
||||
(True, "A: 3\na: 1\nb: 2\n"),
|
||||
],
|
||||
)
|
||||
def test_groupby_case(env_async, case_sensitive, expect):
|
||||
tmpl = env_async.from_string(
|
||||
"{% for k, vs in data|groupby('k', case_sensitive=cs) %}"
|
||||
"{{ k }}: {{ vs|join(', ', attribute='v') }}\n"
|
||||
"{% endfor %}"
|
||||
)
|
||||
out = tmpl.render(
|
||||
data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}],
|
||||
cs=case_sensitive,
|
||||
)
|
||||
assert out == expect
|
||||
|
||||
|
||||
@mark_dualiter("items", lambda: [("a", 1), ("a", 2), ("b", 1)])
|
||||
def test_groupby_tuple_index(env_async, items):
|
||||
tmpl = env_async.from_string(
|
||||
"""
|
||||
{%- 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|"
|
||||
|
||||
|
||||
def make_articles():
|
||||
Date = namedtuple("Date", "day,month,year")
|
||||
Article = namedtuple("Article", "title,date")
|
||||
return [
|
||||
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)
|
||||
def test_groupby_multidot(env_async, articles):
|
||||
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]",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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>"
|
||||
|
||||
|
||||
def make_users():
|
||||
User = namedtuple("User", "username")
|
||||
return map(User, ["foo", "bar"])
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
def make_users(): # type: ignore
|
||||
User = namedtuple("User", "name,is_active")
|
||||
return [
|
||||
User("john", True),
|
||||
User("jane", True),
|
||||
User("mike", False),
|
||||
]
|
||||
|
||||
|
||||
@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("|") }}'
|
||||
)
|
||||
assert tmpl.render(users=users) == "john|jane"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
def test_empty_map(env_async):
|
||||
tmpl = env_async.from_string('{{ none|map("upper")|list }}')
|
||||
assert tmpl.render() == "[]"
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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') }}""")
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@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 }}"
|
||||
)
|
||||
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']]"
|
||||
)
|
||||
|
||||
|
||||
def test_custom_async_filter(env_async):
|
||||
async def customfilter(val):
|
||||
return str(val)
|
||||
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string("{{ 'static'|customfilter }} {{ arg|customfilter }}")
|
||||
out = tmpl.render(arg="dynamic")
|
||||
assert out == "static dynamic"
|
||||
|
||||
|
||||
@mark_dualiter("items", lambda: range(10))
|
||||
def test_custom_async_iteratable_filter(env_async, items):
|
||||
async def customfilter(iterable):
|
||||
items = []
|
||||
async for item in auto_aiter(iterable):
|
||||
items.append(str(item))
|
||||
if len(items) == 3:
|
||||
break
|
||||
return ",".join(items)
|
||||
|
||||
env_async.filters["customfilter"] = customfilter
|
||||
tmpl = env_async.from_string(
|
||||
"{{ items()|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
|
||||
)
|
||||
out = tmpl.render(items=items)
|
||||
assert out == "0,1,2 .. 3,4,5"
|
||||
@@ -1,77 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from jinja2 import Environment
|
||||
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, tmp_path):
|
||||
bytecode_cache = FileSystemBytecodeCache(str(tmp_path))
|
||||
return Environment(loader=package_loader, bytecode_cache=bytecode_cache)
|
||||
|
||||
|
||||
class TestByteCodeCache:
|
||||
def test_simple(self, env):
|
||||
tmpl = env.get_template("test.html")
|
||||
assert tmpl.render().strip() == "BAR"
|
||||
pytest.raises(TemplateNotFound, env.get_template, "missing.html")
|
||||
|
||||
|
||||
class MockMemcached:
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
key = None
|
||||
value = None
|
||||
timeout = None
|
||||
|
||||
def get(self, key):
|
||||
return self.value
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.timeout = timeout
|
||||
|
||||
def get_side_effect(self, key):
|
||||
raise self.Error()
|
||||
|
||||
def set_side_effect(self, *args):
|
||||
raise self.Error()
|
||||
|
||||
|
||||
class TestMemcachedBytecodeCache:
|
||||
def test_dump_load(self):
|
||||
memcached = MockMemcached()
|
||||
m = MemcachedBytecodeCache(memcached)
|
||||
|
||||
b = Bucket(None, "key", "")
|
||||
b.code = "code"
|
||||
m.dump_bytecode(b)
|
||||
assert memcached.key == "jinja2/bytecode/key"
|
||||
|
||||
b = Bucket(None, "key", "")
|
||||
m.load_bytecode(b)
|
||||
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"
|
||||
|
||||
m.dump_bytecode(b)
|
||||
m.load_bytecode(b)
|
||||
|
||||
m.ignore_memcache_errors = False
|
||||
|
||||
with pytest.raises(MockMemcached.Error):
|
||||
m.dump_bytecode(b)
|
||||
|
||||
with pytest.raises(MockMemcached.Error):
|
||||
m.load_bytecode(b)
|
||||
@@ -1,28 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from jinja2.environment import Environment
|
||||
from jinja2.loaders import DictLoader
|
||||
|
||||
|
||||
def test_filters_deterministic(tmp_path):
|
||||
src = "".join(f"{{{{ {i}|filter{i} }}}}" for i in range(10))
|
||||
env = Environment(loader=DictLoader({"foo": src}))
|
||||
env.filters.update(dict.fromkeys((f"filter{i}" for i in range(10)), lambda: None))
|
||||
env.compile_templates(tmp_path, zip=None)
|
||||
name = os.listdir(tmp_path)[0]
|
||||
content = (tmp_path / name).read_text("utf8")
|
||||
expect = [f"filters['filter{i}']" for i in range(10)]
|
||||
found = re.findall(r"filters\['filter\d']", content)
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_import_as_with_context_deterministic(tmp_path):
|
||||
src = "\n".join(f'{{% import "bar" as bar{i} with context %}}' for i in range(10))
|
||||
env = Environment(loader=DictLoader({"foo": src}))
|
||||
env.compile_templates(tmp_path, zip=None)
|
||||
name = os.listdir(tmp_path)[0]
|
||||
content = (tmp_path / name).read_text("utf8")
|
||||
expect = [f"'bar{i}': " for i in range(10)]
|
||||
found = re.findall(r"'bar\d': ", content)[:10]
|
||||
assert found == expect
|
||||
@@ -1,595 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from jinja2 import UndefinedError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env_trim():
|
||||
return Environment(trim_blocks=True)
|
||||
|
||||
|
||||
class TestForLoop:
|
||||
def test_simple(self, env):
|
||||
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() == "..."
|
||||
|
||||
def test_else_scoping_item(self, env):
|
||||
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() == "<>"
|
||||
|
||||
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 -%}
|
||||
{{ 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"
|
||||
|
||||
def test_cycling(self, env):
|
||||
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
|
||||
|
||||
def test_lookaround(self, env):
|
||||
tmpl = env.from_string(
|
||||
"""{% for item in seq -%}
|
||||
{{ loop.previtem|default('x') }}-{{ item }}-{{
|
||||
loop.nextitem|default('x') }}|
|
||||
{%- endfor %}"""
|
||||
)
|
||||
output = tmpl.render(seq=list(range(4)))
|
||||
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 -%}
|
||||
{{ loop.changed(item) }},
|
||||
{%- 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,"
|
||||
|
||||
def test_scope(self, env):
|
||||
tmpl = env.from_string("{% for item in seq %}{% endfor %}{{ item }}")
|
||||
output = tmpl.render(seq=list(range(10)))
|
||||
assert not output
|
||||
|
||||
def test_varlen(self, env):
|
||||
tmpl = env.from_string("{% for item in iter %}{{ item }}{% endfor %}")
|
||||
output = tmpl.render(iter=range(5))
|
||||
assert output == "01234"
|
||||
|
||||
def test_noniter(self, env):
|
||||
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 -%}
|
||||
[{{ 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]>]"
|
||||
)
|
||||
|
||||
def test_recursive_lookaround(self, env):
|
||||
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]>]"
|
||||
)
|
||||
|
||||
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]>]"
|
||||
)
|
||||
|
||||
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]>]"
|
||||
)
|
||||
|
||||
def test_looploop(self, env):
|
||||
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]"
|
||||
|
||||
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"
|
||||
|
||||
def test_loop_errors(self, env):
|
||||
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() == ""
|
||||
|
||||
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(
|
||||
"""
|
||||
{%- 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]"
|
||||
|
||||
def test_loop_unassignable(self, env):
|
||||
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]"
|
||||
|
||||
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"
|
||||
|
||||
def test_recursive_empty_loop_iter(self, env):
|
||||
t = env.from_string(
|
||||
"""
|
||||
{%- for item in foo recursive -%}{%- endfor -%}
|
||||
"""
|
||||
)
|
||||
assert t.render(dict(foo=[])) == ""
|
||||
|
||||
def test_call_in_loop(self, env):
|
||||
t = env.from_string(
|
||||
"""
|
||||
{%- macro do_something() -%}
|
||||
[{{ caller() }}]
|
||||
{%- endmacro %}
|
||||
|
||||
{%- for i in [1, 2, 3] %}
|
||||
{%- call do_something() -%}
|
||||
{{ i }}
|
||||
{%- endcall %}
|
||||
{%- endfor -%}
|
||||
"""
|
||||
)
|
||||
assert t.render() == "[1][2][3]"
|
||||
|
||||
def test_scoping_bug(self, env):
|
||||
t = env.from_string(
|
||||
"""
|
||||
{%- for item in foo %}...{{ item }}...{% endfor %}
|
||||
{%- macro item(a) %}...{{ a }}...{% endmacro %}
|
||||
{{- item(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"
|
||||
|
||||
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(
|
||||
"{% set x = 9 %}{% for item in seq %}{{ x }}"
|
||||
"{% set x = item %}{{ x }}{% endfor %}"
|
||||
)
|
||||
assert tmpl.render(x=0, seq=[1, 2, 3]) == "919293"
|
||||
|
||||
|
||||
class TestIfCondition:
|
||||
def test_simple(self, env):
|
||||
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() == "..."
|
||||
|
||||
def test_elif_deep(self, env):
|
||||
elifs = "\n".join(f"{{% elif a == {i} %}}{i}" for i in range(1, 1000))
|
||||
tmpl = env.from_string(f"{{% if a == 0 %}}0{elifs}{{% else %}}x{{% endif %}}")
|
||||
for x in (0, 10, 999):
|
||||
assert tmpl.render(a=x).strip() == str(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() == "..."
|
||||
|
||||
def test_empty(self, env):
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TestMacros:
|
||||
def test_simple(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% macro say_hello(name) %}Hello {{ name }}!{% endmacro %}
|
||||
{{ say_hello('Peter') }}"""
|
||||
)
|
||||
assert tmpl.render() == "Hello Peter!"
|
||||
|
||||
def test_scoping(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% macro level1(data1) %}
|
||||
{% macro level2(data2) %}{{ data1 }}|{{ data2 }}{% endmacro %}
|
||||
{{ level2('bar') }}{% endmacro %}
|
||||
{{ level1('foo') }}"""
|
||||
)
|
||||
assert tmpl.render() == "foo|bar"
|
||||
|
||||
def test_arguments(self, env_trim):
|
||||
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"
|
||||
|
||||
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 %}""",
|
||||
)
|
||||
|
||||
def test_caller_defaults_nonsense(self, env_trim):
|
||||
pytest.raises(
|
||||
TemplateSyntaxError,
|
||||
env_trim.from_string,
|
||||
"""\
|
||||
{% macro a() %}{{ caller() }}{% endmacro %}
|
||||
{% call(x, y=1, z) a() %}{% endcall %}""",
|
||||
)
|
||||
|
||||
def test_varargs(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% macro test() %}{{ varargs|join('|') }}{% endmacro %}\
|
||||
{{ test(1, 2, 3) }}"""
|
||||
)
|
||||
assert tmpl.render() == "1|2|3"
|
||||
|
||||
def test_simple_call(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% macro test() %}[[{{ caller() }}]]{% endmacro %}\
|
||||
{% call test() %}data{% endcall %}"""
|
||||
)
|
||||
assert tmpl.render() == "[[data]]"
|
||||
|
||||
def test_complex_call(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% macro test() %}[[{{ caller('data') }}]]{% endmacro %}\
|
||||
{% call(data) test() %}{{ data }}{% endcall %}"""
|
||||
)
|
||||
assert tmpl.render() == "[[data]]"
|
||||
|
||||
def test_caller_undefined(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"""\
|
||||
{% set caller = 42 %}\
|
||||
{% macro test() %}{{ caller is not defined }}{% endmacro %}\
|
||||
{{ test() }}"""
|
||||
)
|
||||
assert tmpl.render() == "True"
|
||||
|
||||
def test_include(self, env_trim):
|
||||
env_trim = Environment(
|
||||
loader=DictLoader(
|
||||
{"include": "{% macro test(foo) %}[{{ foo }}]{% endmacro %}"}
|
||||
)
|
||||
)
|
||||
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"
|
||||
assert not tmpl.module.foo.caller
|
||||
assert not tmpl.module.foo.catch_kwargs
|
||||
assert not tmpl.module.foo.catch_varargs
|
||||
assert tmpl.module.bar.arguments == ()
|
||||
assert not tmpl.module.bar.caller
|
||||
assert tmpl.module.bar.catch_kwargs
|
||||
assert tmpl.module.bar.catch_varargs
|
||||
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"
|
||||
|
||||
def test_macro_defaults_self_ref(self, env):
|
||||
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"
|
||||
|
||||
|
||||
class TestSet:
|
||||
def test_normal(self, env_trim):
|
||||
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 == "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>"
|
||||
|
||||
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 %}")
|
||||
exc_info = pytest.raises(TemplateRuntimeError, tmpl.render, foo={})
|
||||
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
|
||||
|
||||
def test_namespace(self, env_trim):
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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>"
|
||||
|
||||
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 == "2"
|
||||
|
||||
def test_block_filtered_set(self, env_trim):
|
||||
def _myfilter(val, arg):
|
||||
assert arg == " xxx "
|
||||
return val
|
||||
|
||||
env_trim.filters["myfilter"] = _myfilter
|
||||
tmpl = env_trim.from_string(
|
||||
'{% set a = " xxx " %}'
|
||||
"{% set foo | myfilter(a) | trim | length | string %}"
|
||||
' {% set b = " yy " %} 42 {{ a }}{{ b }} '
|
||||
"{% endset %}"
|
||||
"{{ foo }}"
|
||||
)
|
||||
assert tmpl.render() == "11"
|
||||
assert tmpl.module.foo == "11"
|
||||
|
||||
|
||||
class TestWith:
|
||||
def test_with(self, env):
|
||||
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",
|
||||
]
|
||||
|
||||
def test_with_argument_scoping(self, env):
|
||||
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"
|
||||
@@ -1,117 +0,0 @@
|
||||
import pickle
|
||||
import re
|
||||
from traceback import format_exception
|
||||
|
||||
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."""
|
||||
return Environment(loader=filesystem_loader)
|
||||
|
||||
|
||||
class TestDebug:
|
||||
def assert_traceback_matches(self, callback, expected_tb):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
callback()
|
||||
|
||||
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
|
||||
), f"Traceback did not match:\n\n{''.join(tb)}\nexpected:\n{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"""
|
||||
File ".*?broken.html", line 2, in (top-level template code|<module>)
|
||||
\{\{ fail\(\) \}\}(
|
||||
\^{12})?
|
||||
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):
|
||||
# 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'.
|
||||
""",
|
||||
)
|
||||
|
||||
def test_regular_syntax_error(self, fs_env):
|
||||
def test():
|
||||
raise TemplateSyntaxError("wtf", 42)
|
||||
|
||||
self.assert_traceback_matches(
|
||||
test,
|
||||
r"""
|
||||
File ".*debug.pyc?", line \d+, in test
|
||||
raise TemplateSyntaxError\("wtf", 42\)(
|
||||
\^{36})?
|
||||
(jinja2\.exceptions\.)?TemplateSyntaxError: wtf
|
||||
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_template_locals
|
||||
from jinja2.runtime import missing
|
||||
|
||||
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
|
||||
@@ -1,726 +0,0 @@
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import nodes
|
||||
from jinja2 import pass_context
|
||||
from jinja2.exceptions import TemplateAssertionError
|
||||
from jinja2.ext import Extension
|
||||
from jinja2.lexer import count_newlines
|
||||
from jinja2.lexer import Token
|
||||
|
||||
importable_object = 23
|
||||
|
||||
_gettext_re = re.compile(r"_\((.*?)\)", re.DOTALL)
|
||||
|
||||
|
||||
i18n_templates = {
|
||||
"default.html": '<title>{{ page_title|default(_("missing")) }}</title>'
|
||||
"{% block body %}{% endblock %}",
|
||||
"child.html": '{% extends "default.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 = {
|
||||
"default.html": '<title>{{ page_title|default(_("missing")) }}</title>'
|
||||
"{% block body %}{% endblock %}",
|
||||
"child.html": '{% extends "default.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 %}",
|
||||
"pgettext.html": '{{ pgettext("fruit", "Apple") }}',
|
||||
"npgettext.html": '{{ npgettext("fruit", "%(num)s apple", "%(num)s apples",'
|
||||
" apples) }}",
|
||||
"pgettext_block": "{% trans 'fruit' num=apples %}Apple{% endtrans %}",
|
||||
"npgettext_block": "{% trans 'fruit' num=apples %}{{ num }} apple"
|
||||
"{% pluralize %}{{ num }} apples{% endtrans %}",
|
||||
"transvars1.html": "{% trans %}User: {{ num }}{% endtrans %}",
|
||||
"transvars2.html": "{% trans num=count %}User: {{ num }}{% endtrans %}",
|
||||
"transvars3.html": "{% trans count=num %}User: {{ count }}{% endtrans %}",
|
||||
"novars.html": "{% trans %}%(hello)s{% endtrans %}",
|
||||
"vars.html": "{% trans %}{{ foo }}%(foo)s{% endtrans %}",
|
||||
"explicitvars.html": '{% trans foo="42" %}%(foo)s{% endtrans %}',
|
||||
}
|
||||
|
||||
|
||||
languages = {
|
||||
"de": {
|
||||
"missing": "fehlend",
|
||||
"watch out": "pass auf",
|
||||
"One user online": "Ein Benutzer online",
|
||||
"%(user_count)s users online": "%(user_count)s Benutzer online",
|
||||
"User: %(num)s": "Benutzer: %(num)s",
|
||||
"User: %(count)s": "Benutzer: %(count)s",
|
||||
"Apple": {None: "Apfel", "fruit": "Apple"},
|
||||
"%(num)s apple": {None: "%(num)s Apfel", "fruit": "%(num)s Apple"},
|
||||
"%(num)s apples": {None: "%(num)s Äpfel", "fruit": "%(num)s Apples"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _get_with_context(value, ctx=None):
|
||||
if isinstance(value, dict):
|
||||
return value.get(ctx, value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@pass_context
|
||||
def gettext(context, string):
|
||||
language = context.get("LANGUAGE", "en")
|
||||
value = languages.get(language, {}).get(string, string)
|
||||
return _get_with_context(value)
|
||||
|
||||
|
||||
@pass_context
|
||||
def ngettext(context, s, p, n):
|
||||
language = context.get("LANGUAGE", "en")
|
||||
|
||||
if n != 1:
|
||||
value = languages.get(language, {}).get(p, p)
|
||||
return _get_with_context(value)
|
||||
|
||||
value = languages.get(language, {}).get(s, s)
|
||||
return _get_with_context(value)
|
||||
|
||||
|
||||
@pass_context
|
||||
def pgettext(context, c, s):
|
||||
language = context.get("LANGUAGE", "en")
|
||||
value = languages.get(language, {}).get(s, s)
|
||||
return _get_with_context(value, c)
|
||||
|
||||
|
||||
@pass_context
|
||||
def npgettext(context, c, s, p, n):
|
||||
language = context.get("LANGUAGE", "en")
|
||||
|
||||
if n != 1:
|
||||
value = languages.get(language, {}).get(p, p)
|
||||
return _get_with_context(value, c)
|
||||
|
||||
value = languages.get(language, {}).get(s, s)
|
||||
return _get_with_context(value, c)
|
||||
|
||||
|
||||
i18n_env = Environment(
|
||||
loader=DictLoader(i18n_templates), extensions=["jinja2.ext.i18n"]
|
||||
)
|
||||
i18n_env.globals.update(
|
||||
{
|
||||
"_": gettext,
|
||||
"gettext": gettext,
|
||||
"ngettext": ngettext,
|
||||
"pgettext": pgettext,
|
||||
"npgettext": npgettext,
|
||||
}
|
||||
)
|
||||
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,
|
||||
"pgettext": pgettext,
|
||||
"npgettext": npgettext,
|
||||
}
|
||||
)
|
||||
|
||||
newstyle_i18n_env = Environment(
|
||||
loader=DictLoader(newstyle_i18n_templates), extensions=["jinja2.ext.i18n"]
|
||||
)
|
||||
newstyle_i18n_env.install_gettext_callables( # type: ignore
|
||||
gettext, ngettext, newstyle=True, pgettext=pgettext, npgettext=npgettext
|
||||
)
|
||||
|
||||
|
||||
class ExampleExtension(Extension):
|
||||
tags = {"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"),
|
||||
self.context_reference_node_cls(),
|
||||
],
|
||||
)
|
||||
]
|
||||
).set_lineno(next(parser.stream).lineno)
|
||||
|
||||
def _dump(self, sandboxed, ext_attr, imported_object, context):
|
||||
return (
|
||||
f"{sandboxed}|{ext_attr}|{imported_object}|{context.blocks}"
|
||||
f"|{context.get('test_var')}"
|
||||
)
|
||||
|
||||
|
||||
class DerivedExampleExtension(ExampleExtension):
|
||||
context_reference_node_cls = nodes.DerivedContextReference # type: ignore
|
||||
|
||||
|
||||
class PreprocessorExtension(Extension):
|
||||
def preprocess(self, source, name, filename=None):
|
||||
return source.replace("[[TEST]]", "({{ foo }})")
|
||||
|
||||
|
||||
class StreamFilterExtension(Extension):
|
||||
def filter_stream(self, stream):
|
||||
for token in stream:
|
||||
if token.type == "data":
|
||||
yield from self.interpolate(token)
|
||||
else:
|
||||
yield token
|
||||
|
||||
def interpolate(self, token):
|
||||
pos = 0
|
||||
end = len(token.value)
|
||||
lineno = token.lineno
|
||||
while True:
|
||||
match = _gettext_re.search(token.value, pos)
|
||||
if match is None:
|
||||
break
|
||||
value = token.value[pos : match.start()]
|
||||
if 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)
|
||||
pos = match.end()
|
||||
if pos < end:
|
||||
yield Token(lineno, "data", token.value[pos:])
|
||||
|
||||
|
||||
class TestExtensions:
|
||||
def test_extend_late(self):
|
||||
env = Environment()
|
||||
t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
|
||||
assert t.render() == "<test>"
|
||||
|
||||
def test_loop_controls(self):
|
||||
env = Environment(extensions=["jinja2.ext.loopcontrols"])
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
{%- for item in [1, 2, 3, 4] %}
|
||||
{%- if item % 2 == 0 %}{% continue %}{% endif -%}
|
||||
{{ item }}
|
||||
{%- endfor %}"""
|
||||
)
|
||||
assert tmpl.render() == "13"
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
{%- for item in [1, 2, 3, 4] %}
|
||||
{%- if item > 2 %}{% break %}{% endif -%}
|
||||
{{ item }}
|
||||
{%- endfor %}"""
|
||||
)
|
||||
assert tmpl.render() == "12"
|
||||
|
||||
def test_do(self):
|
||||
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"
|
||||
|
||||
def test_extension_nodes(self):
|
||||
env = Environment(extensions=[ExampleExtension])
|
||||
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"
|
||||
|
||||
def test_rebinding(self):
|
||||
original = Environment(extensions=[ExampleExtension])
|
||||
overlay = original.overlay()
|
||||
for env in original, overlay:
|
||||
for ext in env.extensions.values():
|
||||
assert ext.environment is env
|
||||
|
||||
def test_preprocessor_extension(self):
|
||||
env = Environment(extensions=[PreprocessorExtension])
|
||||
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")
|
||||
out = tmpl.render()
|
||||
assert out == "Foo BAR Baz"
|
||||
|
||||
def test_extension_ordering(self):
|
||||
class T1(Extension):
|
||||
priority = 1
|
||||
|
||||
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 f"'{value}'" in out
|
||||
|
||||
|
||||
class TestInternationalization:
|
||||
def test_trans(self):
|
||||
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"
|
||||
|
||||
def test_trans_plural_with_functions(self):
|
||||
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 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 %}",
|
||||
)
|
||||
|
||||
def test_trans_stringformatting(self):
|
||||
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"
|
||||
|
||||
def test_trimmed_policy(self):
|
||||
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"
|
||||
|
||||
def test_trimmed_policy_override(self):
|
||||
tmpl = i18n_env_trimmed.from_string(
|
||||
"{%- 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"
|
||||
|
||||
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 "
|
||||
|
||||
def test_extract(self):
|
||||
from jinja2.ext import babel_extract
|
||||
|
||||
source = BytesIO(
|
||||
b"""
|
||||
{{ gettext('Hello World') }}
|
||||
{% trans %}Hello World{% endtrans %}
|
||||
{% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
|
||||
"""
|
||||
)
|
||||
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
|
||||
(2, "gettext", "Hello World", []),
|
||||
(3, "gettext", "Hello World", []),
|
||||
(4, "ngettext", ("%(users)s user", "%(users)s users", None), []),
|
||||
]
|
||||
|
||||
def test_extract_trimmed(self):
|
||||
from jinja2.ext import babel_extract
|
||||
|
||||
source = BytesIO(
|
||||
b"""
|
||||
{{ gettext(' Hello \n World') }}
|
||||
{% trans trimmed %} Hello \n World{% endtrans %}
|
||||
{% trans trimmed %}{{ users }} \n user
|
||||
{%- pluralize %}{{ users }} \n users{% endtrans %}
|
||||
"""
|
||||
)
|
||||
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], {})) == [
|
||||
(2, "gettext", " Hello \n World", []),
|
||||
(4, "gettext", "Hello World", []),
|
||||
(6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
|
||||
]
|
||||
|
||||
def test_extract_trimmed_option(self):
|
||||
from jinja2.ext import babel_extract
|
||||
|
||||
source = BytesIO(
|
||||
b"""
|
||||
{{ gettext(' Hello \n World') }}
|
||||
{% trans %} Hello \n World{% endtrans %}
|
||||
{% trans %}{{ users }} \n user
|
||||
{%- pluralize %}{{ users }} \n users{% endtrans %}
|
||||
"""
|
||||
)
|
||||
opts = {"trimmed": "true"}
|
||||
assert list(babel_extract(source, ("gettext", "ngettext", "_"), [], opts)) == [
|
||||
(2, "gettext", " Hello \n World", []),
|
||||
(4, "gettext", "Hello World", []),
|
||||
(6, "ngettext", ("%(users)s user", "%(users)s users", None), []),
|
||||
]
|
||||
|
||||
def test_comment_extract(self):
|
||||
from jinja2.ext import babel_extract
|
||||
|
||||
source = BytesIO(
|
||||
b"""
|
||||
{# trans first #}
|
||||
{{ gettext('Hello World') }}
|
||||
{% trans %}Hello World{% endtrans %}{# trans second #}
|
||||
{#: third #}
|
||||
{% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
|
||||
"""
|
||||
)
|
||||
assert list(
|
||||
babel_extract(source, ("gettext", "ngettext", "_"), ["trans", ":"], {})
|
||||
) == [
|
||||
(3, "gettext", "Hello World", ["first"]),
|
||||
(4, "gettext", "Hello World", ["second"]),
|
||||
(6, "ngettext", ("%(users)s user", "%(users)s users", None), ["third"]),
|
||||
]
|
||||
|
||||
def test_extract_context(self):
|
||||
from jinja2.ext import babel_extract
|
||||
|
||||
source = BytesIO(
|
||||
b"""
|
||||
{{ pgettext("babel", "Hello World") }}
|
||||
{{ npgettext("babel", "%(users)s user", "%(users)s users", users) }}
|
||||
"""
|
||||
)
|
||||
assert list(babel_extract(source, ("pgettext", "npgettext", "_"), [], {})) == [
|
||||
(2, "pgettext", ("babel", "Hello World"), []),
|
||||
(3, "npgettext", ("babel", "%(users)s user", "%(users)s users", None), []),
|
||||
]
|
||||
|
||||
|
||||
class TestScope:
|
||||
def test_basic_scope_behavior(self):
|
||||
# This is what the old with statement compiled down to
|
||||
class ScopeExt(Extension):
|
||||
tags = {"scope"}
|
||||
|
||||
def parse(self, parser):
|
||||
node = nodes.Scope(lineno=next(parser.stream).lineno)
|
||||
assignments = []
|
||||
while parser.stream.current.type != "block_end":
|
||||
lineno = parser.stream.current.lineno
|
||||
if assignments:
|
||||
parser.stream.expect("comma")
|
||||
target = parser.parse_assign_target()
|
||||
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)
|
||||
)
|
||||
return node
|
||||
|
||||
env = Environment(extensions=[ScopeExt])
|
||||
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"
|
||||
|
||||
|
||||
class TestNewstyleInternationalization:
|
||||
def test_trans(self):
|
||||
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"
|
||||
|
||||
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 %}",
|
||||
)
|
||||
|
||||
def test_trans_stringformatting(self):
|
||||
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) == "5 Äpfel"
|
||||
|
||||
def test_autoescape_support(self):
|
||||
env = Environment(extensions=["jinja2.ext.i18n"])
|
||||
env.install_gettext_callables(
|
||||
lambda x: "<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)
|
||||
template = (
|
||||
"{% macro m() %}<html>{% endmacro %}"
|
||||
"{% autoescape true %}{{ m() }}{% endautoescape %}"
|
||||
)
|
||||
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") == "5 Äpfel"
|
||||
|
||||
def test_num_called_num(self):
|
||||
source = newstyle_i18n_env.compile(
|
||||
"""
|
||||
{% trans num=3 %}{{ num }} apple{% pluralize
|
||||
%}{{ num }} apples{% endtrans %}
|
||||
""",
|
||||
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 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"
|
||||
|
||||
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"
|
||||
|
||||
def test_context(self):
|
||||
tmpl = newstyle_i18n_env.get_template("pgettext.html")
|
||||
assert tmpl.render(LANGUAGE="de") == "Apple"
|
||||
|
||||
def test_context_plural(self):
|
||||
tmpl = newstyle_i18n_env.get_template("npgettext.html")
|
||||
assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple"
|
||||
assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples"
|
||||
|
||||
def test_context_block(self):
|
||||
tmpl = newstyle_i18n_env.get_template("pgettext_block")
|
||||
assert tmpl.render(LANGUAGE="de") == "Apple"
|
||||
|
||||
def test_context_plural_block(self):
|
||||
tmpl = newstyle_i18n_env.get_template("npgettext_block")
|
||||
assert tmpl.render(LANGUAGE="de", apples=1) == "1 Apple"
|
||||
assert tmpl.render(LANGUAGE="de", apples=5) == "5 Apples"
|
||||
|
||||
|
||||
class TestAutoEscape:
|
||||
def test_scoped_setting(self):
|
||||
env = Environment(autoescape=True)
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
{{ "<HelloWorld>" }}
|
||||
{% autoescape false %}
|
||||
{{ "<HelloWorld>" }}
|
||||
{% endautoescape %}
|
||||
{{ "<HelloWorld>" }}
|
||||
"""
|
||||
)
|
||||
assert tmpl.render().split() == [
|
||||
"<HelloWorld>",
|
||||
"<HelloWorld>",
|
||||
"<HelloWorld>",
|
||||
]
|
||||
|
||||
env = Environment(autoescape=False)
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
{{ "<HelloWorld>" }}
|
||||
{% autoescape true %}
|
||||
{{ "<HelloWorld>" }}
|
||||
{% endautoescape %}
|
||||
{{ "<HelloWorld>" }}
|
||||
"""
|
||||
)
|
||||
assert tmpl.render().split() == [
|
||||
"<HelloWorld>",
|
||||
"<HelloWorld>",
|
||||
"<HelloWorld>",
|
||||
]
|
||||
|
||||
def test_nonvolatile(self):
|
||||
env = Environment(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;""
|
||||
|
||||
def test_volatile(self):
|
||||
env = Environment(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()
|
||||
tmpl = env.from_string(
|
||||
'{% autoescape true %}{% set x = "<x>" %}{{ x }}'
|
||||
'{% endautoescape %}{{ x }}{{ "<y>" }}'
|
||||
)
|
||||
assert tmpl.render(x=1) == "<x>1<y>"
|
||||
|
||||
def test_volatile_scoping(self):
|
||||
env = Environment()
|
||||
tmplsource = """
|
||||
{% autoescape val %}
|
||||
{% macro foo(x) %}
|
||||
[{{ x }}]
|
||||
{% endmacro %}
|
||||
{{ foo().__class__.__name__ }}
|
||||
{% endautoescape %}
|
||||
{{ '<testing>' }}
|
||||
"""
|
||||
tmpl = env.from_string(tmplsource)
|
||||
assert tmpl.render(val=True).split()[0] == "Markup"
|
||||
assert tmpl.render(val=False).split()[0] == "str"
|
||||
|
||||
# looking at the source we should see <testing> there in raw
|
||||
# (and then escaped as well)
|
||||
env = Environment()
|
||||
pysource = env.compile(tmplsource, raw=True)
|
||||
assert "<testing>\\n" in pysource
|
||||
|
||||
env = Environment(autoescape=True)
|
||||
pysource = env.compile(tmplsource, raw=True)
|
||||
assert "<testing>\\n" in pysource
|
||||
|
||||
def test_overlay_scopes(self):
|
||||
class MagicScopeExtension(Extension):
|
||||
tags = {"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")
|
||||
return node
|
||||
|
||||
def get_scope(self):
|
||||
return {"x": [1, 2, 3]}
|
||||
|
||||
env = Environment(extensions=[MagicScopeExtension])
|
||||
|
||||
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"
|
||||
@@ -1,873 +0,0 @@
|
||||
import random
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from markupsafe import Markup
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2.exceptions import TemplateAssertionError
|
||||
|
||||
|
||||
class Magic:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
|
||||
class Magic2:
|
||||
def __init__(self, value1, value2):
|
||||
self.value1 = value1
|
||||
self.value2 = value2
|
||||
|
||||
def __str__(self):
|
||||
return f"({self.value1},{self.value2})"
|
||||
|
||||
|
||||
class TestFilter:
|
||||
def test_filter_calling(self, env):
|
||||
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"
|
||||
|
||||
def test_center(self, env):
|
||||
tmpl = env.from_string('{{ "foo"|center(9) }}')
|
||||
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)]"),
|
||||
),
|
||||
)
|
||||
def test_dictsort(self, env, args, expect):
|
||||
t = env.from_string(f"{{{{ foo|dictsort({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 }}")
|
||||
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']]"
|
||||
)
|
||||
|
||||
def test_slice(self, env):
|
||||
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']]"
|
||||
)
|
||||
|
||||
def test_escape(self, env):
|
||||
tmpl = env.from_string("""{{ '<">&'|escape }}""")
|
||||
out = tmpl.render()
|
||||
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"
|
||||
|
||||
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) }}"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
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) }}"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
def test_first(self, env):
|
||||
tmpl = env.from_string("{{ foo|first }}")
|
||||
out = tmpl.render(foo=list(range(10)))
|
||||
assert out == "0"
|
||||
|
||||
@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("{{ value|float }}")
|
||||
assert t.render(value=value) == 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') }}")
|
||||
out = tmpl.render()
|
||||
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):
|
||||
self._test_indent_multiline_template(env)
|
||||
t = env.from_string('{{ "jinja"|indent }}')
|
||||
assert t.render() == "jinja"
|
||||
t = env.from_string('{{ "jinja"|indent(first=true) }}')
|
||||
assert t.render() == " jinja"
|
||||
t = env.from_string('{{ "jinja"|indent(blank=true) }}')
|
||||
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)
|
||||
|
||||
def test_indent_width_string(self, env):
|
||||
t = env.from_string("{{ 'jinja\nflask'|indent(width='>>> ', first=True) }}")
|
||||
assert t.render() == ">>> jinja\n>>> flask"
|
||||
|
||||
@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("{{ value|int }}")
|
||||
assert t.render(value=value) == expect
|
||||
|
||||
@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("{{ value|int(base=base) }}")
|
||||
assert t.render(value=value, base=base) == expect
|
||||
|
||||
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:
|
||||
def __int__(self):
|
||||
return 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"
|
||||
|
||||
env2 = Environment(autoescape=True)
|
||||
tmpl = env2.from_string('{{ ["<foo>", "<span>foo</span>"|safe]|join }}')
|
||||
assert tmpl.render() == "<foo><span>foo</span>"
|
||||
|
||||
def test_join_attribute(self, env):
|
||||
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 }}""")
|
||||
out = tmpl.render(foo=list(range(10)))
|
||||
assert out == "9"
|
||||
|
||||
def test_length(self, env):
|
||||
tmpl = env.from_string("""{{ "hello world"|length }}""")
|
||||
out = tmpl.render()
|
||||
assert out == "11"
|
||||
|
||||
def test_lower(self, env):
|
||||
tmpl = env.from_string("""{{ "FOO"|lower }}""")
|
||||
out = tmpl.render()
|
||||
assert out == "foo"
|
||||
|
||||
def test_items(self, env):
|
||||
d = {i: c for i, c in enumerate("abc")}
|
||||
tmpl = env.from_string("""{{ d|items|list }}""")
|
||||
out = tmpl.render(d=d)
|
||||
assert out == "[(0, 'a'), (1, 'b'), (2, 'c')]"
|
||||
|
||||
def test_items_undefined(self, env):
|
||||
tmpl = env.from_string("""{{ d|items|list }}""")
|
||||
out = tmpl.render()
|
||||
assert out == "[]"
|
||||
|
||||
def test_pprint(self, env):
|
||||
from pprint import pformat
|
||||
|
||||
tmpl = env.from_string("""{{ data|pprint }}""")
|
||||
data = list(range(1000))
|
||||
assert tmpl.render(data=data) == pformat(data)
|
||||
|
||||
def test_random(self, env, request):
|
||||
# restore the random state when the test ends
|
||||
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)]
|
||||
|
||||
# check that the random sequence is generated again by a template
|
||||
# ensures that filter result is not constant folded
|
||||
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]"
|
||||
|
||||
def test_string(self, env):
|
||||
x = [1, 2, 3, 4, 5]
|
||||
tmpl = env.from_string("""{{ obj|string }}""")
|
||||
assert tmpl.render(obj=x) == str(x)
|
||||
|
||||
def test_title(self, env):
|
||||
tmpl = env.from_string("""{{ "foo bar"|title }}""")
|
||||
assert tmpl.render() == "Foo Bar"
|
||||
tmpl = env.from_string("""{{ "foo's bar"|title }}""")
|
||||
assert tmpl.render() == "Foo's Bar"
|
||||
tmpl = env.from_string("""{{ "foo bar"|title }}""")
|
||||
assert tmpl.render() == "Foo Bar"
|
||||
tmpl = env.from_string("""{{ "f bar f"|title }}""")
|
||||
assert tmpl.render() == "F Bar F"
|
||||
tmpl = env.from_string("""{{ "foo-bar"|title }}""")
|
||||
assert tmpl.render() == "Foo-Bar"
|
||||
tmpl = env.from_string("""{{ "foo\tbar"|title }}""")
|
||||
assert tmpl.render() == "Foo\tBar"
|
||||
tmpl = env.from_string("""{{ "FOO\tBAR"|title }}""")
|
||||
assert tmpl.render() == "Foo\tBar"
|
||||
tmpl = env.from_string("""{{ "foo (bar)"|title }}""")
|
||||
assert tmpl.render() == "Foo (Bar)"
|
||||
tmpl = env.from_string("""{{ "foo {bar}"|title }}""")
|
||||
assert tmpl.render() == "Foo {Bar}"
|
||||
tmpl = env.from_string("""{{ "foo [bar]"|title }}""")
|
||||
assert tmpl.render() == "Foo [Bar]"
|
||||
tmpl = env.from_string("""{{ "foo <bar>"|title }}""")
|
||||
assert tmpl.render() == "Foo <Bar>"
|
||||
|
||||
class Foo:
|
||||
def __str__(self):
|
||||
return "foo-bar"
|
||||
|
||||
tmpl = env.from_string("""{{ data|title }}""")
|
||||
out = tmpl.render(data=Foo())
|
||||
assert out == "Foo-Bar"
|
||||
|
||||
def test_truncate(self, env):
|
||||
tmpl = env.from_string(
|
||||
'{{ data|truncate(15, true, ">>>") }}|'
|
||||
'{{ data|truncate(15, false, ">>>") }}|'
|
||||
"{{ smalldata|truncate(15) }}"
|
||||
)
|
||||
out = tmpl.render(data="foobar baz bar" * 1000, smalldata="foobar baz bar")
|
||||
assert out == "foobar baz b>>>|foobar baz>>>|foobar baz bar"
|
||||
|
||||
def test_truncate_very_short(self, env):
|
||||
tmpl = env.from_string(
|
||||
'{{ "foo bar baz"|truncate(9) }}|{{ "foo bar baz"|truncate(9, true) }}'
|
||||
)
|
||||
out = tmpl.render()
|
||||
assert out == "foo bar baz|foo bar baz"
|
||||
|
||||
def test_truncate_end_length(self, env):
|
||||
tmpl = env.from_string('{{ "Joel is a slug"|truncate(7, true) }}')
|
||||
out = tmpl.render()
|
||||
assert out == "Joel..."
|
||||
|
||||
def test_upper(self, env):
|
||||
tmpl = env.from_string('{{ "foo"|upper }}')
|
||||
assert tmpl.render() == "FOO"
|
||||
|
||||
def test_urlize(self, env):
|
||||
tmpl = env.from_string('{{ "foo example.org bar"|urlize }}')
|
||||
assert tmpl.render() == (
|
||||
'foo <a href="https://example.org" rel="noopener">' "example.org</a> bar"
|
||||
)
|
||||
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"
|
||||
)
|
||||
tmpl = env.from_string('{{ "foo mailto:email@example.com bar"|urlize }}')
|
||||
assert tmpl.render() == (
|
||||
'foo <a href="mailto:email@example.com">email@example.com</a> bar'
|
||||
)
|
||||
tmpl = env.from_string('{{ "foo email@example.com bar"|urlize }}')
|
||||
assert tmpl.render() == (
|
||||
'foo <a href="mailto:email@example.com">email@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 }}')
|
||||
assert tmpl.render() == (
|
||||
'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"
|
||||
)
|
||||
|
||||
def test_urlize_extra_schemes_parameter(self, env):
|
||||
tmpl = env.from_string(
|
||||
'{{ "foo tel:+1-514-555-1234 ftp://localhost bar"|'
|
||||
'urlize(extra_schemes=["tel:", "ftp:"]) }}'
|
||||
)
|
||||
assert tmpl.render() == (
|
||||
'foo <a href="tel:+1-514-555-1234" rel="noopener">'
|
||||
'tel:+1-514-555-1234</a> <a href="ftp://localhost" rel="noopener">'
|
||||
"ftp://localhost</a> bar"
|
||||
)
|
||||
|
||||
def test_wordcount(self, env):
|
||||
tmpl = env.from_string('{{ "foo bar baz"|wordcount }}')
|
||||
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>"
|
||||
|
||||
def test_chaining(self, env):
|
||||
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"
|
||||
|
||||
def test_sum_attributes(self, env):
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
def test_abs(self, env):
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
def test_xmlattr(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{{ {'foo': 42, 'bar': 23, 'fish': none, "
|
||||
"'spam': missing, 'blub:blub': '<?>'}|xmlattr }}"
|
||||
)
|
||||
out = tmpl.render().split()
|
||||
assert len(out) == 3
|
||||
assert 'foo="42"' in out
|
||||
assert 'bar="23"' in out
|
||||
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]"
|
||||
|
||||
def test_sort2(self, env):
|
||||
tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort) }}')
|
||||
assert tmpl.render() == "AbcD"
|
||||
|
||||
def test_sort3(self, env):
|
||||
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"
|
||||
|
||||
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) }}')
|
||||
assert t.render() == "bA"
|
||||
|
||||
def test_unique_case_sensitive(self, env):
|
||||
t = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique(true)) }}')
|
||||
assert t.render() == "bAa"
|
||||
|
||||
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 }}", ""),
|
||||
),
|
||||
)
|
||||
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")])
|
||||
def test_min_max_attribute(self, env, name, expect):
|
||||
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(
|
||||
"""
|
||||
{%- 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", ""]
|
||||
|
||||
def test_groupby_tuple_index(self, env):
|
||||
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|"
|
||||
|
||||
def test_groupby_multidot(self, env):
|
||||
Date = namedtuple("Date", "day,month,year")
|
||||
Article = namedtuple("Article", "title,date")
|
||||
articles = [
|
||||
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(
|
||||
"""
|
||||
{%- 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]",
|
||||
"",
|
||||
]
|
||||
|
||||
def test_groupby_default(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{% for city, items in users|groupby('city', default='NY') %}"
|
||||
"{{ city }}: {{ items|map(attribute='name')|join(', ') }}\n"
|
||||
"{% endfor %}"
|
||||
)
|
||||
out = tmpl.render(
|
||||
users=[
|
||||
{"name": "emma", "city": "NY"},
|
||||
{"name": "smith", "city": "WA"},
|
||||
{"name": "john"},
|
||||
]
|
||||
)
|
||||
assert out == "NY: emma, john\nWA: smith\n"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("case_sensitive", "expect"),
|
||||
[
|
||||
(False, "a: 1, 3\nb: 2\n"),
|
||||
(True, "A: 3\na: 1\nb: 2\n"),
|
||||
],
|
||||
)
|
||||
def test_groupby_case(self, env, case_sensitive, expect):
|
||||
tmpl = env.from_string(
|
||||
"{% for k, vs in data|groupby('k', case_sensitive=cs) %}"
|
||||
"{{ k }}: {{ vs|join(', ', attribute='v') }}\n"
|
||||
"{% endfor %}"
|
||||
)
|
||||
out = tmpl.render(
|
||||
data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}],
|
||||
cs=case_sensitive,
|
||||
)
|
||||
assert out == expect
|
||||
|
||||
def test_filtertag(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{% filter upper|replace('FOO', 'foo') %}foobar{% endfilter %}"
|
||||
)
|
||||
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>"
|
||||
env = Environment(autoescape=True)
|
||||
tmpl = env.from_string('{{ string|replace("o", 42) }}')
|
||||
assert tmpl.render(string="<foo>") == "<f4242>"
|
||||
tmpl = env.from_string('{{ string|replace("<", 42) }}')
|
||||
assert tmpl.render(string="<foo>") == "42foo>"
|
||||
tmpl = env.from_string('{{ string|replace("o", ">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 />")) == "<div />"
|
||||
|
||||
def test_safe(self, env):
|
||||
env = Environment(autoescape=True)
|
||||
tmpl = env.from_string('{{ "<div>foo</div>"|safe }}')
|
||||
assert tmpl.render() == "<div>foo</div>"
|
||||
tmpl = env.from_string('{{ "<div>foo</div>" }}')
|
||||
assert tmpl.render() == "<div>foo</div>"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expect"),
|
||||
[
|
||||
("Hello, world!", "Hello%2C%20world%21"),
|
||||
("Hello, world\u203d", "Hello%2C%20world%E2%80%BD"),
|
||||
({"f": 1}, "f=1"),
|
||||
([("f", 1), ("z", 2)], "f=1&z=2"),
|
||||
({"\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"
|
||||
|
||||
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):
|
||||
User = namedtuple("User", "name")
|
||||
env = Environment()
|
||||
users = [
|
||||
User("john"),
|
||||
User("jane"),
|
||||
User("mike"),
|
||||
]
|
||||
tmpl = env.from_string('{{ users|map(attribute="name")|join("|") }}')
|
||||
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() == "[]"
|
||||
|
||||
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(", ") }}'
|
||||
)
|
||||
test_list = env.from_string(
|
||||
'{{ users|map(attribute="lastname", default=["smith","x"])|join(", ") }}'
|
||||
)
|
||||
test_str = env.from_string(
|
||||
'{{ users|map(attribute="lastname", default="")|join(", ") }}'
|
||||
)
|
||||
users = [
|
||||
Fullname("john", "lennon"),
|
||||
Fullname("jane", "edwards"),
|
||||
Fullname("jon", None),
|
||||
Firstname("mike"),
|
||||
]
|
||||
assert tmpl.render(users=users) == "lennon, edwards, None, smith"
|
||||
assert test_list.render(users=users) == "lennon, edwards, None, ['smith', 'x']"
|
||||
assert test_str.render(users=users) == "lennon, edwards, None, "
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
def test_simple_select_attr(self, env):
|
||||
User = namedtuple("User", "name,is_active")
|
||||
env = Environment()
|
||||
users = [
|
||||
User("john", True),
|
||||
User("jane", True),
|
||||
User("mike", False),
|
||||
]
|
||||
tmpl = env.from_string(
|
||||
'{{ users|selectattr("is_active")|map(attribute="name")|join("|") }}'
|
||||
)
|
||||
assert tmpl.render(users=users) == "john|jane"
|
||||
|
||||
def test_simple_reject_attr(self, env):
|
||||
User = namedtuple("User", "name,is_active")
|
||||
env = Environment()
|
||||
users = [
|
||||
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"
|
||||
|
||||
def test_func_select_attr(self, env):
|
||||
User = namedtuple("User", "id,name")
|
||||
env = Environment()
|
||||
users = [
|
||||
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"
|
||||
|
||||
def test_func_reject_attr(self, env):
|
||||
User = namedtuple("User", "id,name")
|
||||
env = Environment()
|
||||
users = [
|
||||
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"
|
||||
|
||||
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"'
|
||||
|
||||
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"
|
||||
|
||||
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."
|
||||
|
||||
def test_filter_undefined(self, env):
|
||||
with pytest.raises(TemplateAssertionError, match="No filter named 'f'"):
|
||||
env.from_string("{{ var|f }}")
|
||||
|
||||
def test_filter_undefined_in_if(self, env):
|
||||
t = env.from_string("{%- if x is defined -%}{{ x|f }}{%- else -%}x{% endif %}")
|
||||
assert t.render() == "x"
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=42)
|
||||
|
||||
def test_filter_undefined_in_elif(self, env):
|
||||
t = env.from_string(
|
||||
"{%- if x is defined -%}{{ x }}{%- elif y is defined -%}"
|
||||
"{{ y|f }}{%- else -%}foo{%- endif -%}"
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(y=42)
|
||||
|
||||
def test_filter_undefined_in_else(self, env):
|
||||
t = env.from_string(
|
||||
"{%- if x is not defined -%}foo{%- else -%}{{ x|f }}{%- endif -%}"
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=42)
|
||||
|
||||
def test_filter_undefined_in_nested_if(self, env):
|
||||
t = env.from_string(
|
||||
"{%- if x is not defined -%}foo{%- else -%}{%- if y "
|
||||
"is defined -%}{{ y|f }}{%- endif -%}{{ x }}{%- endif -%}"
|
||||
)
|
||||
assert t.render() == "foo"
|
||||
assert t.render(x=42) == "42"
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t.render(x=24, y=42)
|
||||
|
||||
def test_filter_undefined_in_condexpr(self, env):
|
||||
t1 = env.from_string("{{ x|f if x is defined else 'foo' }}")
|
||||
t2 = env.from_string("{{ 'foo' if x is not defined else x|f }}")
|
||||
assert t1.render() == t2.render() == "foo"
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
|
||||
t1.render(x=42)
|
||||
t2.render(x=42)
|
||||
@@ -1,290 +0,0 @@
|
||||
from jinja2 import nodes
|
||||
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]
|
||||
)
|
||||
|
||||
sym = symbols_for_node(tmpl)
|
||||
assert sym.refs == {
|
||||
"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"),
|
||||
}
|
||||
|
||||
sym = symbols_for_node(for_loop, sym)
|
||||
assert sym.refs == {
|
||||
"foo": "l_1_foo",
|
||||
}
|
||||
assert sym.loads == {
|
||||
"l_1_foo": ("param", None),
|
||||
}
|
||||
|
||||
|
||||
def test_complex():
|
||||
title_block = nodes.Block(
|
||||
"title", [nodes.Output([nodes.TemplateData("Page Title")])], False, False
|
||||
)
|
||||
|
||||
render_title_macro = nodes.Macro(
|
||||
"render_title",
|
||||
[nodes.Name("title", "param")],
|
||||
[],
|
||||
[
|
||||
nodes.Output(
|
||||
[
|
||||
nodes.TemplateData('\n <div class="title">\n <h1>'),
|
||||
nodes.Name("title", "load"),
|
||||
nodes.TemplateData("</h1>\n <p>"),
|
||||
nodes.Name("subtitle", "load"),
|
||||
nodes.TemplateData("</p>\n "),
|
||||
]
|
||||
),
|
||||
nodes.Assign(
|
||||
nodes.Name("subtitle", "store"), nodes.Const("something else")
|
||||
),
|
||||
nodes.Output(
|
||||
[
|
||||
nodes.TemplateData("\n <p>"),
|
||||
nodes.Name("subtitle", "load"),
|
||||
nodes.TemplateData("</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("\n <li>"),
|
||||
nodes.Name("item", "load"),
|
||||
nodes.TemplateData("</li>\n <span>"),
|
||||
]
|
||||
),
|
||||
nodes.Include(nodes.Const("helper.html"), True, False),
|
||||
nodes.Output([nodes.TemplateData("</span>\n ")]),
|
||||
],
|
||||
[],
|
||||
None,
|
||||
False,
|
||||
)
|
||||
|
||||
body_block = nodes.Block(
|
||||
"body",
|
||||
[
|
||||
nodes.Output(
|
||||
[
|
||||
nodes.TemplateData("\n "),
|
||||
nodes.Call(
|
||||
nodes.Name("render_title", "load"),
|
||||
[nodes.Name("item", "load")],
|
||||
[],
|
||||
None,
|
||||
None,
|
||||
),
|
||||
nodes.TemplateData("\n <ul>\n "),
|
||||
]
|
||||
),
|
||||
for_loop,
|
||||
nodes.Output([nodes.TemplateData("\n </ul>\n")]),
|
||||
],
|
||||
False,
|
||||
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",
|
||||
}
|
||||
assert tmpl_sym.loads == {
|
||||
"l_0_render_title": ("undefined", None),
|
||||
}
|
||||
assert tmpl_sym.stores == {"render_title"}
|
||||
assert tmpl_sym.dump_stores() == {
|
||||
"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",
|
||||
}
|
||||
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"),
|
||||
}
|
||||
assert macro_sym.stores == {"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",
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
assert body_sym.loads == {
|
||||
"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",
|
||||
}
|
||||
assert for_sym.loads == {
|
||||
"l_1_item": ("param", None),
|
||||
}
|
||||
assert for_sym.stores == {"item"}
|
||||
assert for_sym.dump_stores() == {
|
||||
"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))],
|
||||
[],
|
||||
[],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
sym = symbols_for_node(tmpl)
|
||||
assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"}
|
||||
assert sym.stores == {"variable"}
|
||||
assert sym.loads == {
|
||||
"l_0_variable": ("resolve", "variable"),
|
||||
"l_0_expression": ("resolve", "expression"),
|
||||
}
|
||||
assert sym.dump_stores() == {
|
||||
"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))],
|
||||
[],
|
||||
[],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
sym = symbols_for_node(tmpl)
|
||||
assert sym.refs == {"variable": "l_0_variable", "expression": "l_0_expression"}
|
||||
assert sym.stores == {"variable"}
|
||||
assert sym.loads == {
|
||||
"l_0_variable": ("undefined", None),
|
||||
"l_0_expression": ("resolve", "expression"),
|
||||
}
|
||||
assert sym.dump_stores() == {
|
||||
"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,
|
||||
)
|
||||
|
||||
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 == {"item", "x"}
|
||||
assert for_sym.loads == {
|
||||
"l_1_x": ("alias", "l_0_x"),
|
||||
"l_1_item": ("param", None),
|
||||
"l_1_expression": ("resolve", "expression"),
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from jinja2.environment import Environment
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from jinja2.exceptions import TemplatesNotFound
|
||||
from jinja2.exceptions import TemplateSyntaxError
|
||||
from jinja2.exceptions import UndefinedError
|
||||
from jinja2.loaders import DictLoader
|
||||
|
||||
|
||||
@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
|
||||
return env
|
||||
|
||||
|
||||
class TestImports:
|
||||
def test_context_imports(self, test_env):
|
||||
t = test_env.from_string('{% import "module" as m %}{{ m.test() }}')
|
||||
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]"
|
||||
t = test_env.from_string(
|
||||
'{% import "module" as m with context %}{{ m.test() }}'
|
||||
)
|
||||
assert t.render(foo=42) == "[42|23]"
|
||||
t = test_env.from_string('{% from "module" import test %}{{ test() }}')
|
||||
assert t.render(foo=42) == "[|23]"
|
||||
t = test_env.from_string(
|
||||
'{% from "module" import test without context %}{{ test() }}'
|
||||
)
|
||||
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]"
|
||||
|
||||
def test_import_needs_name(self, test_env):
|
||||
test_env.from_string('{% from "foo" import bar %}')
|
||||
test_env.from_string('{% from "foo" import bar, baz %}')
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import %}')
|
||||
|
||||
def test_no_trailing_comma(self, test_env):
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import bar, %}')
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import bar,, %}')
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import, %}')
|
||||
|
||||
def test_trailing_comma_with_context(self, test_env):
|
||||
test_env.from_string('{% from "foo" import bar, baz with context %}')
|
||||
test_env.from_string('{% from "foo" import bar, baz, with context %}')
|
||||
test_env.from_string('{% from "foo" import bar, with context %}')
|
||||
test_env.from_string('{% from "foo" import bar, with, context %}')
|
||||
test_env.from_string('{% from "foo" import bar, with with context %}')
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import bar,, with context %}')
|
||||
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
test_env.from_string('{% from "foo" import bar with context, %}')
|
||||
|
||||
def test_exports(self, test_env):
|
||||
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")
|
||||
assert m.variable == 42
|
||||
assert not hasattr(m, "notthere")
|
||||
|
||||
def test_not_exported(self, test_env):
|
||||
t = test_env.from_string("{% from 'module' import nothing %}{{ nothing() }}")
|
||||
|
||||
with pytest.raises(UndefinedError, match="does not export the requested name"):
|
||||
t.render()
|
||||
|
||||
def test_import_with_globals(self, test_env):
|
||||
t = test_env.from_string(
|
||||
'{% import "module" as m %}{{ m.test() }}', globals={"foo": 42}
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
t = test_env.from_string('{% import "module" as m %}{{ m.test() }}')
|
||||
assert t.render() == "[|23]"
|
||||
|
||||
def test_import_with_globals_override(self, test_env):
|
||||
t = test_env.from_string(
|
||||
'{% set foo = 41 %}{% import "module" as m %}{{ m.test() }}',
|
||||
globals={"foo": 42},
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
def test_from_import_with_globals(self, test_env):
|
||||
t = test_env.from_string(
|
||||
'{% from "module" import test %}{{ test() }}',
|
||||
globals={"foo": 42},
|
||||
)
|
||||
assert t.render() == "[42|23]"
|
||||
|
||||
|
||||
class TestIncludes:
|
||||
def test_context_include(self, test_env):
|
||||
t = test_env.from_string('{% include "header" %}')
|
||||
assert t.render(foo=42) == "[42|23]"
|
||||
t = test_env.from_string('{% include "header" with context %}')
|
||||
assert t.render(foo=42) == "[42|23]"
|
||||
t = test_env.from_string('{% include "header" without context %}')
|
||||
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]"
|
||||
|
||||
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)
|
||||
with pytest.raises(TemplatesNotFound) as e:
|
||||
t.render()
|
||||
|
||||
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]"
|
||||
|
||||
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, "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() == ""
|
||||
|
||||
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 }}",
|
||||
)
|
||||
)
|
||||
)
|
||||
assert env.get_template("main").render() == "123"
|
||||
|
||||
def test_unoptimized_scopes(self, test_env):
|
||||
t = test_env.from_string(
|
||||
"""
|
||||
{% macro outer(o) %}
|
||||
{% macro inner() %}
|
||||
{% include "o_printer" %}
|
||||
{% endmacro %}
|
||||
{{ inner() }}
|
||||
{% endmacro %}
|
||||
{{ outer("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"
|
||||
@@ -1,405 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import TemplateSyntaxError
|
||||
|
||||
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 %}|"""
|
||||
|
||||
LEVEL1TEMPLATE = """\
|
||||
{% extends "layout" %}
|
||||
{% block block1 %}block 1 from level1{% endblock %}"""
|
||||
|
||||
LEVEL2TEMPLATE = """\
|
||||
{% extends "level1" %}
|
||||
{% block block2 %}{% block block5 %}nested block 5 from level2{%
|
||||
endblock %}{% endblock %}"""
|
||||
|
||||
LEVEL3TEMPLATE = """\
|
||||
{% extends "level2" %}
|
||||
{% block block5 %}block 5 from level3{% endblock %}
|
||||
{% block block4 %}block 4 from level3{% endblock %}
|
||||
"""
|
||||
|
||||
LEVEL4TEMPLATE = """\
|
||||
{% extends "level3" %}
|
||||
{% block block3 %}block 3 from level4{% endblock %}
|
||||
"""
|
||||
|
||||
WORKINGTEMPLATE = """\
|
||||
{% extends "layout" %}
|
||||
{% block block1 %}
|
||||
{% if false %}
|
||||
{% block block2 %}
|
||||
this should work
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
DOUBLEEXTENDS = """\
|
||||
{% extends "layout" %}
|
||||
{% extends "layout" %}
|
||||
{% block block1 %}
|
||||
{% if false %}
|
||||
{% block block2 %}
|
||||
this should work
|
||||
{% 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,
|
||||
)
|
||||
|
||||
|
||||
class TestInheritance:
|
||||
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|"
|
||||
)
|
||||
|
||||
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|"
|
||||
)
|
||||
|
||||
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|"
|
||||
)
|
||||
|
||||
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|"
|
||||
)
|
||||
|
||||
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|"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
def test_working(self, env):
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
def test_dynamic_inheritance(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default1": "DEFAULT1{% block x %}{% endblock %}",
|
||||
"default2": "DEFAULT2{% block x %}{% endblock %}",
|
||||
"child": "{% extends default %}{% block x %}CHILD{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
tmpl = env.get_template("child")
|
||||
for m in range(1, 3):
|
||||
assert tmpl.render(default=f"default{m}") == f"DEFAULT{m}CHILD"
|
||||
|
||||
def test_multi_inheritance(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default1": "DEFAULT1{% block x %}{% endblock %}",
|
||||
"default2": "DEFAULT2{% block x %}{% endblock %}",
|
||||
"child": (
|
||||
"{% if default %}{% extends default %}{% else %}"
|
||||
"{% extends 'default1' %}{% endif %}"
|
||||
"{% block x %}CHILD{% endblock %}"
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
tmpl = env.get_template("child")
|
||||
assert tmpl.render(default="default2") == "DEFAULT2CHILD"
|
||||
assert tmpl.render(default="default1") == "DEFAULT1CHILD"
|
||||
assert tmpl.render() == "DEFAULT1CHILD"
|
||||
|
||||
def test_scoped_block(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default.html": "{% for item in seq %}[{% block item scoped %}"
|
||||
"{% endblock %}]{% endfor %}"
|
||||
}
|
||||
)
|
||||
)
|
||||
t = env.from_string(
|
||||
"{% extends 'default.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(
|
||||
{
|
||||
"default.html": "{% for item in seq %}[{% block item scoped %}"
|
||||
"{{ item }}{% endblock %}]{% endfor %}"
|
||||
}
|
||||
)
|
||||
)
|
||||
t = env.from_string(
|
||||
'{% extends "default.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": """
|
||||
{% block useless %}{% endblock %}
|
||||
""",
|
||||
"index.html": """
|
||||
{%- extends 'layout.html' %}
|
||||
{% from 'helpers.html' import foo with context %}
|
||||
{% block useless %}
|
||||
{% for x in [1, 2, 3] %}
|
||||
{% block testing scoped %}
|
||||
{{ foo(x) }}
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
""",
|
||||
"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"]
|
||||
|
||||
def test_level1_required(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default": "{% block x required %}{# comment #}\n {% endblock %}",
|
||||
"level1": "{% extends 'default' %}{% block x %}[1]{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
rv = env.get_template("level1").render()
|
||||
assert rv == "[1]"
|
||||
|
||||
def test_level2_required(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default": "{% block x required %}{% endblock %}",
|
||||
"level1": "{% extends 'default' %}{% block x %}[1]{% endblock %}",
|
||||
"level2": "{% extends 'default' %}{% block x %}[2]{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
rv1 = env.get_template("level1").render()
|
||||
rv2 = env.get_template("level2").render()
|
||||
|
||||
assert rv1 == "[1]"
|
||||
assert rv2 == "[2]"
|
||||
|
||||
def test_level3_required(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default": "{% block x required %}{% endblock %}",
|
||||
"level1": "{% extends 'default' %}",
|
||||
"level2": "{% extends 'level1' %}{% block x %}[2]{% endblock %}",
|
||||
"level3": "{% extends 'level2' %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
t1 = env.get_template("level1")
|
||||
t2 = env.get_template("level2")
|
||||
t3 = env.get_template("level3")
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="Required block 'x' not found"):
|
||||
assert t1.render()
|
||||
|
||||
assert t2.render() == "[2]"
|
||||
assert t3.render() == "[2]"
|
||||
|
||||
def test_invalid_required(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default": "{% block x required %}data {# #}{% endblock %}",
|
||||
"default1": "{% block x required %}{% block y %}"
|
||||
"{% endblock %} {% endblock %}",
|
||||
"default2": "{% block x required %}{% if true %}"
|
||||
"{% endif %} {% endblock %}",
|
||||
"level1": "{% if default %}{% extends default %}"
|
||||
"{% else %}{% extends 'default' %}{% endif %}"
|
||||
"{%- block x %}CHILD{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
t = env.get_template("level1")
|
||||
|
||||
with pytest.raises(
|
||||
TemplateSyntaxError,
|
||||
match="Required blocks can only contain comments or whitespace",
|
||||
):
|
||||
assert t.render(default="default")
|
||||
assert t.render(default="default2")
|
||||
assert t.render(default="default3")
|
||||
|
||||
def test_required_with_scope(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default1": "{% for item in seq %}[{% block item scoped required %}"
|
||||
"{% endblock %}]{% endfor %}",
|
||||
"child1": "{% extends 'default1' %}{% block item %}"
|
||||
"{{ item }}{% endblock %}",
|
||||
"default2": "{% for item in seq %}[{% block item required scoped %}"
|
||||
"{% endblock %}]{% endfor %}",
|
||||
"child2": "{% extends 'default2' %}{% block item %}"
|
||||
"{{ item }}{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
t1 = env.get_template("child1")
|
||||
t2 = env.get_template("child2")
|
||||
|
||||
assert t1.render(seq=list(range(3))) == "[0][1][2]"
|
||||
|
||||
# scoped must come before required
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
t2.render(seq=list(range(3)))
|
||||
|
||||
def test_duplicate_required_or_scoped(self, env):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"default1": "{% for item in seq %}[{% block item "
|
||||
"scoped scoped %}}{{% endblock %}}]{{% endfor %}}",
|
||||
"default2": "{% for item in seq %}[{% block item "
|
||||
"required required %}}{{% endblock %}}]{{% endfor %}}",
|
||||
"child": "{% if default %}{% extends default %}{% else %}"
|
||||
"{% extends 'default1' %}{% endif %}{%- block x %}"
|
||||
"CHILD{% endblock %}",
|
||||
}
|
||||
)
|
||||
)
|
||||
tmpl = env.get_template("child")
|
||||
with pytest.raises(TemplateSyntaxError):
|
||||
tmpl.render(default="default1", seq=list(range(3)))
|
||||
tmpl.render(default="default2", seq=list(range(3)))
|
||||
|
||||
|
||||
class TestBugFix:
|
||||
def test_fixed_macro_scoping_bug(self, env):
|
||||
assert (
|
||||
Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"test.html": """\
|
||||
{% extends 'details.html' %}
|
||||
|
||||
{% macro my_macro() %}
|
||||
my_macro
|
||||
{% endmacro %}
|
||||
|
||||
{% block inner_box %}
|
||||
{{ my_macro() }}
|
||||
{% endblock %}
|
||||
""",
|
||||
"details.html": """\
|
||||
{% extends 'standard.html' %}
|
||||
|
||||
{% macro my_macro() %}
|
||||
my_macro
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% block outer_box %}
|
||||
outer_box
|
||||
{% block inner_box %}
|
||||
inner_box
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
""",
|
||||
"standard.html": """
|
||||
{% block content %} {% endblock %}
|
||||
""",
|
||||
}
|
||||
)
|
||||
)
|
||||
.get_template("test.html")
|
||||
.render()
|
||||
.split()
|
||||
== ["outer_box", "my_macro"]
|
||||
)
|
||||
|
||||
def test_double_extends(self, env):
|
||||
"""Ensures that a template with more than 1 {% extends ... %} usage
|
||||
raises a ``TemplateError``.
|
||||
"""
|
||||
with pytest.raises(TemplateRuntimeError, match="extended multiple times"):
|
||||
env.get_template("doublee").render()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,412 +0,0 @@
|
||||
import importlib.abc
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import loaders
|
||||
from jinja2 import PackageLoader
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from jinja2.loaders import split_template_path
|
||||
|
||||
|
||||
class TestLoaders:
|
||||
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")
|
||||
|
||||
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_overlapping_names(self, filesystem_loader):
|
||||
t2_dir = Path(filesystem_loader.searchpath[0]) / ".." / "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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
def test_caching(self):
|
||||
changed = False
|
||||
|
||||
class TestLoader(loaders.BaseLoader):
|
||||
def get_source(self, environment, template):
|
||||
return "foo", None, lambda: not changed
|
||||
|
||||
env = Environment(loader=TestLoader(), cache_size=-1)
|
||||
tmpl = env.get_template("template")
|
||||
assert tmpl is env.get_template("template")
|
||||
changed = True
|
||||
assert tmpl is not env.get_template("template")
|
||||
changed = False
|
||||
|
||||
def test_no_cache(self):
|
||||
mapping = {"foo": "one"}
|
||||
env = Environment(loader=loaders.DictLoader(mapping), cache_size=0)
|
||||
assert env.get_template("foo") is not env.get_template("foo")
|
||||
|
||||
def test_limited_size_cache(self):
|
||||
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")
|
||||
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
|
||||
|
||||
def test_cache_loader_change(self):
|
||||
loader1 = loaders.DictLoader({"foo": "one"})
|
||||
loader2 = loaders.DictLoader({"foo": "two"})
|
||||
env = Environment(loader=loader1, cache_size=2)
|
||||
assert env.get_template("foo").render() == "one"
|
||||
env.loader = loader2
|
||||
assert env.get_template("foo").render() == "two"
|
||||
|
||||
def test_dict_loader_cache_invalidates(self):
|
||||
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"
|
||||
|
||||
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")
|
||||
|
||||
|
||||
class TestFileSystemLoader:
|
||||
searchpath = (Path(__file__) / ".." / "res" / "templates").resolve()
|
||||
|
||||
@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(str(self.searchpath))
|
||||
|
||||
env = Environment(loader=filesystem_loader)
|
||||
self._test_common(env)
|
||||
|
||||
def test_searchpath_as_pathlib(self):
|
||||
filesystem_loader = loaders.FileSystemLoader(self.searchpath)
|
||||
env = Environment(loader=filesystem_loader)
|
||||
self._test_common(env)
|
||||
|
||||
def test_searchpath_as_list_including_pathlib(self):
|
||||
filesystem_loader = loaders.FileSystemLoader(
|
||||
["/tmp/templates", self.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(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", "文字化け"),
|
||||
("iso-8859-1", "æ\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
|
||||
|
||||
def test_filename_normpath(self):
|
||||
"""Nested template names should only contain ``os.sep`` in the
|
||||
loaded filename.
|
||||
"""
|
||||
loader = loaders.FileSystemLoader(self.searchpath)
|
||||
e = Environment(loader=loader)
|
||||
t = e.get_template("foo/test.html")
|
||||
assert t.filename == str(self.searchpath / "foo" / "test.html")
|
||||
|
||||
|
||||
class TestModuleLoader:
|
||||
archive = None
|
||||
|
||||
def compile_down(self, prefix_loader, zip="deflated"):
|
||||
log = []
|
||||
self.reg_env = Environment(loader=prefix_loader)
|
||||
if zip is not None:
|
||||
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)
|
||||
self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive))
|
||||
return "".join(log)
|
||||
|
||||
def teardown(self):
|
||||
if hasattr(self, "mod_env"):
|
||||
if os.path.isfile(self.archive):
|
||||
os.remove(self.archive)
|
||||
else:
|
||||
shutil.rmtree(self.archive)
|
||||
self.archive = None
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def _test_common(self):
|
||||
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")
|
||||
assert tmpl1.render() == tmpl2.render()
|
||||
|
||||
def test_deflated_zip_compile(self, prefix_loader):
|
||||
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._test_common()
|
||||
|
||||
def test_filesystem_compile(self, prefix_loader):
|
||||
self.compile_down(prefix_loader, zip=None)
|
||||
self._test_common()
|
||||
|
||||
def test_weak_references(self, prefix_loader):
|
||||
self.compile_down(prefix_loader)
|
||||
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 = None
|
||||
|
||||
try:
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
assert name not in sys.modules
|
||||
|
||||
def test_choice_loader(self, 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"
|
||||
|
||||
def test_path_as_pathlib(self, prefix_loader):
|
||||
self.compile_down(prefix_loader)
|
||||
|
||||
mod_path = self.mod_env.loader.module.__path__[0]
|
||||
mod_loader = loaders.ModuleLoader(Path(mod_path))
|
||||
self.mod_env = Environment(loader=mod_loader)
|
||||
|
||||
self._test_common()
|
||||
|
||||
def test_supports_pathlib_in_list_of_paths(self, prefix_loader):
|
||||
self.compile_down(prefix_loader)
|
||||
|
||||
mod_path = self.mod_env.loader.module.__path__[0]
|
||||
mod_loader = loaders.ModuleLoader([Path(mod_path), "/tmp/templates"])
|
||||
self.mod_env = Environment(loader=mod_loader)
|
||||
|
||||
self._test_common()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def package_dir_loader(monkeypatch):
|
||||
monkeypatch.syspath_prepend(Path(__file__).parent)
|
||||
return PackageLoader("res")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
|
||||
)
|
||||
def test_package_dir_source(package_dir_loader, template, expect):
|
||||
source, name, up_to_date = package_dir_loader.get_source(None, template)
|
||||
assert source.rstrip() == expect
|
||||
assert name.endswith(os.path.join(*split_template_path(template)))
|
||||
assert up_to_date()
|
||||
|
||||
|
||||
def test_package_dir_list(package_dir_loader):
|
||||
templates = package_dir_loader.list_templates()
|
||||
assert "foo/test.html" in templates
|
||||
assert "test.html" in templates
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def package_file_loader(monkeypatch):
|
||||
monkeypatch.syspath_prepend(Path(__file__).parent / "res")
|
||||
return PackageLoader("__init__")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
|
||||
)
|
||||
def test_package_file_source(package_file_loader, template, expect):
|
||||
source, name, up_to_date = package_file_loader.get_source(None, template)
|
||||
assert source.rstrip() == expect
|
||||
assert name.endswith(os.path.join(*split_template_path(template)))
|
||||
assert up_to_date()
|
||||
|
||||
|
||||
def test_package_file_list(package_file_loader):
|
||||
templates = package_file_loader.list_templates()
|
||||
assert "foo/test.html" in templates
|
||||
assert "test.html" in templates
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def package_zip_loader(monkeypatch):
|
||||
package_zip = (Path(__file__) / ".." / "res" / "package.zip").resolve()
|
||||
monkeypatch.syspath_prepend(package_zip)
|
||||
return PackageLoader("t_pack")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
|
||||
)
|
||||
def test_package_zip_source(package_zip_loader, template, expect):
|
||||
source, name, up_to_date = package_zip_loader.get_source(None, template)
|
||||
assert source.rstrip() == expect
|
||||
assert name.endswith(os.path.join(*split_template_path(template)))
|
||||
assert up_to_date is None
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="PyPy's zipimporter doesn't have a '_files' attribute.",
|
||||
raises=TypeError,
|
||||
)
|
||||
def test_package_zip_list(package_zip_loader):
|
||||
assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package_path", ["", ".", "./"])
|
||||
def test_package_zip_omit_curdir(package_zip_loader, package_path):
|
||||
"""PackageLoader should not add or include "." or "./" in the root
|
||||
path, it is invalid in zip paths.
|
||||
"""
|
||||
loader = PackageLoader("t_pack", package_path)
|
||||
assert loader.package_path == ""
|
||||
source, _, _ = loader.get_source(None, "templates/foo/test.html")
|
||||
assert source.rstrip() == "FOO"
|
||||
|
||||
|
||||
def test_pep_451_import_hook():
|
||||
class ImportHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
def find_spec(self, name, path=None, target=None):
|
||||
if name != "res":
|
||||
return None
|
||||
|
||||
spec = importlib.machinery.PathFinder.find_spec(name)
|
||||
return importlib.util.spec_from_file_location(
|
||||
name,
|
||||
spec.origin,
|
||||
loader=self,
|
||||
submodule_search_locations=spec.submodule_search_locations,
|
||||
)
|
||||
|
||||
def create_module(self, spec):
|
||||
return None # default behaviour is fine
|
||||
|
||||
def exec_module(self, module):
|
||||
return None # we need this to satisfy the interface, it's wrong
|
||||
|
||||
# ensure we restore `sys.meta_path` after putting in our loader
|
||||
before = sys.meta_path[:]
|
||||
|
||||
try:
|
||||
sys.meta_path.insert(0, ImportHook())
|
||||
package_loader = PackageLoader("res")
|
||||
assert "test.html" in package_loader.list_templates()
|
||||
finally:
|
||||
sys.meta_path[:] = before
|
||||
@@ -1,162 +0,0 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2.exceptions import UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
from jinja2.nativetypes import NativeTemplate
|
||||
from jinja2.runtime import Undefined
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
return NativeEnvironment()
|
||||
|
||||
|
||||
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, str)
|
||||
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, str)
|
||||
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 math.isclose(result, 0.0007)
|
||||
|
||||
|
||||
def test_spontaneous_env():
|
||||
t = NativeTemplate("{{ true }}")
|
||||
assert isinstance(t.environment, NativeEnvironment)
|
||||
|
||||
|
||||
def test_leading_spaces(env):
|
||||
t = env.from_string(" {{ True }}")
|
||||
result = t.render()
|
||||
assert result == " True"
|
||||
|
||||
|
||||
def test_macro(env):
|
||||
t = env.from_string("{%- macro x() -%}{{- [1,2] -}}{%- endmacro -%}{{- x()[1] -}}")
|
||||
result = t.render()
|
||||
assert result == 2
|
||||
assert isinstance(result, int)
|
||||
@@ -1,3 +0,0 @@
|
||||
def test_template_hash(env):
|
||||
template = env.parse("hash test")
|
||||
hash(template)
|
||||
@@ -1,6 +0,0 @@
|
||||
import pickle
|
||||
|
||||
|
||||
def test_environment(env):
|
||||
env = pickle.loads(pickle.dumps(env))
|
||||
assert env.from_string("x={{ x }}").render(x=42) == "x=42"
|
||||
@@ -1,744 +0,0 @@
|
||||
import pytest
|
||||
|
||||
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.utils import pass_context
|
||||
|
||||
|
||||
class TestCorner:
|
||||
def test_assigned_scoping(self, env):
|
||||
t = env.from_string(
|
||||
"""
|
||||
{%- for item in (1, 2, 3, 4) -%}
|
||||
[{{ item }}]
|
||||
{%- endfor %}
|
||||
{{- item -}}
|
||||
"""
|
||||
)
|
||||
assert t.render(item=42) == "[1][2][3][4]42"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
def test_closure_scoping(self, env):
|
||||
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>"
|
||||
|
||||
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>"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TestBug:
|
||||
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"
|
||||
)
|
||||
|
||||
def test_extends_output_bugs(self, env):
|
||||
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 %}"
|
||||
)
|
||||
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>"
|
||||
)
|
||||
|
||||
def test_urlize_filter_closing_punctuation(self, env):
|
||||
tmpl = env.from_string(
|
||||
'{{ "(see http://www.example.org/?page=subj_<desc.h>)"|urlize }}'
|
||||
)
|
||||
assert tmpl.render() == (
|
||||
'(see <a href="http://www.example.org/?page=subj_<desc.h>" '
|
||||
'rel="noopener">http://www.example.org/?page=subj_<desc.h></a>)'
|
||||
)
|
||||
|
||||
def test_loop_call_loop(self, env):
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
|
||||
{% macro test() %}
|
||||
{{ caller() }}
|
||||
{% endmacro %}
|
||||
|
||||
{% for num1 in range(5) %}
|
||||
{% call test() %}
|
||||
{% for num2 in range(10) %}
|
||||
{{ loop.index }}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
assert tmpl.render().split() == [str(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",
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
def test_stacked_locals_scoping_bug(self, env):
|
||||
env = Environment(line_statement_prefix="#")
|
||||
t = env.from_string(
|
||||
"""\
|
||||
# for j in [1, 2]:
|
||||
# set x = 1
|
||||
# for i in [1, 2]:
|
||||
# print x
|
||||
# if i % 2 == 0:
|
||||
# set x = x + 1
|
||||
# endif
|
||||
# endfor
|
||||
# endfor
|
||||
# if a
|
||||
# print 'A'
|
||||
# elif b
|
||||
# print 'B'
|
||||
# elif c == d
|
||||
# print 'C'
|
||||
# else
|
||||
# print 'D'
|
||||
# endif
|
||||
"""
|
||||
)
|
||||
assert t.render(a=0, b=False, c=42, d=42.0) == "1111C"
|
||||
|
||||
def test_stacked_locals_scoping_bug_twoframe(self, env):
|
||||
t = Template(
|
||||
"""
|
||||
{% set x = 1 %}
|
||||
{% for item in foo %}
|
||||
{% if item == 1 %}
|
||||
{% set x = 2 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ x }}
|
||||
"""
|
||||
)
|
||||
rv = t.render(foo=[1]).strip()
|
||||
assert rv == "1"
|
||||
|
||||
def test_call_with_args(self, env):
|
||||
t = Template(
|
||||
"""{% macro dump_users(users) -%}
|
||||
<ul>
|
||||
{%- for user in users -%}
|
||||
<li><p>{{ user.username|e }}</p>{{ caller(user) }}</li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- endmacro -%}
|
||||
|
||||
{% call(user) dump_users(list_of_user) -%}
|
||||
<dl>
|
||||
<dl>Realname</dl>
|
||||
<dd>{{ user.realname|e }}</dd>
|
||||
<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()
|
||||
] == [
|
||||
"<ul><li><p>apo</p><dl>",
|
||||
"<dl>Realname</dl>",
|
||||
"<dd>something else</dd>",
|
||||
"<dl>Description</dl>",
|
||||
"<dd>test</dd>",
|
||||
"</dl>",
|
||||
"</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 %}")
|
||||
|
||||
def test_recursive_loop_compile(self, env):
|
||||
Template(
|
||||
"""
|
||||
{% for p in foo recursive%}
|
||||
{{p.bar}}
|
||||
{% for f in p.fields recursive%}
|
||||
{{f.baz}}
|
||||
{{p.bar}}
|
||||
{% if f.rec %}
|
||||
{{ loop(f.sub) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
)
|
||||
Template(
|
||||
"""
|
||||
{% for p in foo%}
|
||||
{{p.bar}}
|
||||
{% for f in p.fields recursive%}
|
||||
{{f.baz}}
|
||||
{{p.bar}}
|
||||
{% if f.rec %}
|
||||
{{ loop(f.sub) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
)
|
||||
|
||||
def test_else_loop_bug(self, env):
|
||||
t = Template(
|
||||
"""
|
||||
{% for x in y %}
|
||||
{{ loop.index0 }}
|
||||
{% else %}
|
||||
{% for i in range(3) %}{{ i }}{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
)
|
||||
assert t.render(y=[]).strip() == "012"
|
||||
|
||||
def test_correct_prefix_loader_name(self, env):
|
||||
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_pass_context_callable_class(self, env):
|
||||
class CallableClass:
|
||||
@pass_context
|
||||
def __call__(self, ctx):
|
||||
return ctx.resolve("hello")
|
||||
|
||||
tpl = Template("""{{ callableclass() }}""")
|
||||
output = tpl.render(callableclass=CallableClass(), hello="TEST")
|
||||
expected = "TEST"
|
||||
|
||||
assert output == expected
|
||||
|
||||
def test_block_set_with_extends(self):
|
||||
env = Environment(
|
||||
loader=DictLoader({"main": "{% block body %}[{{ x }}]{% endblock %}"})
|
||||
)
|
||||
t = env.from_string('{% extends "main" %}{% set x %}42{% endset %}')
|
||||
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"
|
||||
|
||||
def test_macro_var_bug(self, env):
|
||||
tmpl = env.from_string(
|
||||
"""
|
||||
{% set i = 1 %}
|
||||
{% macro test() %}
|
||||
{% for i in range(0, 10) %}{{ i }}{% endfor %}
|
||||
{% endmacro %}{{ test() }}
|
||||
"""
|
||||
)
|
||||
assert tmpl.render().strip() == "0123456789"
|
||||
|
||||
def test_macro_var_bug_advanced(self, env):
|
||||
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"
|
||||
|
||||
def test_callable_defaults(self):
|
||||
env = Environment()
|
||||
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"
|
||||
|
||||
def test_macro_escaping(self):
|
||||
env = Environment(autoescape=lambda x: False)
|
||||
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(
|
||||
"""
|
||||
{% set n=[1,2,3,4,5] %}
|
||||
{% for n in [[1,2,3], [3,4,5], [5,6,7]] %}
|
||||
|
||||
{% macro x(l) %}
|
||||
{{ l.pop() }}
|
||||
{% if l %}{{ x(l) }}{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{{ x(n) }}
|
||||
|
||||
{% endfor %}
|
||||
"""
|
||||
)
|
||||
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": """
|
||||
{%- set foo = 'bar' -%}
|
||||
{% include 'x.html' -%}
|
||||
""",
|
||||
"b.html": """
|
||||
{%- set foo = 'bar' -%}
|
||||
{% block test %}{% include 'x.html' %}{% endblock -%}
|
||||
""",
|
||||
"c.html": """
|
||||
{%- set foo = 'bar' -%}
|
||||
{% block test %}{% set foo = foo
|
||||
%}{% include 'x.html' %}{% endblock -%}
|
||||
""",
|
||||
"x.html": """{{ foo }}|{{ test }}""",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
def test_caller_scoping(self, env):
|
||||
t = env.from_string(
|
||||
"""
|
||||
{% macro detail(icon, value) -%}
|
||||
{% if value -%}
|
||||
<p><span class="fa fa-fw fa-{{ icon }}"></span>
|
||||
{%- if caller is undefined -%}
|
||||
{{ value }}
|
||||
{%- else -%}
|
||||
{{ caller(value, *varargs) }}
|
||||
{%- endif -%}</p>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro link_detail(icon, value, href) -%}
|
||||
{% call(value, href) detail(icon, value, href) -%}
|
||||
<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>'
|
||||
)
|
||||
|
||||
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 %}{{ 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"
|
||||
|
||||
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!]"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
def test_macro_blocks(self, env):
|
||||
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"
|
||||
|
||||
def test_recursive_loop_filter(self, env):
|
||||
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 %}
|
||||
<url><loc>{{ page.url }}</loc></url>
|
||||
{{- loop(page.children) }}
|
||||
{%- endfor %}
|
||||
</urlset>
|
||||
"""
|
||||
)
|
||||
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>",
|
||||
]
|
||||
|
||||
def test_empty_if(self, env):
|
||||
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"
|
||||
|
||||
def test_loop_include(self):
|
||||
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"
|
||||
assert t.list == [1, 2]
|
||||
assert repr(t) == "('foo', [1, 2])"
|
||||
assert str(t) == "('foo', [1, 2])"
|
||||
|
||||
def test_custom_context(self, env):
|
||||
from jinja2.runtime import Context
|
||||
|
||||
class MyContext(Context):
|
||||
pass
|
||||
|
||||
class MyEnvironment(Environment):
|
||||
context_class = MyContext
|
||||
|
||||
loader = DictLoader({"base": "{{ foobar }}", "test": '{% extends "base" %}'})
|
||||
env = MyEnvironment(loader=loader)
|
||||
assert env.get_template("test").render(foobar="test") == "test"
|
||||
|
||||
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"
|
||||
|
||||
def test_markup_and_chainable_undefined(self):
|
||||
from markupsafe import Markup
|
||||
from jinja2.runtime import ChainableUndefined
|
||||
|
||||
assert str(Markup(ChainableUndefined())) == ""
|
||||
|
||||
def test_scoped_block_loop_vars(self, env):
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
Start
|
||||
{% for i in ["foo", "bar"] -%}
|
||||
{% block body scoped -%}
|
||||
{{ loop.index }}) {{ i }}{% if loop.last %} last{% endif -%}
|
||||
{%- endblock %}
|
||||
{% endfor -%}
|
||||
End"""
|
||||
)
|
||||
assert tmpl.render() == "Start\n1) foo\n2) bar last\nEnd"
|
||||
|
||||
def test_pass_context_loop_vars(self, env):
|
||||
@pass_context
|
||||
def test(ctx):
|
||||
return f"{ctx['i']}{ctx['j']}"
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
{% set i = 42 %}
|
||||
{%- for idx in range(2) -%}
|
||||
{{ i }}{{ j }}
|
||||
{% set i = idx -%}
|
||||
{%- set j = loop.index -%}
|
||||
{{ test() }}
|
||||
{{ i }}{{ j }}
|
||||
{% endfor -%}
|
||||
{{ i }}{{ j }}"""
|
||||
)
|
||||
tmpl.globals["test"] = test
|
||||
assert tmpl.render() == "42\n01\n01\n42\n12\n12\n42"
|
||||
|
||||
def test_pass_context_scoped_loop_vars(self, env):
|
||||
@pass_context
|
||||
def test(ctx):
|
||||
return f"{ctx['i']}"
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
{% set i = 42 %}
|
||||
{%- for idx in range(2) -%}
|
||||
{{ i }}
|
||||
{%- set i = loop.index0 -%}
|
||||
{% block body scoped %}
|
||||
{{ test() }}
|
||||
{% endblock -%}
|
||||
{% endfor -%}
|
||||
{{ i }}"""
|
||||
)
|
||||
tmpl.globals["test"] = test
|
||||
assert tmpl.render() == "42\n0\n42\n1\n42"
|
||||
|
||||
def test_pass_context_in_blocks(self, env):
|
||||
@pass_context
|
||||
def test(ctx):
|
||||
return f"{ctx['i']}"
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
{%- set i = 42 -%}
|
||||
{{ i }}
|
||||
{% block body -%}
|
||||
{% set i = 24 -%}
|
||||
{{ test() }}
|
||||
{% endblock -%}
|
||||
{{ i }}"""
|
||||
)
|
||||
tmpl.globals["test"] = test
|
||||
assert tmpl.render() == "42\n24\n42"
|
||||
|
||||
def test_pass_context_block_and_loop(self, env):
|
||||
@pass_context
|
||||
def test(ctx):
|
||||
return f"{ctx['i']}"
|
||||
|
||||
tmpl = env.from_string(
|
||||
"""\
|
||||
{%- set i = 42 -%}
|
||||
{% for idx in range(2) -%}
|
||||
{{ test() }}
|
||||
{%- set i = idx -%}
|
||||
{% block body scoped %}
|
||||
{{ test() }}
|
||||
{% set i = 24 -%}
|
||||
{{ test() }}
|
||||
{% endblock -%}
|
||||
{{ test() }}
|
||||
{% endfor -%}
|
||||
{{ test() }}"""
|
||||
)
|
||||
tmpl.globals["test"] = test
|
||||
|
||||
# values set within a block or loop should not
|
||||
# show up outside of it
|
||||
assert tmpl.render() == "42\n0\n24\n0\n42\n1\n24\n1\n42"
|
||||
|
||||
@pytest.mark.parametrize("op", ["extends", "include"])
|
||||
def test_cached_extends(self, op):
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{"base": "{{ x }} {{ y }}", "main": f"{{% {op} 'base' %}}"}
|
||||
)
|
||||
)
|
||||
env.globals["x"] = "x"
|
||||
env.globals["y"] = "y"
|
||||
|
||||
# template globals overlay env globals
|
||||
tmpl = env.get_template("main", globals={"x": "bar"})
|
||||
assert tmpl.render() == "bar y"
|
||||
|
||||
# base was loaded indirectly, it just has env globals
|
||||
tmpl = env.get_template("base")
|
||||
assert tmpl.render() == "x y"
|
||||
|
||||
# set template globals for base, no longer uses env globals
|
||||
tmpl = env.get_template("base", globals={"x": 42})
|
||||
assert tmpl.render() == "42 y"
|
||||
|
||||
# templates are cached, they keep template globals set earlier
|
||||
tmpl = env.get_template("main")
|
||||
assert tmpl.render() == "bar y"
|
||||
|
||||
tmpl = env.get_template("base")
|
||||
assert tmpl.render() == "42 y"
|
||||
|
||||
def test_nested_loop_scoping(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{% set output %}{% for x in [1,2,3] %}hello{% endfor %}"
|
||||
"{% endset %}{{ output }}"
|
||||
)
|
||||
assert tmpl.render() == "hellohellohello"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"])
|
||||
def test_unicode_whitespace(env, unicode_char):
|
||||
content = "Lorem ipsum\n" + unicode_char + "\nMore text"
|
||||
tmpl = env.from_string(content)
|
||||
assert tmpl.render() == content
|
||||
@@ -1,75 +0,0 @@
|
||||
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_pass_arg_marker():
|
||||
"""If a callable class has a ``__getattr__`` that returns True-like
|
||||
values for arbitrary attrs, it should not be incorrectly identified
|
||||
as a ``pass_context`` function.
|
||||
"""
|
||||
|
||||
class Calc:
|
||||
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"
|
||||
@@ -1,173 +0,0 @@
|
||||
import pytest
|
||||
from markupsafe import escape
|
||||
|
||||
from jinja2 import Environment
|
||||
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:
|
||||
def bar(self):
|
||||
return 23
|
||||
|
||||
@unsafe
|
||||
def foo(self):
|
||||
return 42
|
||||
|
||||
def __repr__(self):
|
||||
return "PrivateStuff"
|
||||
|
||||
|
||||
class PublicStuff:
|
||||
def bar(self):
|
||||
return 23
|
||||
|
||||
def _foo(self):
|
||||
return 42
|
||||
|
||||
def __repr__(self):
|
||||
return "PublicStuff"
|
||||
|
||||
|
||||
class TestSandbox:
|
||||
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) == ""
|
||||
# security error comes from __class__ already.
|
||||
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)
|
||||
|
||||
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 %}",
|
||||
)
|
||||
|
||||
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>"
|
||||
assert t.render() == escaped_out
|
||||
assert str(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
|
||||
|
||||
def test_attr_filter(self, env):
|
||||
env = SandboxedEnvironment()
|
||||
tmpl = env.from_string('{{ cls|attr("__subclasses__")() }}')
|
||||
pytest.raises(SecurityError, tmpl.render, cls=int)
|
||||
|
||||
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"):
|
||||
env = SandboxedEnvironment()
|
||||
env.binop_table["+"] = disable_op
|
||||
t = env.from_string(f"{{{{ {expr} }}}}")
|
||||
assert t.render(ctx) == rv
|
||||
env.intercepted_binops = frozenset(["+"])
|
||||
t = env.from_string(f"{{{{ {expr} }}}}")
|
||||
with pytest.raises(TemplateRuntimeError):
|
||||
t.render(ctx)
|
||||
|
||||
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"):
|
||||
env = SandboxedEnvironment()
|
||||
env.unop_table["-"] = disable_op
|
||||
t = env.from_string(f"{{{{ {expr} }}}}")
|
||||
assert t.render(ctx) == rv
|
||||
env.intercepted_unops = frozenset(["-"])
|
||||
t = env.from_string(f"{{{{ {expr} }}}}")
|
||||
with pytest.raises(TemplateRuntimeError):
|
||||
t.render(ctx)
|
||||
|
||||
|
||||
class TestStringFormat:
|
||||
def test_basic_format_safety(self):
|
||||
env = SandboxedEnvironment()
|
||||
t = env.from_string('{{ "a{0.__class__}b".format(42) }}')
|
||||
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"
|
||||
|
||||
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>"
|
||||
|
||||
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>"
|
||||
|
||||
def test_empty_braces_format(self):
|
||||
env = SandboxedEnvironment()
|
||||
t1 = env.from_string('{{ ("a{}b{}").format("foo", "42")}}')
|
||||
t2 = env.from_string('{{ ("a{}b{}"|safe).format(42, "<foo>") }}')
|
||||
assert t1.render() == "afoob42"
|
||||
assert t2.render() == "a42b<foo>"
|
||||
|
||||
|
||||
class TestStringFormatMap:
|
||||
def test_basic_format_safety(self):
|
||||
env = SandboxedEnvironment()
|
||||
t = env.from_string('{{ "a{x.__class__}b".format_map({"x":42}) }}')
|
||||
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"
|
||||
|
||||
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>"
|
||||
@@ -1,233 +0,0 @@
|
||||
import pytest
|
||||
from markupsafe import Markup
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import TemplateAssertionError
|
||||
from jinja2 import TemplateRuntimeError
|
||||
|
||||
|
||||
class MyDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class TestTestsCase:
|
||||
def test_defined(self, env):
|
||||
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"
|
||||
|
||||
def test_odd(self, env):
|
||||
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"
|
||||
|
||||
# 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(f"{{{{ {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"
|
||||
|
||||
def test_equalto(self, env):
|
||||
tmpl = env.from_string(
|
||||
"{{ 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 }}"
|
||||
)
|
||||
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(f"{{{{ 2 is {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"
|
||||
|
||||
def test_no_paren_for_arg1(self, env):
|
||||
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"
|
||||
|
||||
def test_greaterthan(self, env):
|
||||
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"
|
||||
|
||||
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)"),
|
||||
]
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_name_undefined(env):
|
||||
with pytest.raises(TemplateAssertionError, match="No test named 'f'"):
|
||||
env.from_string("{{ x is f }}")
|
||||
|
||||
|
||||
def test_name_undefined_in_if(env):
|
||||
t = env.from_string("{% if x is defined %}{{ x is f }}{% endif %}")
|
||||
assert t.render() == ""
|
||||
|
||||
with pytest.raises(TemplateRuntimeError, match="No test named 'f'"):
|
||||
t.render(x=1)
|
||||
|
||||
|
||||
def test_is_filter(env):
|
||||
assert env.call_test("filter", "title")
|
||||
assert not env.call_test("filter", "bad-name")
|
||||
|
||||
|
||||
def test_is_test(env):
|
||||
assert env.call_test("test", "number")
|
||||
assert not env.call_test("test", "bad-name")
|
||||
@@ -1,185 +0,0 @@
|
||||
import pickle
|
||||
import random
|
||||
from collections import deque
|
||||
from copy import copy as shallow_copy
|
||||
|
||||
import pytest
|
||||
from markupsafe import Markup
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestLRUCache:
|
||||
def test_simple(self):
|
||||
d = LRUCache(3)
|
||||
d["a"] = 1
|
||||
d["b"] = 2
|
||||
d["c"] = 3
|
||||
d["a"]
|
||||
d["d"] = 4
|
||||
assert d.keys() == ["d", "a", "c"]
|
||||
|
||||
def test_values(self):
|
||||
cache = LRUCache(3)
|
||||
cache["b"] = 1
|
||||
cache["a"] = 2
|
||||
assert cache.values() == [2, 1]
|
||||
|
||||
def test_values_empty(self):
|
||||
cache = LRUCache(2)
|
||||
assert cache.values() == []
|
||||
|
||||
def test_pickleable(self):
|
||||
cache = LRUCache(2)
|
||||
cache["foo"] = 42
|
||||
cache["bar"] = 23
|
||||
cache["foo"]
|
||||
|
||||
for protocol in range(3):
|
||||
copy = pickle.loads(pickle.dumps(cache, protocol))
|
||||
assert copy.capacity == cache.capacity
|
||||
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 copy.keys() == ["c", "b"]
|
||||
|
||||
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([])}
|
||||
|
||||
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("<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:
|
||||
def test_object_type_repr(self):
|
||||
class X:
|
||||
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"
|
||||
|
||||
def test_autoescape_select(self):
|
||||
func = select_autoescape(
|
||||
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")
|
||||
assert func("foo.htm")
|
||||
assert not func("foo.txt")
|
||||
assert func("FOO.HTML")
|
||||
assert not func("FOO.TXT")
|
||||
|
||||
|
||||
class TestEscapeUrlizeTarget:
|
||||
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>"
|
||||
)
|
||||
|
||||
|
||||
class TestLoremIpsum:
|
||||
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), str)
|
||||
|
||||
def test_lorem_ipsum_n(self):
|
||||
"""Test that the n (number of lines) works as expected."""
|
||||
assert generate_lorem_ipsum(n=0, html=False) == ""
|
||||
for n in range(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(5):
|
||||
m = random.randrange(20, 99)
|
||||
for _ in range(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(5):
|
||||
m = random.randrange(21, 100)
|
||||
for _ in range(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) == "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)
|
||||
Reference in New Issue
Block a user