mirror of https://gitee.com/openkylin/jinja2.git
Import Upstream version 3.0.3
This commit is contained in:
commit
3088e602dd
|
@ -0,0 +1,915 @@
|
||||||
|
.. currentmodule:: jinja2
|
||||||
|
|
||||||
|
Version 3.0.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-11-09
|
||||||
|
|
||||||
|
- Fix traceback rewriting internals for Python 3.10 and 3.11.
|
||||||
|
:issue:`1535`
|
||||||
|
- Fix how the native environment treats leading and trailing spaces
|
||||||
|
when parsing values on Python 3.10. :pr:`1537`
|
||||||
|
- Improve async performance by avoiding checks for common types.
|
||||||
|
:issue:`1514`
|
||||||
|
- Revert change to ``hash(Node)`` behavior. Nodes are hashed by id
|
||||||
|
again :issue:`1521`
|
||||||
|
- ``PackageLoader`` works when the package is a single module file.
|
||||||
|
:issue:`1512`
|
||||||
|
|
||||||
|
|
||||||
|
Version 3.0.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-10-04
|
||||||
|
|
||||||
|
- Fix a loop scoping bug that caused assignments in nested loops
|
||||||
|
to still be referenced outside of it. :issue:`1427`
|
||||||
|
- Make ``compile_templates`` deterministic for filter and import
|
||||||
|
names. :issue:`1452, 1453`
|
||||||
|
- Revert an unintended change that caused ``Undefined`` to act like
|
||||||
|
``StrictUndefined`` for the ``in`` operator. :issue:`1448`
|
||||||
|
- Imported macros have access to the current template globals in async
|
||||||
|
environments. :issue:`1494`
|
||||||
|
- ``PackageLoader`` will not include a current directory (.) path
|
||||||
|
segment. This allows loading templates from the root of a zip
|
||||||
|
import. :issue:`1467`
|
||||||
|
|
||||||
|
|
||||||
|
Version 3.0.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-05-18
|
||||||
|
|
||||||
|
- Update MarkupSafe dependency to >= 2.0. :pr:`1418`
|
||||||
|
- Mark top-level names as exported so type checking understands
|
||||||
|
imports in user projects. :issue:`1426`
|
||||||
|
- Fix some types that weren't available in Python 3.6.0. :issue:`1433`
|
||||||
|
- The deprecation warning for unneeded ``autoescape`` and ``with_``
|
||||||
|
extensions shows more relevant context. :issue:`1429`
|
||||||
|
- Fixed calling deprecated ``jinja2.Markup`` without an argument.
|
||||||
|
Use ``markupsafe.Markup`` instead. :issue:`1438`
|
||||||
|
- Calling sync ``render`` for an async template uses ``asyncio.run``
|
||||||
|
on Python >= 3.7. This fixes a deprecation that Python 3.10
|
||||||
|
introduces. :issue:`1443`
|
||||||
|
|
||||||
|
|
||||||
|
Version 3.0.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2021-05-11
|
||||||
|
|
||||||
|
- Drop support for Python 2.7 and 3.5.
|
||||||
|
- Bump MarkupSafe dependency to >=1.1.
|
||||||
|
- Bump Babel optional dependency to >=2.1.
|
||||||
|
- Remove code that was marked deprecated.
|
||||||
|
- Add type hinting. :pr:`1412`
|
||||||
|
- Use :pep:`451` API to load templates with
|
||||||
|
:class:`~loaders.PackageLoader`. :issue:`1168`
|
||||||
|
- Fix a bug that caused imported macros to not have access to the
|
||||||
|
current template's globals. :issue:`688`
|
||||||
|
- Add ability to ignore ``trim_blocks`` using ``+%}``. :issue:`1036`
|
||||||
|
- Fix a bug that caused custom async-only filters to fail with
|
||||||
|
constant input. :issue:`1279`
|
||||||
|
- Fix UndefinedError incorrectly being thrown on an undefined variable
|
||||||
|
instead of ``Undefined`` being returned on
|
||||||
|
``NativeEnvironment`` on Python 3.10. :issue:`1335`
|
||||||
|
- Blocks can be marked as ``required``. They must be overridden at
|
||||||
|
some point, but not necessarily by the direct child. :issue:`1147`
|
||||||
|
- Deprecate the ``autoescape`` and ``with`` extensions, they are
|
||||||
|
built-in to the compiler. :issue:`1203`
|
||||||
|
- The ``urlize`` filter recognizes ``mailto:`` links and takes
|
||||||
|
``extra_schemes`` (or ``env.policies["urlize.extra_schemes"]``) to
|
||||||
|
recognize other schemes. It tries to balance parentheses within a
|
||||||
|
URL instead of ignoring trailing characters. The parsing in general
|
||||||
|
has been updated to be more efficient and match more cases. URLs
|
||||||
|
without a scheme are linked as ``https://`` instead of ``http://``.
|
||||||
|
:issue:`522, 827, 1172`, :pr:`1195`
|
||||||
|
- Filters that get attributes, such as ``map`` and ``groupby``, can
|
||||||
|
use a false or empty value as a default. :issue:`1331`
|
||||||
|
- Fix a bug that prevented variables set in blocks or loops from
|
||||||
|
being accessed in custom context functions. :issue:`768`
|
||||||
|
- Fix a bug that caused scoped blocks from accessing special loop
|
||||||
|
variables. :issue:`1088`
|
||||||
|
- Update the template globals when calling
|
||||||
|
``Environment.get_template(globals=...)`` even if the template was
|
||||||
|
already loaded. :issue:`295`
|
||||||
|
- Do not raise an error for undefined filters in unexecuted
|
||||||
|
if-statements and conditional expressions. :issue:`842`
|
||||||
|
- Add ``is filter`` and ``is test`` tests to test if a name is a
|
||||||
|
registered filter or test. This allows checking if a filter is
|
||||||
|
available in a template before using it. Test functions can be
|
||||||
|
decorated with ``@pass_environment``, ``@pass_eval_context``,
|
||||||
|
or ``@pass_context``. :issue:`842`, :pr:`1248`
|
||||||
|
- Support ``pgettext`` and ``npgettext`` (message contexts) in i18n
|
||||||
|
extension. :issue:`441`
|
||||||
|
- The ``|indent`` filter's ``width`` argument can be a string to
|
||||||
|
indent by. :pr:`1167`
|
||||||
|
- The parser understands hex, octal, and binary integer literals.
|
||||||
|
:issue:`1170`
|
||||||
|
- ``Undefined.__contains__`` (``in``) raises an ``UndefinedError``
|
||||||
|
instead of a ``TypeError``. :issue:`1198`
|
||||||
|
- ``Undefined`` is iterable in an async environment. :issue:`1294`
|
||||||
|
- ``NativeEnvironment`` supports async mode. :issue:`1362`
|
||||||
|
- Template rendering only treats ``\n``, ``\r\n`` and ``\r`` as line
|
||||||
|
breaks. Other characters are left unchanged. :issue:`769, 952, 1313`
|
||||||
|
- ``|groupby`` filter takes an optional ``default`` argument.
|
||||||
|
:issue:`1359`
|
||||||
|
- The function and filter decorators have been renamed and unified.
|
||||||
|
The old names are deprecated. :issue:`1381`
|
||||||
|
|
||||||
|
- ``pass_context`` replaces ``contextfunction`` and
|
||||||
|
``contextfilter``.
|
||||||
|
- ``pass_eval_context`` replaces ``evalcontextfunction`` and
|
||||||
|
``evalcontextfilter``
|
||||||
|
- ``pass_environment`` replaces ``environmentfunction`` and
|
||||||
|
``environmentfilter``.
|
||||||
|
|
||||||
|
- Async support no longer requires Jinja to patch itself. It must
|
||||||
|
still be enabled with ``Environment(enable_async=True)``.
|
||||||
|
:issue:`1390`
|
||||||
|
- Overriding ``Context.resolve`` is deprecated, override
|
||||||
|
``resolve_or_missing`` instead. :issue:`1380`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.11.3
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2021-01-31
|
||||||
|
|
||||||
|
- Improve the speed of the ``urlize`` filter by reducing regex
|
||||||
|
backtracking. Email matching requires a word character at the start
|
||||||
|
of the domain part, and only word characters in the TLD. :pr:`1343`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.11.2
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2020-04-13
|
||||||
|
|
||||||
|
- Fix a bug that caused callable objects with ``__getattr__``, like
|
||||||
|
:class:`~unittest.mock.Mock` to be treated as a
|
||||||
|
:func:`contextfunction`. :issue:`1145`
|
||||||
|
- Update ``wordcount`` filter to trigger :class:`Undefined` methods
|
||||||
|
by wrapping the input in :func:`soft_str`. :pr:`1160`
|
||||||
|
- Fix a hang when displaying tracebacks on Python 32-bit.
|
||||||
|
:issue:`1162`
|
||||||
|
- Showing an undefined error for an object that raises
|
||||||
|
``AttributeError`` on access doesn't cause a recursion error.
|
||||||
|
:issue:`1177`
|
||||||
|
- Revert changes to :class:`~loaders.PackageLoader` from 2.10 which
|
||||||
|
removed the dependency on setuptools and pkg_resources, and added
|
||||||
|
limited support for namespace packages. The changes caused issues
|
||||||
|
when using Pytest. Due to the difficulty in supporting Python 2 and
|
||||||
|
:pep:`451` simultaneously, the changes are reverted until 3.0.
|
||||||
|
:pr:`1182`
|
||||||
|
- Fix line numbers in error messages when newlines are stripped.
|
||||||
|
:pr:`1178`
|
||||||
|
- The special ``namespace()`` assignment object in templates works in
|
||||||
|
async environments. :issue:`1180`
|
||||||
|
- Fix whitespace being removed before tags in the middle of lines when
|
||||||
|
``lstrip_blocks`` is enabled. :issue:`1138`
|
||||||
|
- :class:`~nativetypes.NativeEnvironment` doesn't evaluate
|
||||||
|
intermediate strings during rendering. This prevents early
|
||||||
|
evaluation which could change the value of an expression.
|
||||||
|
:issue:`1186`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.11.1
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2020-01-30
|
||||||
|
|
||||||
|
- Fix a bug that prevented looking up a key after an attribute
|
||||||
|
(``{{ data.items[1:] }}``) in an async template. :issue:`1141`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.11.0
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2020-01-27
|
||||||
|
|
||||||
|
- Drop support for Python 2.6, 3.3, and 3.4. This will be the last
|
||||||
|
version to support Python 2.7 and 3.5.
|
||||||
|
- Added a new ``ChainableUndefined`` class to support getitem and
|
||||||
|
getattr on an undefined object. :issue:`977`
|
||||||
|
- Allow ``{%+`` syntax (with NOP behavior) when ``lstrip_blocks`` is
|
||||||
|
disabled. :issue:`748`
|
||||||
|
- Added a ``default`` parameter for the ``map`` filter. :issue:`557`
|
||||||
|
- Exclude environment globals from
|
||||||
|
:func:`meta.find_undeclared_variables`. :issue:`931`
|
||||||
|
- Float literals can be written with scientific notation, like
|
||||||
|
2.56e-3. :issue:`912`, :pr:`922`
|
||||||
|
- Int and float literals can be written with the '_' separator for
|
||||||
|
legibility, like 12_345. :pr:`923`
|
||||||
|
- Fix a bug causing deadlocks in ``LRUCache.setdefault``. :pr:`1000`
|
||||||
|
- The ``trim`` filter takes an optional string of characters to trim.
|
||||||
|
:pr:`828`
|
||||||
|
- A new ``jinja2.ext.debug`` extension adds a ``{% debug %}`` tag to
|
||||||
|
quickly dump the current context and available filters and tests.
|
||||||
|
:issue:`174`, :pr:`798, 983`
|
||||||
|
- Lexing templates with large amounts of whitespace is much faster.
|
||||||
|
:issue:`857`, :pr:`858`
|
||||||
|
- Parentheses around comparisons are preserved, so
|
||||||
|
``{{ 2 * (3 < 5) }}`` outputs "2" instead of "False".
|
||||||
|
:issue:`755`, :pr:`938`
|
||||||
|
- Add new ``boolean``, ``false``, ``true``, ``integer`` and ``float``
|
||||||
|
tests. :pr:`824`
|
||||||
|
- The environment's ``finalize`` function is only applied to the
|
||||||
|
output of expressions (constant or not), not static template data.
|
||||||
|
:issue:`63`
|
||||||
|
- When providing multiple paths to ``FileSystemLoader``, a template
|
||||||
|
can have the same name as a directory. :issue:`821`
|
||||||
|
- Always return :class:`Undefined` when omitting the ``else`` clause
|
||||||
|
in a ``{{ 'foo' if bar }}`` expression, regardless of the
|
||||||
|
environment's ``undefined`` class. Omitting the ``else`` clause is a
|
||||||
|
valid shortcut and should not raise an error when using
|
||||||
|
:class:`StrictUndefined`. :issue:`710`, :pr:`1079`
|
||||||
|
- Fix behavior of ``loop`` control variables such as ``length`` and
|
||||||
|
``revindex0`` when looping over a generator. :issue:`459, 751, 794`,
|
||||||
|
:pr:`993`
|
||||||
|
- Async support is only loaded the first time an environment enables
|
||||||
|
it, in order to avoid a slow initial import. :issue:`765`
|
||||||
|
- In async environments, the ``|map`` filter will await the filter
|
||||||
|
call if needed. :pr:`913`
|
||||||
|
- In for loops that access ``loop`` attributes, the iterator is not
|
||||||
|
advanced ahead of the current iteration unless ``length``,
|
||||||
|
``revindex``, ``nextitem``, or ``last`` are accessed. This makes it
|
||||||
|
less likely to break ``groupby`` results. :issue:`555`, :pr:`1101`
|
||||||
|
- In async environments, the ``loop`` attributes ``length`` and
|
||||||
|
``revindex`` work for async iterators. :pr:`1101`
|
||||||
|
- In async environments, values from attribute/property access will
|
||||||
|
be awaited if needed. :pr:`1101`
|
||||||
|
- :class:`~loader.PackageLoader` doesn't depend on setuptools or
|
||||||
|
pkg_resources. :issue:`970`
|
||||||
|
- ``PackageLoader`` has limited support for :pep:`420` namespace
|
||||||
|
packages. :issue:`1097`
|
||||||
|
- Support :class:`os.PathLike` objects in
|
||||||
|
:class:`~loader.FileSystemLoader` and :class:`~loader.ModuleLoader`.
|
||||||
|
:issue:`870`
|
||||||
|
- :class:`~nativetypes.NativeTemplate` correctly handles quotes
|
||||||
|
between expressions. ``"'{{ a }}', '{{ b }}'"`` renders as the tuple
|
||||||
|
``('1', '2')`` rather than the string ``'1, 2'``. :issue:`1020`
|
||||||
|
- Creating a :class:`~nativetypes.NativeTemplate` directly creates a
|
||||||
|
:class:`~nativetypes.NativeEnvironment` instead of a default
|
||||||
|
:class:`Environment`. :issue:`1091`
|
||||||
|
- After calling ``LRUCache.copy()``, the copy's queue methods point to
|
||||||
|
the correct queue. :issue:`843`
|
||||||
|
- Compiling templates always writes UTF-8 instead of defaulting to the
|
||||||
|
system encoding. :issue:`889`
|
||||||
|
- ``|wordwrap`` filter treats existing newlines as separate paragraphs
|
||||||
|
to be wrapped individually, rather than creating short intermediate
|
||||||
|
lines. :issue:`175`
|
||||||
|
- Add ``break_on_hyphens`` parameter to ``|wordwrap`` filter.
|
||||||
|
:issue:`550`
|
||||||
|
- Cython compiled functions decorated as context functions will be
|
||||||
|
passed the context. :pr:`1108`
|
||||||
|
- When chained comparisons of constants are evaluated at compile time,
|
||||||
|
the result follows Python's behavior of returning ``False`` if any
|
||||||
|
comparison returns ``False``, rather than only the last one.
|
||||||
|
:issue:`1102`
|
||||||
|
- Tracebacks for exceptions in templates show the correct line numbers
|
||||||
|
and source for Python >= 3.7. :issue:`1104`
|
||||||
|
- Tracebacks for template syntax errors in Python 3 no longer show
|
||||||
|
internal compiler frames. :issue:`763`
|
||||||
|
- Add a ``DerivedContextReference`` node that can be used by
|
||||||
|
extensions to get the current context and local variables such as
|
||||||
|
``loop``. :issue:`860`
|
||||||
|
- Constant folding during compilation is applied to some node types
|
||||||
|
that were previously overlooked. :issue:`733`
|
||||||
|
- ``TemplateSyntaxError.source`` is not empty when raised from an
|
||||||
|
included template. :issue:`457`
|
||||||
|
- Passing an ``Undefined`` value to ``get_template`` (such as through
|
||||||
|
``extends``, ``import``, or ``include``), raises an
|
||||||
|
``UndefinedError`` consistently. ``select_template`` will show the
|
||||||
|
undefined message in the list of attempts rather than the empty
|
||||||
|
string. :issue:`1037`
|
||||||
|
- ``TemplateSyntaxError`` can be pickled. :pr:`1117`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.10.3
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2019-10-04
|
||||||
|
|
||||||
|
- Fix a typo in Babel entry point in ``setup.py`` that was preventing
|
||||||
|
installation.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.10.2
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2019-10-04
|
||||||
|
|
||||||
|
- Fix Python 3.7 deprecation warnings.
|
||||||
|
- Using ``range`` in the sandboxed environment uses ``xrange`` on
|
||||||
|
Python 2 to avoid memory use. :issue:`933`
|
||||||
|
- Use Python 3.7's better traceback support to avoid a core dump when
|
||||||
|
using debug builds of Python 3.7. :issue:`1050`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.10.1
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2019-04-06
|
||||||
|
|
||||||
|
- ``SandboxedEnvironment`` securely handles ``str.format_map`` in
|
||||||
|
order to prevent code execution through untrusted format strings.
|
||||||
|
The sandbox already handled ``str.format``.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.10
|
||||||
|
------------
|
||||||
|
|
||||||
|
Released 2017-11-08
|
||||||
|
|
||||||
|
- Added a new extension node called ``OverlayScope`` which can be used
|
||||||
|
to create an unoptimized scope that will look up all variables from
|
||||||
|
a derived context.
|
||||||
|
- Added an ``in`` test that works like the in operator. This can be
|
||||||
|
used in combination with ``reject`` and ``select``.
|
||||||
|
- Added ``previtem`` and ``nextitem`` to loop contexts, providing
|
||||||
|
access to the previous/next item in the loop. If such an item does
|
||||||
|
not exist, the value is undefined.
|
||||||
|
- Added ``changed(*values)`` to loop contexts, providing an easy way
|
||||||
|
of checking whether a value has changed since the last iteration (or
|
||||||
|
rather since the last call of the method)
|
||||||
|
- Added a ``namespace`` function that creates a special object which
|
||||||
|
allows attribute assignment using the ``set`` tag. This can be used
|
||||||
|
to carry data across scopes, e.g. from a loop body to code that
|
||||||
|
comes after the loop.
|
||||||
|
- Added a ``trimmed`` modifier to ``{% trans %}`` to strip linebreaks
|
||||||
|
and surrounding whitespace. Also added a new policy to enable this
|
||||||
|
for all ``trans`` blocks.
|
||||||
|
- The ``random`` filter is no longer incorrectly constant folded and
|
||||||
|
will produce a new random choice each time the template is rendered.
|
||||||
|
:pr:`478`
|
||||||
|
- Added a ``unique`` filter. :pr:`469`
|
||||||
|
- Added ``min`` and ``max`` filters. :pr:`475`
|
||||||
|
- Added tests for all comparison operators: ``eq``, ``ne``, ``lt``,
|
||||||
|
``le``, ``gt``, ``ge``. :pr:`665`
|
||||||
|
- ``import`` statement cannot end with a trailing comma. :pr:`617`,
|
||||||
|
:pr:`618`
|
||||||
|
- ``indent`` filter will not indent blank lines by default. :pr:`685`
|
||||||
|
- Add ``reverse`` argument for ``dictsort`` filter. :pr:`692`
|
||||||
|
- Add a ``NativeEnvironment`` that renders templates to native Python
|
||||||
|
types instead of strings. :pr:`708`
|
||||||
|
- Added filter support to the block ``set`` tag. :pr:`489`
|
||||||
|
- ``tojson`` filter marks output as safe to match documented behavior.
|
||||||
|
:pr:`718`
|
||||||
|
- Resolved a bug where getting debug locals for tracebacks could
|
||||||
|
modify template context.
|
||||||
|
- Fixed a bug where having many ``{% elif ... %}`` blocks resulted in
|
||||||
|
a "too many levels of indentation" error. These blocks now compile
|
||||||
|
to native ``elif ..:`` instead of ``else: if ..:`` :issue:`759`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.6
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-04-03
|
||||||
|
|
||||||
|
- Fixed custom context behavior in fast resolve mode :issue:`675`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-01-28
|
||||||
|
|
||||||
|
- Restored the original repr of the internal ``_GroupTuple`` because
|
||||||
|
this caused issues with ansible and it was an unintended change.
|
||||||
|
:issue:`654`
|
||||||
|
- Added back support for custom contexts that override the old
|
||||||
|
``resolve`` method since it was hard for people to spot that this
|
||||||
|
could cause a regression.
|
||||||
|
- Correctly use the buffer for the else block of for loops. This
|
||||||
|
caused invalid syntax errors to be caused on 2.x and completely
|
||||||
|
wrong behavior on Python 3 :issue:`669`
|
||||||
|
- Resolve an issue where the ``{% extends %}`` tag could not be used
|
||||||
|
with async environments. :issue:`668`
|
||||||
|
- Reduce memory footprint slightly by reducing our unicode database
|
||||||
|
dump we use for identifier matching on Python 3 :issue:`666`
|
||||||
|
- Fixed autoescaping not working for macros in async compilation mode.
|
||||||
|
:issue:`671`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-01-10
|
||||||
|
|
||||||
|
- Solved some warnings for string literals. :issue:`646`
|
||||||
|
- Increment the bytecode cache version which was not done due to an
|
||||||
|
oversight before.
|
||||||
|
- Corrected bad code generation and scoping for filtered loops.
|
||||||
|
:issue:`649`
|
||||||
|
- Resolved an issue where top-level output silencing after known
|
||||||
|
extend blocks could generate invalid code when blocks where
|
||||||
|
contained in if statements. :issue:`651`
|
||||||
|
- Made the ``truncate.leeway`` default configurable to improve
|
||||||
|
compatibility with older templates.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-01-08
|
||||||
|
|
||||||
|
- Restored the use of blocks in macros to the extend that was possible
|
||||||
|
before. On Python 3 it would render a generator repr instead of the
|
||||||
|
block contents. :issue:`645`
|
||||||
|
- Set a consistent behavior for assigning of variables in inner scopes
|
||||||
|
when the variable is also read from an outer scope. This now sets
|
||||||
|
the intended behavior in all situations however it does not restore
|
||||||
|
the old behavior where limited assignments to outer scopes was
|
||||||
|
possible. For more information and a discussion see :issue:`641`
|
||||||
|
- Resolved an issue where ``block scoped`` would not take advantage of
|
||||||
|
the new scoping rules. In some more exotic cases a variable
|
||||||
|
overridden in a local scope would not make it into a block.
|
||||||
|
- Change the code generation of the ``with`` statement to be in line
|
||||||
|
with the new scoping rules. This resolves some unlikely bugs in edge
|
||||||
|
cases. This also introduces a new internal ``With`` node that can be
|
||||||
|
used by extensions.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-01-08
|
||||||
|
|
||||||
|
- Fixed a regression that caused for loops to not be able to use the
|
||||||
|
same variable for the target as well as source iterator.
|
||||||
|
:issue:`640`
|
||||||
|
- Add support for a previously unknown behavior of macros. It used to
|
||||||
|
be possible in some circumstances to explicitly provide a caller
|
||||||
|
argument to macros. While badly buggy and unintended it turns out
|
||||||
|
that this is a common case that gets copy pasted around. To not
|
||||||
|
completely break backwards compatibility with the most common cases
|
||||||
|
it's now possible to provide an explicit keyword argument for caller
|
||||||
|
if it's given an explicit default. :issue:`642`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2017-01-07
|
||||||
|
|
||||||
|
- Resolved a regression with call block scoping for macros. Nested
|
||||||
|
caller blocks that used the same identifiers as outer macros could
|
||||||
|
refer to the wrong variable incorrectly.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.9
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2017-01-07, codename Derivation
|
||||||
|
|
||||||
|
- Change cache key definition in environment. This fixes a performance
|
||||||
|
regression introduced in 2.8.
|
||||||
|
- Added support for ``generator_stop`` on supported Python versions
|
||||||
|
(Python 3.5 and later)
|
||||||
|
- Corrected a long standing issue with operator precedence of math
|
||||||
|
operations not being what was expected.
|
||||||
|
- Added support for Python 3.6 async iterators through a new async
|
||||||
|
mode.
|
||||||
|
- Added policies for filter defaults and similar things.
|
||||||
|
- Urlize now sets "rel noopener" by default.
|
||||||
|
- Support attribute fallback for old-style classes in 2.x.
|
||||||
|
- Support toplevel set statements in extend situations.
|
||||||
|
- Restored behavior of Cycler for Python 3 users.
|
||||||
|
- Subtraction now follows the same behavior as other operators on
|
||||||
|
undefined values.
|
||||||
|
- ``map`` and friends will now give better error messages if you
|
||||||
|
forgot to quote the parameter.
|
||||||
|
- Depend on MarkupSafe 0.23 or higher.
|
||||||
|
- Improved the ``truncate`` filter to support better truncation in
|
||||||
|
case the string is barely truncated at all.
|
||||||
|
- Change the logic for macro autoescaping to be based on the runtime
|
||||||
|
autoescaping information at call time instead of macro define time.
|
||||||
|
- Ported a modified version of the ``tojson`` filter from Flask to
|
||||||
|
Jinja and hooked it up with the new policy framework.
|
||||||
|
- Block sets are now marked ``safe`` by default.
|
||||||
|
- On Python 2 the asciification of ASCII strings can now be disabled
|
||||||
|
with the ``compiler.ascii_str`` policy.
|
||||||
|
- Tests now no longer accept an arbitrary expression as first argument
|
||||||
|
but a restricted one. This means that you can now properly use
|
||||||
|
multiple tests in one expression without extra parentheses. In
|
||||||
|
particular you can now write ``foo is divisibleby 2 or foo is
|
||||||
|
divisibleby 3`` as you would expect.
|
||||||
|
- Greatly changed the scoping system to be more consistent with what
|
||||||
|
template designers and developers expect. There is now no more magic
|
||||||
|
difference between the different include and import constructs.
|
||||||
|
Context is now always propagated the same way. The only remaining
|
||||||
|
differences is the defaults for ``with context`` and ``without
|
||||||
|
context``.
|
||||||
|
- The ``with`` and ``autoescape`` tags are now built-in.
|
||||||
|
- Added the new ``select_autoescape`` function which helps configuring
|
||||||
|
better autoescaping easier.
|
||||||
|
- Fixed a runtime error in the sandbox when attributes of async
|
||||||
|
generators were accessed.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.8.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2016-12-29
|
||||||
|
|
||||||
|
- Fixed the ``for_qs`` flag for ``urlencode``.
|
||||||
|
- Fixed regression when applying ``int`` to non-string values.
|
||||||
|
- SECURITY: if the sandbox mode is used format expressions are now
|
||||||
|
sandboxed with the same rules as in Jinja. This solves various
|
||||||
|
information leakage problems that can occur with format strings.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.8
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2015-07-26, codename Replacement
|
||||||
|
|
||||||
|
- Added ``target`` parameter to urlize function.
|
||||||
|
- Added support for ``followsymlinks`` to the file system loader.
|
||||||
|
- The truncate filter now counts the length.
|
||||||
|
- Added equalto filter that helps with select filters.
|
||||||
|
- Changed cache keys to use absolute file names if available instead
|
||||||
|
of load names.
|
||||||
|
- Fixed loop length calculation for some iterators.
|
||||||
|
- Changed how Jinja enforces strings to be native strings in Python 2
|
||||||
|
to work when people break their default encoding.
|
||||||
|
- Added ``make_logging_undefined`` which returns an undefined
|
||||||
|
object that logs failures into a logger.
|
||||||
|
- If unmarshalling of cached data fails the template will be reloaded
|
||||||
|
now.
|
||||||
|
- Implemented a block ``set`` tag.
|
||||||
|
- Default cache size was increased to 400 from a low 50.
|
||||||
|
- Fixed ``is number`` test to accept long integers in all Python
|
||||||
|
versions.
|
||||||
|
- Changed ``is number`` to accept Decimal as a number.
|
||||||
|
- Added a check for default arguments followed by non-default
|
||||||
|
arguments. This change makes ``{% macro m(x, y=1, z) %}`` a syntax
|
||||||
|
error. The previous behavior for this code was broken anyway
|
||||||
|
(resulting in the default value being applied to ``y``).
|
||||||
|
- Add ability to use custom subclasses of
|
||||||
|
``jinja2.compiler.CodeGenerator`` and ``jinja2.runtime.Context`` by
|
||||||
|
adding two new attributes to the environment
|
||||||
|
(``code_generator_class`` and ``context_class``). :pr:`404`
|
||||||
|
- Added support for context/environment/evalctx decorator functions on
|
||||||
|
the finalize callback of the environment.
|
||||||
|
- Escape query strings for urlencode properly. Previously slashes were
|
||||||
|
not escaped in that place.
|
||||||
|
- Add 'base' parameter to 'int' filter.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.7.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2014-06-06
|
||||||
|
|
||||||
|
- Security issue: Corrected the security fix for the cache folder.
|
||||||
|
This fix was provided by RedHat.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.7.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2014-01-10
|
||||||
|
|
||||||
|
- Prefix loader was not forwarding the locals properly to inner
|
||||||
|
loaders. This is now fixed.
|
||||||
|
- Security issue: Changed the default folder for the filesystem cache
|
||||||
|
to be user specific and read and write protected on UNIX systems.
|
||||||
|
See `Debian bug 734747`_ for more information.
|
||||||
|
|
||||||
|
.. _Debian bug 734747: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=734747
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.7.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2013-08-07
|
||||||
|
|
||||||
|
- Fixed a bug with ``call_filter`` not working properly on environment
|
||||||
|
and context filters.
|
||||||
|
- Fixed lack of Python 3 support for bytecode caches.
|
||||||
|
- Reverted support for defining blocks in included templates as this
|
||||||
|
broke existing templates for users.
|
||||||
|
- Fixed some warnings with hashing of undefineds and nodes if Python
|
||||||
|
is run with warnings for Python 3.
|
||||||
|
- Added support for properly hashing undefined objects.
|
||||||
|
- Fixed a bug with the title filter not working on already uppercase
|
||||||
|
strings.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.7
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2013-05-20, codename Translation
|
||||||
|
|
||||||
|
- Choice and prefix loaders now dispatch source and template lookup
|
||||||
|
separately in order to work in combination with module loaders as
|
||||||
|
advertised.
|
||||||
|
- Fixed filesizeformat.
|
||||||
|
- Added a non-silent option for babel extraction.
|
||||||
|
- Added ``urlencode`` filter that automatically quotes values for URL
|
||||||
|
safe usage with utf-8 as only supported encoding. If applications
|
||||||
|
want to change this encoding they can override the filter.
|
||||||
|
- Added ``keep-trailing-newline`` configuration to environments and
|
||||||
|
templates to optionally preserve the final trailing newline.
|
||||||
|
- Accessing ``last`` on the loop context no longer causes the iterator
|
||||||
|
to be consumed into a list.
|
||||||
|
- Python requirement changed: 2.6, 2.7 or >= 3.3 are required now,
|
||||||
|
supported by same source code, using the "six" compatibility
|
||||||
|
library.
|
||||||
|
- Allow ``contextfunction`` and other decorators to be applied to
|
||||||
|
``__call__``.
|
||||||
|
- Added support for changing from newline to different signs in the
|
||||||
|
``wordwrap`` filter.
|
||||||
|
- Added support for ignoring memcache errors silently.
|
||||||
|
- Added support for keeping the trailing newline in templates.
|
||||||
|
- Added finer grained support for stripping whitespace on the left
|
||||||
|
side of blocks.
|
||||||
|
- Added ``map``, ``select``, ``reject``, ``selectattr`` and
|
||||||
|
``rejectattr`` filters.
|
||||||
|
- Added support for ``loop.depth`` to figure out how deep inside a
|
||||||
|
recursive loop the code is.
|
||||||
|
- Disabled py_compile for pypy and python 3.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.6
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2011-07-24, codename Convolution
|
||||||
|
|
||||||
|
- Internal attributes now raise an internal attribute error now
|
||||||
|
instead of returning an undefined. This fixes problems when passing
|
||||||
|
undefined objects to Python semantics expecting APIs.
|
||||||
|
- Traceback support now works properly for PyPy. (Tested with 1.4)
|
||||||
|
- Implemented operator intercepting for sandboxed environments. This
|
||||||
|
allows application developers to disable builtin operators for
|
||||||
|
better security. (For instance limit the mathematical operators to
|
||||||
|
actual integers instead of longs)
|
||||||
|
- Groupby filter now supports dotted notation for grouping by
|
||||||
|
attributes of attributes.
|
||||||
|
- Scoped blocks now properly treat toplevel assignments and imports.
|
||||||
|
Previously an import suddenly "disappeared" in a scoped block.
|
||||||
|
- Automatically detect newer Python interpreter versions before
|
||||||
|
loading code from bytecode caches to prevent segfaults on invalid
|
||||||
|
opcodes. The segfault in earlier Jinja versions here was not a
|
||||||
|
Jinja bug but a limitation in the underlying Python interpreter. If
|
||||||
|
you notice Jinja segfaulting in earlier versions after an upgrade
|
||||||
|
of the Python interpreter you don't have to upgrade, it's enough to
|
||||||
|
flush the bytecode cache. This just no longer makes this necessary,
|
||||||
|
Jinja will automatically detect these cases now.
|
||||||
|
- The sum filter can now sum up values by attribute. This is a
|
||||||
|
backwards incompatible change. The argument to the filter previously
|
||||||
|
was the optional starting index which defaults to zero. This now
|
||||||
|
became the second argument to the function because it's rarely used.
|
||||||
|
- Like sum, sort now also makes it possible to order items by
|
||||||
|
attribute.
|
||||||
|
- Like sum and sort, join now also is able to join attributes of
|
||||||
|
objects as string.
|
||||||
|
- The internal eval context now has a reference to the environment.
|
||||||
|
- Added a mapping test to see if an object is a dict or an object with
|
||||||
|
a similar interface.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5.5
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-10-18
|
||||||
|
|
||||||
|
- Built documentation is no longer part of release.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5.4
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-10-17
|
||||||
|
|
||||||
|
- Fixed extensions not loading properly with overlays.
|
||||||
|
- Work around a bug in cpython for the debugger that causes segfaults
|
||||||
|
on 64bit big-endian architectures.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-10-17
|
||||||
|
|
||||||
|
- Fixed an operator precedence error introduced in 2.5.2. Statements
|
||||||
|
like "-foo.bar" had their implicit parentheses applied around the
|
||||||
|
first part of the expression ("(-foo).bar") instead of the more
|
||||||
|
correct "-(foo.bar)".
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5.2
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-08-18
|
||||||
|
|
||||||
|
- Improved setup.py script to better work with assumptions people
|
||||||
|
might still have from it (``--with-speedups``).
|
||||||
|
- Fixed a packaging error that excluded the new debug support.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-08-17
|
||||||
|
|
||||||
|
- StopIteration exceptions raised by functions called from templates
|
||||||
|
are now intercepted and converted to undefineds. This solves a lot
|
||||||
|
of debugging grief. (StopIteration is used internally to abort
|
||||||
|
template execution)
|
||||||
|
- Improved performance of macro calls slightly.
|
||||||
|
- Babel extraction can now properly extract newstyle gettext calls.
|
||||||
|
- Using the variable ``num`` in newstyle gettext for something else
|
||||||
|
than the pluralize count will no longer raise a :exc:`KeyError`.
|
||||||
|
- Removed builtin markup class and switched to markupsafe. For
|
||||||
|
backwards compatibility the pure Python implementation still exists
|
||||||
|
but is pulled from markupsafe by the Jinja developers. The debug
|
||||||
|
support went into a separate feature called "debugsupport" and is
|
||||||
|
disabled by default because it is only relevant for Python 2.4
|
||||||
|
- Fixed an issue with unary operators having the wrong precedence.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.5
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2010-05-29, codename Incoherence
|
||||||
|
|
||||||
|
- Improved the sort filter (should have worked like this for a long
|
||||||
|
time) by adding support for case insensitive searches.
|
||||||
|
- Fixed a bug for getattribute constant folding.
|
||||||
|
- Support for newstyle gettext translations which result in a nicer
|
||||||
|
in-template user interface and more consistent catalogs.
|
||||||
|
- It's now possible to register extensions after an environment was
|
||||||
|
created.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.4.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-04-20
|
||||||
|
|
||||||
|
- Fixed an error reporting bug for undefined.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.4
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2010-04-13, codename Correlation
|
||||||
|
|
||||||
|
- The environment template loading functions now transparently pass
|
||||||
|
through a template object if it was passed to it. This makes it
|
||||||
|
possible to import or extend from a template object that was passed
|
||||||
|
to the template.
|
||||||
|
- Added a ``ModuleLoader`` that can load templates from
|
||||||
|
precompiled sources. The environment now features a method to
|
||||||
|
compile the templates from a configured loader into a zip file or
|
||||||
|
folder.
|
||||||
|
- The _speedups C extension now supports Python 3.
|
||||||
|
- Added support for autoescaping toggling sections and support for
|
||||||
|
evaluation contexts.
|
||||||
|
- Extensions have a priority now.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.3.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2010-02-19
|
||||||
|
|
||||||
|
- Fixed an error reporting bug on all python versions
|
||||||
|
- Fixed an error reporting bug on Python 2.4
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.3
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2010-02-10, codename 3000 Pythons
|
||||||
|
|
||||||
|
- Fixes issue with code generator that causes unbound variables to be
|
||||||
|
generated if set was used in if-blocks and other small identifier
|
||||||
|
problems.
|
||||||
|
- Include tags are now able to select between multiple templates and
|
||||||
|
take the first that exists, if a list of templates is given.
|
||||||
|
- Fixed a problem with having call blocks in outer scopes that have an
|
||||||
|
argument that is also used as local variable in an inner frame
|
||||||
|
:issue:`360`.
|
||||||
|
- Greatly improved error message reporting :pr:`339`
|
||||||
|
- Implicit tuple expressions can no longer be totally empty. This
|
||||||
|
change makes ``{% if %}`` a syntax error now. :issue:`364`
|
||||||
|
- Added support for translator comments if extracted via babel.
|
||||||
|
- Added with-statement extension.
|
||||||
|
- Experimental Python 3 support.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.2.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2009-09-14
|
||||||
|
|
||||||
|
- Fixes some smaller problems for Jinja on Jython.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.2
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2009-09-13, codename Kong
|
||||||
|
|
||||||
|
- Include statements can now be marked with ``ignore missing`` to skip
|
||||||
|
non existing templates.
|
||||||
|
- Priority of ``not`` raised. It's now possible to write ``not foo in
|
||||||
|
bar`` as an alias to ``foo not in bar`` like in python. Previously
|
||||||
|
the grammar required parentheses (``not (foo in bar)``) which was
|
||||||
|
odd.
|
||||||
|
- Fixed a bug that caused syntax errors when defining macros or using
|
||||||
|
the ``{% call %}`` tag inside loops.
|
||||||
|
- Fixed a bug in the parser that made ``{{ foo[1, 2] }}`` impossible.
|
||||||
|
- Made it possible to refer to names from outer scopes in included
|
||||||
|
templates that were unused in the callers frame :issue:`327`
|
||||||
|
- Fixed a bug that caused internal errors if names where used as
|
||||||
|
iteration variable and regular variable *after* the loop if that
|
||||||
|
variable was unused *before* the loop. :pr:`331`
|
||||||
|
- Added support for optional ``scoped`` modifier to blocks.
|
||||||
|
- Added support for line-comments.
|
||||||
|
- Added the ``meta`` module.
|
||||||
|
- Renamed (undocumented) attribute "overlay" to "overlayed" on the
|
||||||
|
environment because it was clashing with a method of the same name.
|
||||||
|
- Speedup extension is now disabled by default.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.1.1
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Released 2008-12-25
|
||||||
|
|
||||||
|
- Fixed a translation error caused by looping over empty recursive
|
||||||
|
loops.
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.1
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2008-11-23, codename Yasuzō
|
||||||
|
|
||||||
|
- Fixed a bug with nested loops and the special loop variable. Before
|
||||||
|
the change an inner loop overwrote the loop variable from the outer
|
||||||
|
one after iteration.
|
||||||
|
- Fixed a bug with the i18n extension that caused the explicit
|
||||||
|
pluralization block to look up the wrong variable.
|
||||||
|
- Fixed a limitation in the lexer that made ``{{ foo.0.0 }}``
|
||||||
|
impossible.
|
||||||
|
- Index based subscribing of variables with a constant value returns
|
||||||
|
an undefined object now instead of raising an index error. This was
|
||||||
|
a bug caused by eager optimizing.
|
||||||
|
- The i18n extension looks up ``foo.ugettext`` now followed by
|
||||||
|
``foo.gettext`` if an translations object is installed. This makes
|
||||||
|
dealing with custom translations classes easier.
|
||||||
|
- Fixed a confusing behavior with conditional extending. loops were
|
||||||
|
partially executed under some conditions even though they were not
|
||||||
|
part of a visible area.
|
||||||
|
- Added ``sort`` filter that works like ``dictsort`` but for arbitrary
|
||||||
|
sequences.
|
||||||
|
- Fixed a bug with empty statements in macros.
|
||||||
|
- Implemented a bytecode cache system.
|
||||||
|
- The template context is now weakref-able
|
||||||
|
- Inclusions and imports "with context" forward all variables now, not
|
||||||
|
only the initial context.
|
||||||
|
- Added a cycle helper called ``cycler``.
|
||||||
|
- Added a joining helper called ``joiner``.
|
||||||
|
- Added a ``compile_expression`` method to the environment that allows
|
||||||
|
compiling of Jinja expressions into callable Python objects.
|
||||||
|
- Fixed an escaping bug in urlize
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Released 2008-07-17, codename Jinjavitus
|
||||||
|
|
||||||
|
- The subscribing of objects (looking up attributes and items) changed
|
||||||
|
from slightly. It's now possible to give attributes or items a
|
||||||
|
higher priority by either using dot-notation lookup or the bracket
|
||||||
|
syntax. This also changed the AST slightly. ``Subscript`` is gone
|
||||||
|
and was replaced with ``Getitem`` and ``Getattr``.
|
||||||
|
- Added support for preprocessing and token stream filtering for
|
||||||
|
extensions. This would allow extensions to allow simplified gettext
|
||||||
|
calls in template data and something similar.
|
||||||
|
- Added ``TemplateStream.dump``.
|
||||||
|
- Added missing support for implicit string literal concatenation.
|
||||||
|
``{{ "foo" "bar" }}`` is equivalent to ``{{ "foobar" }}``
|
||||||
|
- ``else`` is optional for conditional expressions. If not given it
|
||||||
|
evaluates to ``false``.
|
||||||
|
- Improved error reporting for undefined values by providing a
|
||||||
|
position.
|
||||||
|
- ``filesizeformat`` filter uses decimal prefixes now per default and
|
||||||
|
can be set to binary mode with the second parameter.
|
||||||
|
- Fixed bug in finalizer
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.0rc1
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Released 2008-06-09
|
||||||
|
|
||||||
|
- First release of Jinja 2.
|
|
@ -0,0 +1,28 @@
|
||||||
|
Copyright 2007 Pallets
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||||
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,10 @@
|
||||||
|
include CHANGES.rst
|
||||||
|
include tox.ini
|
||||||
|
include requirements/*.txt
|
||||||
|
graft artwork
|
||||||
|
graft docs
|
||||||
|
prune docs/_build
|
||||||
|
graft examples
|
||||||
|
graft tests
|
||||||
|
include src/jinja2/py.typed
|
||||||
|
global-exclude *.pyc
|
|
@ -0,0 +1,111 @@
|
||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Jinja2
|
||||||
|
Version: 3.0.3
|
||||||
|
Summary: A very fast and expressive template engine.
|
||||||
|
Home-page: https://palletsprojects.com/p/jinja/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://jinja.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://jinja.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/jinja/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||||
|
Requires-Python: >=3.6
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
Provides-Extra: i18n
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
- AsyncIO support for generating templates and calling async
|
||||||
|
functions.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U Jinja2
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
In A Nutshell
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Members{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<ul>
|
||||||
|
{% for user in users %}
|
||||||
|
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Jinja and other popular
|
||||||
|
packages. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://jinja.palletsprojects.com/
|
||||||
|
- Changes: https://jinja.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Jinja2/
|
||||||
|
- Source Code: https://github.com/pallets/jinja/
|
||||||
|
- Issue Tracker: https://github.com/pallets/jinja/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/jinja/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
- AsyncIO support for generating templates and calling async
|
||||||
|
functions.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U Jinja2
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
In A Nutshell
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Members{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<ul>
|
||||||
|
{% for user in users %}
|
||||||
|
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Jinja and other popular
|
||||||
|
packages. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://jinja.palletsprojects.com/
|
||||||
|
- Changes: https://jinja.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Jinja2/
|
||||||
|
- Source Code: https://github.com/pallets/jinja/
|
||||||
|
- Issue Tracker: https://github.com/pallets/jinja/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/jinja/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,19 @@
|
||||||
|
# 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)
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,931 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
.. admonition:: Implementation
|
||||||
|
|
||||||
|
Context is immutable for the same reason Python's frame locals are
|
||||||
|
immutable inside functions. Both Jinja and Python are not using the
|
||||||
|
context / frame locals as data storage for variables but only as primary
|
||||||
|
data source.
|
||||||
|
|
||||||
|
When a template accesses a variable the template does not define, Jinja
|
||||||
|
looks up the variable in the context, after that the variable is treated
|
||||||
|
as if it was defined in the template.
|
||||||
|
|
||||||
|
|
||||||
|
.. _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.contextfilter
|
||||||
|
|
||||||
|
.. autofunction:: jinja2.evalcontextfilter
|
||||||
|
|
||||||
|
.. autofunction:: jinja2.environmentfilter
|
||||||
|
|
||||||
|
.. autofunction:: jinja2.contextfunction
|
||||||
|
|
||||||
|
.. autofunction:: jinja2.evalcontextfunction
|
||||||
|
|
||||||
|
.. autofunction:: jinja2.environmentfunction
|
||||||
|
|
||||||
|
.. 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
|
|
@ -0,0 +1,4 @@
|
||||||
|
Changes
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. include:: ../CHANGES.rst
|
|
@ -0,0 +1,53 @@
|
||||||
|
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")]
|
|
@ -0,0 +1,54 @@
|
||||||
|
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
|
|
@ -0,0 +1,72 @@
|
||||||
|
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,
|
||||||
|
)
|
|
@ -0,0 +1,423 @@
|
||||||
|
.. _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
|
|
@ -0,0 +1,175 @@
|
||||||
|
Frequently Asked Questions
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This page answers some of the often asked questions about Jinja.
|
||||||
|
|
||||||
|
.. highlight:: html+jinja
|
||||||
|
|
||||||
|
Why is it called Jinja?
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The name Jinja was chosen because it's the name of a Japanese temple and
|
||||||
|
temple and template share a similar pronunciation. It is not named after
|
||||||
|
the city in Uganda.
|
||||||
|
|
||||||
|
How fast is it?
|
||||||
|
---------------
|
||||||
|
|
||||||
|
We really hate benchmarks especially since they don't reflect much. The
|
||||||
|
performance of a template depends on many factors and you would have to
|
||||||
|
benchmark different engines in different situations. The benchmarks from the
|
||||||
|
testsuite show that Jinja has a similar performance to `Mako`_ and is between
|
||||||
|
10 and 20 times faster than Django's template engine or Genshi. These numbers
|
||||||
|
should be taken with tons of salt as the benchmarks that took these numbers
|
||||||
|
only test a few performance related situations such as looping. Generally
|
||||||
|
speaking the performance of a template engine doesn't matter much as the
|
||||||
|
usual bottleneck in a web application is either the database or the application
|
||||||
|
code.
|
||||||
|
|
||||||
|
.. _Mako: https://www.makotemplates.org/
|
||||||
|
|
||||||
|
How Compatible is Jinja with Django?
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
The default syntax of Jinja matches Django syntax in many ways. However
|
||||||
|
this similarity doesn't mean that you can use a Django template unmodified
|
||||||
|
in Jinja. For example filter arguments use a function call syntax rather
|
||||||
|
than a colon to separate filter name and arguments. Additionally the
|
||||||
|
extension interface in Jinja is fundamentally different from the Django one
|
||||||
|
which means that your custom tags won't work any longer.
|
||||||
|
|
||||||
|
Generally speaking you will use much less custom extensions as the Jinja
|
||||||
|
template system allows you to use a certain subset of Python expressions
|
||||||
|
which can replace most Django extensions. For example instead of using
|
||||||
|
something like this::
|
||||||
|
|
||||||
|
{% load comments %}
|
||||||
|
{% get_latest_comments 10 as latest_comments %}
|
||||||
|
{% for comment in latest_comments %}
|
||||||
|
...
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
You will most likely provide an object with attributes to retrieve
|
||||||
|
comments from the database::
|
||||||
|
|
||||||
|
{% for comment in models.comments.latest(10) %}
|
||||||
|
...
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Or directly provide the model for quick testing::
|
||||||
|
|
||||||
|
{% for comment in Comment.objects.order_by('-pub_date')[:10] %}
|
||||||
|
...
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Please keep in mind that even though you may put such things into templates
|
||||||
|
it still isn't a good idea. Queries should go into the view code and not
|
||||||
|
the template!
|
||||||
|
|
||||||
|
Isn't it a terrible idea to put Logic into Templates?
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Without a doubt you should try to remove as much logic from templates as
|
||||||
|
possible. But templates without any logic mean that you have to do all
|
||||||
|
the processing in the code which is boring and stupid. A template engine
|
||||||
|
that does that is shipped with Python and called `string.Template`. Comes
|
||||||
|
without loops and if conditions and is by far the fastest template engine
|
||||||
|
you can get for Python.
|
||||||
|
|
||||||
|
So some amount of logic is required in templates to keep everyone happy.
|
||||||
|
And Jinja leaves it pretty much to you how much logic you want to put into
|
||||||
|
templates. There are some restrictions in what you can do and what not.
|
||||||
|
|
||||||
|
Jinja neither allows you to put arbitrary Python code into templates nor
|
||||||
|
does it allow all Python expressions. The operators are limited to the
|
||||||
|
most common ones and more advanced expressions such as list comprehensions
|
||||||
|
and generator expressions are not supported. This keeps the template engine
|
||||||
|
easier to maintain and templates more readable.
|
||||||
|
|
||||||
|
Why is Autoescaping not the Default?
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
There are multiple reasons why automatic escaping is not the default mode
|
||||||
|
and also not the recommended one. While automatic escaping of variables
|
||||||
|
means that you will less likely have an XSS problem it also causes a huge
|
||||||
|
amount of extra processing in the template engine which can cause serious
|
||||||
|
performance problems. As Python doesn't provide a way to mark strings as
|
||||||
|
unsafe Jinja has to hack around that limitation by providing a custom
|
||||||
|
string class (the :class:`Markup` string) that safely interacts with safe
|
||||||
|
and unsafe strings.
|
||||||
|
|
||||||
|
With explicit escaping however the template engine doesn't have to perform
|
||||||
|
any safety checks on variables. Also a human knows not to escape integers
|
||||||
|
or strings that may never contain characters one has to escape or already
|
||||||
|
HTML markup. For example when iterating over a list over a table of
|
||||||
|
integers and floats for a table of statistics the template designer can
|
||||||
|
omit the escaping because he knows that integers or floats don't contain
|
||||||
|
any unsafe parameters.
|
||||||
|
|
||||||
|
Additionally Jinja is a general purpose template engine and not only used
|
||||||
|
for HTML/XML generation. For example you may generate LaTeX, emails,
|
||||||
|
CSS, JavaScript, or configuration files.
|
||||||
|
|
||||||
|
Why is the Context immutable?
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
When writing a :func:`pass_context` function, you may have noticed that
|
||||||
|
the context tries to stop you from modifying it. If you have managed to
|
||||||
|
modify the context by using an internal context API you may have noticed
|
||||||
|
that changes in the context don't seem to be visible in the template.
|
||||||
|
The reason for this is that Jinja uses the context only as primary data
|
||||||
|
source for template variables for performance reasons.
|
||||||
|
|
||||||
|
If you want to modify the context write a function that returns a variable
|
||||||
|
instead that one can assign to a variable by using set::
|
||||||
|
|
||||||
|
{% set comments = get_latest_comments() %}
|
||||||
|
|
||||||
|
My tracebacks look weird. What's happening?
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Jinja can rewrite tracebacks so they show the template lines numbers and
|
||||||
|
source rather than the underlying compiled code, but this requires
|
||||||
|
special Python support. CPython <3.7 requires ``ctypes``, and PyPy
|
||||||
|
requires transparent proxy support.
|
||||||
|
|
||||||
|
If you are using Google App Engine, ``ctypes`` is not available. You can
|
||||||
|
make it available in development, but not in production.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import os
|
||||||
|
if os.environ.get('SERVER_SOFTWARE', '').startswith('Dev'):
|
||||||
|
from google.appengine.tools.devappserver2.python import sandbox
|
||||||
|
sandbox._WHITE_LIST_C_MODULES += ['_ctypes', 'gestalt']
|
||||||
|
|
||||||
|
Credit for this snippet goes to `Thomas Johansson
|
||||||
|
<https://stackoverflow.com/questions/3086091/debug-jinja2-in-google-app-engine/3694434#3694434>`_
|
||||||
|
|
||||||
|
My Macros are overridden by something
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
In some situations the Jinja scoping appears arbitrary:
|
||||||
|
|
||||||
|
layout.tmpl:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% macro foo() %}LAYOUT{% endmacro %}
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
|
||||||
|
child.tmpl:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% extends 'layout.tmpl' %}
|
||||||
|
{% macro foo() %}CHILD{% endmacro %}
|
||||||
|
{% block body %}{{ foo() }}{% endblock %}
|
||||||
|
|
||||||
|
This will print ``LAYOUT`` in Jinja. This is a side effect of having
|
||||||
|
the parent template evaluated after the child one. This allows child
|
||||||
|
templates passing information to the parent template. To avoid this
|
||||||
|
issue rename the macro or variable in the parent template to have an
|
||||||
|
uncommon prefix.
|
||||||
|
|
||||||
|
.. _Jinja 1: https://pypi.org/project/Jinja/
|
|
@ -0,0 +1,29 @@
|
||||||
|
.. 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
|
|
@ -0,0 +1,75 @@
|
||||||
|
Integration
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. _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/
|
|
@ -0,0 +1,63 @@
|
||||||
|
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.6 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/
|
|
@ -0,0 +1,4 @@
|
||||||
|
BSD-3-Clause License
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. include:: ../LICENSE.rst
|
|
@ -0,0 +1,35 @@
|
||||||
|
@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
|
|
@ -0,0 +1,64 @@
|
||||||
|
.. 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
|
|
@ -0,0 +1,94 @@
|
||||||
|
Sandbox
|
||||||
|
=======
|
||||||
|
|
||||||
|
The Jinja sandbox can be used to evaluate untrusted code. Access to unsafe
|
||||||
|
attributes and methods is prohibited.
|
||||||
|
|
||||||
|
Assuming `env` is a :class:`SandboxedEnvironment` in the default configuration
|
||||||
|
the following piece of code shows how it works:
|
||||||
|
|
||||||
|
>>> env.from_string("{{ func.func_code }}").render(func=lambda:None)
|
||||||
|
u''
|
||||||
|
>>> env.from_string("{{ func.func_code.do_something }}").render(func=lambda:None)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
SecurityError: access to attribute 'func_code' of 'function' object is unsafe.
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
|
||||||
|
.. admonition:: Note
|
||||||
|
|
||||||
|
The Jinja sandbox alone is no solution for perfect security. Especially
|
||||||
|
for web applications you have to keep in mind that users may create
|
||||||
|
templates with arbitrary HTML in so it's crucial to ensure that (if you
|
||||||
|
are running multiple users on the same server) they can't harm each other
|
||||||
|
via JavaScript insertions and much more.
|
||||||
|
|
||||||
|
Also the sandbox is only as good as the configuration. We strongly
|
||||||
|
recommend only passing non-shared resources to the template and use
|
||||||
|
some sort of whitelisting for attributes.
|
||||||
|
|
||||||
|
Also keep in mind that templates may raise runtime or compile time errors,
|
||||||
|
so make sure to catch them.
|
||||||
|
|
||||||
|
Operator Intercepting
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
|
||||||
|
For maximum performance Jinja will let operators call directly the type
|
||||||
|
specific callback methods. This means that it's not possible to have this
|
||||||
|
intercepted by overriding :meth:`Environment.call`. Furthermore a
|
||||||
|
conversion from operator to special method is not always directly possible
|
||||||
|
due to how operators work. For instance for divisions more than one
|
||||||
|
special method exist.
|
||||||
|
|
||||||
|
With Jinja 2.6 there is now support for explicit operator intercepting.
|
||||||
|
This can be used to customize specific operators as necessary. In order
|
||||||
|
to intercept an operator one has to override the
|
||||||
|
:attr:`SandboxedEnvironment.intercepted_binops` attribute. Once the
|
||||||
|
operator that needs to be intercepted is added to that set Jinja will
|
||||||
|
generate bytecode that calls the :meth:`SandboxedEnvironment.call_binop`
|
||||||
|
function. For unary operators the `unary` attributes and methods have to
|
||||||
|
be used instead.
|
||||||
|
|
||||||
|
The default implementation of :attr:`SandboxedEnvironment.call_binop`
|
||||||
|
will use the :attr:`SandboxedEnvironment.binop_table` to translate
|
||||||
|
operator symbols into callbacks performing the default operator behavior.
|
||||||
|
|
||||||
|
This example shows how the power (``**``) operator can be disabled in
|
||||||
|
Jinja::
|
||||||
|
|
||||||
|
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 SandboxedEnvironment.call_binop(self, context,
|
||||||
|
operator, left, right)
|
||||||
|
|
||||||
|
Make sure to always call into the super method, even if you are not
|
||||||
|
intercepting the call. Jinja might internally call the method to
|
||||||
|
evaluate expressions.
|
|
@ -0,0 +1,226 @@
|
||||||
|
Switching from other Template Engines
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
.. highlight:: html+jinja
|
||||||
|
|
||||||
|
If you have used a different template engine in the past and want to switch
|
||||||
|
to Jinja here is a small guide that shows the basic syntactic and semantic
|
||||||
|
changes between some common, similar text template engines for Python.
|
||||||
|
|
||||||
|
Jinja 1
|
||||||
|
-------
|
||||||
|
|
||||||
|
Jinja 2 is mostly compatible with Jinja 1 in terms of API usage and template
|
||||||
|
syntax. The differences between Jinja 1 and 2 are explained in the following
|
||||||
|
list.
|
||||||
|
|
||||||
|
API
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Loaders
|
||||||
|
Jinja 2 uses a different loader API. Because the internal representation
|
||||||
|
of templates changed there is no longer support for external caching
|
||||||
|
systems such as memcached. The memory consumed by templates is comparable
|
||||||
|
with regular Python modules now and external caching doesn't give any
|
||||||
|
advantage. If you have used a custom loader in the past have a look at
|
||||||
|
the new :ref:`loader API <loaders>`.
|
||||||
|
|
||||||
|
Loading templates from strings
|
||||||
|
In the past it was possible to generate templates from a string with the
|
||||||
|
default environment configuration by using `jinja.from_string`. Jinja 2
|
||||||
|
provides a :class:`Template` class that can be used to do the same, but
|
||||||
|
with optional additional configuration.
|
||||||
|
|
||||||
|
Automatic unicode conversion
|
||||||
|
Jinja 1 performed automatic conversion of bytes in a given encoding
|
||||||
|
into unicode objects. This conversion is no longer implemented as it
|
||||||
|
was inconsistent as most libraries are using the regular Python
|
||||||
|
ASCII bytes to Unicode conversion. An application powered by Jinja 2
|
||||||
|
*has to* use unicode internally everywhere or make sure that Jinja 2
|
||||||
|
only gets unicode strings passed.
|
||||||
|
|
||||||
|
i18n
|
||||||
|
Jinja 1 used custom translators for internationalization. i18n is now
|
||||||
|
available as Jinja 2 extension and uses a simpler, more gettext friendly
|
||||||
|
interface and has support for babel. For more details see
|
||||||
|
:ref:`i18n-extension`.
|
||||||
|
|
||||||
|
Internal methods
|
||||||
|
Jinja 1 exposed a few internal methods on the environment object such
|
||||||
|
as `call_function`, `get_attribute` and others. While they were marked
|
||||||
|
as being an internal method it was possible to override them. Jinja 2
|
||||||
|
doesn't have equivalent methods.
|
||||||
|
|
||||||
|
Sandbox
|
||||||
|
Jinja 1 was running sandbox mode by default. Few applications actually
|
||||||
|
used that feature so it became optional in Jinja 2. For more details
|
||||||
|
about the sandboxed execution see :class:`SandboxedEnvironment`.
|
||||||
|
|
||||||
|
Context
|
||||||
|
Jinja 1 had a stacked context as storage for variables passed to the
|
||||||
|
environment. In Jinja 2 a similar object exists but it doesn't allow
|
||||||
|
modifications nor is it a singleton. As inheritance is dynamic now
|
||||||
|
multiple context objects may exist during template evaluation.
|
||||||
|
|
||||||
|
Filters and Tests
|
||||||
|
Filters and tests are regular functions now. It's no longer necessary
|
||||||
|
and allowed to use factory functions.
|
||||||
|
|
||||||
|
|
||||||
|
Templates
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Jinja 2 has mostly the same syntax as Jinja 1. What's different is that
|
||||||
|
macros require parentheses around the argument list now.
|
||||||
|
|
||||||
|
Additionally Jinja 2 allows dynamic inheritance now and dynamic includes.
|
||||||
|
The old helper function `rendertemplate` is gone now, `include` can be used
|
||||||
|
instead. Includes no longer import macros and variable assignments, for
|
||||||
|
that the new `import` tag is used. This concept is explained in the
|
||||||
|
:ref:`import` documentation.
|
||||||
|
|
||||||
|
Another small change happened in the `for`-tag. The special loop variable
|
||||||
|
doesn't have a `parent` attribute, instead you have to alias the loop
|
||||||
|
yourself. See :ref:`accessing-the-parent-loop` for more details.
|
||||||
|
|
||||||
|
|
||||||
|
Django
|
||||||
|
------
|
||||||
|
|
||||||
|
If you have previously worked with Django templates, you should find
|
||||||
|
Jinja very familiar. In fact, most of the syntax elements look and
|
||||||
|
work the same.
|
||||||
|
|
||||||
|
However, Jinja provides some more syntax elements covered in the
|
||||||
|
documentation and some work a bit different.
|
||||||
|
|
||||||
|
This section covers the template changes. As the API is fundamentally
|
||||||
|
different we won't cover it here.
|
||||||
|
|
||||||
|
Method Calls
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In Django method calls work implicitly, while Jinja requires the explicit
|
||||||
|
Python syntax. Thus this Django code::
|
||||||
|
|
||||||
|
{% for page in user.get_created_pages %}
|
||||||
|
...
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
...looks like this in Jinja::
|
||||||
|
|
||||||
|
{% for page in user.get_created_pages() %}
|
||||||
|
...
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
This allows you to pass variables to the method, which is not possible in
|
||||||
|
Django. This syntax is also used for macros.
|
||||||
|
|
||||||
|
Filter Arguments
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Jinja provides more than one argument for filters. Also the syntax for
|
||||||
|
argument passing is different. A template that looks like this in Django::
|
||||||
|
|
||||||
|
{{ items|join:", " }}
|
||||||
|
|
||||||
|
looks like this in Jinja::
|
||||||
|
|
||||||
|
{{ items|join(', ') }}
|
||||||
|
|
||||||
|
It is a bit more verbose, but it allows different types of arguments -
|
||||||
|
including variables - and more than one of them.
|
||||||
|
|
||||||
|
Tests
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
In addition to filters there also are tests you can perform using the is
|
||||||
|
operator. Here are some examples::
|
||||||
|
|
||||||
|
{% if user.user_id is odd %}
|
||||||
|
{{ user.username|e }} is odd
|
||||||
|
{% else %}
|
||||||
|
hmm. {{ user.username|e }} looks pretty normal
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Loops
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
For loops work very similarly to Django, but notably the Jinja special
|
||||||
|
variable for the loop context is called `loop`, not `forloop` as in Django.
|
||||||
|
|
||||||
|
In addition, the Django `empty` argument is called `else` in Jinja. For
|
||||||
|
example, the Django template::
|
||||||
|
|
||||||
|
{% for item in items %}
|
||||||
|
{{ item }}
|
||||||
|
{% empty %}
|
||||||
|
No items!
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
...looks like this in Jinja::
|
||||||
|
|
||||||
|
{% for item in items %}
|
||||||
|
{{ item }}
|
||||||
|
{% else %}
|
||||||
|
No items!
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
Cycle
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
The ``{% cycle %}`` tag does not exist in Jinja; however, you can achieve the
|
||||||
|
same output by using the `cycle` method on the loop context special variable.
|
||||||
|
|
||||||
|
The following Django template::
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
<li class="{% cycle 'odd' 'even' %}">{{ user }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
...looks like this in Jinja::
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
<li class="{{ loop.cycle('odd', 'even') }}">{{ user }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
There is no equivalent of ``{% cycle ... as variable %}``.
|
||||||
|
|
||||||
|
|
||||||
|
Mako
|
||||||
|
----
|
||||||
|
|
||||||
|
.. highlight:: html+mako
|
||||||
|
|
||||||
|
If you have used Mako so far and want to switch to Jinja you can configure
|
||||||
|
Jinja to look more like Mako:
|
||||||
|
|
||||||
|
.. sourcecode:: python
|
||||||
|
|
||||||
|
env = Environment('<%', '%>', '${', '}', '<%doc>', '</%doc>', '%', '##')
|
||||||
|
|
||||||
|
With an environment configured like that, Jinja should be able to interpret
|
||||||
|
a small subset of Mako templates. Jinja does not support embedded Python
|
||||||
|
code, so you would have to move that out of the template. The syntax for defs
|
||||||
|
(which are called macros in Jinja) and template inheritance is different too.
|
||||||
|
The following Mako template::
|
||||||
|
|
||||||
|
<%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::
|
||||||
|
|
||||||
|
<% extends "layout.html" %>
|
||||||
|
<% block title %>Page Title<% endblock %>
|
||||||
|
<% block body %>
|
||||||
|
<ul>
|
||||||
|
% for item in list:
|
||||||
|
<li>${item}</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
<% endblock %>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,100 @@
|
||||||
|
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 PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||||
|
<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>
|
|
@ -0,0 +1,16 @@
|
||||||
|
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()
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
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]))
|
|
@ -0,0 +1,13 @@
|
||||||
|
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())
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% from 'subbroken.html' import may_break %}
|
||||||
|
<ul>
|
||||||
|
{% for item in seq %}
|
||||||
|
<li>{{ may_break(item) }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% macro may_break(item) -%}
|
||||||
|
[{{ item / 0 }}]
|
||||||
|
{%- endmacro %}
|
|
@ -0,0 +1,29 @@
|
||||||
|
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())
|
|
@ -0,0 +1,27 @@
|
||||||
|
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)))
|
|
@ -0,0 +1,13 @@
|
||||||
|
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))
|
|
@ -0,0 +1,18 @@
|
||||||
|
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])
|
||||||
|
)
|
|
@ -0,0 +1,136 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/dev.in
|
||||||
|
#
|
||||||
|
alabaster==0.7.12
|
||||||
|
# via sphinx
|
||||||
|
attrs==21.2.0
|
||||||
|
# via pytest
|
||||||
|
babel==2.9.1
|
||||||
|
# via sphinx
|
||||||
|
backports.entry-points-selectable==1.1.0
|
||||||
|
# via virtualenv
|
||||||
|
certifi==2021.10.8
|
||||||
|
# via requests
|
||||||
|
cfgv==3.3.1
|
||||||
|
# via pre-commit
|
||||||
|
charset-normalizer==2.0.7
|
||||||
|
# via requests
|
||||||
|
click==8.0.3
|
||||||
|
# via pip-tools
|
||||||
|
distlib==0.3.3
|
||||||
|
# via virtualenv
|
||||||
|
docutils==0.17.1
|
||||||
|
# via sphinx
|
||||||
|
filelock==3.3.2
|
||||||
|
# via
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
identify==2.3.3
|
||||||
|
# via pre-commit
|
||||||
|
idna==3.3
|
||||||
|
# via requests
|
||||||
|
imagesize==1.2.0
|
||||||
|
# via sphinx
|
||||||
|
iniconfig==1.1.1
|
||||||
|
# via pytest
|
||||||
|
jinja2==3.0.2
|
||||||
|
# via sphinx
|
||||||
|
markupsafe==2.0.1
|
||||||
|
# via jinja2
|
||||||
|
mypy==0.910
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
# via mypy
|
||||||
|
nodeenv==1.6.0
|
||||||
|
# via pre-commit
|
||||||
|
packaging==21.2
|
||||||
|
# via
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# pytest
|
||||||
|
# sphinx
|
||||||
|
# tox
|
||||||
|
pallets-sphinx-themes==2.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
pep517==0.12.0
|
||||||
|
# via pip-tools
|
||||||
|
pip-tools==6.4.0
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
platformdirs==2.4.0
|
||||||
|
# via virtualenv
|
||||||
|
pluggy==1.0.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pre-commit==2.15.0
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
py==1.11.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pygments==2.10.0
|
||||||
|
# via sphinx
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytest==6.2.5
|
||||||
|
# via -r requirements/tests.in
|
||||||
|
pytz==2021.3
|
||||||
|
# via babel
|
||||||
|
pyyaml==6.0
|
||||||
|
# via pre-commit
|
||||||
|
requests==2.26.0
|
||||||
|
# via sphinx
|
||||||
|
six==1.16.0
|
||||||
|
# via
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
snowballstemmer==2.1.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx==4.2.0
|
||||||
|
# via
|
||||||
|
# -r requirements/docs.in
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx-issues
|
||||||
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
# via sphinx
|
||||||
|
toml==0.10.2
|
||||||
|
# via
|
||||||
|
# mypy
|
||||||
|
# pre-commit
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
tomli==1.2.2
|
||||||
|
# via pep517
|
||||||
|
tox==3.24.4
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
# via mypy
|
||||||
|
urllib3==1.26.7
|
||||||
|
# via requests
|
||||||
|
virtualenv==20.10.0
|
||||||
|
# via
|
||||||
|
# pre-commit
|
||||||
|
# tox
|
||||||
|
wheel==0.37.0
|
||||||
|
# via pip-tools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# pip
|
||||||
|
# setuptools
|
|
@ -0,0 +1,67 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/docs.in
|
||||||
|
#
|
||||||
|
alabaster==0.7.12
|
||||||
|
# via sphinx
|
||||||
|
babel==2.9.1
|
||||||
|
# via sphinx
|
||||||
|
certifi==2021.10.8
|
||||||
|
# via requests
|
||||||
|
charset-normalizer==2.0.7
|
||||||
|
# via requests
|
||||||
|
docutils==0.17.1
|
||||||
|
# via sphinx
|
||||||
|
idna==3.3
|
||||||
|
# via requests
|
||||||
|
imagesize==1.2.0
|
||||||
|
# via sphinx
|
||||||
|
jinja2==3.0.2
|
||||||
|
# via sphinx
|
||||||
|
markupsafe==2.0.1
|
||||||
|
# via jinja2
|
||||||
|
packaging==21.2
|
||||||
|
# via
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx
|
||||||
|
pallets-sphinx-themes==2.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
pygments==2.10.0
|
||||||
|
# via sphinx
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytz==2021.3
|
||||||
|
# via babel
|
||||||
|
requests==2.26.0
|
||||||
|
# via sphinx
|
||||||
|
snowballstemmer==2.1.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx==4.2.0
|
||||||
|
# via
|
||||||
|
# -r requirements/docs.in
|
||||||
|
# pallets-sphinx-themes
|
||||||
|
# sphinx-issues
|
||||||
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
|
# via sphinx
|
||||||
|
urllib3==1.26.7
|
||||||
|
# via requests
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# setuptools
|
|
@ -0,0 +1,22 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/tests.in
|
||||||
|
#
|
||||||
|
attrs==21.2.0
|
||||||
|
# via pytest
|
||||||
|
iniconfig==1.1.1
|
||||||
|
# via pytest
|
||||||
|
packaging==21.2
|
||||||
|
# via pytest
|
||||||
|
pluggy==1.0.0
|
||||||
|
# via pytest
|
||||||
|
py==1.11.0
|
||||||
|
# via pytest
|
||||||
|
pyparsing==2.4.7
|
||||||
|
# via packaging
|
||||||
|
pytest==6.2.5
|
||||||
|
# via -r requirements/tests.in
|
||||||
|
toml==0.10.2
|
||||||
|
# via pytest
|
|
@ -0,0 +1,14 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/typing.in
|
||||||
|
#
|
||||||
|
mypy==0.910
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
# via mypy
|
||||||
|
toml==0.10.2
|
||||||
|
# via mypy
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
# via mypy
|
|
@ -0,0 +1,99 @@
|
||||||
|
[metadata]
|
||||||
|
name = Jinja2
|
||||||
|
version = attr: jinja2.__version__
|
||||||
|
url = https://palletsprojects.com/p/jinja/
|
||||||
|
project_urls =
|
||||||
|
Donate = https://palletsprojects.com/donate
|
||||||
|
Documentation = https://jinja.palletsprojects.com/
|
||||||
|
Changes = https://jinja.palletsprojects.com/changes/
|
||||||
|
Source Code = https://github.com/pallets/jinja/
|
||||||
|
Issue Tracker = https://github.com/pallets/jinja/issues/
|
||||||
|
Twitter = https://twitter.com/PalletsTeam
|
||||||
|
Chat = https://discord.gg/pallets
|
||||||
|
license = BSD-3-Clause
|
||||||
|
license_files = LICENSE.rst
|
||||||
|
author = Armin Ronacher
|
||||||
|
author_email = armin.ronacher@active-4.com
|
||||||
|
maintainer = Pallets
|
||||||
|
maintainer_email = contact@palletsprojects.com
|
||||||
|
description = A very fast and expressive template engine.
|
||||||
|
long_description = file: README.rst
|
||||||
|
long_description_content_type = text/x-rst
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 5 - Production/Stable
|
||||||
|
Environment :: Web Environment
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Programming Language :: Python
|
||||||
|
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Topic :: Text Processing :: Markup :: HTML
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
package_dir = = src
|
||||||
|
include_package_data = true
|
||||||
|
python_requires = >= 3.6
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
babel.extractors =
|
||||||
|
jinja2 = jinja2.ext:babel_extract[i18n]
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
filterwarnings =
|
||||||
|
error
|
||||||
|
ignore:The loop argument:DeprecationWarning:asyncio[.]base_events:542
|
||||||
|
|
||||||
|
[coverage:run]
|
||||||
|
branch = True
|
||||||
|
source =
|
||||||
|
jinja2
|
||||||
|
tests
|
||||||
|
|
||||||
|
[coverage:paths]
|
||||||
|
source =
|
||||||
|
src
|
||||||
|
*/site-packages
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
select = B, E, F, W, B9, ISC
|
||||||
|
ignore =
|
||||||
|
E203
|
||||||
|
E501
|
||||||
|
E722
|
||||||
|
W503
|
||||||
|
max-line-length = 80
|
||||||
|
per-file-ignores =
|
||||||
|
src/jinja2/__init__.py: F401
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
files = src/jinja2
|
||||||
|
python_version = 3.6
|
||||||
|
disallow_subclassing_any = True
|
||||||
|
disallow_untyped_calls = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
disallow_incomplete_defs = True
|
||||||
|
no_implicit_optional = True
|
||||||
|
local_partial_types = True
|
||||||
|
no_implicit_reexport = True
|
||||||
|
strict_equality = True
|
||||||
|
warn_redundant_casts = True
|
||||||
|
warn_unused_configs = True
|
||||||
|
warn_unused_ignores = True
|
||||||
|
warn_return_any = True
|
||||||
|
warn_unreachable = True
|
||||||
|
|
||||||
|
[mypy-jinja2.defaults]
|
||||||
|
no_implicit_reexport = False
|
||||||
|
|
||||||
|
[mypy-markupsafe]
|
||||||
|
no_implicit_reexport = False
|
||||||
|
|
||||||
|
[egg_info]
|
||||||
|
tag_build =
|
||||||
|
tag_date = 0
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
# Metadata goes in setup.cfg. These are here for GitHub's dependency graph.
|
||||||
|
setup(
|
||||||
|
name="Jinja2",
|
||||||
|
install_requires=["MarkupSafe>=2.0"],
|
||||||
|
extras_require={"i18n": ["Babel>=2.7"]},
|
||||||
|
)
|
|
@ -0,0 +1,111 @@
|
||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Jinja2
|
||||||
|
Version: 3.0.3
|
||||||
|
Summary: A very fast and expressive template engine.
|
||||||
|
Home-page: https://palletsprojects.com/p/jinja/
|
||||||
|
Author: Armin Ronacher
|
||||||
|
Author-email: armin.ronacher@active-4.com
|
||||||
|
Maintainer: Pallets
|
||||||
|
Maintainer-email: contact@palletsprojects.com
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Donate, https://palletsprojects.com/donate
|
||||||
|
Project-URL: Documentation, https://jinja.palletsprojects.com/
|
||||||
|
Project-URL: Changes, https://jinja.palletsprojects.com/changes/
|
||||||
|
Project-URL: Source Code, https://github.com/pallets/jinja/
|
||||||
|
Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/
|
||||||
|
Project-URL: Twitter, https://twitter.com/PalletsTeam
|
||||||
|
Project-URL: Chat, https://discord.gg/pallets
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||||
|
Requires-Python: >=3.6
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
Provides-Extra: i18n
|
||||||
|
License-File: LICENSE.rst
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
- AsyncIO support for generating templates and calling async
|
||||||
|
functions.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
|
||||||
|
Installing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Install and update using `pip`_:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ pip install -U Jinja2
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
|
In A Nutshell
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Members{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<ul>
|
||||||
|
{% for user in users %}
|
||||||
|
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
Donate
|
||||||
|
------
|
||||||
|
|
||||||
|
The Pallets organization develops and supports Jinja and other popular
|
||||||
|
packages. In order to grow the community of contributors and users, and
|
||||||
|
allow the maintainers to devote more time to the projects, `please
|
||||||
|
donate today`_.
|
||||||
|
|
||||||
|
.. _please donate today: https://palletsprojects.com/donate
|
||||||
|
|
||||||
|
|
||||||
|
Links
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Documentation: https://jinja.palletsprojects.com/
|
||||||
|
- Changes: https://jinja.palletsprojects.com/changes/
|
||||||
|
- PyPI Releases: https://pypi.org/project/Jinja2/
|
||||||
|
- Source Code: https://github.com/pallets/jinja/
|
||||||
|
- Issue Tracker: https://github.com/pallets/jinja/issues/
|
||||||
|
- Website: https://palletsprojects.com/p/jinja/
|
||||||
|
- Twitter: https://twitter.com/PalletsTeam
|
||||||
|
- Chat: https://discord.gg/pallets
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
CHANGES.rst
|
||||||
|
LICENSE.rst
|
||||||
|
MANIFEST.in
|
||||||
|
README.rst
|
||||||
|
setup.cfg
|
||||||
|
setup.py
|
||||||
|
tox.ini
|
||||||
|
artwork/jinjalogo.svg
|
||||||
|
docs/Makefile
|
||||||
|
docs/api.rst
|
||||||
|
docs/changes.rst
|
||||||
|
docs/conf.py
|
||||||
|
docs/extensions.rst
|
||||||
|
docs/faq.rst
|
||||||
|
docs/index.rst
|
||||||
|
docs/integration.rst
|
||||||
|
docs/intro.rst
|
||||||
|
docs/license.rst
|
||||||
|
docs/make.bat
|
||||||
|
docs/nativetypes.rst
|
||||||
|
docs/sandbox.rst
|
||||||
|
docs/switching.rst
|
||||||
|
docs/templates.rst
|
||||||
|
docs/tricks.rst
|
||||||
|
docs/_static/jinja-logo-sidebar.png
|
||||||
|
docs/_static/jinja-logo.png
|
||||||
|
docs/examples/cache_extension.py
|
||||||
|
docs/examples/inline_gettext_extension.py
|
||||||
|
examples/basic/cycle.py
|
||||||
|
examples/basic/debugger.py
|
||||||
|
examples/basic/inheritance.py
|
||||||
|
examples/basic/test.py
|
||||||
|
examples/basic/test_filter_and_linestatements.py
|
||||||
|
examples/basic/test_loop_filter.py
|
||||||
|
examples/basic/translate.py
|
||||||
|
examples/basic/templates/broken.html
|
||||||
|
examples/basic/templates/subbroken.html
|
||||||
|
requirements/dev.txt
|
||||||
|
requirements/docs.txt
|
||||||
|
requirements/tests.txt
|
||||||
|
requirements/typing.txt
|
||||||
|
src/Jinja2.egg-info/PKG-INFO
|
||||||
|
src/Jinja2.egg-info/SOURCES.txt
|
||||||
|
src/Jinja2.egg-info/dependency_links.txt
|
||||||
|
src/Jinja2.egg-info/entry_points.txt
|
||||||
|
src/Jinja2.egg-info/requires.txt
|
||||||
|
src/Jinja2.egg-info/top_level.txt
|
||||||
|
src/jinja2/__init__.py
|
||||||
|
src/jinja2/_identifier.py
|
||||||
|
src/jinja2/async_utils.py
|
||||||
|
src/jinja2/bccache.py
|
||||||
|
src/jinja2/compiler.py
|
||||||
|
src/jinja2/constants.py
|
||||||
|
src/jinja2/debug.py
|
||||||
|
src/jinja2/defaults.py
|
||||||
|
src/jinja2/environment.py
|
||||||
|
src/jinja2/exceptions.py
|
||||||
|
src/jinja2/ext.py
|
||||||
|
src/jinja2/filters.py
|
||||||
|
src/jinja2/idtracking.py
|
||||||
|
src/jinja2/lexer.py
|
||||||
|
src/jinja2/loaders.py
|
||||||
|
src/jinja2/meta.py
|
||||||
|
src/jinja2/nativetypes.py
|
||||||
|
src/jinja2/nodes.py
|
||||||
|
src/jinja2/optimizer.py
|
||||||
|
src/jinja2/parser.py
|
||||||
|
src/jinja2/py.typed
|
||||||
|
src/jinja2/runtime.py
|
||||||
|
src/jinja2/sandbox.py
|
||||||
|
src/jinja2/tests.py
|
||||||
|
src/jinja2/utils.py
|
||||||
|
src/jinja2/visitor.py
|
||||||
|
tests/conftest.py
|
||||||
|
tests/test_api.py
|
||||||
|
tests/test_async.py
|
||||||
|
tests/test_async_filters.py
|
||||||
|
tests/test_bytecode_cache.py
|
||||||
|
tests/test_compile.py
|
||||||
|
tests/test_core_tags.py
|
||||||
|
tests/test_debug.py
|
||||||
|
tests/test_ext.py
|
||||||
|
tests/test_features.py
|
||||||
|
tests/test_filters.py
|
||||||
|
tests/test_idtracking.py
|
||||||
|
tests/test_imports.py
|
||||||
|
tests/test_inheritance.py
|
||||||
|
tests/test_lexnparse.py
|
||||||
|
tests/test_loader.py
|
||||||
|
tests/test_nativetypes.py
|
||||||
|
tests/test_nodes.py
|
||||||
|
tests/test_regression.py
|
||||||
|
tests/test_runtime.py
|
||||||
|
tests/test_security.py
|
||||||
|
tests/test_tests.py
|
||||||
|
tests/test_utils.py
|
||||||
|
tests/res/__init__.py
|
||||||
|
tests/res/package.zip
|
||||||
|
tests/res/templates/broken.html
|
||||||
|
tests/res/templates/mojibake.txt
|
||||||
|
tests/res/templates/syntaxerror.html
|
||||||
|
tests/res/templates/test.html
|
||||||
|
tests/res/templates/foo/test.html
|
||||||
|
tests/res/templates2/foo
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[babel.extractors]
|
||||||
|
jinja2 = jinja2.ext:babel_extract [i18n]
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
MarkupSafe>=2.0
|
||||||
|
|
||||||
|
[i18n]
|
||||||
|
Babel>=2.7
|
|
@ -0,0 +1 @@
|
||||||
|
jinja2
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Jinja is a template engine written in pure Python. It provides a
|
||||||
|
non-XML syntax that supports inline expressions and an optional
|
||||||
|
sandboxed environment.
|
||||||
|
"""
|
||||||
|
from .bccache import BytecodeCache as BytecodeCache
|
||||||
|
from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache
|
||||||
|
from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache
|
||||||
|
from .environment import Environment as Environment
|
||||||
|
from .environment import Template as Template
|
||||||
|
from .exceptions import TemplateAssertionError as TemplateAssertionError
|
||||||
|
from .exceptions import TemplateError as TemplateError
|
||||||
|
from .exceptions import TemplateNotFound as TemplateNotFound
|
||||||
|
from .exceptions import TemplateRuntimeError as TemplateRuntimeError
|
||||||
|
from .exceptions import TemplatesNotFound as TemplatesNotFound
|
||||||
|
from .exceptions import TemplateSyntaxError as TemplateSyntaxError
|
||||||
|
from .exceptions import UndefinedError as UndefinedError
|
||||||
|
from .filters import contextfilter
|
||||||
|
from .filters import environmentfilter
|
||||||
|
from .filters import evalcontextfilter
|
||||||
|
from .loaders import BaseLoader as BaseLoader
|
||||||
|
from .loaders import ChoiceLoader as ChoiceLoader
|
||||||
|
from .loaders import DictLoader as DictLoader
|
||||||
|
from .loaders import FileSystemLoader as FileSystemLoader
|
||||||
|
from .loaders import FunctionLoader as FunctionLoader
|
||||||
|
from .loaders import ModuleLoader as ModuleLoader
|
||||||
|
from .loaders import PackageLoader as PackageLoader
|
||||||
|
from .loaders import PrefixLoader as PrefixLoader
|
||||||
|
from .runtime import ChainableUndefined as ChainableUndefined
|
||||||
|
from .runtime import DebugUndefined as DebugUndefined
|
||||||
|
from .runtime import make_logging_undefined as make_logging_undefined
|
||||||
|
from .runtime import StrictUndefined as StrictUndefined
|
||||||
|
from .runtime import Undefined as Undefined
|
||||||
|
from .utils import clear_caches as clear_caches
|
||||||
|
from .utils import contextfunction
|
||||||
|
from .utils import environmentfunction
|
||||||
|
from .utils import escape
|
||||||
|
from .utils import evalcontextfunction
|
||||||
|
from .utils import is_undefined as is_undefined
|
||||||
|
from .utils import Markup
|
||||||
|
from .utils import pass_context as pass_context
|
||||||
|
from .utils import pass_environment as pass_environment
|
||||||
|
from .utils import pass_eval_context as pass_eval_context
|
||||||
|
from .utils import select_autoescape as select_autoescape
|
||||||
|
|
||||||
|
__version__ = "3.0.3"
|
|
@ -0,0 +1,6 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
# generated by scripts/generate_identifier_pattern.py
|
||||||
|
pattern = re.compile(
|
||||||
|
r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950
|
||||||
|
)
|
|
@ -0,0 +1,75 @@
|
||||||
|
import inspect
|
||||||
|
import typing as t
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from .utils import _PassArg
|
||||||
|
from .utils import pass_eval_context
|
||||||
|
|
||||||
|
V = t.TypeVar("V")
|
||||||
|
|
||||||
|
|
||||||
|
def async_variant(normal_func): # type: ignore
|
||||||
|
def decorator(async_func): # type: ignore
|
||||||
|
pass_arg = _PassArg.from_obj(normal_func)
|
||||||
|
need_eval_context = pass_arg is None
|
||||||
|
|
||||||
|
if pass_arg is _PassArg.environment:
|
||||||
|
|
||||||
|
def is_async(args: t.Any) -> bool:
|
||||||
|
return t.cast(bool, args[0].is_async)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def is_async(args: t.Any) -> bool:
|
||||||
|
return t.cast(bool, args[0].environment.is_async)
|
||||||
|
|
||||||
|
@wraps(normal_func)
|
||||||
|
def wrapper(*args, **kwargs): # type: ignore
|
||||||
|
b = is_async(args)
|
||||||
|
|
||||||
|
if need_eval_context:
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
if b:
|
||||||
|
return async_func(*args, **kwargs)
|
||||||
|
|
||||||
|
return normal_func(*args, **kwargs)
|
||||||
|
|
||||||
|
if need_eval_context:
|
||||||
|
wrapper = pass_eval_context(wrapper)
|
||||||
|
|
||||||
|
wrapper.jinja_async_variant = True
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)}
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
|
||||||
|
# Avoid a costly call to isawaitable
|
||||||
|
if type(value) in _common_primitives:
|
||||||
|
return t.cast("V", value)
|
||||||
|
|
||||||
|
if inspect.isawaitable(value):
|
||||||
|
return await t.cast("t.Awaitable[V]", value)
|
||||||
|
|
||||||
|
return t.cast("V", value)
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_aiter(
|
||||||
|
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||||
|
) -> "t.AsyncIterator[V]":
|
||||||
|
if hasattr(iterable, "__aiter__"):
|
||||||
|
async for item in t.cast("t.AsyncIterable[V]", iterable):
|
||||||
|
yield item
|
||||||
|
else:
|
||||||
|
for item in t.cast("t.Iterable[V]", iterable):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
|
||||||
|
async def auto_to_list(
|
||||||
|
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||||
|
) -> t.List["V"]:
|
||||||
|
return [x async for x in auto_aiter(value)]
|
|
@ -0,0 +1,364 @@
|
||||||
|
"""The optional bytecode cache system. This is useful if you have very
|
||||||
|
complex template situations and the compilation of all those templates
|
||||||
|
slows down your application too much.
|
||||||
|
|
||||||
|
Situations where this is useful are often forking web applications that
|
||||||
|
are initialized on the first request.
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
import fnmatch
|
||||||
|
import marshal
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import typing as t
|
||||||
|
from hashlib import sha1
|
||||||
|
from io import BytesIO
|
||||||
|
from types import CodeType
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
class _MemcachedClient(te.Protocol):
|
||||||
|
def get(self, key: str) -> bytes:
|
||||||
|
...
|
||||||
|
|
||||||
|
def set(self, key: str, value: bytes, timeout: t.Optional[int] = None) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
bc_version = 5
|
||||||
|
# Magic bytes to identify Jinja bytecode cache files. Contains the
|
||||||
|
# Python major and minor version to avoid loading incompatible bytecode
|
||||||
|
# if a project upgrades its Python version.
|
||||||
|
bc_magic = (
|
||||||
|
b"j2"
|
||||||
|
+ pickle.dumps(bc_version, 2)
|
||||||
|
+ pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Bucket:
|
||||||
|
"""Buckets are used to store the bytecode for one template. It's created
|
||||||
|
and initialized by the bytecode cache and passed to the loading functions.
|
||||||
|
|
||||||
|
The buckets get an internal checksum from the cache assigned and use this
|
||||||
|
to automatically reject outdated cache material. Individual bytecode
|
||||||
|
cache subclasses don't have to care about cache invalidation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
|
||||||
|
self.environment = environment
|
||||||
|
self.key = key
|
||||||
|
self.checksum = checksum
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Resets the bucket (unloads the bytecode)."""
|
||||||
|
self.code: t.Optional[CodeType] = None
|
||||||
|
|
||||||
|
def load_bytecode(self, f: t.BinaryIO) -> None:
|
||||||
|
"""Loads bytecode from a file or file like object."""
|
||||||
|
# make sure the magic header is correct
|
||||||
|
magic = f.read(len(bc_magic))
|
||||||
|
if magic != bc_magic:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
# the source code of the file changed, we need to reload
|
||||||
|
checksum = pickle.load(f)
|
||||||
|
if self.checksum != checksum:
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
# if marshal_load fails then we need to reload
|
||||||
|
try:
|
||||||
|
self.code = marshal.load(f)
|
||||||
|
except (EOFError, ValueError, TypeError):
|
||||||
|
self.reset()
|
||||||
|
return
|
||||||
|
|
||||||
|
def write_bytecode(self, f: t.BinaryIO) -> None:
|
||||||
|
"""Dump the bytecode into the file or file like object passed."""
|
||||||
|
if self.code is None:
|
||||||
|
raise TypeError("can't write empty bucket")
|
||||||
|
f.write(bc_magic)
|
||||||
|
pickle.dump(self.checksum, f, 2)
|
||||||
|
marshal.dump(self.code, f)
|
||||||
|
|
||||||
|
def bytecode_from_string(self, string: bytes) -> None:
|
||||||
|
"""Load bytecode from bytes."""
|
||||||
|
self.load_bytecode(BytesIO(string))
|
||||||
|
|
||||||
|
def bytecode_to_string(self) -> bytes:
|
||||||
|
"""Return the bytecode as bytes."""
|
||||||
|
out = BytesIO()
|
||||||
|
self.write_bytecode(out)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class BytecodeCache:
|
||||||
|
"""To implement your own bytecode cache you have to subclass this class
|
||||||
|
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
|
||||||
|
these methods are passed a :class:`~jinja2.bccache.Bucket`.
|
||||||
|
|
||||||
|
A very basic bytecode cache that saves the bytecode on the file system::
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
class MyCache(BytecodeCache):
|
||||||
|
|
||||||
|
def __init__(self, directory):
|
||||||
|
self.directory = directory
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket):
|
||||||
|
filename = path.join(self.directory, bucket.key)
|
||||||
|
if path.exists(filename):
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
bucket.load_bytecode(f)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket):
|
||||||
|
filename = path.join(self.directory, bucket.key)
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
bucket.write_bytecode(f)
|
||||||
|
|
||||||
|
A more advanced version of a filesystem based bytecode cache is part of
|
||||||
|
Jinja.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
"""Subclasses have to override this method to load bytecode into a
|
||||||
|
bucket. If they are not able to find code in the cache for the
|
||||||
|
bucket, it must not do anything.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
"""Subclasses have to override this method to write the bytecode
|
||||||
|
from a bucket back to the cache. If it unable to do so it must not
|
||||||
|
fail silently but raise an exception.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clears the cache. This method is not used by Jinja but should be
|
||||||
|
implemented to allow applications to clear the bytecode cache used
|
||||||
|
by a particular environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_cache_key(
|
||||||
|
self, name: str, filename: t.Optional[t.Union[str]] = None
|
||||||
|
) -> str:
|
||||||
|
"""Returns the unique hash key for this template name."""
|
||||||
|
hash = sha1(name.encode("utf-8"))
|
||||||
|
|
||||||
|
if filename is not None:
|
||||||
|
hash.update(f"|{filename}".encode())
|
||||||
|
|
||||||
|
return hash.hexdigest()
|
||||||
|
|
||||||
|
def get_source_checksum(self, source: str) -> str:
|
||||||
|
"""Returns a checksum for the source."""
|
||||||
|
return sha1(source.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def get_bucket(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
filename: t.Optional[str],
|
||||||
|
source: str,
|
||||||
|
) -> Bucket:
|
||||||
|
"""Return a cache bucket for the given template. All arguments are
|
||||||
|
mandatory but filename may be `None`.
|
||||||
|
"""
|
||||||
|
key = self.get_cache_key(name, filename)
|
||||||
|
checksum = self.get_source_checksum(source)
|
||||||
|
bucket = Bucket(environment, key, checksum)
|
||||||
|
self.load_bytecode(bucket)
|
||||||
|
return bucket
|
||||||
|
|
||||||
|
def set_bucket(self, bucket: Bucket) -> None:
|
||||||
|
"""Put the bucket into the cache."""
|
||||||
|
self.dump_bytecode(bucket)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemBytecodeCache(BytecodeCache):
|
||||||
|
"""A bytecode cache that stores bytecode on the filesystem. It accepts
|
||||||
|
two arguments: The directory where the cache items are stored and a
|
||||||
|
pattern string that is used to build the filename.
|
||||||
|
|
||||||
|
If no directory is specified a default cache directory is selected. On
|
||||||
|
Windows the user's temp directory is used, on UNIX systems a directory
|
||||||
|
is created for the user in the system temp directory.
|
||||||
|
|
||||||
|
The pattern can be used to have multiple separate caches operate on the
|
||||||
|
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
|
||||||
|
is replaced with the cache key.
|
||||||
|
|
||||||
|
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
|
||||||
|
|
||||||
|
This bytecode cache supports clearing of the cache using the clear method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
|
||||||
|
) -> None:
|
||||||
|
if directory is None:
|
||||||
|
directory = self._get_default_cache_dir()
|
||||||
|
self.directory = directory
|
||||||
|
self.pattern = pattern
|
||||||
|
|
||||||
|
def _get_default_cache_dir(self) -> str:
|
||||||
|
def _unsafe_dir() -> "te.NoReturn":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot determine safe temp directory. You "
|
||||||
|
"need to explicitly provide one."
|
||||||
|
)
|
||||||
|
|
||||||
|
tmpdir = tempfile.gettempdir()
|
||||||
|
|
||||||
|
# On windows the temporary directory is used specific unless
|
||||||
|
# explicitly forced otherwise. We can just use that.
|
||||||
|
if os.name == "nt":
|
||||||
|
return tmpdir
|
||||||
|
if not hasattr(os, "getuid"):
|
||||||
|
_unsafe_dir()
|
||||||
|
|
||||||
|
dirname = f"_jinja2-cache-{os.getuid()}"
|
||||||
|
actual_dir = os.path.join(tmpdir, dirname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(actual_dir, stat.S_IRWXU)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
os.chmod(actual_dir, stat.S_IRWXU)
|
||||||
|
actual_dir_stat = os.lstat(actual_dir)
|
||||||
|
if (
|
||||||
|
actual_dir_stat.st_uid != os.getuid()
|
||||||
|
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||||
|
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||||
|
):
|
||||||
|
_unsafe_dir()
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
|
actual_dir_stat = os.lstat(actual_dir)
|
||||||
|
if (
|
||||||
|
actual_dir_stat.st_uid != os.getuid()
|
||||||
|
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||||
|
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||||
|
):
|
||||||
|
_unsafe_dir()
|
||||||
|
|
||||||
|
return actual_dir
|
||||||
|
|
||||||
|
def _get_cache_filename(self, bucket: Bucket) -> str:
|
||||||
|
return os.path.join(self.directory, self.pattern % (bucket.key,))
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
filename = self._get_cache_filename(bucket)
|
||||||
|
|
||||||
|
if os.path.exists(filename):
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
bucket.load_bytecode(f)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
with open(self._get_cache_filename(bucket), "wb") as f:
|
||||||
|
bucket.write_bytecode(f)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
# imported lazily here because google app-engine doesn't support
|
||||||
|
# write access on the file system and the function does not exist
|
||||||
|
# normally.
|
||||||
|
from os import remove
|
||||||
|
|
||||||
|
files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
|
||||||
|
for filename in files:
|
||||||
|
try:
|
||||||
|
remove(os.path.join(self.directory, filename))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MemcachedBytecodeCache(BytecodeCache):
|
||||||
|
"""This class implements a bytecode cache that uses a memcache cache for
|
||||||
|
storing the information. It does not enforce a specific memcache library
|
||||||
|
(tummy's memcache or cmemcache) but will accept any class that provides
|
||||||
|
the minimal interface required.
|
||||||
|
|
||||||
|
Libraries compatible with this class:
|
||||||
|
|
||||||
|
- `cachelib <https://github.com/pallets/cachelib>`_
|
||||||
|
- `python-memcached <https://pypi.org/project/python-memcached/>`_
|
||||||
|
|
||||||
|
(Unfortunately the django cache interface is not compatible because it
|
||||||
|
does not support storing binary data, only text. You can however pass
|
||||||
|
the underlying cache client to the bytecode cache which is available
|
||||||
|
as `django.core.cache.cache._client`.)
|
||||||
|
|
||||||
|
The minimal interface for the client passed to the constructor is this:
|
||||||
|
|
||||||
|
.. class:: MinimalClientInterface
|
||||||
|
|
||||||
|
.. method:: set(key, value[, timeout])
|
||||||
|
|
||||||
|
Stores the bytecode in the cache. `value` is a string and
|
||||||
|
`timeout` the timeout of the key. If timeout is not provided
|
||||||
|
a default timeout or no timeout should be assumed, if it's
|
||||||
|
provided it's an integer with the number of seconds the cache
|
||||||
|
item should exist.
|
||||||
|
|
||||||
|
.. method:: get(key)
|
||||||
|
|
||||||
|
Returns the value for the cache key. If the item does not
|
||||||
|
exist in the cache the return value must be `None`.
|
||||||
|
|
||||||
|
The other arguments to the constructor are the prefix for all keys that
|
||||||
|
is added before the actual cache key and the timeout for the bytecode in
|
||||||
|
the cache system. We recommend a high (or no) timeout.
|
||||||
|
|
||||||
|
This bytecode cache does not support clearing of used items in the cache.
|
||||||
|
The clear method is a no-operation function.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7
|
||||||
|
Added support for ignoring memcache errors through the
|
||||||
|
`ignore_memcache_errors` parameter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: "_MemcachedClient",
|
||||||
|
prefix: str = "jinja2/bytecode/",
|
||||||
|
timeout: t.Optional[int] = None,
|
||||||
|
ignore_memcache_errors: bool = True,
|
||||||
|
):
|
||||||
|
self.client = client
|
||||||
|
self.prefix = prefix
|
||||||
|
self.timeout = timeout
|
||||||
|
self.ignore_memcache_errors = ignore_memcache_errors
|
||||||
|
|
||||||
|
def load_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
try:
|
||||||
|
code = self.client.get(self.prefix + bucket.key)
|
||||||
|
except Exception:
|
||||||
|
if not self.ignore_memcache_errors:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
bucket.bytecode_from_string(code)
|
||||||
|
|
||||||
|
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||||
|
key = self.prefix + bucket.key
|
||||||
|
value = bucket.bytecode_to_string()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.timeout is not None:
|
||||||
|
self.client.set(key, value, self.timeout)
|
||||||
|
else:
|
||||||
|
self.client.set(key, value)
|
||||||
|
except Exception:
|
||||||
|
if not self.ignore_memcache_errors:
|
||||||
|
raise
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
#: list of lorem ipsum words used by the lipsum() helper function
|
||||||
|
LOREM_IPSUM_WORDS = """\
|
||||||
|
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
|
||||||
|
auctor augue bibendum blandit class commodo condimentum congue consectetuer
|
||||||
|
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
|
||||||
|
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
|
||||||
|
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
|
||||||
|
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
|
||||||
|
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
|
||||||
|
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
|
||||||
|
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
|
||||||
|
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
|
||||||
|
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
|
||||||
|
penatibus per pharetra phasellus placerat platea porta porttitor posuere
|
||||||
|
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
|
||||||
|
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
|
||||||
|
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
|
||||||
|
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
|
||||||
|
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
|
||||||
|
viverra volutpat vulputate"""
|
|
@ -0,0 +1,259 @@
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
from types import CodeType
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .utils import internal_code
|
||||||
|
from .utils import missing
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .runtime import Context
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
|
||||||
|
"""Rewrite the current exception to replace any tracebacks from
|
||||||
|
within compiled template code with tracebacks that look like they
|
||||||
|
came from the template source.
|
||||||
|
|
||||||
|
This must be called within an ``except`` block.
|
||||||
|
|
||||||
|
:param source: For ``TemplateSyntaxError``, the original source if
|
||||||
|
known.
|
||||||
|
:return: The original exception with the rewritten traceback.
|
||||||
|
"""
|
||||||
|
_, exc_value, tb = sys.exc_info()
|
||||||
|
exc_value = t.cast(BaseException, exc_value)
|
||||||
|
tb = t.cast(TracebackType, tb)
|
||||||
|
|
||||||
|
if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:
|
||||||
|
exc_value.translated = True
|
||||||
|
exc_value.source = source
|
||||||
|
# Remove the old traceback, otherwise the frames from the
|
||||||
|
# compiler still show up.
|
||||||
|
exc_value.with_traceback(None)
|
||||||
|
# Outside of runtime, so the frame isn't executing template
|
||||||
|
# code, but it still needs to point at the template.
|
||||||
|
tb = fake_traceback(
|
||||||
|
exc_value, None, exc_value.filename or "<unknown>", exc_value.lineno
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Skip the frame for the render function.
|
||||||
|
tb = tb.tb_next
|
||||||
|
|
||||||
|
stack = []
|
||||||
|
|
||||||
|
# Build the stack of traceback object, replacing any in template
|
||||||
|
# code with the source file and line information.
|
||||||
|
while tb is not None:
|
||||||
|
# Skip frames decorated with @internalcode. These are internal
|
||||||
|
# calls that aren't useful in template debugging output.
|
||||||
|
if tb.tb_frame.f_code in internal_code:
|
||||||
|
tb = tb.tb_next
|
||||||
|
continue
|
||||||
|
|
||||||
|
template = tb.tb_frame.f_globals.get("__jinja_template__")
|
||||||
|
|
||||||
|
if template is not None:
|
||||||
|
lineno = template.get_corresponding_lineno(tb.tb_lineno)
|
||||||
|
fake_tb = fake_traceback(exc_value, tb, template.filename, lineno)
|
||||||
|
stack.append(fake_tb)
|
||||||
|
else:
|
||||||
|
stack.append(tb)
|
||||||
|
|
||||||
|
tb = tb.tb_next
|
||||||
|
|
||||||
|
tb_next = None
|
||||||
|
|
||||||
|
# Assign tb_next in reverse to avoid circular references.
|
||||||
|
for tb in reversed(stack):
|
||||||
|
tb_next = tb_set_next(tb, tb_next)
|
||||||
|
|
||||||
|
return exc_value.with_traceback(tb_next)
|
||||||
|
|
||||||
|
|
||||||
|
def fake_traceback( # type: ignore
|
||||||
|
exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
|
||||||
|
) -> TracebackType:
|
||||||
|
"""Produce a new traceback object that looks like it came from the
|
||||||
|
template source instead of the compiled code. The filename, line
|
||||||
|
number, and location name will point to the template, and the local
|
||||||
|
variables will be the current template context.
|
||||||
|
|
||||||
|
:param exc_value: The original exception to be re-raised to create
|
||||||
|
the new traceback.
|
||||||
|
:param tb: The original traceback to get the local variables and
|
||||||
|
code info from.
|
||||||
|
:param filename: The template filename.
|
||||||
|
:param lineno: The line number in the template source.
|
||||||
|
"""
|
||||||
|
if tb is not None:
|
||||||
|
# Replace the real locals with the context that would be
|
||||||
|
# available at that point in the template.
|
||||||
|
locals = get_template_locals(tb.tb_frame.f_locals)
|
||||||
|
locals.pop("__jinja_exception__", None)
|
||||||
|
else:
|
||||||
|
locals = {}
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"__name__": filename,
|
||||||
|
"__file__": filename,
|
||||||
|
"__jinja_exception__": exc_value,
|
||||||
|
}
|
||||||
|
# Raise an exception at the correct line number.
|
||||||
|
code: CodeType = compile(
|
||||||
|
"\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build a new code object that points to the template file and
|
||||||
|
# replaces the location with a block name.
|
||||||
|
location = "template"
|
||||||
|
|
||||||
|
if tb is not None:
|
||||||
|
function = tb.tb_frame.f_code.co_name
|
||||||
|
|
||||||
|
if function == "root":
|
||||||
|
location = "top-level template code"
|
||||||
|
elif function.startswith("block_"):
|
||||||
|
location = f"block {function[6:]!r}"
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
code = code.replace(co_name=location)
|
||||||
|
else:
|
||||||
|
code = CodeType(
|
||||||
|
code.co_argcount,
|
||||||
|
code.co_kwonlyargcount,
|
||||||
|
code.co_nlocals,
|
||||||
|
code.co_stacksize,
|
||||||
|
code.co_flags,
|
||||||
|
code.co_code,
|
||||||
|
code.co_consts,
|
||||||
|
code.co_names,
|
||||||
|
code.co_varnames,
|
||||||
|
code.co_filename,
|
||||||
|
location,
|
||||||
|
code.co_firstlineno,
|
||||||
|
code.co_lnotab,
|
||||||
|
code.co_freevars,
|
||||||
|
code.co_cellvars,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute the new code, which is guaranteed to raise, and return
|
||||||
|
# the new traceback without this frame.
|
||||||
|
try:
|
||||||
|
exec(code, globals, locals)
|
||||||
|
except BaseException:
|
||||||
|
return sys.exc_info()[2].tb_next # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:
|
||||||
|
"""Based on the runtime locals, get the context that would be
|
||||||
|
available at that point in the template.
|
||||||
|
"""
|
||||||
|
# Start with the current template context.
|
||||||
|
ctx: "t.Optional[Context]" = real_locals.get("context")
|
||||||
|
|
||||||
|
if ctx is not None:
|
||||||
|
data: t.Dict[str, t.Any] = ctx.get_all().copy()
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Might be in a derived context that only sets local variables
|
||||||
|
# rather than pushing a context. Local variables follow the scheme
|
||||||
|
# l_depth_name. Find the highest-depth local that has a value for
|
||||||
|
# each name.
|
||||||
|
local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}
|
||||||
|
|
||||||
|
for name, value in real_locals.items():
|
||||||
|
if not name.startswith("l_") or value is missing:
|
||||||
|
# Not a template variable, or no longer relevant.
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, depth_str, name = name.split("_", 2)
|
||||||
|
depth = int(depth_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur_depth = local_overrides.get(name, (-1,))[0]
|
||||||
|
|
||||||
|
if cur_depth < depth:
|
||||||
|
local_overrides[name] = (depth, value)
|
||||||
|
|
||||||
|
# Modify the context with any derived context.
|
||||||
|
for name, (_, value) in local_overrides.items():
|
||||||
|
if value is missing:
|
||||||
|
data.pop(name, None)
|
||||||
|
else:
|
||||||
|
data[name] = value
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
# tb_next is directly assignable as of Python 3.7
|
||||||
|
def tb_set_next(
|
||||||
|
tb: TracebackType, tb_next: t.Optional[TracebackType]
|
||||||
|
) -> TracebackType:
|
||||||
|
tb.tb_next = tb_next
|
||||||
|
return tb
|
||||||
|
|
||||||
|
|
||||||
|
elif platform.python_implementation() == "PyPy":
|
||||||
|
# PyPy might have special support, and won't work with ctypes.
|
||||||
|
try:
|
||||||
|
import tputil # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
# Without tproxy support, use the original traceback.
|
||||||
|
def tb_set_next(
|
||||||
|
tb: TracebackType, tb_next: t.Optional[TracebackType]
|
||||||
|
) -> TracebackType:
|
||||||
|
return tb
|
||||||
|
|
||||||
|
else:
|
||||||
|
# With tproxy support, create a proxy around the traceback that
|
||||||
|
# returns the new tb_next.
|
||||||
|
def tb_set_next(
|
||||||
|
tb: TracebackType, tb_next: t.Optional[TracebackType]
|
||||||
|
) -> TracebackType:
|
||||||
|
def controller(op): # type: ignore
|
||||||
|
if op.opname == "__getattribute__" and op.args[0] == "tb_next":
|
||||||
|
return tb_next
|
||||||
|
|
||||||
|
return op.delegate()
|
||||||
|
|
||||||
|
return tputil.make_proxy(controller, obj=tb) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use ctypes to assign tb_next at the C level since it's read-only
|
||||||
|
# from Python.
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
class _CTraceback(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
# Extra PyObject slots when compiled with Py_TRACE_REFS.
|
||||||
|
("PyObject_HEAD", ctypes.c_byte * object().__sizeof__()),
|
||||||
|
# Only care about tb_next as an object, not a traceback.
|
||||||
|
("tb_next", ctypes.py_object),
|
||||||
|
]
|
||||||
|
|
||||||
|
def tb_set_next(
|
||||||
|
tb: TracebackType, tb_next: t.Optional[TracebackType]
|
||||||
|
) -> TracebackType:
|
||||||
|
c_tb = _CTraceback.from_address(id(tb))
|
||||||
|
|
||||||
|
# Clear out the old tb_next.
|
||||||
|
if tb.tb_next is not None:
|
||||||
|
c_tb_next = ctypes.py_object(tb.tb_next)
|
||||||
|
c_tb.tb_next = ctypes.py_object()
|
||||||
|
ctypes.pythonapi.Py_DecRef(c_tb_next)
|
||||||
|
|
||||||
|
# Assign the new tb_next.
|
||||||
|
if tb_next is not None:
|
||||||
|
c_tb_next = ctypes.py_object(tb_next)
|
||||||
|
ctypes.pythonapi.Py_IncRef(c_tb_next)
|
||||||
|
c_tb.tb_next = c_tb_next
|
||||||
|
|
||||||
|
return tb
|
|
@ -0,0 +1,48 @@
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401
|
||||||
|
from .tests import TESTS as DEFAULT_TESTS # noqa: F401
|
||||||
|
from .utils import Cycler
|
||||||
|
from .utils import generate_lorem_ipsum
|
||||||
|
from .utils import Joiner
|
||||||
|
from .utils import Namespace
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
# defaults for the parser / lexer
|
||||||
|
BLOCK_START_STRING = "{%"
|
||||||
|
BLOCK_END_STRING = "%}"
|
||||||
|
VARIABLE_START_STRING = "{{"
|
||||||
|
VARIABLE_END_STRING = "}}"
|
||||||
|
COMMENT_START_STRING = "{#"
|
||||||
|
COMMENT_END_STRING = "#}"
|
||||||
|
LINE_STATEMENT_PREFIX: t.Optional[str] = None
|
||||||
|
LINE_COMMENT_PREFIX: t.Optional[str] = None
|
||||||
|
TRIM_BLOCKS = False
|
||||||
|
LSTRIP_BLOCKS = False
|
||||||
|
NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n"
|
||||||
|
KEEP_TRAILING_NEWLINE = False
|
||||||
|
|
||||||
|
# default filters, tests and namespace
|
||||||
|
|
||||||
|
DEFAULT_NAMESPACE = {
|
||||||
|
"range": range,
|
||||||
|
"dict": dict,
|
||||||
|
"lipsum": generate_lorem_ipsum,
|
||||||
|
"cycler": Cycler,
|
||||||
|
"joiner": Joiner,
|
||||||
|
"namespace": Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
# default policies
|
||||||
|
DEFAULT_POLICIES: t.Dict[str, t.Any] = {
|
||||||
|
"compiler.ascii_str": True,
|
||||||
|
"urlize.rel": "noopener",
|
||||||
|
"urlize.target": None,
|
||||||
|
"urlize.extra_schemes": None,
|
||||||
|
"truncate.leeway": 5,
|
||||||
|
"json.dumps_function": None,
|
||||||
|
"json.dumps_kwargs": {"sort_keys": True},
|
||||||
|
"ext.i18n.trimmed": False,
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,166 @@
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateError(Exception):
|
||||||
|
"""Baseclass for all template errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: t.Optional[str] = None) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self) -> t.Optional[str]:
|
||||||
|
return self.args[0] if self.args else None
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateNotFound(IOError, LookupError, TemplateError):
|
||||||
|
"""Raised if a template does not exist.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.11
|
||||||
|
If the given name is :class:`Undefined` and no message was
|
||||||
|
provided, an :exc:`UndefinedError` is raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Silence the Python warning about message being deprecated since
|
||||||
|
# it's not valid here.
|
||||||
|
message: t.Optional[str] = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: t.Optional[t.Union[str, "Undefined"]],
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
IOError.__init__(self, name)
|
||||||
|
|
||||||
|
if message is None:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
if isinstance(name, Undefined):
|
||||||
|
name._fail_with_undefined_error()
|
||||||
|
|
||||||
|
message = name
|
||||||
|
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.templates = [name]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatesNotFound(TemplateNotFound):
|
||||||
|
"""Like :class:`TemplateNotFound` but raised if multiple templates
|
||||||
|
are selected. This is a subclass of :class:`TemplateNotFound`
|
||||||
|
exception, so just catching the base exception will catch both.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.11
|
||||||
|
If a name in the list of names is :class:`Undefined`, a message
|
||||||
|
about it being undefined is shown rather than the empty string.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
names: t.Sequence[t.Union[str, "Undefined"]] = (),
|
||||||
|
message: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
if message is None:
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
if isinstance(name, Undefined):
|
||||||
|
parts.append(name._undefined_message)
|
||||||
|
else:
|
||||||
|
parts.append(name)
|
||||||
|
|
||||||
|
parts_str = ", ".join(map(str, parts))
|
||||||
|
message = f"none of the templates given were found: {parts_str}"
|
||||||
|
|
||||||
|
super().__init__(names[-1] if names else None, message)
|
||||||
|
self.templates = list(names)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateSyntaxError(TemplateError):
|
||||||
|
"""Raised to tell the user that there is a problem with the template."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
lineno: int,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.lineno = lineno
|
||||||
|
self.name = name
|
||||||
|
self.filename = filename
|
||||||
|
self.source: t.Optional[str] = None
|
||||||
|
|
||||||
|
# this is set to True if the debug.translate_syntax_error
|
||||||
|
# function translated the syntax error into a new traceback
|
||||||
|
self.translated = False
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
# for translated errors we only return the message
|
||||||
|
if self.translated:
|
||||||
|
return t.cast(str, self.message)
|
||||||
|
|
||||||
|
# otherwise attach some stuff
|
||||||
|
location = f"line {self.lineno}"
|
||||||
|
name = self.filename or self.name
|
||||||
|
if name:
|
||||||
|
location = f'File "{name}", {location}'
|
||||||
|
lines = [t.cast(str, self.message), " " + location]
|
||||||
|
|
||||||
|
# if the source is set, add the line to the output
|
||||||
|
if self.source is not None:
|
||||||
|
try:
|
||||||
|
line = self.source.splitlines()[self.lineno - 1]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
lines.append(" " + line.strip())
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def __reduce__(self): # type: ignore
|
||||||
|
# https://bugs.python.org/issue1692335 Exceptions that take
|
||||||
|
# multiple required arguments have problems with pickling.
|
||||||
|
# Without this, raises TypeError: __init__() missing 1 required
|
||||||
|
# positional argument: 'lineno'
|
||||||
|
return self.__class__, (self.message, self.lineno, self.name, self.filename)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateAssertionError(TemplateSyntaxError):
|
||||||
|
"""Like a template syntax error, but covers cases where something in the
|
||||||
|
template caused an error at compile time that wasn't necessarily caused
|
||||||
|
by a syntax error. However it's a direct subclass of
|
||||||
|
:exc:`TemplateSyntaxError` and has the same attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateRuntimeError(TemplateError):
|
||||||
|
"""A generic runtime error in the template engine. Under some situations
|
||||||
|
Jinja may raise this exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UndefinedError(TemplateRuntimeError):
|
||||||
|
"""Raised if a template tries to operate on :class:`Undefined`."""
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityError(TemplateRuntimeError):
|
||||||
|
"""Raised if a template tries to do something insecure if the
|
||||||
|
sandbox is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FilterArgumentError(TemplateRuntimeError):
|
||||||
|
"""This error is raised if a filter was called with inappropriate
|
||||||
|
arguments
|
||||||
|
"""
|
|
@ -0,0 +1,879 @@
|
||||||
|
"""Extension API for adding custom tags and behavior."""
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from . import defaults
|
||||||
|
from . import nodes
|
||||||
|
from .environment import Environment
|
||||||
|
from .exceptions import TemplateAssertionError
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .runtime import concat # type: ignore
|
||||||
|
from .runtime import Context
|
||||||
|
from .runtime import Undefined
|
||||||
|
from .utils import import_string
|
||||||
|
from .utils import pass_context
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .lexer import Token
|
||||||
|
from .lexer import TokenStream
|
||||||
|
from .parser import Parser
|
||||||
|
|
||||||
|
class _TranslationsBasic(te.Protocol):
|
||||||
|
def gettext(self, message: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
def ngettext(self, singular: str, plural: str, n: int) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _TranslationsContext(_TranslationsBasic):
|
||||||
|
def pgettext(self, context: str, message: str) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
_SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
|
||||||
|
|
||||||
|
|
||||||
|
# I18N functions available in Jinja templates. If the I18N library
|
||||||
|
# provides ugettext, it will be assigned to gettext.
|
||||||
|
GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
|
||||||
|
"_",
|
||||||
|
"gettext",
|
||||||
|
"ngettext",
|
||||||
|
"pgettext",
|
||||||
|
"npgettext",
|
||||||
|
)
|
||||||
|
_ws_re = re.compile(r"\s*\n\s*")
|
||||||
|
|
||||||
|
|
||||||
|
class Extension:
|
||||||
|
"""Extensions can be used to add extra functionality to the Jinja template
|
||||||
|
system at the parser level. Custom extensions are bound to an environment
|
||||||
|
but may not store environment specific data on `self`. The reason for
|
||||||
|
this is that an extension can be bound to another environment (for
|
||||||
|
overlays) by creating a copy and reassigning the `environment` attribute.
|
||||||
|
|
||||||
|
As extensions are created by the environment they cannot accept any
|
||||||
|
arguments for configuration. One may want to work around that by using
|
||||||
|
a factory function, but that is not possible as extensions are identified
|
||||||
|
by their import name. The correct way to configure the extension is
|
||||||
|
storing the configuration values on the environment. Because this way the
|
||||||
|
environment ends up acting as central configuration storage the
|
||||||
|
attributes may clash which is why extensions have to ensure that the names
|
||||||
|
they choose for configuration are not too generic. ``prefix`` for example
|
||||||
|
is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
|
||||||
|
name as includes the name of the extension (fragment cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
identifier: t.ClassVar[str]
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
cls.identifier = f"{cls.__module__}.{cls.__name__}"
|
||||||
|
|
||||||
|
#: if this extension parses this is the list of tags it's listening to.
|
||||||
|
tags: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: the priority of that extension. This is especially useful for
|
||||||
|
#: extensions that preprocess values. A lower value means higher
|
||||||
|
#: priority.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.4
|
||||||
|
priority = 100
|
||||||
|
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
def bind(self, environment: Environment) -> "Extension":
|
||||||
|
"""Create a copy of this extension bound to another environment."""
|
||||||
|
rv = t.cast(Extension, object.__new__(self.__class__))
|
||||||
|
rv.__dict__.update(self.__dict__)
|
||||||
|
rv.environment = environment
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def preprocess(
|
||||||
|
self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
"""This method is called before the actual lexing and can be used to
|
||||||
|
preprocess the source. The `filename` is optional. The return value
|
||||||
|
must be the preprocessed source.
|
||||||
|
"""
|
||||||
|
return source
|
||||||
|
|
||||||
|
def filter_stream(
|
||||||
|
self, stream: "TokenStream"
|
||||||
|
) -> t.Union["TokenStream", t.Iterable["Token"]]:
|
||||||
|
"""It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
|
||||||
|
to filter tokens returned. This method has to return an iterable of
|
||||||
|
:class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a
|
||||||
|
:class:`~jinja2.lexer.TokenStream`.
|
||||||
|
"""
|
||||||
|
return stream
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||||
|
"""If any of the :attr:`tags` matched this method is called with the
|
||||||
|
parser as first argument. The token the parser stream is pointing at
|
||||||
|
is the name token that matched. This method has to return one or a
|
||||||
|
list of multiple nodes.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def attr(
|
||||||
|
self, name: str, lineno: t.Optional[int] = None
|
||||||
|
) -> nodes.ExtensionAttribute:
|
||||||
|
"""Return an attribute node for the current extension. This is useful
|
||||||
|
to pass constants on extensions to generated template code.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
self.attr('_my_attribute', lineno=lineno)
|
||||||
|
"""
|
||||||
|
return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
|
||||||
|
|
||||||
|
def call_method(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
args: t.Optional[t.List[nodes.Expr]] = None,
|
||||||
|
kwargs: t.Optional[t.List[nodes.Keyword]] = None,
|
||||||
|
dyn_args: t.Optional[nodes.Expr] = None,
|
||||||
|
dyn_kwargs: t.Optional[nodes.Expr] = None,
|
||||||
|
lineno: t.Optional[int] = None,
|
||||||
|
) -> nodes.Call:
|
||||||
|
"""Call a method of the extension. This is a shortcut for
|
||||||
|
:meth:`attr` + :class:`jinja2.nodes.Call`.
|
||||||
|
"""
|
||||||
|
if args is None:
|
||||||
|
args = []
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = []
|
||||||
|
return nodes.Call(
|
||||||
|
self.attr(name, lineno=lineno),
|
||||||
|
args,
|
||||||
|
kwargs,
|
||||||
|
dyn_args,
|
||||||
|
dyn_kwargs,
|
||||||
|
lineno=lineno,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_context
|
||||||
|
def _gettext_alias(
|
||||||
|
__context: Context, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> t.Union[t.Any, Undefined]:
|
||||||
|
return __context.call(__context.resolve("gettext"), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
|
||||||
|
rv = __context.call(func, __string)
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
# Always treat as a format string, even if there are no
|
||||||
|
# variables. This makes translation strings more consistent
|
||||||
|
# and predictable. This requires escaping
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return gettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def ngettext(
|
||||||
|
__context: Context,
|
||||||
|
__singular: str,
|
||||||
|
__plural: str,
|
||||||
|
__num: int,
|
||||||
|
**variables: t.Any,
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("num", __num)
|
||||||
|
rv = __context.call(func, __singular, __plural, __num)
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return ngettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def pgettext(
|
||||||
|
__context: Context, __string_ctx: str, __string: str, **variables: t.Any
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("context", __string_ctx)
|
||||||
|
rv = __context.call(func, __string_ctx, __string)
|
||||||
|
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return pgettext
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_npgettext(
|
||||||
|
func: t.Callable[[str, str, str, int], str]
|
||||||
|
) -> t.Callable[..., str]:
|
||||||
|
@pass_context
|
||||||
|
def npgettext(
|
||||||
|
__context: Context,
|
||||||
|
__string_ctx: str,
|
||||||
|
__singular: str,
|
||||||
|
__plural: str,
|
||||||
|
__num: int,
|
||||||
|
**variables: t.Any,
|
||||||
|
) -> str:
|
||||||
|
variables.setdefault("context", __string_ctx)
|
||||||
|
variables.setdefault("num", __num)
|
||||||
|
rv = __context.call(func, __string_ctx, __singular, __plural, __num)
|
||||||
|
|
||||||
|
if __context.eval_ctx.autoescape:
|
||||||
|
rv = Markup(rv)
|
||||||
|
|
||||||
|
# Always treat as a format string, see gettext comment above.
|
||||||
|
return rv % variables # type: ignore
|
||||||
|
|
||||||
|
return npgettext
|
||||||
|
|
||||||
|
|
||||||
|
class InternationalizationExtension(Extension):
|
||||||
|
"""This extension adds gettext support to Jinja."""
|
||||||
|
|
||||||
|
tags = {"trans"}
|
||||||
|
|
||||||
|
# TODO: the i18n extension is currently reevaluating values in a few
|
||||||
|
# situations. Take this example:
|
||||||
|
# {% trans count=something() %}{{ count }} foo{% pluralize
|
||||||
|
# %}{{ count }} fooss{% endtrans %}
|
||||||
|
# something is called twice here. One time for the gettext value and
|
||||||
|
# the other time for the n-parameter of the ngettext function.
|
||||||
|
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
super().__init__(environment)
|
||||||
|
environment.globals["_"] = _gettext_alias
|
||||||
|
environment.extend(
|
||||||
|
install_gettext_translations=self._install,
|
||||||
|
install_null_translations=self._install_null,
|
||||||
|
install_gettext_callables=self._install_callables,
|
||||||
|
uninstall_gettext_translations=self._uninstall,
|
||||||
|
extract_translations=self._extract,
|
||||||
|
newstyle_gettext=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install(
|
||||||
|
self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
|
||||||
|
) -> None:
|
||||||
|
# ugettext and ungettext are preferred in case the I18N library
|
||||||
|
# is providing compatibility with older Python versions.
|
||||||
|
gettext = getattr(translations, "ugettext", None)
|
||||||
|
if gettext is None:
|
||||||
|
gettext = translations.gettext
|
||||||
|
ngettext = getattr(translations, "ungettext", None)
|
||||||
|
if ngettext is None:
|
||||||
|
ngettext = translations.ngettext
|
||||||
|
|
||||||
|
pgettext = getattr(translations, "pgettext", None)
|
||||||
|
npgettext = getattr(translations, "npgettext", None)
|
||||||
|
self._install_callables(
|
||||||
|
gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
|
||||||
|
import gettext
|
||||||
|
|
||||||
|
translations = gettext.NullTranslations()
|
||||||
|
|
||||||
|
if hasattr(translations, "pgettext"):
|
||||||
|
# Python < 3.8
|
||||||
|
pgettext = translations.pgettext # type: ignore
|
||||||
|
else:
|
||||||
|
|
||||||
|
def pgettext(c: str, s: str) -> str:
|
||||||
|
return s
|
||||||
|
|
||||||
|
if hasattr(translations, "npgettext"):
|
||||||
|
npgettext = translations.npgettext # type: ignore
|
||||||
|
else:
|
||||||
|
|
||||||
|
def npgettext(c: str, s: str, p: str, n: int) -> str:
|
||||||
|
return s if n == 1 else p
|
||||||
|
|
||||||
|
self._install_callables(
|
||||||
|
gettext=translations.gettext,
|
||||||
|
ngettext=translations.ngettext,
|
||||||
|
newstyle=newstyle,
|
||||||
|
pgettext=pgettext,
|
||||||
|
npgettext=npgettext,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install_callables(
|
||||||
|
self,
|
||||||
|
gettext: t.Callable[[str], str],
|
||||||
|
ngettext: t.Callable[[str, str, int], str],
|
||||||
|
newstyle: t.Optional[bool] = None,
|
||||||
|
pgettext: t.Optional[t.Callable[[str, str], str]] = None,
|
||||||
|
npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
|
||||||
|
) -> None:
|
||||||
|
if newstyle is not None:
|
||||||
|
self.environment.newstyle_gettext = newstyle # type: ignore
|
||||||
|
if self.environment.newstyle_gettext: # type: ignore
|
||||||
|
gettext = _make_new_gettext(gettext)
|
||||||
|
ngettext = _make_new_ngettext(ngettext)
|
||||||
|
|
||||||
|
if pgettext is not None:
|
||||||
|
pgettext = _make_new_pgettext(pgettext)
|
||||||
|
|
||||||
|
if npgettext is not None:
|
||||||
|
npgettext = _make_new_npgettext(npgettext)
|
||||||
|
|
||||||
|
self.environment.globals.update(
|
||||||
|
gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
|
||||||
|
)
|
||||||
|
|
||||||
|
def _uninstall(self, translations: "_SupportedTranslations") -> None:
|
||||||
|
for key in ("gettext", "ngettext", "pgettext", "npgettext"):
|
||||||
|
self.environment.globals.pop(key, None)
|
||||||
|
|
||||||
|
def _extract(
|
||||||
|
self,
|
||||||
|
source: t.Union[str, nodes.Template],
|
||||||
|
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||||
|
]:
|
||||||
|
if isinstance(source, str):
|
||||||
|
source = self.environment.parse(source)
|
||||||
|
return extract_from_ast(source, gettext_functions)
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
|
||||||
|
"""Parse a translatable tag."""
|
||||||
|
lineno = next(parser.stream).lineno
|
||||||
|
num_called_num = False
|
||||||
|
|
||||||
|
# find all the variables referenced. Additionally a variable can be
|
||||||
|
# defined in the body of the trans block too, but this is checked at
|
||||||
|
# a later state.
|
||||||
|
plural_expr: t.Optional[nodes.Expr] = None
|
||||||
|
plural_expr_assignment: t.Optional[nodes.Assign] = None
|
||||||
|
variables: t.Dict[str, nodes.Expr] = {}
|
||||||
|
trimmed = None
|
||||||
|
while parser.stream.current.type != "block_end":
|
||||||
|
if variables:
|
||||||
|
parser.stream.expect("comma")
|
||||||
|
|
||||||
|
# skip colon for python compatibility
|
||||||
|
if parser.stream.skip_if("colon"):
|
||||||
|
break
|
||||||
|
|
||||||
|
token = parser.stream.expect("name")
|
||||||
|
if token.value in variables:
|
||||||
|
parser.fail(
|
||||||
|
f"translatable variable {token.value!r} defined twice.",
|
||||||
|
token.lineno,
|
||||||
|
exc=TemplateAssertionError,
|
||||||
|
)
|
||||||
|
|
||||||
|
# expressions
|
||||||
|
if parser.stream.current.type == "assign":
|
||||||
|
next(parser.stream)
|
||||||
|
variables[token.value] = var = parser.parse_expression()
|
||||||
|
elif trimmed is None and token.value in ("trimmed", "notrimmed"):
|
||||||
|
trimmed = token.value == "trimmed"
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
variables[token.value] = var = nodes.Name(token.value, "load")
|
||||||
|
|
||||||
|
if plural_expr is None:
|
||||||
|
if isinstance(var, nodes.Call):
|
||||||
|
plural_expr = nodes.Name("_trans", "load")
|
||||||
|
variables[token.value] = plural_expr
|
||||||
|
plural_expr_assignment = nodes.Assign(
|
||||||
|
nodes.Name("_trans", "store"), var
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
plural_expr = var
|
||||||
|
num_called_num = token.value == "num"
|
||||||
|
|
||||||
|
parser.stream.expect("block_end")
|
||||||
|
|
||||||
|
plural = None
|
||||||
|
have_plural = False
|
||||||
|
referenced = set()
|
||||||
|
|
||||||
|
# now parse until endtrans or pluralize
|
||||||
|
singular_names, singular = self._parse_block(parser, True)
|
||||||
|
if singular_names:
|
||||||
|
referenced.update(singular_names)
|
||||||
|
if plural_expr is None:
|
||||||
|
plural_expr = nodes.Name(singular_names[0], "load")
|
||||||
|
num_called_num = singular_names[0] == "num"
|
||||||
|
|
||||||
|
# if we have a pluralize block, we parse that too
|
||||||
|
if parser.stream.current.test("name:pluralize"):
|
||||||
|
have_plural = True
|
||||||
|
next(parser.stream)
|
||||||
|
if parser.stream.current.type != "block_end":
|
||||||
|
token = parser.stream.expect("name")
|
||||||
|
if token.value not in variables:
|
||||||
|
parser.fail(
|
||||||
|
f"unknown variable {token.value!r} for pluralization",
|
||||||
|
token.lineno,
|
||||||
|
exc=TemplateAssertionError,
|
||||||
|
)
|
||||||
|
plural_expr = variables[token.value]
|
||||||
|
num_called_num = token.value == "num"
|
||||||
|
parser.stream.expect("block_end")
|
||||||
|
plural_names, plural = self._parse_block(parser, False)
|
||||||
|
next(parser.stream)
|
||||||
|
referenced.update(plural_names)
|
||||||
|
else:
|
||||||
|
next(parser.stream)
|
||||||
|
|
||||||
|
# register free names as simple name expressions
|
||||||
|
for name in referenced:
|
||||||
|
if name not in variables:
|
||||||
|
variables[name] = nodes.Name(name, "load")
|
||||||
|
|
||||||
|
if not have_plural:
|
||||||
|
plural_expr = None
|
||||||
|
elif plural_expr is None:
|
||||||
|
parser.fail("pluralize without variables", lineno)
|
||||||
|
|
||||||
|
if trimmed is None:
|
||||||
|
trimmed = self.environment.policies["ext.i18n.trimmed"]
|
||||||
|
if trimmed:
|
||||||
|
singular = self._trim_whitespace(singular)
|
||||||
|
if plural:
|
||||||
|
plural = self._trim_whitespace(plural)
|
||||||
|
|
||||||
|
node = self._make_node(
|
||||||
|
singular,
|
||||||
|
plural,
|
||||||
|
variables,
|
||||||
|
plural_expr,
|
||||||
|
bool(referenced),
|
||||||
|
num_called_num and have_plural,
|
||||||
|
)
|
||||||
|
node.set_lineno(lineno)
|
||||||
|
if plural_expr_assignment is not None:
|
||||||
|
return [plural_expr_assignment, node]
|
||||||
|
else:
|
||||||
|
return node
|
||||||
|
|
||||||
|
def _trim_whitespace(self, string: str, _ws_re: t.Pattern[str] = _ws_re) -> str:
|
||||||
|
return _ws_re.sub(" ", string.strip())
|
||||||
|
|
||||||
|
def _parse_block(
|
||||||
|
self, parser: "Parser", allow_pluralize: bool
|
||||||
|
) -> t.Tuple[t.List[str], str]:
|
||||||
|
"""Parse until the next block tag with a given name."""
|
||||||
|
referenced = []
|
||||||
|
buf = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if parser.stream.current.type == "data":
|
||||||
|
buf.append(parser.stream.current.value.replace("%", "%%"))
|
||||||
|
next(parser.stream)
|
||||||
|
elif parser.stream.current.type == "variable_begin":
|
||||||
|
next(parser.stream)
|
||||||
|
name = parser.stream.expect("name").value
|
||||||
|
referenced.append(name)
|
||||||
|
buf.append(f"%({name})s")
|
||||||
|
parser.stream.expect("variable_end")
|
||||||
|
elif parser.stream.current.type == "block_begin":
|
||||||
|
next(parser.stream)
|
||||||
|
if parser.stream.current.test("name:endtrans"):
|
||||||
|
break
|
||||||
|
elif parser.stream.current.test("name:pluralize"):
|
||||||
|
if allow_pluralize:
|
||||||
|
break
|
||||||
|
parser.fail(
|
||||||
|
"a translatable section can have only one pluralize section"
|
||||||
|
)
|
||||||
|
parser.fail(
|
||||||
|
"control structures in translatable sections are not allowed"
|
||||||
|
)
|
||||||
|
elif parser.stream.eos:
|
||||||
|
parser.fail("unclosed translation block")
|
||||||
|
else:
|
||||||
|
raise RuntimeError("internal parser error")
|
||||||
|
|
||||||
|
return referenced, concat(buf)
|
||||||
|
|
||||||
|
def _make_node(
|
||||||
|
self,
|
||||||
|
singular: str,
|
||||||
|
plural: t.Optional[str],
|
||||||
|
variables: t.Dict[str, nodes.Expr],
|
||||||
|
plural_expr: t.Optional[nodes.Expr],
|
||||||
|
vars_referenced: bool,
|
||||||
|
num_called_num: bool,
|
||||||
|
) -> nodes.Output:
|
||||||
|
"""Generates a useful node from the data provided."""
|
||||||
|
newstyle = self.environment.newstyle_gettext # type: ignore
|
||||||
|
node: nodes.Expr
|
||||||
|
|
||||||
|
# no variables referenced? no need to escape for old style
|
||||||
|
# gettext invocations only if there are vars.
|
||||||
|
if not vars_referenced and not newstyle:
|
||||||
|
singular = singular.replace("%%", "%")
|
||||||
|
if plural:
|
||||||
|
plural = plural.replace("%%", "%")
|
||||||
|
|
||||||
|
# singular only:
|
||||||
|
if plural_expr is None:
|
||||||
|
gettext = nodes.Name("gettext", "load")
|
||||||
|
node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None)
|
||||||
|
|
||||||
|
# singular and plural
|
||||||
|
else:
|
||||||
|
ngettext = nodes.Name("ngettext", "load")
|
||||||
|
node = nodes.Call(
|
||||||
|
ngettext,
|
||||||
|
[nodes.Const(singular), nodes.Const(plural), plural_expr],
|
||||||
|
[],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# in case newstyle gettext is used, the method is powerful
|
||||||
|
# enough to handle the variable expansion and autoescape
|
||||||
|
# handling itself
|
||||||
|
if newstyle:
|
||||||
|
for key, value in variables.items():
|
||||||
|
# the function adds that later anyways in case num was
|
||||||
|
# called num, so just skip it.
|
||||||
|
if num_called_num and key == "num":
|
||||||
|
continue
|
||||||
|
node.kwargs.append(nodes.Keyword(key, value))
|
||||||
|
|
||||||
|
# otherwise do that here
|
||||||
|
else:
|
||||||
|
# mark the return value as safe if we are in an
|
||||||
|
# environment with autoescaping turned on
|
||||||
|
node = nodes.MarkSafeIfAutoescape(node)
|
||||||
|
if variables:
|
||||||
|
node = nodes.Mod(
|
||||||
|
node,
|
||||||
|
nodes.Dict(
|
||||||
|
[
|
||||||
|
nodes.Pair(nodes.Const(key), value)
|
||||||
|
for key, value in variables.items()
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nodes.Output([node])
|
||||||
|
|
||||||
|
|
||||||
|
class ExprStmtExtension(Extension):
|
||||||
|
"""Adds a `do` tag to Jinja that works like the print statement just
|
||||||
|
that it doesn't print the return value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = {"do"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> nodes.ExprStmt:
|
||||||
|
node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
|
||||||
|
node.node = parser.parse_tuple()
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
class LoopControlExtension(Extension):
|
||||||
|
"""Adds break and continue to the template engine."""
|
||||||
|
|
||||||
|
tags = {"break", "continue"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
|
||||||
|
token = next(parser.stream)
|
||||||
|
if token.value == "break":
|
||||||
|
return nodes.Break(lineno=token.lineno)
|
||||||
|
return nodes.Continue(lineno=token.lineno)
|
||||||
|
|
||||||
|
|
||||||
|
class WithExtension(Extension):
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
super().__init__(environment)
|
||||||
|
warnings.warn(
|
||||||
|
"The 'with' extension is deprecated and will be removed in"
|
||||||
|
" Jinja 3.1. This is built in now.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoEscapeExtension(Extension):
|
||||||
|
def __init__(self, environment: Environment) -> None:
|
||||||
|
super().__init__(environment)
|
||||||
|
warnings.warn(
|
||||||
|
"The 'autoescape' extension is deprecated and will be"
|
||||||
|
" removed in Jinja 3.1. This is built in now.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DebugExtension(Extension):
|
||||||
|
"""A ``{% debug %}`` tag that dumps the available variables,
|
||||||
|
filters, and tests.
|
||||||
|
|
||||||
|
.. code-block:: html+jinja
|
||||||
|
|
||||||
|
<pre>{% debug %}</pre>
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
{'context': {'cycler': <class 'jinja2.utils.Cycler'>,
|
||||||
|
...,
|
||||||
|
'namespace': <class 'jinja2.utils.Namespace'>},
|
||||||
|
'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd',
|
||||||
|
..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'],
|
||||||
|
'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined',
|
||||||
|
..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']}
|
||||||
|
|
||||||
|
.. versionadded:: 2.11.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
tags = {"debug"}
|
||||||
|
|
||||||
|
def parse(self, parser: "Parser") -> nodes.Output:
|
||||||
|
lineno = parser.stream.expect("name:debug").lineno
|
||||||
|
context = nodes.ContextReference()
|
||||||
|
result = self.call_method("_render", [context], lineno=lineno)
|
||||||
|
return nodes.Output([result], lineno=lineno)
|
||||||
|
|
||||||
|
def _render(self, context: Context) -> str:
|
||||||
|
result = {
|
||||||
|
"context": context.get_all(),
|
||||||
|
"filters": sorted(self.environment.filters.keys()),
|
||||||
|
"tests": sorted(self.environment.tests.keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the depth since the intent is to show the top few names.
|
||||||
|
return pprint.pformat(result, depth=3, compact=True)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_from_ast(
|
||||||
|
ast: nodes.Template,
|
||||||
|
gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
|
||||||
|
babel_style: bool = True,
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
|
||||||
|
]:
|
||||||
|
"""Extract localizable strings from the given template node. Per
|
||||||
|
default this function returns matches in babel style that means non string
|
||||||
|
parameters as well as keyword arguments are returned as `None`. This
|
||||||
|
allows Babel to figure out what you really meant if you are using
|
||||||
|
gettext functions that allow keyword arguments for placeholder expansion.
|
||||||
|
If you don't want that behavior set the `babel_style` parameter to `False`
|
||||||
|
which causes only strings to be returned and parameters are always stored
|
||||||
|
in tuples. As a consequence invalid gettext calls (calls without a single
|
||||||
|
string parameter or string parameters after non-string parameters) are
|
||||||
|
skipped.
|
||||||
|
|
||||||
|
This example explains the behavior:
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
|
||||||
|
>>> list(extract_from_ast(node))
|
||||||
|
[(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
|
||||||
|
>>> list(extract_from_ast(node, babel_style=False))
|
||||||
|
[(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
|
||||||
|
|
||||||
|
For every string found this function yields a ``(lineno, function,
|
||||||
|
message)`` tuple, where:
|
||||||
|
|
||||||
|
* ``lineno`` is the number of the line on which the string was found,
|
||||||
|
* ``function`` is the name of the ``gettext`` function used (if the
|
||||||
|
string was extracted from embedded Python code), and
|
||||||
|
* ``message`` is the string, or a tuple of strings for functions
|
||||||
|
with multiple string arguments.
|
||||||
|
|
||||||
|
This extraction function operates on the AST and is because of that unable
|
||||||
|
to extract any comments. For comment support you have to use the babel
|
||||||
|
extraction interface or extract comments yourself.
|
||||||
|
"""
|
||||||
|
out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
|
||||||
|
|
||||||
|
for node in ast.find_all(nodes.Call):
|
||||||
|
if (
|
||||||
|
not isinstance(node.node, nodes.Name)
|
||||||
|
or node.node.name not in gettext_functions
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
strings: t.List[t.Optional[str]] = []
|
||||||
|
|
||||||
|
for arg in node.args:
|
||||||
|
if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
|
||||||
|
strings.append(arg.value)
|
||||||
|
else:
|
||||||
|
strings.append(None)
|
||||||
|
|
||||||
|
for _ in node.kwargs:
|
||||||
|
strings.append(None)
|
||||||
|
if node.dyn_args is not None:
|
||||||
|
strings.append(None)
|
||||||
|
if node.dyn_kwargs is not None:
|
||||||
|
strings.append(None)
|
||||||
|
|
||||||
|
if not babel_style:
|
||||||
|
out = tuple(x for x in strings if x is not None)
|
||||||
|
|
||||||
|
if not out:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(strings) == 1:
|
||||||
|
out = strings[0]
|
||||||
|
else:
|
||||||
|
out = tuple(strings)
|
||||||
|
|
||||||
|
yield node.lineno, node.node.name, out
|
||||||
|
|
||||||
|
|
||||||
|
class _CommentFinder:
|
||||||
|
"""Helper class to find comments in a token stream. Can only
|
||||||
|
find comments for gettext calls forwards. Once the comment
|
||||||
|
from line 4 is found, a comment for line 1 will not return a
|
||||||
|
usable value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
|
||||||
|
) -> None:
|
||||||
|
self.tokens = tokens
|
||||||
|
self.comment_tags = comment_tags
|
||||||
|
self.offset = 0
|
||||||
|
self.last_lineno = 0
|
||||||
|
|
||||||
|
def find_backwards(self, offset: int) -> t.List[str]:
|
||||||
|
try:
|
||||||
|
for _, token_type, token_value in reversed(
|
||||||
|
self.tokens[self.offset : offset]
|
||||||
|
):
|
||||||
|
if token_type in ("comment", "linecomment"):
|
||||||
|
try:
|
||||||
|
prefix, comment = token_value.split(None, 1)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if prefix in self.comment_tags:
|
||||||
|
return [comment.rstrip()]
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
self.offset = offset
|
||||||
|
|
||||||
|
def find_comments(self, lineno: int) -> t.List[str]:
|
||||||
|
if not self.comment_tags or self.last_lineno > lineno:
|
||||||
|
return []
|
||||||
|
for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
|
||||||
|
if token_lineno > lineno:
|
||||||
|
return self.find_backwards(self.offset + idx)
|
||||||
|
return self.find_backwards(len(self.tokens))
|
||||||
|
|
||||||
|
|
||||||
|
def babel_extract(
|
||||||
|
fileobj: t.BinaryIO,
|
||||||
|
keywords: t.Sequence[str],
|
||||||
|
comment_tags: t.Sequence[str],
|
||||||
|
options: t.Dict[str, t.Any],
|
||||||
|
) -> t.Iterator[
|
||||||
|
t.Tuple[
|
||||||
|
int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
|
||||||
|
]
|
||||||
|
]:
|
||||||
|
"""Babel extraction method for Jinja templates.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.3
|
||||||
|
Basic support for translation comments was added. If `comment_tags`
|
||||||
|
is now set to a list of keywords for extraction, the extractor will
|
||||||
|
try to find the best preceding comment that begins with one of the
|
||||||
|
keywords. For best results, make sure to not have more than one
|
||||||
|
gettext call in one line of code and the matching comment in the
|
||||||
|
same line or the line before.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.5.1
|
||||||
|
The `newstyle_gettext` flag can be set to `True` to enable newstyle
|
||||||
|
gettext calls.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.7
|
||||||
|
A `silent` option can now be provided. If set to `False` template
|
||||||
|
syntax errors are propagated instead of being ignored.
|
||||||
|
|
||||||
|
:param fileobj: the file-like object the messages should be extracted from
|
||||||
|
:param keywords: a list of keywords (i.e. function names) that should be
|
||||||
|
recognized as translation functions
|
||||||
|
:param comment_tags: a list of translator tags to search for and include
|
||||||
|
in the results.
|
||||||
|
:param options: a dictionary of additional options (optional)
|
||||||
|
:return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
|
||||||
|
(comments will be empty currently)
|
||||||
|
"""
|
||||||
|
extensions: t.Dict[t.Type[Extension], None] = {}
|
||||||
|
|
||||||
|
for extension_name in options.get("extensions", "").split(","):
|
||||||
|
extension_name = extension_name.strip()
|
||||||
|
|
||||||
|
if not extension_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
extensions[import_string(extension_name)] = None
|
||||||
|
|
||||||
|
if InternationalizationExtension not in extensions:
|
||||||
|
extensions[InternationalizationExtension] = None
|
||||||
|
|
||||||
|
def getbool(options: t.Mapping[str, str], key: str, default: bool = False) -> bool:
|
||||||
|
return options.get(key, str(default)).lower() in {"1", "on", "yes", "true"}
|
||||||
|
|
||||||
|
silent = getbool(options, "silent", True)
|
||||||
|
environment = Environment(
|
||||||
|
options.get("block_start_string", defaults.BLOCK_START_STRING),
|
||||||
|
options.get("block_end_string", defaults.BLOCK_END_STRING),
|
||||||
|
options.get("variable_start_string", defaults.VARIABLE_START_STRING),
|
||||||
|
options.get("variable_end_string", defaults.VARIABLE_END_STRING),
|
||||||
|
options.get("comment_start_string", defaults.COMMENT_START_STRING),
|
||||||
|
options.get("comment_end_string", defaults.COMMENT_END_STRING),
|
||||||
|
options.get("line_statement_prefix") or defaults.LINE_STATEMENT_PREFIX,
|
||||||
|
options.get("line_comment_prefix") or defaults.LINE_COMMENT_PREFIX,
|
||||||
|
getbool(options, "trim_blocks", defaults.TRIM_BLOCKS),
|
||||||
|
getbool(options, "lstrip_blocks", defaults.LSTRIP_BLOCKS),
|
||||||
|
defaults.NEWLINE_SEQUENCE,
|
||||||
|
getbool(options, "keep_trailing_newline", defaults.KEEP_TRAILING_NEWLINE),
|
||||||
|
tuple(extensions),
|
||||||
|
cache_size=0,
|
||||||
|
auto_reload=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if getbool(options, "trimmed"):
|
||||||
|
environment.policies["ext.i18n.trimmed"] = True
|
||||||
|
if getbool(options, "newstyle_gettext"):
|
||||||
|
environment.newstyle_gettext = True # type: ignore
|
||||||
|
|
||||||
|
source = fileobj.read().decode(options.get("encoding", "utf-8"))
|
||||||
|
try:
|
||||||
|
node = environment.parse(source)
|
||||||
|
tokens = list(environment.lex(environment.preprocess(source)))
|
||||||
|
except TemplateSyntaxError:
|
||||||
|
if not silent:
|
||||||
|
raise
|
||||||
|
# skip templates with syntax errors
|
||||||
|
return
|
||||||
|
|
||||||
|
finder = _CommentFinder(tokens, comment_tags)
|
||||||
|
for lineno, func, message in extract_from_ast(node, keywords):
|
||||||
|
yield lineno, func, message, finder.find_comments(lineno)
|
||||||
|
|
||||||
|
|
||||||
|
#: nicer import names
|
||||||
|
i18n = InternationalizationExtension
|
||||||
|
do = ExprStmtExtension
|
||||||
|
loopcontrols = LoopControlExtension
|
||||||
|
with_ = WithExtension
|
||||||
|
autoescape = AutoEscapeExtension
|
||||||
|
debug = DebugExtension
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,318 @@
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .visitor import NodeVisitor
|
||||||
|
|
||||||
|
VAR_LOAD_PARAMETER = "param"
|
||||||
|
VAR_LOAD_RESOLVE = "resolve"
|
||||||
|
VAR_LOAD_ALIAS = "alias"
|
||||||
|
VAR_LOAD_UNDEFINED = "undefined"
|
||||||
|
|
||||||
|
|
||||||
|
def find_symbols(
|
||||||
|
nodes: t.Iterable[nodes.Node], parent_symbols: t.Optional["Symbols"] = None
|
||||||
|
) -> "Symbols":
|
||||||
|
sym = Symbols(parent=parent_symbols)
|
||||||
|
visitor = FrameSymbolVisitor(sym)
|
||||||
|
for node in nodes:
|
||||||
|
visitor.visit(node)
|
||||||
|
return sym
|
||||||
|
|
||||||
|
|
||||||
|
def symbols_for_node(
|
||||||
|
node: nodes.Node, parent_symbols: t.Optional["Symbols"] = None
|
||||||
|
) -> "Symbols":
|
||||||
|
sym = Symbols(parent=parent_symbols)
|
||||||
|
sym.analyze_node(node)
|
||||||
|
return sym
|
||||||
|
|
||||||
|
|
||||||
|
class Symbols:
|
||||||
|
def __init__(
|
||||||
|
self, parent: t.Optional["Symbols"] = None, level: t.Optional[int] = None
|
||||||
|
) -> None:
|
||||||
|
if level is None:
|
||||||
|
if parent is None:
|
||||||
|
level = 0
|
||||||
|
else:
|
||||||
|
level = parent.level + 1
|
||||||
|
|
||||||
|
self.level: int = level
|
||||||
|
self.parent = parent
|
||||||
|
self.refs: t.Dict[str, str] = {}
|
||||||
|
self.loads: t.Dict[str, t.Any] = {}
|
||||||
|
self.stores: t.Set[str] = set()
|
||||||
|
|
||||||
|
def analyze_node(self, node: nodes.Node, **kwargs: t.Any) -> None:
|
||||||
|
visitor = RootVisitor(self)
|
||||||
|
visitor.visit(node, **kwargs)
|
||||||
|
|
||||||
|
def _define_ref(
|
||||||
|
self, name: str, load: t.Optional[t.Tuple[str, t.Optional[str]]] = None
|
||||||
|
) -> str:
|
||||||
|
ident = f"l_{self.level}_{name}"
|
||||||
|
self.refs[name] = ident
|
||||||
|
if load is not None:
|
||||||
|
self.loads[ident] = load
|
||||||
|
return ident
|
||||||
|
|
||||||
|
def find_load(self, target: str) -> t.Optional[t.Any]:
|
||||||
|
if target in self.loads:
|
||||||
|
return self.loads[target]
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
return self.parent.find_load(target)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_ref(self, name: str) -> t.Optional[str]:
|
||||||
|
if name in self.refs:
|
||||||
|
return self.refs[name]
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
return self.parent.find_ref(name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ref(self, name: str) -> str:
|
||||||
|
rv = self.find_ref(name)
|
||||||
|
if rv is None:
|
||||||
|
raise AssertionError(
|
||||||
|
"Tried to resolve a name to a reference that was"
|
||||||
|
f" unknown to the frame ({name!r})"
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def copy(self) -> "Symbols":
|
||||||
|
rv = t.cast(Symbols, object.__new__(self.__class__))
|
||||||
|
rv.__dict__.update(self.__dict__)
|
||||||
|
rv.refs = self.refs.copy()
|
||||||
|
rv.loads = self.loads.copy()
|
||||||
|
rv.stores = self.stores.copy()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def store(self, name: str) -> None:
|
||||||
|
self.stores.add(name)
|
||||||
|
|
||||||
|
# If we have not see the name referenced yet, we need to figure
|
||||||
|
# out what to set it to.
|
||||||
|
if name not in self.refs:
|
||||||
|
# If there is a parent scope we check if the name has a
|
||||||
|
# reference there. If it does it means we might have to alias
|
||||||
|
# to a variable there.
|
||||||
|
if self.parent is not None:
|
||||||
|
outer_ref = self.parent.find_ref(name)
|
||||||
|
if outer_ref is not None:
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_ALIAS, outer_ref))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise we can just set it to undefined.
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_UNDEFINED, None))
|
||||||
|
|
||||||
|
def declare_parameter(self, name: str) -> str:
|
||||||
|
self.stores.add(name)
|
||||||
|
return self._define_ref(name, load=(VAR_LOAD_PARAMETER, None))
|
||||||
|
|
||||||
|
def load(self, name: str) -> None:
|
||||||
|
if self.find_ref(name) is None:
|
||||||
|
self._define_ref(name, load=(VAR_LOAD_RESOLVE, name))
|
||||||
|
|
||||||
|
def branch_update(self, branch_symbols: t.Sequence["Symbols"]) -> None:
|
||||||
|
stores: t.Dict[str, int] = {}
|
||||||
|
for branch in branch_symbols:
|
||||||
|
for target in branch.stores:
|
||||||
|
if target in self.stores:
|
||||||
|
continue
|
||||||
|
stores[target] = stores.get(target, 0) + 1
|
||||||
|
|
||||||
|
for sym in branch_symbols:
|
||||||
|
self.refs.update(sym.refs)
|
||||||
|
self.loads.update(sym.loads)
|
||||||
|
self.stores.update(sym.stores)
|
||||||
|
|
||||||
|
for name, branch_count in stores.items():
|
||||||
|
if branch_count == len(branch_symbols):
|
||||||
|
continue
|
||||||
|
|
||||||
|
target = self.find_ref(name) # type: ignore
|
||||||
|
assert target is not None, "should not happen"
|
||||||
|
|
||||||
|
if self.parent is not None:
|
||||||
|
outer_target = self.parent.find_ref(name)
|
||||||
|
if outer_target is not None:
|
||||||
|
self.loads[target] = (VAR_LOAD_ALIAS, outer_target)
|
||||||
|
continue
|
||||||
|
self.loads[target] = (VAR_LOAD_RESOLVE, name)
|
||||||
|
|
||||||
|
def dump_stores(self) -> t.Dict[str, str]:
|
||||||
|
rv: t.Dict[str, str] = {}
|
||||||
|
node: t.Optional["Symbols"] = self
|
||||||
|
|
||||||
|
while node is not None:
|
||||||
|
for name in sorted(node.stores):
|
||||||
|
if name not in rv:
|
||||||
|
rv[name] = self.find_ref(name) # type: ignore
|
||||||
|
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def dump_param_targets(self) -> t.Set[str]:
|
||||||
|
rv = set()
|
||||||
|
node: t.Optional["Symbols"] = self
|
||||||
|
|
||||||
|
while node is not None:
|
||||||
|
for target, (instr, _) in self.loads.items():
|
||||||
|
if instr == VAR_LOAD_PARAMETER:
|
||||||
|
rv.add(target)
|
||||||
|
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class RootVisitor(NodeVisitor):
|
||||||
|
def __init__(self, symbols: "Symbols") -> None:
|
||||||
|
self.sym_visitor = FrameSymbolVisitor(symbols)
|
||||||
|
|
||||||
|
def _simple_visit(self, node: nodes.Node, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.iter_child_nodes():
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
visit_Template = _simple_visit
|
||||||
|
visit_Block = _simple_visit
|
||||||
|
visit_Macro = _simple_visit
|
||||||
|
visit_FilterBlock = _simple_visit
|
||||||
|
visit_Scope = _simple_visit
|
||||||
|
visit_If = _simple_visit
|
||||||
|
visit_ScopedEvalContextModifier = _simple_visit
|
||||||
|
|
||||||
|
def visit_AssignBlock(self, node: nodes.AssignBlock, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_CallBlock(self, node: nodes.CallBlock, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.iter_child_nodes(exclude=("call",)):
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_OverlayScope(self, node: nodes.OverlayScope, **kwargs: t.Any) -> None:
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def visit_For(
|
||||||
|
self, node: nodes.For, for_branch: str = "body", **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
if for_branch == "body":
|
||||||
|
self.sym_visitor.visit(node.target, store_as_param=True)
|
||||||
|
branch = node.body
|
||||||
|
elif for_branch == "else":
|
||||||
|
branch = node.else_
|
||||||
|
elif for_branch == "test":
|
||||||
|
self.sym_visitor.visit(node.target, store_as_param=True)
|
||||||
|
if node.test is not None:
|
||||||
|
self.sym_visitor.visit(node.test)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unknown for branch")
|
||||||
|
|
||||||
|
if branch:
|
||||||
|
for item in branch:
|
||||||
|
self.sym_visitor.visit(item)
|
||||||
|
|
||||||
|
def visit_With(self, node: nodes.With, **kwargs: t.Any) -> None:
|
||||||
|
for target in node.targets:
|
||||||
|
self.sym_visitor.visit(target)
|
||||||
|
for child in node.body:
|
||||||
|
self.sym_visitor.visit(child)
|
||||||
|
|
||||||
|
def generic_visit(self, node: nodes.Node, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
|
raise NotImplementedError(f"Cannot find symbols for {type(node).__name__!r}")
|
||||||
|
|
||||||
|
|
||||||
|
class FrameSymbolVisitor(NodeVisitor):
|
||||||
|
"""A visitor for `Frame.inspect`."""
|
||||||
|
|
||||||
|
def __init__(self, symbols: "Symbols") -> None:
|
||||||
|
self.symbols = symbols
|
||||||
|
|
||||||
|
def visit_Name(
|
||||||
|
self, node: nodes.Name, store_as_param: bool = False, **kwargs: t.Any
|
||||||
|
) -> None:
|
||||||
|
"""All assignments to names go through this function."""
|
||||||
|
if store_as_param or node.ctx == "param":
|
||||||
|
self.symbols.declare_parameter(node.name)
|
||||||
|
elif node.ctx == "store":
|
||||||
|
self.symbols.store(node.name)
|
||||||
|
elif node.ctx == "load":
|
||||||
|
self.symbols.load(node.name)
|
||||||
|
|
||||||
|
def visit_NSRef(self, node: nodes.NSRef, **kwargs: t.Any) -> None:
|
||||||
|
self.symbols.load(node.name)
|
||||||
|
|
||||||
|
def visit_If(self, node: nodes.If, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.test, **kwargs)
|
||||||
|
original_symbols = self.symbols
|
||||||
|
|
||||||
|
def inner_visit(nodes: t.Iterable[nodes.Node]) -> "Symbols":
|
||||||
|
self.symbols = rv = original_symbols.copy()
|
||||||
|
|
||||||
|
for subnode in nodes:
|
||||||
|
self.visit(subnode, **kwargs)
|
||||||
|
|
||||||
|
self.symbols = original_symbols
|
||||||
|
return rv
|
||||||
|
|
||||||
|
body_symbols = inner_visit(node.body)
|
||||||
|
elif_symbols = inner_visit(node.elif_)
|
||||||
|
else_symbols = inner_visit(node.else_ or ())
|
||||||
|
self.symbols.branch_update([body_symbols, elif_symbols, else_symbols])
|
||||||
|
|
||||||
|
def visit_Macro(self, node: nodes.Macro, **kwargs: t.Any) -> None:
|
||||||
|
self.symbols.store(node.name)
|
||||||
|
|
||||||
|
def visit_Import(self, node: nodes.Import, **kwargs: t.Any) -> None:
|
||||||
|
self.generic_visit(node, **kwargs)
|
||||||
|
self.symbols.store(node.target)
|
||||||
|
|
||||||
|
def visit_FromImport(self, node: nodes.FromImport, **kwargs: t.Any) -> None:
|
||||||
|
self.generic_visit(node, **kwargs)
|
||||||
|
|
||||||
|
for name in node.names:
|
||||||
|
if isinstance(name, tuple):
|
||||||
|
self.symbols.store(name[1])
|
||||||
|
else:
|
||||||
|
self.symbols.store(name)
|
||||||
|
|
||||||
|
def visit_Assign(self, node: nodes.Assign, **kwargs: t.Any) -> None:
|
||||||
|
"""Visit assignments in the correct order."""
|
||||||
|
self.visit(node.node, **kwargs)
|
||||||
|
self.visit(node.target, **kwargs)
|
||||||
|
|
||||||
|
def visit_For(self, node: nodes.For, **kwargs: t.Any) -> None:
|
||||||
|
"""Visiting stops at for blocks. However the block sequence
|
||||||
|
is visited as part of the outer scope.
|
||||||
|
"""
|
||||||
|
self.visit(node.iter, **kwargs)
|
||||||
|
|
||||||
|
def visit_CallBlock(self, node: nodes.CallBlock, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.call, **kwargs)
|
||||||
|
|
||||||
|
def visit_FilterBlock(self, node: nodes.FilterBlock, **kwargs: t.Any) -> None:
|
||||||
|
self.visit(node.filter, **kwargs)
|
||||||
|
|
||||||
|
def visit_With(self, node: nodes.With, **kwargs: t.Any) -> None:
|
||||||
|
for target in node.values:
|
||||||
|
self.visit(target)
|
||||||
|
|
||||||
|
def visit_AssignBlock(self, node: nodes.AssignBlock, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at block assigns."""
|
||||||
|
self.visit(node.target, **kwargs)
|
||||||
|
|
||||||
|
def visit_Scope(self, node: nodes.Scope, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at scopes."""
|
||||||
|
|
||||||
|
def visit_Block(self, node: nodes.Block, **kwargs: t.Any) -> None:
|
||||||
|
"""Stop visiting at blocks."""
|
||||||
|
|
||||||
|
def visit_OverlayScope(self, node: nodes.OverlayScope, **kwargs: t.Any) -> None:
|
||||||
|
"""Do not visit into overlay scopes."""
|
|
@ -0,0 +1,869 @@
|
||||||
|
"""Implements a Jinja / Python combination lexer. The ``Lexer`` class
|
||||||
|
is used to do some preprocessing. It filters out invalid operators like
|
||||||
|
the bitshift operators we don't allow in templates. It separates
|
||||||
|
template code and python code in expressions.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
from ast import literal_eval
|
||||||
|
from collections import deque
|
||||||
|
from sys import intern
|
||||||
|
|
||||||
|
from ._identifier import pattern as name_re
|
||||||
|
from .exceptions import TemplateSyntaxError
|
||||||
|
from .utils import LRUCache
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
# cache for the lexers. Exists in order to be able to have multiple
|
||||||
|
# environments with the same lexer
|
||||||
|
_lexer_cache: t.MutableMapping[t.Tuple, "Lexer"] = LRUCache(50) # type: ignore
|
||||||
|
|
||||||
|
# static regular expressions
|
||||||
|
whitespace_re = re.compile(r"\s+")
|
||||||
|
newline_re = re.compile(r"(\r\n|\r|\n)")
|
||||||
|
string_re = re.compile(
|
||||||
|
r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S
|
||||||
|
)
|
||||||
|
integer_re = re.compile(
|
||||||
|
r"""
|
||||||
|
(
|
||||||
|
0b(_?[0-1])+ # binary
|
||||||
|
|
|
||||||
|
0o(_?[0-7])+ # octal
|
||||||
|
|
|
||||||
|
0x(_?[\da-f])+ # hex
|
||||||
|
|
|
||||||
|
[1-9](_?\d)* # decimal
|
||||||
|
|
|
||||||
|
0(_?0)* # decimal zero
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
float_re = re.compile(
|
||||||
|
r"""
|
||||||
|
(?<!\.) # doesn't start with a .
|
||||||
|
(\d+_)*\d+ # digits, possibly _ separated
|
||||||
|
(
|
||||||
|
(\.(\d+_)*\d+)? # optional fractional part
|
||||||
|
e[+\-]?(\d+_)*\d+ # exponent part
|
||||||
|
|
|
||||||
|
\.(\d+_)*\d+ # required fractional part
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# internal the tokens and keep references to them
|
||||||
|
TOKEN_ADD = intern("add")
|
||||||
|
TOKEN_ASSIGN = intern("assign")
|
||||||
|
TOKEN_COLON = intern("colon")
|
||||||
|
TOKEN_COMMA = intern("comma")
|
||||||
|
TOKEN_DIV = intern("div")
|
||||||
|
TOKEN_DOT = intern("dot")
|
||||||
|
TOKEN_EQ = intern("eq")
|
||||||
|
TOKEN_FLOORDIV = intern("floordiv")
|
||||||
|
TOKEN_GT = intern("gt")
|
||||||
|
TOKEN_GTEQ = intern("gteq")
|
||||||
|
TOKEN_LBRACE = intern("lbrace")
|
||||||
|
TOKEN_LBRACKET = intern("lbracket")
|
||||||
|
TOKEN_LPAREN = intern("lparen")
|
||||||
|
TOKEN_LT = intern("lt")
|
||||||
|
TOKEN_LTEQ = intern("lteq")
|
||||||
|
TOKEN_MOD = intern("mod")
|
||||||
|
TOKEN_MUL = intern("mul")
|
||||||
|
TOKEN_NE = intern("ne")
|
||||||
|
TOKEN_PIPE = intern("pipe")
|
||||||
|
TOKEN_POW = intern("pow")
|
||||||
|
TOKEN_RBRACE = intern("rbrace")
|
||||||
|
TOKEN_RBRACKET = intern("rbracket")
|
||||||
|
TOKEN_RPAREN = intern("rparen")
|
||||||
|
TOKEN_SEMICOLON = intern("semicolon")
|
||||||
|
TOKEN_SUB = intern("sub")
|
||||||
|
TOKEN_TILDE = intern("tilde")
|
||||||
|
TOKEN_WHITESPACE = intern("whitespace")
|
||||||
|
TOKEN_FLOAT = intern("float")
|
||||||
|
TOKEN_INTEGER = intern("integer")
|
||||||
|
TOKEN_NAME = intern("name")
|
||||||
|
TOKEN_STRING = intern("string")
|
||||||
|
TOKEN_OPERATOR = intern("operator")
|
||||||
|
TOKEN_BLOCK_BEGIN = intern("block_begin")
|
||||||
|
TOKEN_BLOCK_END = intern("block_end")
|
||||||
|
TOKEN_VARIABLE_BEGIN = intern("variable_begin")
|
||||||
|
TOKEN_VARIABLE_END = intern("variable_end")
|
||||||
|
TOKEN_RAW_BEGIN = intern("raw_begin")
|
||||||
|
TOKEN_RAW_END = intern("raw_end")
|
||||||
|
TOKEN_COMMENT_BEGIN = intern("comment_begin")
|
||||||
|
TOKEN_COMMENT_END = intern("comment_end")
|
||||||
|
TOKEN_COMMENT = intern("comment")
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN = intern("linestatement_begin")
|
||||||
|
TOKEN_LINESTATEMENT_END = intern("linestatement_end")
|
||||||
|
TOKEN_LINECOMMENT_BEGIN = intern("linecomment_begin")
|
||||||
|
TOKEN_LINECOMMENT_END = intern("linecomment_end")
|
||||||
|
TOKEN_LINECOMMENT = intern("linecomment")
|
||||||
|
TOKEN_DATA = intern("data")
|
||||||
|
TOKEN_INITIAL = intern("initial")
|
||||||
|
TOKEN_EOF = intern("eof")
|
||||||
|
|
||||||
|
# bind operators to token types
|
||||||
|
operators = {
|
||||||
|
"+": TOKEN_ADD,
|
||||||
|
"-": TOKEN_SUB,
|
||||||
|
"/": TOKEN_DIV,
|
||||||
|
"//": TOKEN_FLOORDIV,
|
||||||
|
"*": TOKEN_MUL,
|
||||||
|
"%": TOKEN_MOD,
|
||||||
|
"**": TOKEN_POW,
|
||||||
|
"~": TOKEN_TILDE,
|
||||||
|
"[": TOKEN_LBRACKET,
|
||||||
|
"]": TOKEN_RBRACKET,
|
||||||
|
"(": TOKEN_LPAREN,
|
||||||
|
")": TOKEN_RPAREN,
|
||||||
|
"{": TOKEN_LBRACE,
|
||||||
|
"}": TOKEN_RBRACE,
|
||||||
|
"==": TOKEN_EQ,
|
||||||
|
"!=": TOKEN_NE,
|
||||||
|
">": TOKEN_GT,
|
||||||
|
">=": TOKEN_GTEQ,
|
||||||
|
"<": TOKEN_LT,
|
||||||
|
"<=": TOKEN_LTEQ,
|
||||||
|
"=": TOKEN_ASSIGN,
|
||||||
|
".": TOKEN_DOT,
|
||||||
|
":": TOKEN_COLON,
|
||||||
|
"|": TOKEN_PIPE,
|
||||||
|
",": TOKEN_COMMA,
|
||||||
|
";": TOKEN_SEMICOLON,
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_operators = {v: k for k, v in operators.items()}
|
||||||
|
assert len(operators) == len(reverse_operators), "operators dropped"
|
||||||
|
operator_re = re.compile(
|
||||||
|
f"({'|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))})"
|
||||||
|
)
|
||||||
|
|
||||||
|
ignored_tokens = frozenset(
|
||||||
|
[
|
||||||
|
TOKEN_COMMENT_BEGIN,
|
||||||
|
TOKEN_COMMENT,
|
||||||
|
TOKEN_COMMENT_END,
|
||||||
|
TOKEN_WHITESPACE,
|
||||||
|
TOKEN_LINECOMMENT_BEGIN,
|
||||||
|
TOKEN_LINECOMMENT_END,
|
||||||
|
TOKEN_LINECOMMENT,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ignore_if_empty = frozenset(
|
||||||
|
[TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_token_type(token_type: str) -> str:
|
||||||
|
if token_type in reverse_operators:
|
||||||
|
return reverse_operators[token_type]
|
||||||
|
|
||||||
|
return {
|
||||||
|
TOKEN_COMMENT_BEGIN: "begin of comment",
|
||||||
|
TOKEN_COMMENT_END: "end of comment",
|
||||||
|
TOKEN_COMMENT: "comment",
|
||||||
|
TOKEN_LINECOMMENT: "comment",
|
||||||
|
TOKEN_BLOCK_BEGIN: "begin of statement block",
|
||||||
|
TOKEN_BLOCK_END: "end of statement block",
|
||||||
|
TOKEN_VARIABLE_BEGIN: "begin of print statement",
|
||||||
|
TOKEN_VARIABLE_END: "end of print statement",
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN: "begin of line statement",
|
||||||
|
TOKEN_LINESTATEMENT_END: "end of line statement",
|
||||||
|
TOKEN_DATA: "template data / text",
|
||||||
|
TOKEN_EOF: "end of template",
|
||||||
|
}.get(token_type, token_type)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_token(token: "Token") -> str:
|
||||||
|
"""Returns a description of the token."""
|
||||||
|
if token.type == TOKEN_NAME:
|
||||||
|
return token.value
|
||||||
|
|
||||||
|
return _describe_token_type(token.type)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_token_expr(expr: str) -> str:
|
||||||
|
"""Like `describe_token` but for token expressions."""
|
||||||
|
if ":" in expr:
|
||||||
|
type, value = expr.split(":", 1)
|
||||||
|
|
||||||
|
if type == TOKEN_NAME:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
type = expr
|
||||||
|
|
||||||
|
return _describe_token_type(type)
|
||||||
|
|
||||||
|
|
||||||
|
def count_newlines(value: str) -> int:
|
||||||
|
"""Count the number of newline characters in the string. This is
|
||||||
|
useful for extensions that filter a stream.
|
||||||
|
"""
|
||||||
|
return len(newline_re.findall(value))
|
||||||
|
|
||||||
|
|
||||||
|
def compile_rules(environment: "Environment") -> t.List[t.Tuple[str, str]]:
|
||||||
|
"""Compiles all the rules from the environment into a list of rules."""
|
||||||
|
e = re.escape
|
||||||
|
rules = [
|
||||||
|
(
|
||||||
|
len(environment.comment_start_string),
|
||||||
|
TOKEN_COMMENT_BEGIN,
|
||||||
|
e(environment.comment_start_string),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
len(environment.block_start_string),
|
||||||
|
TOKEN_BLOCK_BEGIN,
|
||||||
|
e(environment.block_start_string),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
len(environment.variable_start_string),
|
||||||
|
TOKEN_VARIABLE_BEGIN,
|
||||||
|
e(environment.variable_start_string),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if environment.line_statement_prefix is not None:
|
||||||
|
rules.append(
|
||||||
|
(
|
||||||
|
len(environment.line_statement_prefix),
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN,
|
||||||
|
r"^[ \t\v]*" + e(environment.line_statement_prefix),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if environment.line_comment_prefix is not None:
|
||||||
|
rules.append(
|
||||||
|
(
|
||||||
|
len(environment.line_comment_prefix),
|
||||||
|
TOKEN_LINECOMMENT_BEGIN,
|
||||||
|
r"(?:^|(?<=\S))[^\S\r\n]*" + e(environment.line_comment_prefix),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [x[1:] for x in sorted(rules, reverse=True)]
|
||||||
|
|
||||||
|
|
||||||
|
class Failure:
|
||||||
|
"""Class that raises a `TemplateSyntaxError` if called.
|
||||||
|
Used by the `Lexer` to specify known errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, message: str, cls: t.Type[TemplateSyntaxError] = TemplateSyntaxError
|
||||||
|
) -> None:
|
||||||
|
self.message = message
|
||||||
|
self.error_class = cls
|
||||||
|
|
||||||
|
def __call__(self, lineno: int, filename: str) -> "te.NoReturn":
|
||||||
|
raise self.error_class(self.message, lineno, filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(t.NamedTuple):
|
||||||
|
lineno: int
|
||||||
|
type: str
|
||||||
|
value: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return describe_token(self)
|
||||||
|
|
||||||
|
def test(self, expr: str) -> bool:
|
||||||
|
"""Test a token against a token expression. This can either be a
|
||||||
|
token type or ``'token_type:token_value'``. This can only test
|
||||||
|
against string values and types.
|
||||||
|
"""
|
||||||
|
# here we do a regular string equality check as test_any is usually
|
||||||
|
# passed an iterable of not interned strings.
|
||||||
|
if self.type == expr:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ":" in expr:
|
||||||
|
return expr.split(":", 1) == [self.type, self.value]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_any(self, *iterable: str) -> bool:
|
||||||
|
"""Test against multiple token expressions."""
|
||||||
|
return any(self.test(expr) for expr in iterable)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStreamIterator:
|
||||||
|
"""The iterator for tokenstreams. Iterate over the stream
|
||||||
|
until the eof token is reached.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream: "TokenStream") -> None:
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def __iter__(self) -> "TokenStreamIterator":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> Token:
|
||||||
|
token = self.stream.current
|
||||||
|
|
||||||
|
if token.type is TOKEN_EOF:
|
||||||
|
self.stream.close()
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
next(self.stream)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStream:
|
||||||
|
"""A token stream is an iterable that yields :class:`Token`\\s. The
|
||||||
|
parser however does not iterate over it but calls :meth:`next` to go
|
||||||
|
one token ahead. The current active token is stored as :attr:`current`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
generator: t.Iterable[Token],
|
||||||
|
name: t.Optional[str],
|
||||||
|
filename: t.Optional[str],
|
||||||
|
):
|
||||||
|
self._iter = iter(generator)
|
||||||
|
self._pushed: "te.Deque[Token]" = deque()
|
||||||
|
self.name = name
|
||||||
|
self.filename = filename
|
||||||
|
self.closed = False
|
||||||
|
self.current = Token(1, TOKEN_INITIAL, "")
|
||||||
|
next(self)
|
||||||
|
|
||||||
|
def __iter__(self) -> TokenStreamIterator:
|
||||||
|
return TokenStreamIterator(self)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(self._pushed) or self.current.type is not TOKEN_EOF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eos(self) -> bool:
|
||||||
|
"""Are we at the end of the stream?"""
|
||||||
|
return not self
|
||||||
|
|
||||||
|
def push(self, token: Token) -> None:
|
||||||
|
"""Push a token back to the stream."""
|
||||||
|
self._pushed.append(token)
|
||||||
|
|
||||||
|
def look(self) -> Token:
|
||||||
|
"""Look at the next token."""
|
||||||
|
old_token = next(self)
|
||||||
|
result = self.current
|
||||||
|
self.push(result)
|
||||||
|
self.current = old_token
|
||||||
|
return result
|
||||||
|
|
||||||
|
def skip(self, n: int = 1) -> None:
|
||||||
|
"""Got n tokens ahead."""
|
||||||
|
for _ in range(n):
|
||||||
|
next(self)
|
||||||
|
|
||||||
|
def next_if(self, expr: str) -> t.Optional[Token]:
|
||||||
|
"""Perform the token test and return the token if it matched.
|
||||||
|
Otherwise the return value is `None`.
|
||||||
|
"""
|
||||||
|
if self.current.test(expr):
|
||||||
|
return next(self)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def skip_if(self, expr: str) -> bool:
|
||||||
|
"""Like :meth:`next_if` but only returns `True` or `False`."""
|
||||||
|
return self.next_if(expr) is not None
|
||||||
|
|
||||||
|
def __next__(self) -> Token:
|
||||||
|
"""Go one token ahead and return the old one.
|
||||||
|
|
||||||
|
Use the built-in :func:`next` instead of calling this directly.
|
||||||
|
"""
|
||||||
|
rv = self.current
|
||||||
|
|
||||||
|
if self._pushed:
|
||||||
|
self.current = self._pushed.popleft()
|
||||||
|
elif self.current.type is not TOKEN_EOF:
|
||||||
|
try:
|
||||||
|
self.current = next(self._iter)
|
||||||
|
except StopIteration:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the stream."""
|
||||||
|
self.current = Token(self.current.lineno, TOKEN_EOF, "")
|
||||||
|
self._iter = iter(())
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
def expect(self, expr: str) -> Token:
|
||||||
|
"""Expect a given token type and return it. This accepts the same
|
||||||
|
argument as :meth:`jinja2.lexer.Token.test`.
|
||||||
|
"""
|
||||||
|
if not self.current.test(expr):
|
||||||
|
expr = describe_token_expr(expr)
|
||||||
|
|
||||||
|
if self.current.type is TOKEN_EOF:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected end of template, expected {expr!r}.",
|
||||||
|
self.current.lineno,
|
||||||
|
self.name,
|
||||||
|
self.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"expected token {expr!r}, got {describe_token(self.current)!r}",
|
||||||
|
self.current.lineno,
|
||||||
|
self.name,
|
||||||
|
self.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
return next(self)
|
||||||
|
|
||||||
|
|
||||||
|
def get_lexer(environment: "Environment") -> "Lexer":
|
||||||
|
"""Return a lexer which is probably cached."""
|
||||||
|
key = (
|
||||||
|
environment.block_start_string,
|
||||||
|
environment.block_end_string,
|
||||||
|
environment.variable_start_string,
|
||||||
|
environment.variable_end_string,
|
||||||
|
environment.comment_start_string,
|
||||||
|
environment.comment_end_string,
|
||||||
|
environment.line_statement_prefix,
|
||||||
|
environment.line_comment_prefix,
|
||||||
|
environment.trim_blocks,
|
||||||
|
environment.lstrip_blocks,
|
||||||
|
environment.newline_sequence,
|
||||||
|
environment.keep_trailing_newline,
|
||||||
|
)
|
||||||
|
lexer = _lexer_cache.get(key)
|
||||||
|
|
||||||
|
if lexer is None:
|
||||||
|
_lexer_cache[key] = lexer = Lexer(environment)
|
||||||
|
|
||||||
|
return lexer
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalLStrip(tuple):
|
||||||
|
"""A special tuple for marking a point in the state that can have
|
||||||
|
lstrip applied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
# Even though it looks like a no-op, creating instances fails
|
||||||
|
# without this.
|
||||||
|
def __new__(cls, *members, **kwargs): # type: ignore
|
||||||
|
return super().__new__(cls, members)
|
||||||
|
|
||||||
|
|
||||||
|
class _Rule(t.NamedTuple):
|
||||||
|
pattern: t.Pattern[str]
|
||||||
|
tokens: t.Union[str, t.Tuple[str, ...], t.Tuple[Failure]]
|
||||||
|
command: t.Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Lexer:
|
||||||
|
"""Class that implements a lexer for a given environment. Automatically
|
||||||
|
created by the environment class, usually you don't have to do that.
|
||||||
|
|
||||||
|
Note that the lexer is not automatically bound to an environment.
|
||||||
|
Multiple environments can share the same lexer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment") -> None:
|
||||||
|
# shortcuts
|
||||||
|
e = re.escape
|
||||||
|
|
||||||
|
def c(x: str) -> t.Pattern[str]:
|
||||||
|
return re.compile(x, re.M | re.S)
|
||||||
|
|
||||||
|
# lexing rules for tags
|
||||||
|
tag_rules: t.List[_Rule] = [
|
||||||
|
_Rule(whitespace_re, TOKEN_WHITESPACE, None),
|
||||||
|
_Rule(float_re, TOKEN_FLOAT, None),
|
||||||
|
_Rule(integer_re, TOKEN_INTEGER, None),
|
||||||
|
_Rule(name_re, TOKEN_NAME, None),
|
||||||
|
_Rule(string_re, TOKEN_STRING, None),
|
||||||
|
_Rule(operator_re, TOKEN_OPERATOR, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
# assemble the root lexing rule. because "|" is ungreedy
|
||||||
|
# we have to sort by length so that the lexer continues working
|
||||||
|
# as expected when we have parsing rules like <% for block and
|
||||||
|
# <%= for variables. (if someone wants asp like syntax)
|
||||||
|
# variables are just part of the rules if variable processing
|
||||||
|
# is required.
|
||||||
|
root_tag_rules = compile_rules(environment)
|
||||||
|
|
||||||
|
block_start_re = e(environment.block_start_string)
|
||||||
|
block_end_re = e(environment.block_end_string)
|
||||||
|
comment_end_re = e(environment.comment_end_string)
|
||||||
|
variable_end_re = e(environment.variable_end_string)
|
||||||
|
|
||||||
|
# block suffix if trimming is enabled
|
||||||
|
block_suffix_re = "\\n?" if environment.trim_blocks else ""
|
||||||
|
|
||||||
|
# If lstrip is enabled, it should not be applied if there is any
|
||||||
|
# non-whitespace between the newline and block.
|
||||||
|
self.lstrip_unless_re = c(r"[^ \t]") if environment.lstrip_blocks else None
|
||||||
|
|
||||||
|
self.newline_sequence = environment.newline_sequence
|
||||||
|
self.keep_trailing_newline = environment.keep_trailing_newline
|
||||||
|
|
||||||
|
root_raw_re = (
|
||||||
|
fr"(?P<raw_begin>{block_start_re}(\-|\+|)\s*raw\s*"
|
||||||
|
fr"(?:\-{block_end_re}\s*|{block_end_re}))"
|
||||||
|
)
|
||||||
|
root_parts_re = "|".join(
|
||||||
|
[root_raw_re] + [fr"(?P<{n}>{r}(\-|\+|))" for n, r in root_tag_rules]
|
||||||
|
)
|
||||||
|
|
||||||
|
# global lexing rules
|
||||||
|
self.rules: t.Dict[str, t.List[_Rule]] = {
|
||||||
|
"root": [
|
||||||
|
# directives
|
||||||
|
_Rule(
|
||||||
|
c(fr"(.*?)(?:{root_parts_re})"),
|
||||||
|
OptionalLStrip(TOKEN_DATA, "#bygroup"), # type: ignore
|
||||||
|
"#bygroup",
|
||||||
|
),
|
||||||
|
# data
|
||||||
|
_Rule(c(".+"), TOKEN_DATA, None),
|
||||||
|
],
|
||||||
|
# comments
|
||||||
|
TOKEN_COMMENT_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
fr"(.*?)((?:\+{comment_end_re}|\-{comment_end_re}\s*"
|
||||||
|
fr"|{comment_end_re}{block_suffix_re}))"
|
||||||
|
),
|
||||||
|
(TOKEN_COMMENT, TOKEN_COMMENT_END),
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
_Rule(c(r"(.)"), (Failure("Missing end of comment tag"),), None),
|
||||||
|
],
|
||||||
|
# blocks
|
||||||
|
TOKEN_BLOCK_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
fr"(?:\+{block_end_re}|\-{block_end_re}\s*"
|
||||||
|
fr"|{block_end_re}{block_suffix_re})"
|
||||||
|
),
|
||||||
|
TOKEN_BLOCK_END,
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# variables
|
||||||
|
TOKEN_VARIABLE_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(fr"\-{variable_end_re}\s*|{variable_end_re}"),
|
||||||
|
TOKEN_VARIABLE_END,
|
||||||
|
"#pop",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# raw block
|
||||||
|
TOKEN_RAW_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(
|
||||||
|
fr"(.*?)((?:{block_start_re}(\-|\+|))\s*endraw\s*"
|
||||||
|
fr"(?:\+{block_end_re}|\-{block_end_re}\s*"
|
||||||
|
fr"|{block_end_re}{block_suffix_re}))"
|
||||||
|
),
|
||||||
|
OptionalLStrip(TOKEN_DATA, TOKEN_RAW_END), # type: ignore
|
||||||
|
"#pop",
|
||||||
|
),
|
||||||
|
_Rule(c(r"(.)"), (Failure("Missing end of raw directive"),), None),
|
||||||
|
],
|
||||||
|
# line statements
|
||||||
|
TOKEN_LINESTATEMENT_BEGIN: [
|
||||||
|
_Rule(c(r"\s*(\n|$)"), TOKEN_LINESTATEMENT_END, "#pop")
|
||||||
|
]
|
||||||
|
+ tag_rules,
|
||||||
|
# line comments
|
||||||
|
TOKEN_LINECOMMENT_BEGIN: [
|
||||||
|
_Rule(
|
||||||
|
c(r"(.*?)()(?=\n|$)"),
|
||||||
|
(TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END),
|
||||||
|
"#pop",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _normalize_newlines(self, value: str) -> str:
|
||||||
|
"""Replace all newlines with the configured sequence in strings
|
||||||
|
and template data.
|
||||||
|
"""
|
||||||
|
return newline_re.sub(self.newline_sequence, value)
|
||||||
|
|
||||||
|
def tokenize(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
state: t.Optional[str] = None,
|
||||||
|
) -> TokenStream:
|
||||||
|
"""Calls tokeniter + tokenize and wraps it in a token stream."""
|
||||||
|
stream = self.tokeniter(source, name, filename, state)
|
||||||
|
return TokenStream(self.wrap(stream, name, filename), name, filename)
|
||||||
|
|
||||||
|
def wrap(
|
||||||
|
self,
|
||||||
|
stream: t.Iterable[t.Tuple[int, str, str]],
|
||||||
|
name: t.Optional[str] = None,
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
) -> t.Iterator[Token]:
|
||||||
|
"""This is called with the stream as returned by `tokenize` and wraps
|
||||||
|
every token in a :class:`Token` and converts the value.
|
||||||
|
"""
|
||||||
|
for lineno, token, value_str in stream:
|
||||||
|
if token in ignored_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value: t.Any = value_str
|
||||||
|
|
||||||
|
if token == TOKEN_LINESTATEMENT_BEGIN:
|
||||||
|
token = TOKEN_BLOCK_BEGIN
|
||||||
|
elif token == TOKEN_LINESTATEMENT_END:
|
||||||
|
token = TOKEN_BLOCK_END
|
||||||
|
# we are not interested in those tokens in the parser
|
||||||
|
elif token in (TOKEN_RAW_BEGIN, TOKEN_RAW_END):
|
||||||
|
continue
|
||||||
|
elif token == TOKEN_DATA:
|
||||||
|
value = self._normalize_newlines(value_str)
|
||||||
|
elif token == "keyword":
|
||||||
|
token = value_str
|
||||||
|
elif token == TOKEN_NAME:
|
||||||
|
value = value_str
|
||||||
|
|
||||||
|
if not value.isidentifier():
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
"Invalid character in identifier", lineno, name, filename
|
||||||
|
)
|
||||||
|
elif token == TOKEN_STRING:
|
||||||
|
# try to unescape string
|
||||||
|
try:
|
||||||
|
value = (
|
||||||
|
self._normalize_newlines(value_str[1:-1])
|
||||||
|
.encode("ascii", "backslashreplace")
|
||||||
|
.decode("unicode-escape")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = str(e).split(":")[-1].strip()
|
||||||
|
raise TemplateSyntaxError(msg, lineno, name, filename) from e
|
||||||
|
elif token == TOKEN_INTEGER:
|
||||||
|
value = int(value_str.replace("_", ""), 0)
|
||||||
|
elif token == TOKEN_FLOAT:
|
||||||
|
# remove all "_" first to support more Python versions
|
||||||
|
value = literal_eval(value_str.replace("_", ""))
|
||||||
|
elif token == TOKEN_OPERATOR:
|
||||||
|
token = operators[value_str]
|
||||||
|
|
||||||
|
yield Token(lineno, token, value)
|
||||||
|
|
||||||
|
def tokeniter(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
name: t.Optional[str],
|
||||||
|
filename: t.Optional[str] = None,
|
||||||
|
state: t.Optional[str] = None,
|
||||||
|
) -> t.Iterator[t.Tuple[int, str, str]]:
|
||||||
|
"""This method tokenizes the text and returns the tokens in a
|
||||||
|
generator. Use this method if you just want to tokenize a template.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Only ``\\n``, ``\\r\\n`` and ``\\r`` are treated as line
|
||||||
|
breaks.
|
||||||
|
"""
|
||||||
|
lines = newline_re.split(source)[::2]
|
||||||
|
|
||||||
|
if not self.keep_trailing_newline and lines[-1] == "":
|
||||||
|
del lines[-1]
|
||||||
|
|
||||||
|
source = "\n".join(lines)
|
||||||
|
pos = 0
|
||||||
|
lineno = 1
|
||||||
|
stack = ["root"]
|
||||||
|
|
||||||
|
if state is not None and state != "root":
|
||||||
|
assert state in ("variable", "block"), "invalid state"
|
||||||
|
stack.append(state + "_begin")
|
||||||
|
|
||||||
|
statetokens = self.rules[stack[-1]]
|
||||||
|
source_length = len(source)
|
||||||
|
balancing_stack: t.List[str] = []
|
||||||
|
lstrip_unless_re = self.lstrip_unless_re
|
||||||
|
newlines_stripped = 0
|
||||||
|
line_starting = True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# tokenizer loop
|
||||||
|
for regex, tokens, new_state in statetokens:
|
||||||
|
m = regex.match(source, pos)
|
||||||
|
|
||||||
|
# if no match we try again with the next rule
|
||||||
|
if m is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# we only match blocks and variables if braces / parentheses
|
||||||
|
# are balanced. continue parsing with the lower rule which
|
||||||
|
# is the operator rule. do this only if the end tags look
|
||||||
|
# like operators
|
||||||
|
if balancing_stack and tokens in (
|
||||||
|
TOKEN_VARIABLE_END,
|
||||||
|
TOKEN_BLOCK_END,
|
||||||
|
TOKEN_LINESTATEMENT_END,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# tuples support more options
|
||||||
|
if isinstance(tokens, tuple):
|
||||||
|
groups = m.groups()
|
||||||
|
|
||||||
|
if isinstance(tokens, OptionalLStrip):
|
||||||
|
# Rule supports lstrip. Match will look like
|
||||||
|
# text, block type, whitespace control, type, control, ...
|
||||||
|
text = groups[0]
|
||||||
|
# Skipping the text and first type, every other group is the
|
||||||
|
# whitespace control for each type. One of the groups will be
|
||||||
|
# -, +, or empty string instead of None.
|
||||||
|
strip_sign = next(g for g in groups[2::2] if g is not None)
|
||||||
|
|
||||||
|
if strip_sign == "-":
|
||||||
|
# Strip all whitespace between the text and the tag.
|
||||||
|
stripped = text.rstrip()
|
||||||
|
newlines_stripped = text[len(stripped) :].count("\n")
|
||||||
|
groups = [stripped, *groups[1:]]
|
||||||
|
elif (
|
||||||
|
# Not marked for preserving whitespace.
|
||||||
|
strip_sign != "+"
|
||||||
|
# lstrip is enabled.
|
||||||
|
and lstrip_unless_re is not None
|
||||||
|
# Not a variable expression.
|
||||||
|
and not m.groupdict().get(TOKEN_VARIABLE_BEGIN)
|
||||||
|
):
|
||||||
|
# The start of text between the last newline and the tag.
|
||||||
|
l_pos = text.rfind("\n") + 1
|
||||||
|
|
||||||
|
if l_pos > 0 or line_starting:
|
||||||
|
# If there's only whitespace between the newline and the
|
||||||
|
# tag, strip it.
|
||||||
|
if not lstrip_unless_re.search(text, l_pos):
|
||||||
|
groups = [text[:l_pos], *groups[1:]]
|
||||||
|
|
||||||
|
for idx, token in enumerate(tokens):
|
||||||
|
# failure group
|
||||||
|
if token.__class__ is Failure:
|
||||||
|
raise token(lineno, filename)
|
||||||
|
# bygroup is a bit more complex, in that case we
|
||||||
|
# yield for the current token the first named
|
||||||
|
# group that matched
|
||||||
|
elif token == "#bygroup":
|
||||||
|
for key, value in m.groupdict().items():
|
||||||
|
if value is not None:
|
||||||
|
yield lineno, key, value
|
||||||
|
lineno += value.count("\n")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} wanted to resolve the token dynamically"
|
||||||
|
" but no group matched"
|
||||||
|
)
|
||||||
|
# normal group
|
||||||
|
else:
|
||||||
|
data = groups[idx]
|
||||||
|
|
||||||
|
if data or token not in ignore_if_empty:
|
||||||
|
yield lineno, token, data
|
||||||
|
|
||||||
|
lineno += data.count("\n") + newlines_stripped
|
||||||
|
newlines_stripped = 0
|
||||||
|
|
||||||
|
# strings as token just are yielded as it.
|
||||||
|
else:
|
||||||
|
data = m.group()
|
||||||
|
|
||||||
|
# update brace/parentheses balance
|
||||||
|
if tokens == TOKEN_OPERATOR:
|
||||||
|
if data == "{":
|
||||||
|
balancing_stack.append("}")
|
||||||
|
elif data == "(":
|
||||||
|
balancing_stack.append(")")
|
||||||
|
elif data == "[":
|
||||||
|
balancing_stack.append("]")
|
||||||
|
elif data in ("}", ")", "]"):
|
||||||
|
if not balancing_stack:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected '{data}'", lineno, name, filename
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_op = balancing_stack.pop()
|
||||||
|
|
||||||
|
if expected_op != data:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected '{data}', expected '{expected_op}'",
|
||||||
|
lineno,
|
||||||
|
name,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
# yield items
|
||||||
|
if data or tokens not in ignore_if_empty:
|
||||||
|
yield lineno, tokens, data
|
||||||
|
|
||||||
|
lineno += data.count("\n")
|
||||||
|
|
||||||
|
line_starting = m.group()[-1:] == "\n"
|
||||||
|
# fetch new position into new variable so that we can check
|
||||||
|
# if there is a internal parsing error which would result
|
||||||
|
# in an infinite loop
|
||||||
|
pos2 = m.end()
|
||||||
|
|
||||||
|
# handle state changes
|
||||||
|
if new_state is not None:
|
||||||
|
# remove the uppermost state
|
||||||
|
if new_state == "#pop":
|
||||||
|
stack.pop()
|
||||||
|
# resolve the new state by group checking
|
||||||
|
elif new_state == "#bygroup":
|
||||||
|
for key, value in m.groupdict().items():
|
||||||
|
if value is not None:
|
||||||
|
stack.append(key)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} wanted to resolve the new state dynamically"
|
||||||
|
f" but no group matched"
|
||||||
|
)
|
||||||
|
# direct state name given
|
||||||
|
else:
|
||||||
|
stack.append(new_state)
|
||||||
|
|
||||||
|
statetokens = self.rules[stack[-1]]
|
||||||
|
# we are still at the same position and no stack change.
|
||||||
|
# this means a loop without break condition, avoid that and
|
||||||
|
# raise error
|
||||||
|
elif pos2 == pos:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{regex!r} yielded empty string without stack change"
|
||||||
|
)
|
||||||
|
|
||||||
|
# publish new function and start again
|
||||||
|
pos = pos2
|
||||||
|
break
|
||||||
|
# if loop terminated without break we haven't found a single match
|
||||||
|
# either we are at the end of the file or we have a problem
|
||||||
|
else:
|
||||||
|
# end of text
|
||||||
|
if pos >= source_length:
|
||||||
|
return
|
||||||
|
|
||||||
|
# something went wrong
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"unexpected char {source[pos]!r} at {pos}", lineno, name, filename
|
||||||
|
)
|
|
@ -0,0 +1,652 @@
|
||||||
|
"""API and implementations for loading templates from different data
|
||||||
|
sources.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import typing as t
|
||||||
|
import weakref
|
||||||
|
import zipimport
|
||||||
|
from collections import abc
|
||||||
|
from hashlib import sha1
|
||||||
|
from importlib import import_module
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
from .exceptions import TemplateNotFound
|
||||||
|
from .utils import internalcode
|
||||||
|
from .utils import open_if_exists
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
from .environment import Template
|
||||||
|
|
||||||
|
|
||||||
|
def split_template_path(template: str) -> t.List[str]:
|
||||||
|
"""Split a path into segments and perform a sanity check. If it detects
|
||||||
|
'..' in the path it will raise a `TemplateNotFound` error.
|
||||||
|
"""
|
||||||
|
pieces = []
|
||||||
|
for piece in template.split("/"):
|
||||||
|
if (
|
||||||
|
os.path.sep in piece
|
||||||
|
or (os.path.altsep and os.path.altsep in piece)
|
||||||
|
or piece == os.path.pardir
|
||||||
|
):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
elif piece and piece != ".":
|
||||||
|
pieces.append(piece)
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLoader:
|
||||||
|
"""Baseclass for all loaders. Subclass this and override `get_source` to
|
||||||
|
implement a custom loading mechanism. The environment provides a
|
||||||
|
`get_template` method that calls the loader's `load` method to get the
|
||||||
|
:class:`Template` object.
|
||||||
|
|
||||||
|
A very basic example for a loader that looks up templates on the file
|
||||||
|
system could look like this::
|
||||||
|
|
||||||
|
from jinja2 import BaseLoader, TemplateNotFound
|
||||||
|
from os.path import join, exists, getmtime
|
||||||
|
|
||||||
|
class MyLoader(BaseLoader):
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def get_source(self, environment, template):
|
||||||
|
path = join(self.path, template)
|
||||||
|
if not exists(path):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
mtime = getmtime(path)
|
||||||
|
with open(path) as f:
|
||||||
|
source = f.read()
|
||||||
|
return source, path, lambda: mtime == getmtime(path)
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: if set to `False` it indicates that the loader cannot provide access
|
||||||
|
#: to the source of templates.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.4
|
||||||
|
has_source_access = True
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
"""Get the template source, filename and reload helper for a template.
|
||||||
|
It's passed the environment and template name and has to return a
|
||||||
|
tuple in the form ``(source, filename, uptodate)`` or raise a
|
||||||
|
`TemplateNotFound` error if it can't locate the template.
|
||||||
|
|
||||||
|
The source part of the returned tuple must be the source of the
|
||||||
|
template as a string. The filename should be the name of the
|
||||||
|
file on the filesystem if it was loaded from there, otherwise
|
||||||
|
``None``. The filename is used by Python for the tracebacks
|
||||||
|
if no loader extension is used.
|
||||||
|
|
||||||
|
The last item in the tuple is the `uptodate` function. If auto
|
||||||
|
reloading is enabled it's always called to check if the template
|
||||||
|
changed. No arguments are passed so the function must store the
|
||||||
|
old state somewhere (for example in a closure). If it returns `False`
|
||||||
|
the template will be reloaded.
|
||||||
|
"""
|
||||||
|
if not self.has_source_access:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{type(self).__name__} cannot provide access to the source"
|
||||||
|
)
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
"""Iterates over all templates. If the loader does not support that
|
||||||
|
it should raise a :exc:`TypeError` which is the default behavior.
|
||||||
|
"""
|
||||||
|
raise TypeError("this loader cannot iterate over all templates")
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
"""Loads a template. This method looks up the template in the cache
|
||||||
|
or loads one by calling :meth:`get_source`. Subclasses should not
|
||||||
|
override this method as loaders working on collections of other
|
||||||
|
loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`)
|
||||||
|
will not call this method but `get_source` directly.
|
||||||
|
"""
|
||||||
|
code = None
|
||||||
|
if globals is None:
|
||||||
|
globals = {}
|
||||||
|
|
||||||
|
# first we try to get the source for this template together
|
||||||
|
# with the filename and the uptodate function.
|
||||||
|
source, filename, uptodate = self.get_source(environment, name)
|
||||||
|
|
||||||
|
# try to load the code from the bytecode cache if there is a
|
||||||
|
# bytecode cache configured.
|
||||||
|
bcc = environment.bytecode_cache
|
||||||
|
if bcc is not None:
|
||||||
|
bucket = bcc.get_bucket(environment, name, filename, source)
|
||||||
|
code = bucket.code
|
||||||
|
|
||||||
|
# if we don't have code so far (not cached, no longer up to
|
||||||
|
# date) etc. we compile the template
|
||||||
|
if code is None:
|
||||||
|
code = environment.compile(source, name, filename)
|
||||||
|
|
||||||
|
# if the bytecode cache is available and the bucket doesn't
|
||||||
|
# have a code so far, we give the bucket the new code and put
|
||||||
|
# it back to the bytecode cache.
|
||||||
|
if bcc is not None and bucket.code is None:
|
||||||
|
bucket.code = code
|
||||||
|
bcc.set_bucket(bucket)
|
||||||
|
|
||||||
|
return environment.template_class.from_code(
|
||||||
|
environment, code, globals, uptodate
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemLoader(BaseLoader):
|
||||||
|
"""Load templates from a directory in the file system.
|
||||||
|
|
||||||
|
The path can be relative or absolute. Relative paths are relative to
|
||||||
|
the current working directory.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = FileSystemLoader("templates")
|
||||||
|
|
||||||
|
A list of paths can be given. The directories will be searched in
|
||||||
|
order, stopping at the first matching template.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = FileSystemLoader(["/override/templates", "/default/templates"])
|
||||||
|
|
||||||
|
:param searchpath: A path, or list of paths, to the directory that
|
||||||
|
contains the templates.
|
||||||
|
:param encoding: Use this encoding to read the text from template
|
||||||
|
files.
|
||||||
|
:param followlinks: Follow symbolic links in the path.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.8
|
||||||
|
Added the ``followlinks`` parameter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
searchpath: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]],
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
followlinks: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str):
|
||||||
|
searchpath = [searchpath]
|
||||||
|
|
||||||
|
self.searchpath = [os.fspath(p) for p in searchpath]
|
||||||
|
self.encoding = encoding
|
||||||
|
self.followlinks = followlinks
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, str, t.Callable[[], bool]]:
|
||||||
|
pieces = split_template_path(template)
|
||||||
|
for searchpath in self.searchpath:
|
||||||
|
filename = os.path.join(searchpath, *pieces)
|
||||||
|
f = open_if_exists(filename)
|
||||||
|
if f is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
contents = f.read().decode(self.encoding)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(filename)
|
||||||
|
|
||||||
|
def uptodate() -> bool:
|
||||||
|
try:
|
||||||
|
return os.path.getmtime(filename) == mtime
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return contents, filename, uptodate
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
found = set()
|
||||||
|
for searchpath in self.searchpath:
|
||||||
|
walk_dir = os.walk(searchpath, followlinks=self.followlinks)
|
||||||
|
for dirpath, _, filenames in walk_dir:
|
||||||
|
for filename in filenames:
|
||||||
|
template = (
|
||||||
|
os.path.join(dirpath, filename)[len(searchpath) :]
|
||||||
|
.strip(os.path.sep)
|
||||||
|
.replace(os.path.sep, "/")
|
||||||
|
)
|
||||||
|
if template[:2] == "./":
|
||||||
|
template = template[2:]
|
||||||
|
if template not in found:
|
||||||
|
found.add(template)
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageLoader(BaseLoader):
|
||||||
|
"""Load templates from a directory in a Python package.
|
||||||
|
|
||||||
|
:param package_name: Import name of the package that contains the
|
||||||
|
template directory.
|
||||||
|
:param package_path: Directory within the imported package that
|
||||||
|
contains the templates.
|
||||||
|
:param encoding: Encoding of template files.
|
||||||
|
|
||||||
|
The following example looks up templates in the ``pages`` directory
|
||||||
|
within the ``project.ui`` package.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loader = PackageLoader("project.ui", "pages")
|
||||||
|
|
||||||
|
Only packages installed as directories (standard pip behavior) or
|
||||||
|
zip/egg files (less common) are supported. The Python API for
|
||||||
|
introspecting data in packages is too limited to support other
|
||||||
|
installation methods the way this loader requires.
|
||||||
|
|
||||||
|
There is limited support for :pep:`420` namespace packages. The
|
||||||
|
template directory is assumed to only be in one namespace
|
||||||
|
contributor. Zip files contributing to a namespace are not
|
||||||
|
supported.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
No longer uses ``setuptools`` as a dependency.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Limited PEP 420 namespace package support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
package_name: str,
|
||||||
|
package_path: "str" = "templates",
|
||||||
|
encoding: str = "utf-8",
|
||||||
|
) -> None:
|
||||||
|
package_path = os.path.normpath(package_path).rstrip(os.path.sep)
|
||||||
|
|
||||||
|
# normpath preserves ".", which isn't valid in zip paths.
|
||||||
|
if package_path == os.path.curdir:
|
||||||
|
package_path = ""
|
||||||
|
elif package_path[:2] == os.path.curdir + os.path.sep:
|
||||||
|
package_path = package_path[2:]
|
||||||
|
|
||||||
|
self.package_path = package_path
|
||||||
|
self.package_name = package_name
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
# Make sure the package exists. This also makes namespace
|
||||||
|
# packages work, otherwise get_loader returns None.
|
||||||
|
import_module(package_name)
|
||||||
|
spec = importlib.util.find_spec(package_name)
|
||||||
|
assert spec is not None, "An import spec was not found for the package."
|
||||||
|
loader = spec.loader
|
||||||
|
assert loader is not None, "A loader was not found for the package."
|
||||||
|
self._loader = loader
|
||||||
|
self._archive = None
|
||||||
|
template_root = None
|
||||||
|
|
||||||
|
if isinstance(loader, zipimport.zipimporter):
|
||||||
|
self._archive = loader.archive
|
||||||
|
pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore
|
||||||
|
template_root = os.path.join(pkgdir, package_path)
|
||||||
|
else:
|
||||||
|
roots: t.List[str] = []
|
||||||
|
|
||||||
|
# One element for regular packages, multiple for namespace
|
||||||
|
# packages, or None for single module file.
|
||||||
|
if spec.submodule_search_locations:
|
||||||
|
roots.extend(spec.submodule_search_locations)
|
||||||
|
# A single module file, use the parent directory instead.
|
||||||
|
elif spec.origin is not None:
|
||||||
|
roots.append(os.path.dirname(spec.origin))
|
||||||
|
|
||||||
|
for root in roots:
|
||||||
|
root = os.path.join(root, package_path)
|
||||||
|
|
||||||
|
if os.path.isdir(root):
|
||||||
|
template_root = root
|
||||||
|
break
|
||||||
|
|
||||||
|
if template_root is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"The {package_name!r} package was not installed in a"
|
||||||
|
" way that PackageLoader understands."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._template_root = template_root
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]:
|
||||||
|
p = os.path.join(self._template_root, *split_template_path(template))
|
||||||
|
up_to_date: t.Optional[t.Callable[[], bool]]
|
||||||
|
|
||||||
|
if self._archive is None:
|
||||||
|
# Package is a directory.
|
||||||
|
if not os.path.isfile(p):
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
with open(p, "rb") as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(p)
|
||||||
|
|
||||||
|
def up_to_date() -> bool:
|
||||||
|
return os.path.isfile(p) and os.path.getmtime(p) == mtime
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Package is a zip file.
|
||||||
|
try:
|
||||||
|
source = self._loader.get_data(p) # type: ignore
|
||||||
|
except OSError as e:
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
|
||||||
|
# Could use the zip's mtime for all template mtimes, but
|
||||||
|
# would need to safely reload the module if it's out of
|
||||||
|
# date, so just report it as always current.
|
||||||
|
up_to_date = None
|
||||||
|
|
||||||
|
return source.decode(self.encoding), p, up_to_date
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
results: t.List[str] = []
|
||||||
|
|
||||||
|
if self._archive is None:
|
||||||
|
# Package is a directory.
|
||||||
|
offset = len(self._template_root)
|
||||||
|
|
||||||
|
for dirpath, _, filenames in os.walk(self._template_root):
|
||||||
|
dirpath = dirpath[offset:].lstrip(os.path.sep)
|
||||||
|
results.extend(
|
||||||
|
os.path.join(dirpath, name).replace(os.path.sep, "/")
|
||||||
|
for name in filenames
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not hasattr(self._loader, "_files"):
|
||||||
|
raise TypeError(
|
||||||
|
"This zip import does not have the required"
|
||||||
|
" metadata to list templates."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Package is a zip file.
|
||||||
|
prefix = (
|
||||||
|
self._template_root[len(self._archive) :].lstrip(os.path.sep)
|
||||||
|
+ os.path.sep
|
||||||
|
)
|
||||||
|
offset = len(prefix)
|
||||||
|
|
||||||
|
for name in self._loader._files.keys(): # type: ignore
|
||||||
|
# Find names under the templates directory that aren't directories.
|
||||||
|
if name.startswith(prefix) and name[-1] != os.path.sep:
|
||||||
|
results.append(name[offset:].replace(os.path.sep, "/"))
|
||||||
|
|
||||||
|
results.sort()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class DictLoader(BaseLoader):
|
||||||
|
"""Loads a template from a Python dict mapping template names to
|
||||||
|
template source. This loader is useful for unittesting:
|
||||||
|
|
||||||
|
>>> loader = DictLoader({'index.html': 'source here'})
|
||||||
|
|
||||||
|
Because auto reloading is rarely useful this is disabled per default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapping: t.Mapping[str, str]) -> None:
|
||||||
|
self.mapping = mapping
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, None, t.Callable[[], bool]]:
|
||||||
|
if template in self.mapping:
|
||||||
|
source = self.mapping[template]
|
||||||
|
return source, None, lambda: source == self.mapping.get(template)
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
return sorted(self.mapping)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionLoader(BaseLoader):
|
||||||
|
"""A loader that is passed a function which does the loading. The
|
||||||
|
function receives the name of the template and has to return either
|
||||||
|
a string with the template source, a tuple in the form ``(source,
|
||||||
|
filename, uptodatefunc)`` or `None` if the template does not exist.
|
||||||
|
|
||||||
|
>>> def load_template(name):
|
||||||
|
... if name == 'index.html':
|
||||||
|
... return '...'
|
||||||
|
...
|
||||||
|
>>> loader = FunctionLoader(load_template)
|
||||||
|
|
||||||
|
The `uptodatefunc` is a function that is called if autoreload is enabled
|
||||||
|
and has to return `True` if the template is still up to date. For more
|
||||||
|
details have a look at :meth:`BaseLoader.get_source` which has the same
|
||||||
|
return value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
load_func: t.Callable[
|
||||||
|
[str],
|
||||||
|
t.Optional[
|
||||||
|
t.Union[
|
||||||
|
str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
self.load_func = load_func
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
rv = self.load_func(template)
|
||||||
|
|
||||||
|
if rv is None:
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
if isinstance(rv, str):
|
||||||
|
return rv, None, None
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixLoader(BaseLoader):
|
||||||
|
"""A loader that is passed a dict of loaders where each loader is bound
|
||||||
|
to a prefix. The prefix is delimited from the template by a slash per
|
||||||
|
default, which can be changed by setting the `delimiter` argument to
|
||||||
|
something else::
|
||||||
|
|
||||||
|
loader = PrefixLoader({
|
||||||
|
'app1': PackageLoader('mypackage.app1'),
|
||||||
|
'app2': PackageLoader('mypackage.app2')
|
||||||
|
})
|
||||||
|
|
||||||
|
By loading ``'app1/index.html'`` the file from the app1 package is loaded,
|
||||||
|
by loading ``'app2/index.html'`` the file from the second.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/"
|
||||||
|
) -> None:
|
||||||
|
self.mapping = mapping
|
||||||
|
self.delimiter = delimiter
|
||||||
|
|
||||||
|
def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]:
|
||||||
|
try:
|
||||||
|
prefix, name = template.split(self.delimiter, 1)
|
||||||
|
loader = self.mapping[prefix]
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
return loader, name
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
loader, name = self.get_loader(template)
|
||||||
|
try:
|
||||||
|
return loader.get_source(environment, name)
|
||||||
|
except TemplateNotFound as e:
|
||||||
|
# re-raise the exception with the correct filename here.
|
||||||
|
# (the one that includes the prefix)
|
||||||
|
raise TemplateNotFound(template) from e
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
loader, local_name = self.get_loader(name)
|
||||||
|
try:
|
||||||
|
return loader.load(environment, local_name, globals)
|
||||||
|
except TemplateNotFound as e:
|
||||||
|
# re-raise the exception with the correct filename here.
|
||||||
|
# (the one that includes the prefix)
|
||||||
|
raise TemplateNotFound(name) from e
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
result = []
|
||||||
|
for prefix, loader in self.mapping.items():
|
||||||
|
for template in loader.list_templates():
|
||||||
|
result.append(prefix + self.delimiter + template)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceLoader(BaseLoader):
|
||||||
|
"""This loader works like the `PrefixLoader` just that no prefix is
|
||||||
|
specified. If a template could not be found by one loader the next one
|
||||||
|
is tried.
|
||||||
|
|
||||||
|
>>> loader = ChoiceLoader([
|
||||||
|
... FileSystemLoader('/path/to/user/templates'),
|
||||||
|
... FileSystemLoader('/path/to/system/templates')
|
||||||
|
... ])
|
||||||
|
|
||||||
|
This is useful if you want to allow users to override builtin templates
|
||||||
|
from a different location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, loaders: t.Sequence[BaseLoader]) -> None:
|
||||||
|
self.loaders = loaders
|
||||||
|
|
||||||
|
def get_source(
|
||||||
|
self, environment: "Environment", template: str
|
||||||
|
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]:
|
||||||
|
for loader in self.loaders:
|
||||||
|
try:
|
||||||
|
return loader.get_source(environment, template)
|
||||||
|
except TemplateNotFound:
|
||||||
|
pass
|
||||||
|
raise TemplateNotFound(template)
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
for loader in self.loaders:
|
||||||
|
try:
|
||||||
|
return loader.load(environment, name, globals)
|
||||||
|
except TemplateNotFound:
|
||||||
|
pass
|
||||||
|
raise TemplateNotFound(name)
|
||||||
|
|
||||||
|
def list_templates(self) -> t.List[str]:
|
||||||
|
found = set()
|
||||||
|
for loader in self.loaders:
|
||||||
|
found.update(loader.list_templates())
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
class _TemplateModule(ModuleType):
|
||||||
|
"""Like a normal module but with support for weak references"""
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleLoader(BaseLoader):
|
||||||
|
"""This loader loads templates from precompiled templates.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
>>> loader = ChoiceLoader([
|
||||||
|
... ModuleLoader('/path/to/compiled/templates'),
|
||||||
|
... FileSystemLoader('/path/to/templates')
|
||||||
|
... ])
|
||||||
|
|
||||||
|
Templates can be precompiled with :meth:`Environment.compile_templates`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
has_source_access = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, path: t.Union[str, os.PathLike, t.Sequence[t.Union[str, os.PathLike]]]
|
||||||
|
) -> None:
|
||||||
|
package_name = f"_jinja2_module_templates_{id(self):x}"
|
||||||
|
|
||||||
|
# create a fake module that looks for the templates in the
|
||||||
|
# path given.
|
||||||
|
mod = _TemplateModule(package_name)
|
||||||
|
|
||||||
|
if not isinstance(path, abc.Iterable) or isinstance(path, str):
|
||||||
|
path = [path]
|
||||||
|
|
||||||
|
mod.__path__ = [os.fspath(p) for p in path] # type: ignore
|
||||||
|
|
||||||
|
sys.modules[package_name] = weakref.proxy(
|
||||||
|
mod, lambda x: sys.modules.pop(package_name, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# the only strong reference, the sys.modules entry is weak
|
||||||
|
# so that the garbage collector can remove it once the
|
||||||
|
# loader that created it goes out of business.
|
||||||
|
self.module = mod
|
||||||
|
self.package_name = package_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_template_key(name: str) -> str:
|
||||||
|
return "tmpl_" + sha1(name.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_module_filename(name: str) -> str:
|
||||||
|
return ModuleLoader.get_template_key(name) + ".py"
|
||||||
|
|
||||||
|
@internalcode
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
environment: "Environment",
|
||||||
|
name: str,
|
||||||
|
globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
|
||||||
|
) -> "Template":
|
||||||
|
key = self.get_template_key(name)
|
||||||
|
module = f"{self.package_name}.{key}"
|
||||||
|
mod = getattr(self.module, module, None)
|
||||||
|
|
||||||
|
if mod is None:
|
||||||
|
try:
|
||||||
|
mod = __import__(module, None, None, ["root"])
|
||||||
|
except ImportError as e:
|
||||||
|
raise TemplateNotFound(name) from e
|
||||||
|
|
||||||
|
# remove the entry from sys.modules, we only want the attribute
|
||||||
|
# on the module object we have stored on the loader.
|
||||||
|
sys.modules.pop(module, None)
|
||||||
|
|
||||||
|
if globals is None:
|
||||||
|
globals = {}
|
||||||
|
|
||||||
|
return environment.template_class.from_module_dict(
|
||||||
|
environment, mod.__dict__, globals
|
||||||
|
)
|
|
@ -0,0 +1,111 @@
|
||||||
|
"""Functions that expose information about templates that might be
|
||||||
|
interesting for introspection.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .compiler import CodeGenerator
|
||||||
|
from .compiler import Frame
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
class TrackingCodeGenerator(CodeGenerator):
|
||||||
|
"""We abuse the code generator for introspection."""
|
||||||
|
|
||||||
|
def __init__(self, environment: "Environment") -> None:
|
||||||
|
super().__init__(environment, "<introspection>", "<introspection>")
|
||||||
|
self.undeclared_identifiers: t.Set[str] = set()
|
||||||
|
|
||||||
|
def write(self, x: str) -> None:
|
||||||
|
"""Don't write."""
|
||||||
|
|
||||||
|
def enter_frame(self, frame: Frame) -> None:
|
||||||
|
"""Remember all undeclared identifiers."""
|
||||||
|
super().enter_frame(frame)
|
||||||
|
|
||||||
|
for _, (action, param) in frame.symbols.loads.items():
|
||||||
|
if action == "resolve" and param not in self.environment.globals:
|
||||||
|
self.undeclared_identifiers.add(param)
|
||||||
|
|
||||||
|
|
||||||
|
def find_undeclared_variables(ast: nodes.Template) -> t.Set[str]:
|
||||||
|
"""Returns a set of all variables in the AST that will be looked up from
|
||||||
|
the context at runtime. Because at compile time it's not known which
|
||||||
|
variables will be used depending on the path the execution takes at
|
||||||
|
runtime, all variables are returned.
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment, meta
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}')
|
||||||
|
>>> meta.find_undeclared_variables(ast) == {'bar'}
|
||||||
|
True
|
||||||
|
|
||||||
|
.. admonition:: Implementation
|
||||||
|
|
||||||
|
Internally the code generator is used for finding undeclared variables.
|
||||||
|
This is good to know because the code generator might raise a
|
||||||
|
:exc:`TemplateAssertionError` during compilation and as a matter of
|
||||||
|
fact this function can currently raise that exception as well.
|
||||||
|
"""
|
||||||
|
codegen = TrackingCodeGenerator(ast.environment) # type: ignore
|
||||||
|
codegen.visit(ast)
|
||||||
|
return codegen.undeclared_identifiers
|
||||||
|
|
||||||
|
|
||||||
|
_ref_types = (nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include)
|
||||||
|
_RefType = t.Union[nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include]
|
||||||
|
|
||||||
|
|
||||||
|
def find_referenced_templates(ast: nodes.Template) -> t.Iterator[t.Optional[str]]:
|
||||||
|
"""Finds all the referenced templates from the AST. This will return an
|
||||||
|
iterator over all the hardcoded template extensions, inclusions and
|
||||||
|
imports. If dynamic inheritance or inclusion is used, `None` will be
|
||||||
|
yielded.
|
||||||
|
|
||||||
|
>>> from jinja2 import Environment, meta
|
||||||
|
>>> env = Environment()
|
||||||
|
>>> ast = env.parse('{% extends "layout.html" %}{% include helper %}')
|
||||||
|
>>> list(meta.find_referenced_templates(ast))
|
||||||
|
['layout.html', None]
|
||||||
|
|
||||||
|
This function is useful for dependency tracking. For example if you want
|
||||||
|
to rebuild parts of the website after a layout template has changed.
|
||||||
|
"""
|
||||||
|
template_name: t.Any
|
||||||
|
|
||||||
|
for node in ast.find_all(_ref_types):
|
||||||
|
template: nodes.Expr = node.template # type: ignore
|
||||||
|
|
||||||
|
if not isinstance(template, nodes.Const):
|
||||||
|
# a tuple with some non consts in there
|
||||||
|
if isinstance(template, (nodes.Tuple, nodes.List)):
|
||||||
|
for template_name in template.items:
|
||||||
|
# something const, only yield the strings and ignore
|
||||||
|
# non-string consts that really just make no sense
|
||||||
|
if isinstance(template_name, nodes.Const):
|
||||||
|
if isinstance(template_name.value, str):
|
||||||
|
yield template_name.value
|
||||||
|
# something dynamic in there
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
# something dynamic we don't know about here
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
continue
|
||||||
|
# constant is a basestring, direct template name
|
||||||
|
if isinstance(template.value, str):
|
||||||
|
yield template.value
|
||||||
|
# a tuple or list (latter *should* not happen) made of consts,
|
||||||
|
# yield the consts that are strings. We could warn here for
|
||||||
|
# non string values
|
||||||
|
elif isinstance(node, nodes.Include) and isinstance(
|
||||||
|
template.value, (tuple, list)
|
||||||
|
):
|
||||||
|
for template_name in template.value:
|
||||||
|
if isinstance(template_name, str):
|
||||||
|
yield template_name
|
||||||
|
# something else we don't care about, we could warn here
|
||||||
|
else:
|
||||||
|
yield None
|
|
@ -0,0 +1,124 @@
|
||||||
|
import typing as t
|
||||||
|
from ast import literal_eval
|
||||||
|
from ast import parse
|
||||||
|
from itertools import chain
|
||||||
|
from itertools import islice
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .compiler import CodeGenerator
|
||||||
|
from .compiler import Frame
|
||||||
|
from .compiler import has_safe_repr
|
||||||
|
from .environment import Environment
|
||||||
|
from .environment import Template
|
||||||
|
|
||||||
|
|
||||||
|
def native_concat(values: t.Iterable[t.Any]) -> t.Optional[t.Any]:
|
||||||
|
"""Return a native Python type from the list of compiled nodes. If
|
||||||
|
the result is a single node, its value is returned. Otherwise, the
|
||||||
|
nodes are concatenated as strings. If the result can be parsed with
|
||||||
|
:func:`ast.literal_eval`, the parsed value is returned. Otherwise,
|
||||||
|
the string is returned.
|
||||||
|
|
||||||
|
:param values: Iterable of outputs to concatenate.
|
||||||
|
"""
|
||||||
|
head = list(islice(values, 2))
|
||||||
|
|
||||||
|
if not head:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(head) == 1:
|
||||||
|
raw = head[0]
|
||||||
|
if not isinstance(raw, str):
|
||||||
|
return raw
|
||||||
|
else:
|
||||||
|
raw = "".join([str(v) for v in chain(head, values)])
|
||||||
|
|
||||||
|
try:
|
||||||
|
return literal_eval(
|
||||||
|
# In Python 3.10+ ast.literal_eval removes leading spaces/tabs
|
||||||
|
# from the given string. For backwards compatibility we need to
|
||||||
|
# parse the string ourselves without removing leading spaces/tabs.
|
||||||
|
parse(raw, mode="eval")
|
||||||
|
)
|
||||||
|
except (ValueError, SyntaxError, MemoryError):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
class NativeCodeGenerator(CodeGenerator):
|
||||||
|
"""A code generator which renders Python types by not adding
|
||||||
|
``str()`` around output nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_finalize(value: t.Any) -> t.Any:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _output_const_repr(self, group: t.Iterable[t.Any]) -> str:
|
||||||
|
return repr("".join([str(v) for v in group]))
|
||||||
|
|
||||||
|
def _output_child_to_const(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> t.Any:
|
||||||
|
const = node.as_const(frame.eval_ctx)
|
||||||
|
|
||||||
|
if not has_safe_repr(const):
|
||||||
|
raise nodes.Impossible()
|
||||||
|
|
||||||
|
if isinstance(node, nodes.TemplateData):
|
||||||
|
return const
|
||||||
|
|
||||||
|
return finalize.const(const) # type: ignore
|
||||||
|
|
||||||
|
def _output_child_pre(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> None:
|
||||||
|
if finalize.src is not None:
|
||||||
|
self.write(finalize.src)
|
||||||
|
|
||||||
|
def _output_child_post(
|
||||||
|
self, node: nodes.Expr, frame: Frame, finalize: CodeGenerator._FinalizeInfo
|
||||||
|
) -> None:
|
||||||
|
if finalize.src is not None:
|
||||||
|
self.write(")")
|
||||||
|
|
||||||
|
|
||||||
|
class NativeEnvironment(Environment):
|
||||||
|
"""An environment that renders templates to native Python types."""
|
||||||
|
|
||||||
|
code_generator_class = NativeCodeGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class NativeTemplate(Template):
|
||||||
|
environment_class = NativeEnvironment
|
||||||
|
|
||||||
|
def render(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Render the template to produce a native Python type. If the
|
||||||
|
result is a single node, its value is returned. Otherwise, the
|
||||||
|
nodes are concatenated as strings. If the result can be parsed
|
||||||
|
with :func:`ast.literal_eval`, the parsed value is returned.
|
||||||
|
Otherwise, the string is returned.
|
||||||
|
"""
|
||||||
|
ctx = self.new_context(dict(*args, **kwargs))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return native_concat(self.root_render_func(ctx)) # type: ignore
|
||||||
|
except Exception:
|
||||||
|
return self.environment.handle_exception()
|
||||||
|
|
||||||
|
async def render_async(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
if not self.environment.is_async:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The environment was not created with async mode enabled."
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = self.new_context(dict(*args, **kwargs))
|
||||||
|
|
||||||
|
try:
|
||||||
|
return native_concat(
|
||||||
|
[n async for n in self.root_render_func(ctx)] # type: ignore
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return self.environment.handle_exception()
|
||||||
|
|
||||||
|
|
||||||
|
NativeEnvironment.template_class = NativeTemplate
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,47 @@
|
||||||
|
"""The optimizer tries to constant fold expressions and modify the AST
|
||||||
|
in place so that it should be faster to evaluate.
|
||||||
|
|
||||||
|
Because the AST does not contain all the scoping information and the
|
||||||
|
compiler has to find that out, we cannot do all the optimizations we
|
||||||
|
want. For example, loop unrolling doesn't work because unrolled loops
|
||||||
|
would have a different scope. The solution would be a second syntax tree
|
||||||
|
that stored the scoping rules.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from . import nodes
|
||||||
|
from .visitor import NodeTransformer
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def optimize(node: nodes.Node, environment: "Environment") -> nodes.Node:
|
||||||
|
"""The context hint can be used to perform an static optimization
|
||||||
|
based on the context given."""
|
||||||
|
optimizer = Optimizer(environment)
|
||||||
|
return t.cast(nodes.Node, optimizer.visit(node))
|
||||||
|
|
||||||
|
|
||||||
|
class Optimizer(NodeTransformer):
|
||||||
|
def __init__(self, environment: "t.Optional[Environment]") -> None:
|
||||||
|
self.environment = environment
|
||||||
|
|
||||||
|
def generic_visit(
|
||||||
|
self, node: nodes.Node, *args: t.Any, **kwargs: t.Any
|
||||||
|
) -> nodes.Node:
|
||||||
|
node = super().generic_visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
# Do constant folding. Some other nodes besides Expr have
|
||||||
|
# as_const, but folding them causes errors later on.
|
||||||
|
if isinstance(node, nodes.Expr):
|
||||||
|
try:
|
||||||
|
return nodes.Const.from_untrusted(
|
||||||
|
node.as_const(args[0] if args else None),
|
||||||
|
lineno=node.lineno,
|
||||||
|
environment=self.environment,
|
||||||
|
)
|
||||||
|
except nodes.Impossible:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return node
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,428 @@
|
||||||
|
"""A sandbox layer that ensures unsafe operations cannot be performed.
|
||||||
|
Useful when the template itself comes from an untrusted source.
|
||||||
|
"""
|
||||||
|
import operator
|
||||||
|
import types
|
||||||
|
import typing as t
|
||||||
|
from _string import formatter_field_name_split # type: ignore
|
||||||
|
from collections import abc
|
||||||
|
from collections import deque
|
||||||
|
from string import Formatter
|
||||||
|
|
||||||
|
from markupsafe import EscapeFormatter
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from .environment import Environment
|
||||||
|
from .exceptions import SecurityError
|
||||||
|
from .runtime import Context
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
#: maximum number of items a range may produce
|
||||||
|
MAX_RANGE = 100000
|
||||||
|
|
||||||
|
#: Unsafe function attributes.
|
||||||
|
UNSAFE_FUNCTION_ATTRIBUTES: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: Unsafe method attributes. Function attributes are unsafe for methods too.
|
||||||
|
UNSAFE_METHOD_ATTRIBUTES: t.Set[str] = set()
|
||||||
|
|
||||||
|
#: unsafe generator attributes.
|
||||||
|
UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
|
||||||
|
|
||||||
|
#: unsafe attributes on coroutines
|
||||||
|
UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
|
||||||
|
|
||||||
|
#: unsafe attributes on async generators
|
||||||
|
UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
|
||||||
|
|
||||||
|
_mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = (
|
||||||
|
(
|
||||||
|
abc.MutableSet,
|
||||||
|
frozenset(
|
||||||
|
[
|
||||||
|
"add",
|
||||||
|
"clear",
|
||||||
|
"difference_update",
|
||||||
|
"discard",
|
||||||
|
"pop",
|
||||||
|
"remove",
|
||||||
|
"symmetric_difference_update",
|
||||||
|
"update",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
abc.MutableMapping,
|
||||||
|
frozenset(["clear", "pop", "popitem", "setdefault", "update"]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
abc.MutableSequence,
|
||||||
|
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
deque,
|
||||||
|
frozenset(
|
||||||
|
[
|
||||||
|
"append",
|
||||||
|
"appendleft",
|
||||||
|
"clear",
|
||||||
|
"extend",
|
||||||
|
"extendleft",
|
||||||
|
"pop",
|
||||||
|
"popleft",
|
||||||
|
"remove",
|
||||||
|
"rotate",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_format_method(callable: t.Callable) -> t.Optional[str]:
|
||||||
|
if not isinstance(
|
||||||
|
callable, (types.MethodType, types.BuiltinMethodType)
|
||||||
|
) or callable.__name__ not in ("format", "format_map"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
obj = callable.__self__
|
||||||
|
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def safe_range(*args: int) -> range:
|
||||||
|
"""A range that can't generate ranges with a length of more than
|
||||||
|
MAX_RANGE items.
|
||||||
|
"""
|
||||||
|
rng = range(*args)
|
||||||
|
|
||||||
|
if len(rng) > MAX_RANGE:
|
||||||
|
raise OverflowError(
|
||||||
|
"Range too big. The sandbox blocks ranges larger than"
|
||||||
|
f" MAX_RANGE ({MAX_RANGE})."
|
||||||
|
)
|
||||||
|
|
||||||
|
return rng
|
||||||
|
|
||||||
|
|
||||||
|
def unsafe(f: F) -> F:
|
||||||
|
"""Marks a function or method as unsafe.
|
||||||
|
|
||||||
|
.. code-block: python
|
||||||
|
|
||||||
|
@unsafe
|
||||||
|
def delete(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
f.unsafe_callable = True # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def is_internal_attribute(obj: t.Any, attr: str) -> bool:
|
||||||
|
"""Test if the attribute given is an internal python attribute. For
|
||||||
|
example this function returns `True` for the `func_code` attribute of
|
||||||
|
python objects. This is useful if the environment method
|
||||||
|
:meth:`~SandboxedEnvironment.is_safe_attribute` is overridden.
|
||||||
|
|
||||||
|
>>> from jinja2.sandbox import is_internal_attribute
|
||||||
|
>>> is_internal_attribute(str, "mro")
|
||||||
|
True
|
||||||
|
>>> is_internal_attribute(str, "upper")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
if isinstance(obj, types.FunctionType):
|
||||||
|
if attr in UNSAFE_FUNCTION_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, types.MethodType):
|
||||||
|
if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, type):
|
||||||
|
if attr == "mro":
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):
|
||||||
|
return True
|
||||||
|
elif isinstance(obj, types.GeneratorType):
|
||||||
|
if attr in UNSAFE_GENERATOR_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):
|
||||||
|
if attr in UNSAFE_COROUTINE_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
elif hasattr(types, "AsyncGeneratorType") and isinstance(
|
||||||
|
obj, types.AsyncGeneratorType
|
||||||
|
):
|
||||||
|
if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
|
||||||
|
return True
|
||||||
|
return attr.startswith("__")
|
||||||
|
|
||||||
|
|
||||||
|
def modifies_known_mutable(obj: t.Any, attr: str) -> bool:
|
||||||
|
"""This function checks if an attribute on a builtin mutable object
|
||||||
|
(list, dict, set or deque) or the corresponding ABCs would modify it
|
||||||
|
if called.
|
||||||
|
|
||||||
|
>>> modifies_known_mutable({}, "clear")
|
||||||
|
True
|
||||||
|
>>> modifies_known_mutable({}, "keys")
|
||||||
|
False
|
||||||
|
>>> modifies_known_mutable([], "append")
|
||||||
|
True
|
||||||
|
>>> modifies_known_mutable([], "index")
|
||||||
|
False
|
||||||
|
|
||||||
|
If called with an unsupported object, ``False`` is returned.
|
||||||
|
|
||||||
|
>>> modifies_known_mutable("foo", "upper")
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
for typespec, unsafe in _mutable_spec:
|
||||||
|
if isinstance(obj, typespec):
|
||||||
|
return attr in unsafe
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedEnvironment(Environment):
|
||||||
|
"""The sandboxed environment. It works like the regular environment but
|
||||||
|
tells the compiler to generate sandboxed code. Additionally subclasses of
|
||||||
|
this environment may override the methods that tell the runtime what
|
||||||
|
attributes or functions are safe to access.
|
||||||
|
|
||||||
|
If the template tries to access insecure code a :exc:`SecurityError` is
|
||||||
|
raised. However also other exceptions may occur during the rendering so
|
||||||
|
the caller has to ensure that all exceptions are caught.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sandboxed = True
|
||||||
|
|
||||||
|
#: default callback table for the binary operators. A copy of this is
|
||||||
|
#: available on each instance of a sandboxed environment as
|
||||||
|
#: :attr:`binop_table`
|
||||||
|
default_binop_table: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
|
||||||
|
"+": operator.add,
|
||||||
|
"-": operator.sub,
|
||||||
|
"*": operator.mul,
|
||||||
|
"/": operator.truediv,
|
||||||
|
"//": operator.floordiv,
|
||||||
|
"**": operator.pow,
|
||||||
|
"%": operator.mod,
|
||||||
|
}
|
||||||
|
|
||||||
|
#: default callback table for the unary operators. A copy of this is
|
||||||
|
#: available on each instance of a sandboxed environment as
|
||||||
|
#: :attr:`unop_table`
|
||||||
|
default_unop_table: t.Dict[str, t.Callable[[t.Any], t.Any]] = {
|
||||||
|
"+": operator.pos,
|
||||||
|
"-": operator.neg,
|
||||||
|
}
|
||||||
|
|
||||||
|
#: a set of binary operators that should be intercepted. Each operator
|
||||||
|
#: that is added to this set (empty by default) is delegated to the
|
||||||
|
#: :meth:`call_binop` method that will perform the operator. The default
|
||||||
|
#: operator callback is specified by :attr:`binop_table`.
|
||||||
|
#:
|
||||||
|
#: The following binary operators are interceptable:
|
||||||
|
#: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**``
|
||||||
|
#:
|
||||||
|
#: The default operation form the operator table corresponds to the
|
||||||
|
#: builtin function. Intercepted calls are always slower than the native
|
||||||
|
#: operator call, so make sure only to intercept the ones you are
|
||||||
|
#: interested in.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.6
|
||||||
|
intercepted_binops: t.FrozenSet[str] = frozenset()
|
||||||
|
|
||||||
|
#: a set of unary operators that should be intercepted. Each operator
|
||||||
|
#: that is added to this set (empty by default) is delegated to the
|
||||||
|
#: :meth:`call_unop` method that will perform the operator. The default
|
||||||
|
#: operator callback is specified by :attr:`unop_table`.
|
||||||
|
#:
|
||||||
|
#: The following unary operators are interceptable: ``+``, ``-``
|
||||||
|
#:
|
||||||
|
#: The default operation form the operator table corresponds to the
|
||||||
|
#: builtin function. Intercepted calls are always slower than the native
|
||||||
|
#: operator call, so make sure only to intercept the ones you are
|
||||||
|
#: interested in.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.6
|
||||||
|
intercepted_unops: t.FrozenSet[str] = frozenset()
|
||||||
|
|
||||||
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.globals["range"] = safe_range
|
||||||
|
self.binop_table = self.default_binop_table.copy()
|
||||||
|
self.unop_table = self.default_unop_table.copy()
|
||||||
|
|
||||||
|
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
|
||||||
|
"""The sandboxed environment will call this method to check if the
|
||||||
|
attribute of an object is safe to access. Per default all attributes
|
||||||
|
starting with an underscore are considered private as well as the
|
||||||
|
special attributes of internal python objects as returned by the
|
||||||
|
:func:`is_internal_attribute` function.
|
||||||
|
"""
|
||||||
|
return not (attr.startswith("_") or is_internal_attribute(obj, attr))
|
||||||
|
|
||||||
|
def is_safe_callable(self, obj: t.Any) -> bool:
|
||||||
|
"""Check if an object is safely callable. By default callables
|
||||||
|
are considered safe unless decorated with :func:`unsafe`.
|
||||||
|
|
||||||
|
This also recognizes the Django convention of setting
|
||||||
|
``func.alters_data = True``.
|
||||||
|
"""
|
||||||
|
return not (
|
||||||
|
getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
def call_binop(
|
||||||
|
self, context: Context, operator: str, left: t.Any, right: t.Any
|
||||||
|
) -> t.Any:
|
||||||
|
"""For intercepted binary operator calls (:meth:`intercepted_binops`)
|
||||||
|
this function is executed instead of the builtin operator. This can
|
||||||
|
be used to fine tune the behavior of certain operators.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return self.binop_table[operator](left, right)
|
||||||
|
|
||||||
|
def call_unop(self, context: Context, operator: str, arg: t.Any) -> t.Any:
|
||||||
|
"""For intercepted unary operator calls (:meth:`intercepted_unops`)
|
||||||
|
this function is executed instead of the builtin operator. This can
|
||||||
|
be used to fine tune the behavior of certain operators.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return self.unop_table[operator](arg)
|
||||||
|
|
||||||
|
def getitem(
|
||||||
|
self, obj: t.Any, argument: t.Union[str, t.Any]
|
||||||
|
) -> t.Union[t.Any, Undefined]:
|
||||||
|
"""Subscribe an object from sandboxed code."""
|
||||||
|
try:
|
||||||
|
return obj[argument]
|
||||||
|
except (TypeError, LookupError):
|
||||||
|
if isinstance(argument, str):
|
||||||
|
try:
|
||||||
|
attr = str(argument)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = getattr(obj, attr)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if self.is_safe_attribute(obj, argument, value):
|
||||||
|
return value
|
||||||
|
return self.unsafe_undefined(obj, argument)
|
||||||
|
return self.undefined(obj=obj, name=argument)
|
||||||
|
|
||||||
|
def getattr(self, obj: t.Any, attribute: str) -> t.Union[t.Any, Undefined]:
|
||||||
|
"""Subscribe an object from sandboxed code and prefer the
|
||||||
|
attribute. The attribute passed *must* be a bytestring.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
value = getattr(obj, attribute)
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
return obj[attribute]
|
||||||
|
except (TypeError, LookupError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if self.is_safe_attribute(obj, attribute, value):
|
||||||
|
return value
|
||||||
|
return self.unsafe_undefined(obj, attribute)
|
||||||
|
return self.undefined(obj=obj, name=attribute)
|
||||||
|
|
||||||
|
def unsafe_undefined(self, obj: t.Any, attribute: str) -> Undefined:
|
||||||
|
"""Return an undefined object for unsafe attributes."""
|
||||||
|
return self.undefined(
|
||||||
|
f"access to attribute {attribute!r} of"
|
||||||
|
f" {type(obj).__name__!r} object is unsafe.",
|
||||||
|
name=attribute,
|
||||||
|
obj=obj,
|
||||||
|
exc=SecurityError,
|
||||||
|
)
|
||||||
|
|
||||||
|
def format_string(
|
||||||
|
self,
|
||||||
|
s: str,
|
||||||
|
args: t.Tuple[t.Any, ...],
|
||||||
|
kwargs: t.Dict[str, t.Any],
|
||||||
|
format_func: t.Optional[t.Callable] = None,
|
||||||
|
) -> str:
|
||||||
|
"""If a format call is detected, then this is routed through this
|
||||||
|
method so that our safety sandbox can be used for it.
|
||||||
|
"""
|
||||||
|
formatter: SandboxedFormatter
|
||||||
|
if isinstance(s, Markup):
|
||||||
|
formatter = SandboxedEscapeFormatter(self, escape=s.escape)
|
||||||
|
else:
|
||||||
|
formatter = SandboxedFormatter(self)
|
||||||
|
|
||||||
|
if format_func is not None and format_func.__name__ == "format_map":
|
||||||
|
if len(args) != 1 or kwargs:
|
||||||
|
raise TypeError(
|
||||||
|
"format_map() takes exactly one argument"
|
||||||
|
f" {len(args) + (kwargs is not None)} given"
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = args[0]
|
||||||
|
args = ()
|
||||||
|
|
||||||
|
rv = formatter.vformat(s, args, kwargs)
|
||||||
|
return type(s)(rv)
|
||||||
|
|
||||||
|
def call(
|
||||||
|
__self, # noqa: B902
|
||||||
|
__context: Context,
|
||||||
|
__obj: t.Any,
|
||||||
|
*args: t.Any,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Any:
|
||||||
|
"""Call an object from sandboxed code."""
|
||||||
|
fmt = inspect_format_method(__obj)
|
||||||
|
if fmt is not None:
|
||||||
|
return __self.format_string(fmt, args, kwargs, __obj)
|
||||||
|
|
||||||
|
# the double prefixes are to avoid double keyword argument
|
||||||
|
# errors when proxying the call.
|
||||||
|
if not __self.is_safe_callable(__obj):
|
||||||
|
raise SecurityError(f"{__obj!r} is not safely callable")
|
||||||
|
return __context.call(__obj, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ImmutableSandboxedEnvironment(SandboxedEnvironment):
|
||||||
|
"""Works exactly like the regular `SandboxedEnvironment` but does not
|
||||||
|
permit modifications on the builtin mutable objects `list`, `set`, and
|
||||||
|
`dict` by using the :func:`modifies_known_mutable` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
|
||||||
|
if not super().is_safe_attribute(obj, attr, value):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return not modifies_known_mutable(obj, attr)
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedFormatter(Formatter):
|
||||||
|
def __init__(self, env: Environment, **kwargs: t.Any) -> None:
|
||||||
|
self._env = env
|
||||||
|
super().__init__(**kwargs) # type: ignore
|
||||||
|
|
||||||
|
def get_field(
|
||||||
|
self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any]
|
||||||
|
) -> t.Tuple[t.Any, str]:
|
||||||
|
first, rest = formatter_field_name_split(field_name)
|
||||||
|
obj = self.get_value(first, args, kwargs)
|
||||||
|
for is_attr, i in rest:
|
||||||
|
if is_attr:
|
||||||
|
obj = self._env.getattr(obj, i)
|
||||||
|
else:
|
||||||
|
obj = self._env.getitem(obj, i)
|
||||||
|
return obj, first
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxedEscapeFormatter(SandboxedFormatter, EscapeFormatter):
|
||||||
|
pass
|
|
@ -0,0 +1,255 @@
|
||||||
|
"""Built-in template tests used with the ``is`` operator."""
|
||||||
|
import operator
|
||||||
|
import typing as t
|
||||||
|
from collections import abc
|
||||||
|
from numbers import Number
|
||||||
|
|
||||||
|
from .runtime import Undefined
|
||||||
|
from .utils import pass_environment
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from .environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
def test_odd(value: int) -> bool:
|
||||||
|
"""Return true if the variable is odd."""
|
||||||
|
return value % 2 == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_even(value: int) -> bool:
|
||||||
|
"""Return true if the variable is even."""
|
||||||
|
return value % 2 == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_divisibleby(value: int, num: int) -> bool:
|
||||||
|
"""Check if a variable is divisible by a number."""
|
||||||
|
return value % num == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_defined(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is defined:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% if variable is defined %}
|
||||||
|
value of variable: {{ variable }}
|
||||||
|
{% else %}
|
||||||
|
variable is not defined
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
See the :func:`default` filter for a simple way to set undefined
|
||||||
|
variables.
|
||||||
|
"""
|
||||||
|
return not isinstance(value, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
def test_undefined(value: t.Any) -> bool:
|
||||||
|
"""Like :func:`defined` but the other way round."""
|
||||||
|
return isinstance(value, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
@pass_environment
|
||||||
|
def test_filter(env: "Environment", value: str) -> bool:
|
||||||
|
"""Check if a filter exists by name. Useful if a filter may be
|
||||||
|
optionally available.
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% if 'markdown' is filter %}
|
||||||
|
{{ value | markdown }}
|
||||||
|
{% else %}
|
||||||
|
{{ value }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
return value in env.filters
|
||||||
|
|
||||||
|
|
||||||
|
@pass_environment
|
||||||
|
def test_test(env: "Environment", value: str) -> bool:
|
||||||
|
"""Check if a test exists by name. Useful if a test may be
|
||||||
|
optionally available.
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% if 'loud' is test %}
|
||||||
|
{% if value is loud %}
|
||||||
|
{{ value|upper }}
|
||||||
|
{% else %}
|
||||||
|
{{ value|lower }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{{ value }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
"""
|
||||||
|
return value in env.tests
|
||||||
|
|
||||||
|
|
||||||
|
def test_none(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is none."""
|
||||||
|
return value is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_boolean(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a boolean value.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is True or value is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_false(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is False.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_true(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is True.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return value is True
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: The existing 'number' test matches booleans and floats
|
||||||
|
def test_integer(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is an integer.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return isinstance(value, int) and value is not True and value is not False
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: The existing 'number' test matches booleans and integers
|
||||||
|
def test_float(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a float.
|
||||||
|
|
||||||
|
.. versionadded:: 2.11
|
||||||
|
"""
|
||||||
|
return isinstance(value, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lower(value: str) -> bool:
|
||||||
|
"""Return true if the variable is lowercased."""
|
||||||
|
return str(value).islower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_upper(value: str) -> bool:
|
||||||
|
"""Return true if the variable is uppercased."""
|
||||||
|
return str(value).isupper()
|
||||||
|
|
||||||
|
|
||||||
|
def test_string(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a string."""
|
||||||
|
return isinstance(value, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mapping(value: t.Any) -> bool:
|
||||||
|
"""Return true if the object is a mapping (dict etc.).
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
return isinstance(value, abc.Mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def test_number(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is a number."""
|
||||||
|
return isinstance(value, Number)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sequence(value: t.Any) -> bool:
|
||||||
|
"""Return true if the variable is a sequence. Sequences are variables
|
||||||
|
that are iterable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
len(value)
|
||||||
|
value.__getitem__
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_sameas(value: t.Any, other: t.Any) -> bool:
|
||||||
|
"""Check if an object points to the same memory address than another
|
||||||
|
object:
|
||||||
|
|
||||||
|
.. sourcecode:: jinja
|
||||||
|
|
||||||
|
{% if foo.attribute is sameas false %}
|
||||||
|
the foo attribute really is the `False` singleton
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
return value is other
|
||||||
|
|
||||||
|
|
||||||
|
def test_iterable(value: t.Any) -> bool:
|
||||||
|
"""Check if it's possible to iterate over an object."""
|
||||||
|
try:
|
||||||
|
iter(value)
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_escaped(value: t.Any) -> bool:
|
||||||
|
"""Check if the value is escaped."""
|
||||||
|
return hasattr(value, "__html__")
|
||||||
|
|
||||||
|
|
||||||
|
def test_in(value: t.Any, seq: t.Container) -> bool:
|
||||||
|
"""Check if value is in seq.
|
||||||
|
|
||||||
|
.. versionadded:: 2.10
|
||||||
|
"""
|
||||||
|
return value in seq
|
||||||
|
|
||||||
|
|
||||||
|
TESTS = {
|
||||||
|
"odd": test_odd,
|
||||||
|
"even": test_even,
|
||||||
|
"divisibleby": test_divisibleby,
|
||||||
|
"defined": test_defined,
|
||||||
|
"undefined": test_undefined,
|
||||||
|
"filter": test_filter,
|
||||||
|
"test": test_test,
|
||||||
|
"none": test_none,
|
||||||
|
"boolean": test_boolean,
|
||||||
|
"false": test_false,
|
||||||
|
"true": test_true,
|
||||||
|
"integer": test_integer,
|
||||||
|
"float": test_float,
|
||||||
|
"lower": test_lower,
|
||||||
|
"upper": test_upper,
|
||||||
|
"string": test_string,
|
||||||
|
"mapping": test_mapping,
|
||||||
|
"number": test_number,
|
||||||
|
"sequence": test_sequence,
|
||||||
|
"iterable": test_iterable,
|
||||||
|
"callable": callable,
|
||||||
|
"sameas": test_sameas,
|
||||||
|
"escaped": test_escaped,
|
||||||
|
"in": test_in,
|
||||||
|
"==": operator.eq,
|
||||||
|
"eq": operator.eq,
|
||||||
|
"equalto": operator.eq,
|
||||||
|
"!=": operator.ne,
|
||||||
|
"ne": operator.ne,
|
||||||
|
">": operator.gt,
|
||||||
|
"gt": operator.gt,
|
||||||
|
"greaterthan": operator.gt,
|
||||||
|
"ge": operator.ge,
|
||||||
|
">=": operator.ge,
|
||||||
|
"<": operator.lt,
|
||||||
|
"lt": operator.lt,
|
||||||
|
"lessthan": operator.lt,
|
||||||
|
"<=": operator.le,
|
||||||
|
"le": operator.le,
|
||||||
|
}
|
|
@ -0,0 +1,854 @@
|
||||||
|
import enum
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
|
import warnings
|
||||||
|
from collections import abc
|
||||||
|
from collections import deque
|
||||||
|
from random import choice
|
||||||
|
from random import randrange
|
||||||
|
from threading import Lock
|
||||||
|
from types import CodeType
|
||||||
|
from urllib.parse import quote_from_bytes
|
||||||
|
|
||||||
|
import markupsafe
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
# special singleton representing missing values for the runtime
|
||||||
|
missing: t.Any = type("MissingType", (), {"__repr__": lambda x: "missing"})()
|
||||||
|
|
||||||
|
internal_code: t.MutableSet[CodeType] = set()
|
||||||
|
|
||||||
|
concat = "".join
|
||||||
|
|
||||||
|
|
||||||
|
def pass_context(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.runtime.Context` as the first argument
|
||||||
|
to the decorated function when called while rendering a template.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
If only ``Context.eval_context`` is needed, use
|
||||||
|
:func:`pass_eval_context`. If only ``Context.environment`` is
|
||||||
|
needed, use :func:`pass_environment`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``contextfunction`` and ``contextfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.context # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def pass_eval_context(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.nodes.EvalContext` as the first argument
|
||||||
|
to the decorated function when called while rendering a template.
|
||||||
|
See :ref:`eval-context`.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
If only ``EvalContext.environment`` is needed, use
|
||||||
|
:func:`pass_environment`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``evalcontextfunction`` and ``evalcontextfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.eval_context # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def pass_environment(f: F) -> F:
|
||||||
|
"""Pass the :class:`~jinja2.Environment` as the first argument to
|
||||||
|
the decorated function when called while rendering a template.
|
||||||
|
|
||||||
|
Can be used on functions, filters, and tests.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0.0
|
||||||
|
Replaces ``environmentfunction`` and ``environmentfilter``.
|
||||||
|
"""
|
||||||
|
f.jinja_pass_arg = _PassArg.environment # type: ignore
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
class _PassArg(enum.Enum):
|
||||||
|
context = enum.auto()
|
||||||
|
eval_context = enum.auto()
|
||||||
|
environment = enum.auto()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_obj(cls, obj: F) -> t.Optional["_PassArg"]:
|
||||||
|
if hasattr(obj, "jinja_pass_arg"):
|
||||||
|
return obj.jinja_pass_arg # type: ignore
|
||||||
|
|
||||||
|
for prefix in "context", "eval_context", "environment":
|
||||||
|
squashed = prefix.replace("_", "")
|
||||||
|
|
||||||
|
for name in f"{squashed}function", f"{squashed}filter":
|
||||||
|
if getattr(obj, name, False) is True:
|
||||||
|
warnings.warn(
|
||||||
|
f"{name!r} is deprecated and will stop working"
|
||||||
|
f" in Jinja 3.1. Use 'pass_{prefix}' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return cls[prefix]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def contextfunction(f: F) -> F:
|
||||||
|
"""Pass the context as the first argument to the decorated function.
|
||||||
|
|
||||||
|
.. deprecated:: 3.0
|
||||||
|
Will be removed in Jinja 3.1. Use :func:`~jinja2.pass_context`
|
||||||
|
instead.
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"'contextfunction' is renamed to 'pass_context', the old name"
|
||||||
|
" will be removed in Jinja 3.1.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return pass_context(f)
|
||||||
|
|
||||||
|
|
||||||
|
def evalcontextfunction(f: F) -> F:
|
||||||
|
"""Pass the eval context as the first argument to the decorated
|
||||||
|
function.
|
||||||
|
|
||||||
|
.. deprecated:: 3.0
|
||||||
|
Will be removed in Jinja 3.1. Use
|
||||||
|
:func:`~jinja2.pass_eval_context` instead.
|
||||||
|
|
||||||
|
.. versionadded:: 2.4
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"'evalcontextfunction' is renamed to 'pass_eval_context', the"
|
||||||
|
" old name will be removed in Jinja 3.1.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return pass_eval_context(f)
|
||||||
|
|
||||||
|
|
||||||
|
def environmentfunction(f: F) -> F:
|
||||||
|
"""Pass the environment as the first argument to the decorated
|
||||||
|
function.
|
||||||
|
|
||||||
|
.. deprecated:: 3.0
|
||||||
|
Will be removed in Jinja 3.1. Use
|
||||||
|
:func:`~jinja2.pass_environment` instead.
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"'environmentfunction' is renamed to 'pass_environment', the"
|
||||||
|
" old name will be removed in Jinja 3.1.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return pass_environment(f)
|
||||||
|
|
||||||
|
|
||||||
|
def internalcode(f: F) -> F:
|
||||||
|
"""Marks the function as internally used"""
|
||||||
|
internal_code.add(f.__code__)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def is_undefined(obj: t.Any) -> bool:
|
||||||
|
"""Check if the object passed is undefined. This does nothing more than
|
||||||
|
performing an instance check against :class:`Undefined` but looks nicer.
|
||||||
|
This can be used for custom filters or tests that want to react to
|
||||||
|
undefined variables. For example a custom default filter can look like
|
||||||
|
this::
|
||||||
|
|
||||||
|
def default(var, default=''):
|
||||||
|
if is_undefined(var):
|
||||||
|
return default
|
||||||
|
return var
|
||||||
|
"""
|
||||||
|
from .runtime import Undefined
|
||||||
|
|
||||||
|
return isinstance(obj, Undefined)
|
||||||
|
|
||||||
|
|
||||||
|
def consume(iterable: t.Iterable[t.Any]) -> None:
|
||||||
|
"""Consumes an iterable without doing anything with it."""
|
||||||
|
for _ in iterable:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def clear_caches() -> None:
|
||||||
|
"""Jinja keeps internal caches for environments and lexers. These are
|
||||||
|
used so that Jinja doesn't have to recreate environments and lexers all
|
||||||
|
the time. Normally you don't have to care about that but if you are
|
||||||
|
measuring memory consumption you may want to clean the caches.
|
||||||
|
"""
|
||||||
|
from .environment import get_spontaneous_environment
|
||||||
|
from .lexer import _lexer_cache
|
||||||
|
|
||||||
|
get_spontaneous_environment.cache_clear()
|
||||||
|
_lexer_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def import_string(import_name: str, silent: bool = False) -> t.Any:
|
||||||
|
"""Imports an object based on a string. This is useful if you want to
|
||||||
|
use import paths as endpoints or something similar. An import path can
|
||||||
|
be specified either in dotted notation (``xml.sax.saxutils.escape``)
|
||||||
|
or with a colon as object delimiter (``xml.sax.saxutils:escape``).
|
||||||
|
|
||||||
|
If the `silent` is True the return value will be `None` if the import
|
||||||
|
fails.
|
||||||
|
|
||||||
|
:return: imported object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if ":" in import_name:
|
||||||
|
module, obj = import_name.split(":", 1)
|
||||||
|
elif "." in import_name:
|
||||||
|
module, _, obj = import_name.rpartition(".")
|
||||||
|
else:
|
||||||
|
return __import__(import_name)
|
||||||
|
return getattr(__import__(module, None, None, [obj]), obj)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
if not silent:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def open_if_exists(filename: str, mode: str = "rb") -> t.Optional[t.IO]:
|
||||||
|
"""Returns a file descriptor for the filename if that file exists,
|
||||||
|
otherwise ``None``.
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return open(filename, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def object_type_repr(obj: t.Any) -> str:
|
||||||
|
"""Returns the name of the object's type. For some recognized
|
||||||
|
singletons the name of the object is returned instead. (For
|
||||||
|
example for `None` and `Ellipsis`).
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
return "None"
|
||||||
|
elif obj is Ellipsis:
|
||||||
|
return "Ellipsis"
|
||||||
|
|
||||||
|
cls = type(obj)
|
||||||
|
|
||||||
|
if cls.__module__ == "builtins":
|
||||||
|
return f"{cls.__name__} object"
|
||||||
|
|
||||||
|
return f"{cls.__module__}.{cls.__name__} object"
|
||||||
|
|
||||||
|
|
||||||
|
def pformat(obj: t.Any) -> str:
|
||||||
|
"""Format an object using :func:`pprint.pformat`."""
|
||||||
|
from pprint import pformat # type: ignore
|
||||||
|
|
||||||
|
return pformat(obj)
|
||||||
|
|
||||||
|
|
||||||
|
_http_re = re.compile(
|
||||||
|
r"""
|
||||||
|
^
|
||||||
|
(
|
||||||
|
(https?://|www\.) # scheme or www
|
||||||
|
(([\w%-]+\.)+)? # subdomain
|
||||||
|
(
|
||||||
|
[a-z]{2,63} # basic tld
|
||||||
|
|
|
||||||
|
xn--[\w%]{2,59} # idna tld
|
||||||
|
)
|
||||||
|
|
|
||||||
|
([\w%-]{2,63}\.)+ # basic domain
|
||||||
|
(com|net|int|edu|gov|org|info|mil) # basic tld
|
||||||
|
|
|
||||||
|
(https?://) # scheme
|
||||||
|
(
|
||||||
|
(([\d]{1,3})(\.[\d]{1,3}){3}) # IPv4
|
||||||
|
|
|
||||||
|
(\[([\da-f]{0,4}:){2}([\da-f]{0,4}:?){1,6}]) # IPv6
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(?::[\d]{1,5})? # port
|
||||||
|
(?:[/?#]\S*)? # path, query, and fragment
|
||||||
|
$
|
||||||
|
""",
|
||||||
|
re.IGNORECASE | re.VERBOSE,
|
||||||
|
)
|
||||||
|
_email_re = re.compile(r"^\S+@\w[\w.-]*\.\w+$")
|
||||||
|
|
||||||
|
|
||||||
|
def urlize(
|
||||||
|
text: str,
|
||||||
|
trim_url_limit: t.Optional[int] = None,
|
||||||
|
rel: t.Optional[str] = None,
|
||||||
|
target: t.Optional[str] = None,
|
||||||
|
extra_schemes: t.Optional[t.Iterable[str]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Convert URLs in text into clickable links.
|
||||||
|
|
||||||
|
This may not recognize links in some situations. Usually, a more
|
||||||
|
comprehensive formatter, such as a Markdown library, is a better
|
||||||
|
choice.
|
||||||
|
|
||||||
|
Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email
|
||||||
|
addresses. Links with trailing punctuation (periods, commas, closing
|
||||||
|
parentheses) and leading punctuation (opening parentheses) are
|
||||||
|
recognized excluding the punctuation. Email addresses that include
|
||||||
|
header fields are not recognized (for example,
|
||||||
|
``mailto:address@example.com?cc=copy@example.com``).
|
||||||
|
|
||||||
|
:param text: Original text containing URLs to link.
|
||||||
|
:param trim_url_limit: Shorten displayed URL values to this length.
|
||||||
|
:param target: Add the ``target`` attribute to links.
|
||||||
|
:param rel: Add the ``rel`` attribute to links.
|
||||||
|
:param extra_schemes: Recognize URLs that start with these schemes
|
||||||
|
in addition to the default behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The ``extra_schemes`` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Generate ``https://`` links for URLs without a scheme.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The parsing rules were updated. Recognize email addresses with
|
||||||
|
or without the ``mailto:`` scheme. Validate IP addresses. Ignore
|
||||||
|
parentheses and brackets in more cases.
|
||||||
|
"""
|
||||||
|
if trim_url_limit is not None:
|
||||||
|
|
||||||
|
def trim_url(x: str) -> str:
|
||||||
|
if len(x) > trim_url_limit: # type: ignore
|
||||||
|
return f"{x[:trim_url_limit]}..."
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def trim_url(x: str) -> str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
words = re.split(r"(\s+)", str(markupsafe.escape(text)))
|
||||||
|
rel_attr = f' rel="{markupsafe.escape(rel)}"' if rel else ""
|
||||||
|
target_attr = f' target="{markupsafe.escape(target)}"' if target else ""
|
||||||
|
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
head, middle, tail = "", word, ""
|
||||||
|
match = re.match(r"^([(<]|<)+", middle)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
head = match.group()
|
||||||
|
middle = middle[match.end() :]
|
||||||
|
|
||||||
|
# Unlike lead, which is anchored to the start of the string,
|
||||||
|
# need to check that the string ends with any of the characters
|
||||||
|
# before trying to match all of them, to avoid backtracking.
|
||||||
|
if middle.endswith((")", ">", ".", ",", "\n", ">")):
|
||||||
|
match = re.search(r"([)>.,\n]|>)+$", middle)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
tail = match.group()
|
||||||
|
middle = middle[: match.start()]
|
||||||
|
|
||||||
|
# Prefer balancing parentheses in URLs instead of ignoring a
|
||||||
|
# trailing character.
|
||||||
|
for start_char, end_char in ("(", ")"), ("<", ">"), ("<", ">"):
|
||||||
|
start_count = middle.count(start_char)
|
||||||
|
|
||||||
|
if start_count <= middle.count(end_char):
|
||||||
|
# Balanced, or lighter on the left
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Move as many as possible from the tail to balance
|
||||||
|
for _ in range(min(start_count, tail.count(end_char))):
|
||||||
|
end_index = tail.index(end_char) + len(end_char)
|
||||||
|
# Move anything in the tail before the end char too
|
||||||
|
middle += tail[:end_index]
|
||||||
|
tail = tail[end_index:]
|
||||||
|
|
||||||
|
if _http_re.match(middle):
|
||||||
|
if middle.startswith("https://") or middle.startswith("http://"):
|
||||||
|
middle = (
|
||||||
|
f'<a href="{middle}"{rel_attr}{target_attr}>{trim_url(middle)}</a>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
middle = (
|
||||||
|
f'<a href="https://{middle}"{rel_attr}{target_attr}>'
|
||||||
|
f"{trim_url(middle)}</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif middle.startswith("mailto:") and _email_re.match(middle[7:]):
|
||||||
|
middle = f'<a href="{middle}">{middle[7:]}</a>'
|
||||||
|
|
||||||
|
elif (
|
||||||
|
"@" in middle
|
||||||
|
and not middle.startswith("www.")
|
||||||
|
and ":" not in middle
|
||||||
|
and _email_re.match(middle)
|
||||||
|
):
|
||||||
|
middle = f'<a href="mailto:{middle}">{middle}</a>'
|
||||||
|
|
||||||
|
elif extra_schemes is not None:
|
||||||
|
for scheme in extra_schemes:
|
||||||
|
if middle != scheme and middle.startswith(scheme):
|
||||||
|
middle = f'<a href="{middle}"{rel_attr}{target_attr}>{middle}</a>'
|
||||||
|
|
||||||
|
words[i] = f"{head}{middle}{tail}"
|
||||||
|
|
||||||
|
return "".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_lorem_ipsum(
|
||||||
|
n: int = 5, html: bool = True, min: int = 20, max: int = 100
|
||||||
|
) -> str:
|
||||||
|
"""Generate some lorem ipsum for the template."""
|
||||||
|
from .constants import LOREM_IPSUM_WORDS
|
||||||
|
|
||||||
|
words = LOREM_IPSUM_WORDS.split()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for _ in range(n):
|
||||||
|
next_capitalized = True
|
||||||
|
last_comma = last_fullstop = 0
|
||||||
|
word = None
|
||||||
|
last = None
|
||||||
|
p = []
|
||||||
|
|
||||||
|
# each paragraph contains out of 20 to 100 words.
|
||||||
|
for idx, _ in enumerate(range(randrange(min, max))):
|
||||||
|
while True:
|
||||||
|
word = choice(words)
|
||||||
|
if word != last:
|
||||||
|
last = word
|
||||||
|
break
|
||||||
|
if next_capitalized:
|
||||||
|
word = word.capitalize()
|
||||||
|
next_capitalized = False
|
||||||
|
# add commas
|
||||||
|
if idx - randrange(3, 8) > last_comma:
|
||||||
|
last_comma = idx
|
||||||
|
last_fullstop += 2
|
||||||
|
word += ","
|
||||||
|
# add end of sentences
|
||||||
|
if idx - randrange(10, 20) > last_fullstop:
|
||||||
|
last_comma = last_fullstop = idx
|
||||||
|
word += "."
|
||||||
|
next_capitalized = True
|
||||||
|
p.append(word)
|
||||||
|
|
||||||
|
# ensure that the paragraph ends with a dot.
|
||||||
|
p_str = " ".join(p)
|
||||||
|
|
||||||
|
if p_str.endswith(","):
|
||||||
|
p_str = p_str[:-1] + "."
|
||||||
|
elif not p_str.endswith("."):
|
||||||
|
p_str += "."
|
||||||
|
|
||||||
|
result.append(p_str)
|
||||||
|
|
||||||
|
if not html:
|
||||||
|
return "\n\n".join(result)
|
||||||
|
return markupsafe.Markup(
|
||||||
|
"\n".join(f"<p>{markupsafe.escape(x)}</p>" for x in result)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
|
||||||
|
"""Quote a string for use in a URL using the given charset.
|
||||||
|
|
||||||
|
:param obj: String or bytes to quote. Other types are converted to
|
||||||
|
string then encoded to bytes using the given charset.
|
||||||
|
:param charset: Encode text to bytes using this charset.
|
||||||
|
:param for_qs: Quote "/" and use "+" for spaces.
|
||||||
|
"""
|
||||||
|
if not isinstance(obj, bytes):
|
||||||
|
if not isinstance(obj, str):
|
||||||
|
obj = str(obj)
|
||||||
|
|
||||||
|
obj = obj.encode(charset)
|
||||||
|
|
||||||
|
safe = b"" if for_qs else b"/"
|
||||||
|
rv = quote_from_bytes(obj, safe)
|
||||||
|
|
||||||
|
if for_qs:
|
||||||
|
rv = rv.replace("%20", "+")
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def unicode_urlencode(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'unicode_urlencode' has been renamed to 'url_quote'. The old"
|
||||||
|
" name will be removed in Jinja 3.1.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return url_quote(obj, charset=charset, for_qs=for_qs)
|
||||||
|
|
||||||
|
|
||||||
|
@abc.MutableMapping.register
|
||||||
|
class LRUCache:
|
||||||
|
"""A simple LRU Cache implementation."""
|
||||||
|
|
||||||
|
# this is fast for small capacities (something below 1000) but doesn't
|
||||||
|
# scale. But as long as it's only used as storage for templates this
|
||||||
|
# won't do any harm.
|
||||||
|
|
||||||
|
def __init__(self, capacity: int) -> None:
|
||||||
|
self.capacity = capacity
|
||||||
|
self._mapping: t.Dict[t.Any, t.Any] = {}
|
||||||
|
self._queue: "te.Deque[t.Any]" = deque()
|
||||||
|
self._postinit()
|
||||||
|
|
||||||
|
def _postinit(self) -> None:
|
||||||
|
# alias all queue methods for faster lookup
|
||||||
|
self._popleft = self._queue.popleft
|
||||||
|
self._pop = self._queue.pop
|
||||||
|
self._remove = self._queue.remove
|
||||||
|
self._wlock = Lock()
|
||||||
|
self._append = self._queue.append
|
||||||
|
|
||||||
|
def __getstate__(self) -> t.Mapping[str, t.Any]:
|
||||||
|
return {
|
||||||
|
"capacity": self.capacity,
|
||||||
|
"_mapping": self._mapping,
|
||||||
|
"_queue": self._queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __setstate__(self, d: t.Mapping[str, t.Any]) -> None:
|
||||||
|
self.__dict__.update(d)
|
||||||
|
self._postinit()
|
||||||
|
|
||||||
|
def __getnewargs__(self) -> t.Tuple:
|
||||||
|
return (self.capacity,)
|
||||||
|
|
||||||
|
def copy(self) -> "LRUCache":
|
||||||
|
"""Return a shallow copy of the instance."""
|
||||||
|
rv = self.__class__(self.capacity)
|
||||||
|
rv._mapping.update(self._mapping)
|
||||||
|
rv._queue.extend(self._queue)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def get(self, key: t.Any, default: t.Any = None) -> t.Any:
|
||||||
|
"""Return an item from the cache dict or `default`"""
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def setdefault(self, key: t.Any, default: t.Any = None) -> t.Any:
|
||||||
|
"""Set `default` if the key is not in the cache otherwise
|
||||||
|
leave unchanged. Return the value of this key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self[key]
|
||||||
|
except KeyError:
|
||||||
|
self[key] = default
|
||||||
|
return default
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear the cache."""
|
||||||
|
with self._wlock:
|
||||||
|
self._mapping.clear()
|
||||||
|
self._queue.clear()
|
||||||
|
|
||||||
|
def __contains__(self, key: t.Any) -> bool:
|
||||||
|
"""Check if a key exists in this cache."""
|
||||||
|
return key in self._mapping
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Return the current size of the cache."""
|
||||||
|
return len(self._mapping)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{type(self).__name__} {self._mapping!r}>"
|
||||||
|
|
||||||
|
def __getitem__(self, key: t.Any) -> t.Any:
|
||||||
|
"""Get an item from the cache. Moves the item up so that it has the
|
||||||
|
highest priority then.
|
||||||
|
|
||||||
|
Raise a `KeyError` if it does not exist.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
rv = self._mapping[key]
|
||||||
|
|
||||||
|
if self._queue[-1] != key:
|
||||||
|
try:
|
||||||
|
self._remove(key)
|
||||||
|
except ValueError:
|
||||||
|
# if something removed the key from the container
|
||||||
|
# when we read, ignore the ValueError that we would
|
||||||
|
# get otherwise.
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._append(key)
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __setitem__(self, key: t.Any, value: t.Any) -> None:
|
||||||
|
"""Sets the value for an item. Moves the item up so that it
|
||||||
|
has the highest priority then.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
if key in self._mapping:
|
||||||
|
self._remove(key)
|
||||||
|
elif len(self._mapping) == self.capacity:
|
||||||
|
del self._mapping[self._popleft()]
|
||||||
|
|
||||||
|
self._append(key)
|
||||||
|
self._mapping[key] = value
|
||||||
|
|
||||||
|
def __delitem__(self, key: t.Any) -> None:
|
||||||
|
"""Remove an item from the cache dict.
|
||||||
|
Raise a `KeyError` if it does not exist.
|
||||||
|
"""
|
||||||
|
with self._wlock:
|
||||||
|
del self._mapping[key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._remove(key)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def items(self) -> t.Iterable[t.Tuple[t.Any, t.Any]]:
|
||||||
|
"""Return a list of items."""
|
||||||
|
result = [(key, self._mapping[key]) for key in list(self._queue)]
|
||||||
|
result.reverse()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def values(self) -> t.Iterable[t.Any]:
|
||||||
|
"""Return a list of all values."""
|
||||||
|
return [x[1] for x in self.items()]
|
||||||
|
|
||||||
|
def keys(self) -> t.Iterable[t.Any]:
|
||||||
|
"""Return a list of all keys ordered by most recent usage."""
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
def __iter__(self) -> t.Iterator[t.Any]:
|
||||||
|
return reversed(tuple(self._queue))
|
||||||
|
|
||||||
|
def __reversed__(self) -> t.Iterator[t.Any]:
|
||||||
|
"""Iterate over the keys in the cache dict, oldest items
|
||||||
|
coming first.
|
||||||
|
"""
|
||||||
|
return iter(tuple(self._queue))
|
||||||
|
|
||||||
|
__copy__ = copy
|
||||||
|
|
||||||
|
|
||||||
|
def select_autoescape(
|
||||||
|
enabled_extensions: t.Collection[str] = ("html", "htm", "xml"),
|
||||||
|
disabled_extensions: t.Collection[str] = (),
|
||||||
|
default_for_string: bool = True,
|
||||||
|
default: bool = False,
|
||||||
|
) -> t.Callable[[t.Optional[str]], bool]:
|
||||||
|
"""Intelligently sets the initial value of autoescaping based on the
|
||||||
|
filename of the template. This is the recommended way to configure
|
||||||
|
autoescaping if you do not want to write a custom function yourself.
|
||||||
|
|
||||||
|
If you want to enable it for all templates created from strings or
|
||||||
|
for all templates with `.html` and `.xml` extensions::
|
||||||
|
|
||||||
|
from jinja2 import Environment, select_autoescape
|
||||||
|
env = Environment(autoescape=select_autoescape(
|
||||||
|
enabled_extensions=('html', 'xml'),
|
||||||
|
default_for_string=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
Example configuration to turn it on at all times except if the template
|
||||||
|
ends with `.txt`::
|
||||||
|
|
||||||
|
from jinja2 import Environment, select_autoescape
|
||||||
|
env = Environment(autoescape=select_autoescape(
|
||||||
|
disabled_extensions=('txt',),
|
||||||
|
default_for_string=True,
|
||||||
|
default=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
The `enabled_extensions` is an iterable of all the extensions that
|
||||||
|
autoescaping should be enabled for. Likewise `disabled_extensions` is
|
||||||
|
a list of all templates it should be disabled for. If a template is
|
||||||
|
loaded from a string then the default from `default_for_string` is used.
|
||||||
|
If nothing matches then the initial value of autoescaping is set to the
|
||||||
|
value of `default`.
|
||||||
|
|
||||||
|
For security reasons this function operates case insensitive.
|
||||||
|
|
||||||
|
.. versionadded:: 2.9
|
||||||
|
"""
|
||||||
|
enabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in enabled_extensions)
|
||||||
|
disabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in disabled_extensions)
|
||||||
|
|
||||||
|
def autoescape(template_name: t.Optional[str]) -> bool:
|
||||||
|
if template_name is None:
|
||||||
|
return default_for_string
|
||||||
|
template_name = template_name.lower()
|
||||||
|
if template_name.endswith(enabled_patterns):
|
||||||
|
return True
|
||||||
|
if template_name.endswith(disabled_patterns):
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
return autoescape
|
||||||
|
|
||||||
|
|
||||||
|
def htmlsafe_json_dumps(
|
||||||
|
obj: t.Any, dumps: t.Optional[t.Callable[..., str]] = None, **kwargs: t.Any
|
||||||
|
) -> markupsafe.Markup:
|
||||||
|
"""Serialize an object to a string of JSON with :func:`json.dumps`,
|
||||||
|
then replace HTML-unsafe characters with Unicode escapes and mark
|
||||||
|
the result safe with :class:`~markupsafe.Markup`.
|
||||||
|
|
||||||
|
This is available in templates as the ``|tojson`` filter.
|
||||||
|
|
||||||
|
The following characters are escaped: ``<``, ``>``, ``&``, ``'``.
|
||||||
|
|
||||||
|
The returned string is safe to render in HTML documents and
|
||||||
|
``<script>`` tags. The exception is in HTML attributes that are
|
||||||
|
double quoted; either use single quotes or the ``|forceescape``
|
||||||
|
filter.
|
||||||
|
|
||||||
|
:param obj: The object to serialize to JSON.
|
||||||
|
:param dumps: The ``dumps`` function to use. Defaults to
|
||||||
|
``env.policies["json.dumps_function"]``, which defaults to
|
||||||
|
:func:`json.dumps`.
|
||||||
|
:param kwargs: Extra arguments to pass to ``dumps``. Merged onto
|
||||||
|
``env.policies["json.dumps_kwargs"]``.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The ``dumper`` parameter is renamed to ``dumps``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.9
|
||||||
|
"""
|
||||||
|
if dumps is None:
|
||||||
|
dumps = json.dumps
|
||||||
|
|
||||||
|
return markupsafe.Markup(
|
||||||
|
dumps(obj, **kwargs)
|
||||||
|
.replace("<", "\\u003c")
|
||||||
|
.replace(">", "\\u003e")
|
||||||
|
.replace("&", "\\u0026")
|
||||||
|
.replace("'", "\\u0027")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Cycler:
|
||||||
|
"""Cycle through values by yield them one at a time, then restarting
|
||||||
|
once the end is reached. Available as ``cycler`` in templates.
|
||||||
|
|
||||||
|
Similar to ``loop.cycle``, but can be used outside loops or across
|
||||||
|
multiple loops. For example, render a list of folders and files in a
|
||||||
|
list, alternating giving them "odd" and "even" classes.
|
||||||
|
|
||||||
|
.. code-block:: html+jinja
|
||||||
|
|
||||||
|
{% set row_class = cycler("odd", "even") %}
|
||||||
|
<ul class="browser">
|
||||||
|
{% for folder in folders %}
|
||||||
|
<li class="folder {{ row_class.next() }}">{{ folder }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for file in files %}
|
||||||
|
<li class="file {{ row_class.next() }}">{{ file }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
:param items: Each positional argument will be yielded in the order
|
||||||
|
given for each cycle.
|
||||||
|
|
||||||
|
.. versionadded:: 2.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *items: t.Any) -> None:
|
||||||
|
if not items:
|
||||||
|
raise RuntimeError("at least one item has to be provided")
|
||||||
|
self.items = items
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Resets the current item to the first item."""
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> t.Any:
|
||||||
|
"""Return the current item. Equivalent to the item that will be
|
||||||
|
returned next time :meth:`next` is called.
|
||||||
|
"""
|
||||||
|
return self.items[self.pos]
|
||||||
|
|
||||||
|
def next(self) -> t.Any:
|
||||||
|
"""Return the current item, then advance :attr:`current` to the
|
||||||
|
next item.
|
||||||
|
"""
|
||||||
|
rv = self.current
|
||||||
|
self.pos = (self.pos + 1) % len(self.items)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
__next__ = next
|
||||||
|
|
||||||
|
|
||||||
|
class Joiner:
|
||||||
|
"""A joining helper for templates."""
|
||||||
|
|
||||||
|
def __init__(self, sep: str = ", ") -> None:
|
||||||
|
self.sep = sep
|
||||||
|
self.used = False
|
||||||
|
|
||||||
|
def __call__(self) -> str:
|
||||||
|
if not self.used:
|
||||||
|
self.used = True
|
||||||
|
return ""
|
||||||
|
return self.sep
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace:
|
||||||
|
"""A namespace object that can hold arbitrary attributes. It may be
|
||||||
|
initialized from a dictionary or with keyword arguments."""
|
||||||
|
|
||||||
|
def __init__(*args: t.Any, **kwargs: t.Any) -> None: # noqa: B902
|
||||||
|
self, args = args[0], args[1:]
|
||||||
|
self.__attrs = dict(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattribute__(self, name: str) -> t.Any:
|
||||||
|
# __class__ is needed for the awaitable check in async mode
|
||||||
|
if name in {"_Namespace__attrs", "__class__"}:
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
try:
|
||||||
|
return self.__attrs[name]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(name) from None
|
||||||
|
|
||||||
|
def __setitem__(self, name: str, value: t.Any) -> None:
|
||||||
|
self.__attrs[name] = value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Namespace {self.__attrs!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
class Markup(markupsafe.Markup):
|
||||||
|
def __new__(cls, base="", encoding=None, errors="strict"): # type: ignore
|
||||||
|
warnings.warn(
|
||||||
|
"'jinja2.Markup' is deprecated and will be removed in Jinja"
|
||||||
|
" 3.1. Import 'markupsafe.Markup' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return super().__new__(cls, base, encoding, errors)
|
||||||
|
|
||||||
|
|
||||||
|
def escape(s: t.Any) -> str:
|
||||||
|
warnings.warn(
|
||||||
|
"'jinja2.escape' is deprecated and will be removed in Jinja"
|
||||||
|
" 3.1. Import 'markupsafe.escape' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return markupsafe.escape(s)
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""API for traversing the AST nodes. Implemented by the compiler and
|
||||||
|
meta introspection.
|
||||||
|
"""
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
from .nodes import Node
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
class VisitCallable(te.Protocol):
|
||||||
|
def __call__(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class NodeVisitor:
|
||||||
|
"""Walks the abstract syntax tree and call visitor functions for every
|
||||||
|
node found. The visitor functions may return values which will be
|
||||||
|
forwarded by the `visit` method.
|
||||||
|
|
||||||
|
Per default the visitor functions for the nodes are ``'visit_'`` +
|
||||||
|
class name of the node. So a `TryFinally` node visit function would
|
||||||
|
be `visit_TryFinally`. This behavior can be changed by overriding
|
||||||
|
the `get_visitor` function. If no visitor function exists for a node
|
||||||
|
(return value `None`) the `generic_visit` visitor is used instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_visitor(self, node: Node) -> "t.Optional[VisitCallable]":
|
||||||
|
"""Return the visitor function for this node or `None` if no visitor
|
||||||
|
exists for this node. In that case the generic visit function is
|
||||||
|
used instead.
|
||||||
|
"""
|
||||||
|
return getattr(self, f"visit_{type(node).__name__}", None) # type: ignore
|
||||||
|
|
||||||
|
def visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Visit a node."""
|
||||||
|
f = self.get_visitor(node)
|
||||||
|
|
||||||
|
if f is not None:
|
||||||
|
return f(node, *args, **kwargs)
|
||||||
|
|
||||||
|
return self.generic_visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
|
"""Called if no explicit visitor function exists for a node."""
|
||||||
|
for node in node.iter_child_nodes():
|
||||||
|
self.visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeTransformer(NodeVisitor):
|
||||||
|
"""Walks the abstract syntax tree and allows modifications of nodes.
|
||||||
|
|
||||||
|
The `NodeTransformer` will walk the AST and use the return value of the
|
||||||
|
visitor functions to replace or remove the old node. If the return
|
||||||
|
value of the visitor function is `None` the node will be removed
|
||||||
|
from the previous location otherwise it's replaced with the return
|
||||||
|
value. The return value may be the original node in which case no
|
||||||
|
replacement takes place.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generic_visit(self, node: Node, *args: t.Any, **kwargs: t.Any) -> Node:
|
||||||
|
for field, old_value in node.iter_fields():
|
||||||
|
if isinstance(old_value, list):
|
||||||
|
new_values = []
|
||||||
|
for value in old_value:
|
||||||
|
if isinstance(value, Node):
|
||||||
|
value = self.visit(value, *args, **kwargs)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
elif not isinstance(value, Node):
|
||||||
|
new_values.extend(value)
|
||||||
|
continue
|
||||||
|
new_values.append(value)
|
||||||
|
old_value[:] = new_values
|
||||||
|
elif isinstance(old_value, Node):
|
||||||
|
new_node = self.visit(old_value, *args, **kwargs)
|
||||||
|
if new_node is None:
|
||||||
|
delattr(node, field)
|
||||||
|
else:
|
||||||
|
setattr(node, field, new_node)
|
||||||
|
return node
|
||||||
|
|
||||||
|
def visit_list(self, node: Node, *args: t.Any, **kwargs: t.Any) -> t.List[Node]:
|
||||||
|
"""As transformers may return lists in some places this method
|
||||||
|
can be used to enforce a list as return value.
|
||||||
|
"""
|
||||||
|
rv = self.visit(node, *args, **kwargs)
|
||||||
|
|
||||||
|
if not isinstance(rv, list):
|
||||||
|
return [rv]
|
||||||
|
|
||||||
|
return rv
|
|
@ -0,0 +1,49 @@
|
||||||
|
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.
|
@ -0,0 +1,3 @@
|
||||||
|
Before
|
||||||
|
{{ fail() }}
|
||||||
|
After
|
|
@ -0,0 +1 @@
|
||||||
|
FOO
|
|
@ -0,0 +1 @@
|
||||||
|
文字化け
|
|
@ -0,0 +1,4 @@
|
||||||
|
Foo
|
||||||
|
{% for item in broken %}
|
||||||
|
...
|
||||||
|
{% endif %}
|
|
@ -0,0 +1 @@
|
||||||
|
BAR
|
|
@ -0,0 +1,2 @@
|
||||||
|
Looks like the start of templates/foo/test.html
|
||||||
|
Tested by test_filesystem_loader_overlapping_names
|
|
@ -0,0 +1,434 @@
|
||||||
|
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"
|
|
@ -0,0 +1,660 @@
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 7):
|
||||||
|
|
||||||
|
def run(coro):
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return loop.run_until_complete(coro)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def run(coro):
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_async():
|
||||||
|
t = Template(
|
||||||
|
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def func():
|
||||||
|
return await t.render_async()
|
||||||
|
|
||||||
|
rv = 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 = 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 = 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 = 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):
|
||||||
|
m = run(
|
||||||
|
test_env_async.from_string(
|
||||||
|
"""
|
||||||
|
{% macro toplevel() %}...{% endmacro %}
|
||||||
|
{% macro __private() %}...{% endmacro %}
|
||||||
|
{% set variable = 42 %}
|
||||||
|
{% for item in [1] %}
|
||||||
|
{% macro notthere() %}{% endmacro %}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
)._get_default_module_async()
|
||||||
|
)
|
||||||
|
assert run(m.toplevel()) == "..."
|
||||||
|
assert not hasattr(m, "__missing")
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 == ""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
run(_test())
|
|
@ -0,0 +1,253 @@
|
||||||
|
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",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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"
|
|
@ -0,0 +1,77 @@
|
||||||
|
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)
|
|
@ -0,0 +1,28 @@
|
||||||
|
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
|
|
@ -0,0 +1,595 @@
|
||||||
|
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"
|
|
@ -0,0 +1,117 @@
|
||||||
|
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
|
||||||
|
), "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
|
|
@ -0,0 +1,714 @@
|
||||||
|
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) }}",
|
||||||
|
"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_newstyle_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"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
|
@ -0,0 +1,14 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
|
||||||
|
# Python < 3.7
|
||||||
|
def test_generator_stop():
|
||||||
|
class X:
|
||||||
|
def __getattr__(self, name):
|
||||||
|
raise StopIteration()
|
||||||
|
|
||||||
|
t = Template("a{{ bad.bar() }}b")
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
t.render(bad=X())
|
|
@ -0,0 +1,843 @@
|
||||||
|
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_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"
|
||||||
|
|
||||||
|
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)
|
|
@ -0,0 +1,290 @@
|
||||||
|
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"),
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
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"
|
|
@ -0,0 +1,405 @@
|
||||||
|
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
|
@ -0,0 +1,403 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,155 @@
|
||||||
|
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"
|
|
@ -0,0 +1,3 @@
|
||||||
|
def test_template_hash(env):
|
||||||
|
template = env.parse("hash test")
|
||||||
|
hash(template)
|
|
@ -0,0 +1,761 @@
|
||||||
|
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_legacy_custom_context(self, env):
|
||||||
|
from jinja2.runtime import Context, missing
|
||||||
|
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
|
||||||
|
class MyContext(Context):
|
||||||
|
def resolve(self, name):
|
||||||
|
if name == "foo":
|
||||||
|
return 42
|
||||||
|
return super().resolve(name)
|
||||||
|
|
||||||
|
x = MyContext(env, parent={"bar": 23}, name="foo", blocks={})
|
||||||
|
assert x._legacy_resolve_mode
|
||||||
|
assert x.resolve_or_missing("foo") == 42
|
||||||
|
assert x.resolve_or_missing("bar") == 23
|
||||||
|
assert x.resolve_or_missing("baz") is missing
|
||||||
|
|
||||||
|
def test_recursive_loop_bug(self, env):
|
||||||
|
tmpl = env.from_string(
|
||||||
|
"{%- for value in values recursive %}1{% else %}0{% endfor -%}"
|
||||||
|
)
|
||||||
|
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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue