Merge branch 'stable'
This commit is contained in:
commit
6aeab5d1da
8
.github/workflows/publish.yaml
vendored
8
.github/workflows/publish.yaml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
- name: generate hash
|
||||
id: hash
|
||||
run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
path: ./dist
|
||||
provenance:
|
||||
@ -64,10 +64,6 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
- uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
packages-dir: artifact/
|
||||
- uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3
|
||||
- uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
packages-dir: artifact/
|
||||
|
||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- name: cache mypy
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
with:
|
||||
path: ./.mypy_cache
|
||||
key: mypy|${{ hashFiles('pyproject.toml') }}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.1
|
||||
rev: v0.8.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
38
CHANGES.rst
38
CHANGES.rst
@ -14,8 +14,16 @@ Unreleased
|
||||
Version 3.1.5
|
||||
-------------
|
||||
|
||||
Unreleased
|
||||
Released 2024-12-21
|
||||
|
||||
- The sandboxed environment handles indirect calls to ``str.format``, such as
|
||||
by passing a stored reference to a filter that calls its argument.
|
||||
:ghsa:`q2x7-8rv6-6q7h`
|
||||
- Escape template name before formatting it into error messages, to avoid
|
||||
issues with names that contain f-string syntax.
|
||||
:issue:`1792`, :ghsa:`gmj6-6f8f-6699`
|
||||
- Sandbox does not allow ``clear`` and ``pop`` on known mutable sequence
|
||||
types. :issue:`2032`
|
||||
- Calling sync ``render`` for an async template uses ``asyncio.run``.
|
||||
:pr:`1952`
|
||||
- Avoid unclosed ``auto_aiter`` warnings. :pr:`1960`
|
||||
@ -25,6 +33,32 @@ Unreleased
|
||||
``Template.generate_async``. :pr:`1960`
|
||||
- Avoid leaving async generators unclosed in blocks, includes and extends.
|
||||
:pr:`1960`
|
||||
- The runtime uses the correct ``concat`` function for the current environment
|
||||
when calling block references. :issue:`1701`
|
||||
- Make ``|unique`` async-aware, allowing it to be used after another
|
||||
async-aware filter. :issue:`1781`
|
||||
- ``|int`` filter handles ``OverflowError`` from scientific notation.
|
||||
:issue:`1921`
|
||||
- Make compiling deterministic for tuple unpacking in a ``{% set ... %}``
|
||||
call. :issue:`2021`
|
||||
- Fix dunder protocol (`copy`/`pickle`/etc) interaction with ``Undefined``
|
||||
objects. :issue:`2025`
|
||||
- Fix `copy`/`pickle` support for the internal ``missing`` object.
|
||||
:issue:`2027`
|
||||
- ``Environment.overlay(enable_async)`` is applied correctly. :pr:`2061`
|
||||
- The error message from ``FileSystemLoader`` includes the paths that were
|
||||
searched. :issue:`1661`
|
||||
- ``PackageLoader`` shows a clearer error message when the package does not
|
||||
contain the templates directory. :issue:`1705`
|
||||
- Improve annotations for methods returning copies. :pr:`1880`
|
||||
- ``urlize`` does not add ``mailto:`` to values like `@a@b`. :pr:`1870`
|
||||
- Tests decorated with `@pass_context`` can be used with the ``|select``
|
||||
filter. :issue:`1624`
|
||||
- Using ``set`` for multiple assignment (``a, b = 1, 2``) does not fail when the
|
||||
target is a namespace attribute. :issue:`1413`
|
||||
- Using ``set`` in all branches of ``{% if %}{% elif %}{% else %}`` blocks
|
||||
does not cause the variable to be considered initially undefined.
|
||||
:issue:`1253`
|
||||
|
||||
|
||||
Version 3.1.4
|
||||
@ -1012,7 +1046,7 @@ Released 2008-07-17, codename Jinjavitus
|
||||
evaluates to ``false``.
|
||||
- Improved error reporting for undefined values by providing a
|
||||
position.
|
||||
- ``filesizeformat`` filter uses decimal prefixes now per default and
|
||||
- ``filesizeformat`` filter uses decimal prefixes now by default and
|
||||
can be set to binary mode with the second parameter.
|
||||
- Fixed bug in finalizer
|
||||
|
||||
|
||||
@ -666,8 +666,8 @@ Now it can be used in templates:
|
||||
|
||||
.. sourcecode:: jinja
|
||||
|
||||
{{ article.pub_date|datetimeformat }}
|
||||
{{ article.pub_date|datetimeformat("%B %Y") }}
|
||||
{{ article.pub_date|datetime_format }}
|
||||
{{ article.pub_date|datetime_format("%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
|
||||
|
||||
@ -24,7 +24,7 @@ autodoc_preserve_defaults = True
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/pallets/jinja/issues/%s", "#%s"),
|
||||
"pr": ("https://github.com/pallets/jinja/pull/%s", "#%s"),
|
||||
"ghsa": ("https://github.com/advisories/GHSA-%s", "GHSA-%s"),
|
||||
"ghsa": ("https://github.com/pallets/jinja/security/advisories/GHSA-%s", "GHSA-%s"),
|
||||
}
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
|
||||
@ -70,6 +70,8 @@ these document types.
|
||||
|
||||
While automatic escaping means that you are less likely have an XSS
|
||||
problem, it also requires significant extra processing during compiling
|
||||
and rendering, which can reduce performance. Jinja uses MarkupSafe for
|
||||
and rendering, which can reduce performance. Jinja uses `MarkupSafe`_ for
|
||||
escaping, which provides optimized C code for speed, but it still
|
||||
introduces overhead to track escaping across methods and formatting.
|
||||
|
||||
.. _MarkupSafe: https://markupsafe.palletsprojects.com/
|
||||
|
||||
@ -55,6 +55,17 @@ Foo
|
||||
>>> print(result.value)
|
||||
15
|
||||
|
||||
Sandboxed Native Environment
|
||||
----------------------------
|
||||
|
||||
You can combine :class:`.SandboxedEnvironment` and :class:`NativeEnvironment` to
|
||||
get both behaviors.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class SandboxedNativeEnvironment(SandboxedEnvironment, NativeEnvironment):
|
||||
pass
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
|
||||
@ -202,10 +202,11 @@ option can also be set to strip tabs and spaces from the beginning of a
|
||||
line to the start of a block. (Nothing will be stripped if there are
|
||||
other characters before the start of the block.)
|
||||
|
||||
With both `trim_blocks` and `lstrip_blocks` enabled, you can put block tags
|
||||
on their own lines, and the entire block line will be removed when
|
||||
rendered, preserving the whitespace of the contents. For example,
|
||||
without the `trim_blocks` and `lstrip_blocks` options, this template::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` disabled (the default), block
|
||||
tags on their own lines will be removed, but a blank line will remain and the
|
||||
spaces in the content will be preserved. For example, this template:
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<div>
|
||||
{% if True %}
|
||||
@ -213,7 +214,10 @@ without the `trim_blocks` and `lstrip_blocks` options, this template::
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
gets rendered with blank lines inside the div::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` disabled, the template is
|
||||
rendered with blank lines inside the div:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<div>
|
||||
|
||||
@ -221,8 +225,10 @@ gets rendered with blank lines inside the div::
|
||||
|
||||
</div>
|
||||
|
||||
But with both `trim_blocks` and `lstrip_blocks` enabled, the template block
|
||||
lines are removed and other whitespace is preserved::
|
||||
With both ``trim_blocks`` and ``lstrip_blocks`` enabled, the template block
|
||||
lines are completely removed:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<div>
|
||||
yay
|
||||
@ -522,8 +528,8 @@ However, the name after the `endblock` word must match the block name.
|
||||
Block Nesting and Scope
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Blocks can be nested for more complex layouts. However, per default blocks
|
||||
may not access variables from outer scopes::
|
||||
Blocks can be nested for more complex layouts. By default, a block may not
|
||||
access variables from outside the block (outer scopes)::
|
||||
|
||||
{% for item in seq %}
|
||||
<li>{% block loop_item %}{{ item }}{% endblock %}</li>
|
||||
@ -1080,34 +1086,34 @@ Assignments use the `set` tag and can have multiple targets::
|
||||
Block Assignments
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 2.8
|
||||
It's possible to use `set` as a block to assign the content of the block to a
|
||||
variable. This can be used to create multi-line strings, since Jinja doesn't
|
||||
support Python's triple quotes (``"""``, ``'''``).
|
||||
|
||||
Starting with Jinja 2.8, it's possible to also use block assignments to
|
||||
capture the contents of a block into a variable name. This can be useful
|
||||
in some situations as an alternative for macros. In that case, instead of
|
||||
using an equals sign and a value, you just write the variable name and then
|
||||
everything until ``{% endset %}`` is captured.
|
||||
Instead of using an equals sign and a value, you only write the variable name,
|
||||
and everything until ``{% endset %}`` is captured.
|
||||
|
||||
Example::
|
||||
.. code-block:: jinja
|
||||
|
||||
{% set navigation %}
|
||||
<li><a href="/">Index</a>
|
||||
<li><a href="/downloads">Downloads</a>
|
||||
{% endset %}
|
||||
|
||||
The `navigation` variable then contains the navigation HTML source.
|
||||
Filters applied to the variable name will be applied to the block's content.
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
Starting with Jinja 2.10, the block assignment supports filters.
|
||||
|
||||
Example::
|
||||
.. code-block:: jinja
|
||||
|
||||
{% set reply | wordwrap %}
|
||||
You wrote:
|
||||
{{ message }}
|
||||
{% endset %}
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
Block assignment supports filters.
|
||||
|
||||
.. _extends:
|
||||
|
||||
@ -1406,27 +1412,31 @@ Comparisons
|
||||
Logic
|
||||
~~~~~
|
||||
|
||||
For ``if`` statements, ``for`` filtering, and ``if`` expressions, it can be useful to
|
||||
combine multiple expressions:
|
||||
For ``if`` statements, ``for`` filtering, and ``if`` expressions, it can be
|
||||
useful to combine multiple expressions.
|
||||
|
||||
``and``
|
||||
Return true if the left and the right operand are true.
|
||||
For ``x and y``, if ``x`` is false, then the value is ``x``, else ``y``. In
|
||||
a boolean context, this will be treated as ``True`` if both operands are
|
||||
truthy.
|
||||
|
||||
``or``
|
||||
Return true if the left or the right operand are true.
|
||||
For ``x or y``, if ``x`` is true, then the value is ``x``, else ``y``. In a
|
||||
boolean context, this will be treated as ``True`` if at least one operand is
|
||||
truthy.
|
||||
|
||||
``not``
|
||||
negate a statement (see below).
|
||||
For ``not x``, if ``x`` is false, then the value is ``True``, else
|
||||
``False``.
|
||||
|
||||
Prefer negating ``is`` and ``in`` using their infix notation:
|
||||
``foo is not bar`` instead of ``not foo is bar``; ``foo not in bar`` instead
|
||||
of ``not foo in bar``. All other expressions require prefix notation:
|
||||
``not (foo and bar).``
|
||||
|
||||
``(expr)``
|
||||
Parentheses group an expression.
|
||||
|
||||
.. admonition:: Note
|
||||
|
||||
The ``is`` and ``in`` operators support negation using an infix notation,
|
||||
too: ``foo is not bar`` and ``foo not in bar`` instead of ``not foo is bar``
|
||||
and ``not foo in bar``. All other expressions require a prefix notation:
|
||||
``not (foo and bar).``
|
||||
Parentheses group an expression. This is used to change evaluation order, or
|
||||
to make a long expression easier to read or less ambiguous.
|
||||
|
||||
|
||||
Other Operators
|
||||
@ -1668,6 +1678,9 @@ The following functions are available in the global scope by default:
|
||||
|
||||
.. versionadded:: 2.10
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
Namespace attributes can be assigned to in multiple assignment.
|
||||
|
||||
|
||||
Extensions
|
||||
----------
|
||||
@ -1778,7 +1791,7 @@ It's possible to translate strings in expressions with these functions:
|
||||
|
||||
- ``_(message)``: Alias for ``gettext``.
|
||||
- ``gettext(message)``: Translate a message.
|
||||
- ``ngettext(singluar, plural, n)``: Translate a singular or plural
|
||||
- ``ngettext(singular, plural, n)``: Translate a singular or plural
|
||||
message based on a count variable.
|
||||
- ``pgettext(context, message)``: Like ``gettext()``, but picks the
|
||||
translation based on the context string.
|
||||
|
||||
@ -21,7 +21,7 @@ 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
|
||||
to false, which it does by 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::
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ env = Environment(
|
||||
{
|
||||
"child.html": """\
|
||||
{% extends default_layout or 'default.html' %}
|
||||
{% include helpers = 'helpers.html' %}
|
||||
{% import 'helpers.html' as helpers %}
|
||||
{% macro get_the_answer() %}42{% endmacro %}
|
||||
{% title = 'Hello World' %}
|
||||
{% set title = 'Hello World' %}
|
||||
{% block body %}
|
||||
{{ get_the_answer() }}
|
||||
{{ helpers.conspirate() }}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#
|
||||
build==1.2.2.post1
|
||||
# via -r build.in
|
||||
packaging==24.1
|
||||
packaging==24.2
|
||||
# via build
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#
|
||||
alabaster==1.0.0
|
||||
# via sphinx
|
||||
attrs==24.2.0
|
||||
attrs==24.3.0
|
||||
# via
|
||||
# outcome
|
||||
# trio
|
||||
@ -16,7 +16,7 @@ build==1.2.2.post1
|
||||
# via pip-tools
|
||||
cachetools==5.5.0
|
||||
# via tox
|
||||
certifi==2024.8.30
|
||||
certifi==2024.12.14
|
||||
# via requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
@ -38,7 +38,7 @@ filelock==3.16.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
identify==2.6.1
|
||||
identify==2.6.3
|
||||
# via pre-commit
|
||||
idna==3.10
|
||||
# via
|
||||
@ -52,15 +52,15 @@ jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==3.0.2
|
||||
# via jinja2
|
||||
mypy==1.13.0
|
||||
# via -r typing.in
|
||||
mypy==1.14.0
|
||||
# via -r /Users/david/Projects/jinja/requirements/typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
nodeenv==1.9.1
|
||||
# via pre-commit
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
packaging==24.1
|
||||
packaging==24.2
|
||||
# via
|
||||
# build
|
||||
# pallets-sphinx-themes
|
||||
@ -69,8 +69,8 @@ packaging==24.1
|
||||
# sphinx
|
||||
# tox
|
||||
pallets-sphinx-themes==2.3.0
|
||||
# via -r docs.in
|
||||
pip-compile-multi==2.6.4
|
||||
# via -r /Users/david/Projects/jinja/requirements/docs.in
|
||||
pip-compile-multi==2.7.1
|
||||
# via -r dev.in
|
||||
pip-tools==7.4.1
|
||||
# via pip-compile-multi
|
||||
@ -92,8 +92,8 @@ pyproject-hooks==1.2.0
|
||||
# via
|
||||
# build
|
||||
# pip-tools
|
||||
pytest==8.3.3
|
||||
# via -r tests.in
|
||||
pytest==8.3.4
|
||||
# via -r /Users/david/Projects/jinja/requirements/tests.in
|
||||
pyyaml==6.0.2
|
||||
# via pre-commit
|
||||
requests==2.32.3
|
||||
@ -106,13 +106,13 @@ sortedcontainers==2.4.0
|
||||
# via trio
|
||||
sphinx==8.1.3
|
||||
# via
|
||||
# -r docs.in
|
||||
# -r /Users/david/Projects/jinja/requirements/docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinx-notfound-page
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==5.0.0
|
||||
# via -r docs.in
|
||||
# via -r /Users/david/Projects/jinja/requirements/docs.in
|
||||
sphinx-notfound-page==1.0.4
|
||||
# via pallets-sphinx-themes
|
||||
sphinxcontrib-applehelp==2.0.0
|
||||
@ -124,7 +124,7 @@ sphinxcontrib-htmlhelp==2.1.0
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
# via -r /Users/david/Projects/jinja/requirements/docs.in
|
||||
sphinxcontrib-qthelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==2.0.0
|
||||
@ -134,16 +134,16 @@ toposort==1.10
|
||||
tox==4.23.2
|
||||
# via -r dev.in
|
||||
trio==0.27.0
|
||||
# via -r tests.in
|
||||
# via -r /Users/david/Projects/jinja/requirements/tests.in
|
||||
typing-extensions==4.12.2
|
||||
# via mypy
|
||||
urllib3==2.2.3
|
||||
# via requests
|
||||
virtualenv==20.27.0
|
||||
virtualenv==20.28.0
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.44.0
|
||||
wheel==0.45.1
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
|
||||
@ -8,7 +8,7 @@ alabaster==1.0.0
|
||||
# via sphinx
|
||||
babel==2.16.0
|
||||
# via sphinx
|
||||
certifi==2024.8.30
|
||||
certifi==2024.12.14
|
||||
# via requests
|
||||
charset-normalizer==3.4.0
|
||||
# via requests
|
||||
@ -22,7 +22,7 @@ jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==3.0.2
|
||||
# via jinja2
|
||||
packaging==24.1
|
||||
packaging==24.2
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#
|
||||
# pip-compile tests.in
|
||||
#
|
||||
attrs==24.2.0
|
||||
attrs==24.3.0
|
||||
# via
|
||||
# outcome
|
||||
# trio
|
||||
@ -14,11 +14,11 @@ iniconfig==2.0.0
|
||||
# via pytest
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
packaging==24.1
|
||||
packaging==24.2
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pytest==8.3.3
|
||||
pytest==8.3.4
|
||||
# via -r tests.in
|
||||
sniffio==1.3.1
|
||||
# via trio
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#
|
||||
# pip-compile typing.in
|
||||
#
|
||||
mypy==1.13.0
|
||||
mypy==1.14.0
|
||||
# via -r typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
|
||||
@ -216,7 +216,7 @@ class Frame:
|
||||
# or compile time.
|
||||
self.soft_frame = False
|
||||
|
||||
def copy(self) -> "Frame":
|
||||
def copy(self) -> "te.Self":
|
||||
"""Create a copy of the current one."""
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
@ -229,7 +229,7 @@ class Frame:
|
||||
return Frame(self.eval_ctx, level=self.symbols.level + 1)
|
||||
return Frame(self.eval_ctx, self)
|
||||
|
||||
def soft(self) -> "Frame":
|
||||
def soft(self) -> "te.Self":
|
||||
"""Return a soft frame. A soft frame may not be modified as
|
||||
standalone thing as it shares the resources with the frame it
|
||||
was created of, but it's not a rootlevel frame any longer.
|
||||
@ -811,7 +811,7 @@ class CodeGenerator(NodeVisitor):
|
||||
self.writeline("_block_vars.update({")
|
||||
else:
|
||||
self.writeline("context.vars.update({")
|
||||
for idx, name in enumerate(vars):
|
||||
for idx, name in enumerate(sorted(vars)):
|
||||
if idx:
|
||||
self.write(", ")
|
||||
ref = frame.symbols.ref(name)
|
||||
@ -821,7 +821,7 @@ class CodeGenerator(NodeVisitor):
|
||||
if len(public_names) == 1:
|
||||
self.writeline(f"context.exported_vars.add({public_names[0]!r})")
|
||||
else:
|
||||
names_str = ", ".join(map(repr, public_names))
|
||||
names_str = ", ".join(map(repr, sorted(public_names)))
|
||||
self.writeline(f"context.exported_vars.update(({names_str}))")
|
||||
|
||||
# -- Statement Visitors
|
||||
@ -1141,9 +1141,14 @@ class CodeGenerator(NodeVisitor):
|
||||
)
|
||||
self.writeline(f"if {frame.symbols.ref(alias)} is missing:")
|
||||
self.indent()
|
||||
# The position will contain the template name, and will be formatted
|
||||
# into a string that will be compiled into an f-string. Curly braces
|
||||
# in the name must be replaced with escapes so that they will not be
|
||||
# executed as part of the f-string.
|
||||
position = self.position(node).replace("{", "{{").replace("}", "}}")
|
||||
message = (
|
||||
"the template {included_template.__name__!r}"
|
||||
f" (imported on {self.position(node)})"
|
||||
f" (imported on {position})"
|
||||
f" does not export the requested name {name!r}"
|
||||
)
|
||||
self.writeline(
|
||||
@ -1576,6 +1581,29 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:
|
||||
self.push_assign_tracking()
|
||||
|
||||
# ``a.b`` is allowed for assignment, and is parsed as an NSRef. However,
|
||||
# it is only valid if it references a Namespace object. Emit a check for
|
||||
# that for each ref here, before assignment code is emitted. This can't
|
||||
# be done in visit_NSRef as the ref could be in the middle of a tuple.
|
||||
seen_refs: t.Set[str] = set()
|
||||
|
||||
for nsref in node.find_all(nodes.NSRef):
|
||||
if nsref.name in seen_refs:
|
||||
# Only emit the check for each reference once, in case the same
|
||||
# ref is used multiple times in a tuple, `ns.a, ns.b = c, d`.
|
||||
continue
|
||||
|
||||
seen_refs.add(nsref.name)
|
||||
ref = frame.symbols.ref(nsref.name)
|
||||
self.writeline(f"if not isinstance({ref}, Namespace):")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
"raise TemplateRuntimeError"
|
||||
'("cannot assign attribute on non-namespace object")'
|
||||
)
|
||||
self.outdent()
|
||||
|
||||
self.newline(node)
|
||||
self.visit(node.target, frame)
|
||||
self.write(" = ")
|
||||
@ -1632,17 +1660,11 @@ class CodeGenerator(NodeVisitor):
|
||||
self.write(ref)
|
||||
|
||||
def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
|
||||
# NSRefs can only be used to store values; since they use the normal
|
||||
# `foo.bar` notation they will be parsed as a normal attribute access
|
||||
# when used anywhere but in a `set` context
|
||||
# NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally.
|
||||
# visit_Assign emits code to validate that each ref is to a Namespace
|
||||
# object only. That can't be emitted here as the ref could be in the
|
||||
# middle of a tuple assignment.
|
||||
ref = frame.symbols.ref(node.name)
|
||||
self.writeline(f"if not isinstance({ref}, Namespace):")
|
||||
self.indent()
|
||||
self.writeline(
|
||||
"raise TemplateRuntimeError"
|
||||
'("cannot assign attribute on non-namespace object")'
|
||||
)
|
||||
self.outdent()
|
||||
self.writeline(f"{ref}[{node.attr!r}]")
|
||||
|
||||
def visit_Const(self, node: nodes.Const, frame: Frame) -> None:
|
||||
|
||||
@ -125,7 +125,7 @@ def load_extensions(
|
||||
return result
|
||||
|
||||
|
||||
def _environment_config_check(environment: "Environment") -> "Environment":
|
||||
def _environment_config_check(environment: _env_bound) -> _env_bound:
|
||||
"""Perform a sanity check on the environment."""
|
||||
assert issubclass(
|
||||
environment.undefined, Undefined
|
||||
@ -408,8 +408,8 @@ class Environment:
|
||||
cache_size: int = missing,
|
||||
auto_reload: bool = missing,
|
||||
bytecode_cache: t.Optional["BytecodeCache"] = missing,
|
||||
enable_async: bool = False,
|
||||
) -> "Environment":
|
||||
enable_async: bool = missing,
|
||||
) -> "te.Self":
|
||||
"""Create a new overlay environment that shares all the data with the
|
||||
current environment except for cache and the overridden attributes.
|
||||
Extensions cannot be removed for an overlayed environment. An overlayed
|
||||
@ -421,8 +421,11 @@ class Environment:
|
||||
copied over so modifications on the original environment may not shine
|
||||
through.
|
||||
|
||||
.. versionchanged:: 3.1.5
|
||||
``enable_async`` is applied correctly.
|
||||
|
||||
.. versionchanged:: 3.1.2
|
||||
Added the ``newline_sequence``,, ``keep_trailing_newline``,
|
||||
Added the ``newline_sequence``, ``keep_trailing_newline``,
|
||||
and ``enable_async`` parameters to match ``__init__``.
|
||||
"""
|
||||
args = dict(locals())
|
||||
|
||||
@ -89,7 +89,7 @@ class Extension:
|
||||
def __init__(self, environment: Environment) -> None:
|
||||
self.environment = environment
|
||||
|
||||
def bind(self, environment: Environment) -> "Extension":
|
||||
def bind(self, environment: Environment) -> "te.Self":
|
||||
"""Create a copy of this extension bound to another environment."""
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
|
||||
@ -438,7 +438,7 @@ def do_sort(
|
||||
|
||||
|
||||
@pass_environment
|
||||
def do_unique(
|
||||
def sync_do_unique(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
case_sensitive: bool = False,
|
||||
@ -470,6 +470,18 @@ def do_unique(
|
||||
yield item
|
||||
|
||||
|
||||
@async_variant(sync_do_unique) # type: ignore
|
||||
async def do_unique(
|
||||
environment: "Environment",
|
||||
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
|
||||
case_sensitive: bool = False,
|
||||
attribute: t.Optional[t.Union[str, int]] = None,
|
||||
) -> "t.Iterator[V]":
|
||||
return sync_do_unique(
|
||||
environment, await auto_to_list(value), case_sensitive, attribute
|
||||
)
|
||||
|
||||
|
||||
def _min_or_max(
|
||||
environment: "Environment",
|
||||
value: "t.Iterable[V]",
|
||||
@ -987,7 +999,7 @@ def do_int(value: t.Any, default: int = 0, base: int = 10) -> int:
|
||||
# this quirk is necessary so that "42.23"|int gives 42.
|
||||
try:
|
||||
return int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return default
|
||||
|
||||
|
||||
@ -1629,8 +1641,8 @@ def sync_do_selectattr(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(u for user in users if user.is_active)
|
||||
(u for user in users if test_none(user.email))
|
||||
(user for user in users if user.is_active)
|
||||
(user for user in users if test_none(user.email))
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
@ -1667,8 +1679,8 @@ def sync_do_rejectattr(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
(u for user in users if not user.is_active)
|
||||
(u for user in users if not test_none(user.email))
|
||||
(user for user in users if not user.is_active)
|
||||
(user for user in users if not test_none(user.email))
|
||||
|
||||
.. versionadded:: 2.7
|
||||
"""
|
||||
@ -1768,7 +1780,7 @@ def prepare_select_or_reject(
|
||||
args = args[1 + off :]
|
||||
|
||||
def func(item: t.Any) -> t.Any:
|
||||
return context.environment.call_test(name, item, args, kwargs)
|
||||
return context.environment.call_test(name, item, args, kwargs, context)
|
||||
|
||||
except LookupError:
|
||||
func = bool # type: ignore
|
||||
|
||||
@ -3,6 +3,9 @@ import typing as t
|
||||
from . import nodes
|
||||
from .visitor import NodeVisitor
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
VAR_LOAD_PARAMETER = "param"
|
||||
VAR_LOAD_RESOLVE = "resolve"
|
||||
VAR_LOAD_ALIAS = "alias"
|
||||
@ -83,7 +86,7 @@ class Symbols:
|
||||
)
|
||||
return rv
|
||||
|
||||
def copy(self) -> "Symbols":
|
||||
def copy(self) -> "te.Self":
|
||||
rv = object.__new__(self.__class__)
|
||||
rv.__dict__.update(self.__dict__)
|
||||
rv.refs = self.refs.copy()
|
||||
@ -118,23 +121,20 @@ class Symbols:
|
||||
self._define_ref(name, load=(VAR_LOAD_RESOLVE, name))
|
||||
|
||||
def branch_update(self, branch_symbols: t.Sequence["Symbols"]) -> None:
|
||||
stores: t.Dict[str, int] = {}
|
||||
stores: t.Set[str] = set()
|
||||
|
||||
for branch in branch_symbols:
|
||||
for target in branch.stores:
|
||||
if target in self.stores:
|
||||
continue
|
||||
stores[target] = stores.get(target, 0) + 1
|
||||
stores.update(branch.stores)
|
||||
|
||||
stores.difference_update(self.stores)
|
||||
|
||||
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
|
||||
for name in stores:
|
||||
target = self.find_ref(name)
|
||||
assert target is not None, "should not happen"
|
||||
|
||||
if self.parent is not None:
|
||||
|
||||
@ -262,7 +262,7 @@ class Failure:
|
||||
self.message = message
|
||||
self.error_class = cls
|
||||
|
||||
def __call__(self, lineno: int, filename: str) -> "te.NoReturn":
|
||||
def __call__(self, lineno: int, filename: t.Optional[str]) -> "te.NoReturn":
|
||||
raise self.error_class(self.message, lineno, filename)
|
||||
|
||||
|
||||
@ -757,7 +757,7 @@ class Lexer:
|
||||
|
||||
for idx, token in enumerate(tokens):
|
||||
# failure group
|
||||
if token.__class__ is Failure:
|
||||
if isinstance(token, Failure):
|
||||
raise token(lineno, filename)
|
||||
# bygroup is a bit more complex, in that case we
|
||||
# yield for the current token the first named
|
||||
@ -778,7 +778,7 @@ class Lexer:
|
||||
data = groups[idx]
|
||||
|
||||
if data or token not in ignore_if_empty:
|
||||
yield lineno, token, data
|
||||
yield lineno, token, data # type: ignore[misc]
|
||||
|
||||
lineno += data.count("\n") + newlines_stripped
|
||||
newlines_stripped = 0
|
||||
|
||||
@ -204,7 +204,12 @@ class FileSystemLoader(BaseLoader):
|
||||
if os.path.isfile(filename):
|
||||
break
|
||||
else:
|
||||
raise TemplateNotFound(template)
|
||||
plural = "path" if len(self.searchpath) == 1 else "paths"
|
||||
paths_str = ", ".join(repr(p) for p in self.searchpath)
|
||||
raise TemplateNotFound(
|
||||
template,
|
||||
f"{template!r} not found in search {plural}: {paths_str}",
|
||||
)
|
||||
|
||||
with open(filename, encoding=self.encoding) as f:
|
||||
contents = f.read()
|
||||
@ -322,7 +327,6 @@ class PackageLoader(BaseLoader):
|
||||
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
|
||||
@ -339,18 +343,23 @@ class PackageLoader(BaseLoader):
|
||||
elif spec.origin is not None:
|
||||
roots.append(os.path.dirname(spec.origin))
|
||||
|
||||
if not roots:
|
||||
raise ValueError(
|
||||
f"The {package_name!r} package was not installed in a"
|
||||
" way that PackageLoader understands."
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"PackageLoader could not find a {package_path!r} directory"
|
||||
f" in the {package_name!r} package."
|
||||
)
|
||||
|
||||
self._template_root = template_root
|
||||
|
||||
@ -427,7 +436,7 @@ class DictLoader(BaseLoader):
|
||||
|
||||
>>> loader = DictLoader({'index.html': 'source here'})
|
||||
|
||||
Because auto reloading is rarely useful this is disabled per default.
|
||||
Because auto reloading is rarely useful this is disabled by default.
|
||||
"""
|
||||
|
||||
def __init__(self, mapping: t.Mapping[str, str]) -> None:
|
||||
@ -610,10 +619,7 @@ class ModuleLoader(BaseLoader):
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> loader = ChoiceLoader([
|
||||
... ModuleLoader('/path/to/compiled/templates'),
|
||||
... FileSystemLoader('/path/to/templates')
|
||||
... ])
|
||||
>>> loader = ModuleLoader('/path/to/compiled/templates')
|
||||
|
||||
Templates can be precompiled with :meth:`Environment.compile_templates`.
|
||||
"""
|
||||
|
||||
@ -487,21 +487,18 @@ class Parser:
|
||||
"""
|
||||
target: nodes.Expr
|
||||
|
||||
if with_namespace and self.stream.look().type == "dot":
|
||||
token = self.stream.expect("name")
|
||||
next(self.stream) # dot
|
||||
attr = self.stream.expect("name")
|
||||
target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
|
||||
elif name_only:
|
||||
if name_only:
|
||||
token = self.stream.expect("name")
|
||||
target = nodes.Name(token.value, "store", lineno=token.lineno)
|
||||
else:
|
||||
if with_tuple:
|
||||
target = self.parse_tuple(
|
||||
simplified=True, extra_end_rules=extra_end_rules
|
||||
simplified=True,
|
||||
extra_end_rules=extra_end_rules,
|
||||
with_namespace=with_namespace,
|
||||
)
|
||||
else:
|
||||
target = self.parse_primary()
|
||||
target = self.parse_primary(with_namespace=with_namespace)
|
||||
|
||||
target.set_ctx("store")
|
||||
|
||||
@ -643,17 +640,25 @@ class Parser:
|
||||
node = self.parse_filter_expr(node)
|
||||
return node
|
||||
|
||||
def parse_primary(self) -> nodes.Expr:
|
||||
def parse_primary(self, with_namespace: bool = False) -> nodes.Expr:
|
||||
"""Parse a name or literal value. If ``with_namespace`` is enabled, also
|
||||
parse namespace attr refs, for use in assignments."""
|
||||
token = self.stream.current
|
||||
node: nodes.Expr
|
||||
if token.type == "name":
|
||||
next(self.stream)
|
||||
if token.value in ("true", "false", "True", "False"):
|
||||
node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
|
||||
elif token.value in ("none", "None"):
|
||||
node = nodes.Const(None, lineno=token.lineno)
|
||||
elif with_namespace and self.stream.current.type == "dot":
|
||||
# If namespace attributes are allowed at this point, and the next
|
||||
# token is a dot, produce a namespace reference.
|
||||
next(self.stream)
|
||||
attr = self.stream.expect("name")
|
||||
node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
|
||||
else:
|
||||
node = nodes.Name(token.value, "load", lineno=token.lineno)
|
||||
next(self.stream)
|
||||
elif token.type == "string":
|
||||
next(self.stream)
|
||||
buf = [token.value]
|
||||
@ -683,6 +688,7 @@ class Parser:
|
||||
with_condexpr: bool = True,
|
||||
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
|
||||
explicit_parentheses: bool = False,
|
||||
with_namespace: bool = False,
|
||||
) -> t.Union[nodes.Tuple, nodes.Expr]:
|
||||
"""Works like `parse_expression` but if multiple expressions are
|
||||
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
|
||||
@ -690,8 +696,9 @@ class Parser:
|
||||
if no commas where found.
|
||||
|
||||
The default parsing mode is a full tuple. If `simplified` is `True`
|
||||
only names and literals are parsed. The `no_condexpr` parameter is
|
||||
forwarded to :meth:`parse_expression`.
|
||||
only names and literals are parsed; ``with_namespace`` allows namespace
|
||||
attr refs as well. The `no_condexpr` parameter is forwarded to
|
||||
:meth:`parse_expression`.
|
||||
|
||||
Because tuples do not require delimiters and may end in a bogus comma
|
||||
an extra hint is needed that marks the end of a tuple. For example
|
||||
@ -704,13 +711,14 @@ class Parser:
|
||||
"""
|
||||
lineno = self.stream.current.lineno
|
||||
if simplified:
|
||||
parse = self.parse_primary
|
||||
elif with_condexpr:
|
||||
parse = self.parse_expression
|
||||
|
||||
def parse() -> nodes.Expr:
|
||||
return self.parse_primary(with_namespace=with_namespace)
|
||||
|
||||
else:
|
||||
|
||||
def parse() -> nodes.Expr:
|
||||
return self.parse_expression(with_condexpr=False)
|
||||
return self.parse_expression(with_condexpr=with_condexpr)
|
||||
|
||||
args: t.List[nodes.Expr] = []
|
||||
is_tuple = False
|
||||
|
||||
@ -367,7 +367,7 @@ class BlockReference:
|
||||
|
||||
@internalcode
|
||||
async def _async_call(self) -> str:
|
||||
rv = concat(
|
||||
rv = self._context.environment.concat( # type: ignore
|
||||
[x async for x in self._stack[self._depth](self._context)] # type: ignore
|
||||
)
|
||||
|
||||
@ -381,7 +381,9 @@ class BlockReference:
|
||||
if self._context.environment.is_async:
|
||||
return self._async_call() # type: ignore
|
||||
|
||||
rv = concat(self._stack[self._depth](self._context))
|
||||
rv = self._context.environment.concat( # type: ignore
|
||||
self._stack[self._depth](self._context)
|
||||
)
|
||||
|
||||
if self._context.eval_ctx.autoescape:
|
||||
return Markup(rv)
|
||||
@ -792,8 +794,8 @@ class Macro:
|
||||
|
||||
|
||||
class Undefined:
|
||||
"""The default undefined type. This undefined type can be printed and
|
||||
iterated over, but every other access will raise an :exc:`UndefinedError`:
|
||||
"""The default undefined type. This can be printed, iterated, and treated as
|
||||
a boolean. Any other operation will raise an :exc:`UndefinedError`.
|
||||
|
||||
>>> foo = Undefined(name='foo')
|
||||
>>> str(foo)
|
||||
@ -858,7 +860,11 @@ class Undefined:
|
||||
|
||||
@internalcode
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
if name[:2] == "__":
|
||||
# Raise AttributeError on requests for names that appear to be unimplemented
|
||||
# dunder methods to keep Python's internal protocol probing behaviors working
|
||||
# properly in cases where another exception type could cause unexpected or
|
||||
# difficult-to-diagnose failures.
|
||||
if name[:2] == "__" and name[-2:] == "__":
|
||||
raise AttributeError(name)
|
||||
|
||||
return self._fail_with_undefined_error()
|
||||
@ -982,10 +988,20 @@ class ChainableUndefined(Undefined):
|
||||
def __html__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def __getattr__(self, _: str) -> "ChainableUndefined":
|
||||
def __getattr__(self, name: str) -> "ChainableUndefined":
|
||||
# Raise AttributeError on requests for names that appear to be unimplemented
|
||||
# dunder methods to avoid confusing Python with truthy non-method objects that
|
||||
# do not implement the protocol being probed for. e.g., copy.copy(Undefined())
|
||||
# fails spectacularly if getattr(Undefined(), '__setstate__') returns an
|
||||
# Undefined object instead of raising AttributeError to signal that it does not
|
||||
# support that style of object initialization.
|
||||
if name[:2] == "__" and name[-2:] == "__":
|
||||
raise AttributeError(name)
|
||||
|
||||
return self
|
||||
|
||||
__getitem__ = __getattr__ # type: ignore
|
||||
def __getitem__(self, _name: str) -> "ChainableUndefined": # type: ignore[override]
|
||||
return self
|
||||
|
||||
|
||||
class DebugUndefined(Undefined):
|
||||
@ -1044,13 +1060,3 @@ class StrictUndefined(Undefined):
|
||||
__iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error
|
||||
__eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error
|
||||
__contains__ = Undefined._fail_with_undefined_error
|
||||
|
||||
|
||||
# Remove slots attributes, after the metaclass is applied they are
|
||||
# unneeded and contain wrong data for subclasses.
|
||||
del (
|
||||
Undefined.__slots__,
|
||||
ChainableUndefined.__slots__,
|
||||
DebugUndefined.__slots__,
|
||||
StrictUndefined.__slots__,
|
||||
)
|
||||
|
||||
@ -8,6 +8,7 @@ import typing as t
|
||||
from _string import formatter_field_name_split # type: ignore
|
||||
from collections import abc
|
||||
from collections import deque
|
||||
from functools import update_wrapper
|
||||
from string import Formatter
|
||||
|
||||
from markupsafe import EscapeFormatter
|
||||
@ -60,7 +61,9 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
|
||||
),
|
||||
(
|
||||
abc.MutableSequence,
|
||||
frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]),
|
||||
frozenset(
|
||||
["append", "clear", "pop", "reverse", "insert", "sort", "extend", "remove"]
|
||||
),
|
||||
),
|
||||
(
|
||||
deque,
|
||||
@ -81,20 +84,6 @@ _mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def inspect_format_method(callable: t.Callable[..., t.Any]) -> 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.
|
||||
@ -314,6 +303,9 @@ class SandboxedEnvironment(Environment):
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
fmt = self.wrap_str_format(value)
|
||||
if fmt is not None:
|
||||
return fmt
|
||||
if self.is_safe_attribute(obj, argument, value):
|
||||
return value
|
||||
return self.unsafe_undefined(obj, argument)
|
||||
@ -331,6 +323,9 @@ class SandboxedEnvironment(Environment):
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
else:
|
||||
fmt = self.wrap_str_format(value)
|
||||
if fmt is not None:
|
||||
return fmt
|
||||
if self.is_safe_attribute(obj, attribute, value):
|
||||
return value
|
||||
return self.unsafe_undefined(obj, attribute)
|
||||
@ -346,34 +341,49 @@ class SandboxedEnvironment(Environment):
|
||||
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[..., t.Any]] = 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.
|
||||
def wrap_str_format(self, value: t.Any) -> t.Optional[t.Callable[..., str]]:
|
||||
"""If the given value is a ``str.format`` or ``str.format_map`` method,
|
||||
return a new function than handles sandboxing. This is done at access
|
||||
rather than in :meth:`call`, so that calls made without ``call`` are
|
||||
also sandboxed.
|
||||
"""
|
||||
if not isinstance(
|
||||
value, (types.MethodType, types.BuiltinMethodType)
|
||||
) or value.__name__ not in ("format", "format_map"):
|
||||
return None
|
||||
|
||||
f_self: t.Any = value.__self__
|
||||
|
||||
if not isinstance(f_self, str):
|
||||
return None
|
||||
|
||||
str_type: t.Type[str] = type(f_self)
|
||||
is_format_map = value.__name__ == "format_map"
|
||||
formatter: SandboxedFormatter
|
||||
if isinstance(s, Markup):
|
||||
formatter = SandboxedEscapeFormatter(self, escape=s.escape)
|
||||
|
||||
if isinstance(f_self, Markup):
|
||||
formatter = SandboxedEscapeFormatter(self, escape=f_self.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"
|
||||
)
|
||||
vformat = formatter.vformat
|
||||
|
||||
kwargs = args[0]
|
||||
args = ()
|
||||
def wrapper(*args: t.Any, **kwargs: t.Any) -> str:
|
||||
if is_format_map:
|
||||
if kwargs:
|
||||
raise TypeError("format_map() takes no keyword arguments")
|
||||
|
||||
rv = formatter.vformat(s, args, kwargs)
|
||||
return type(s)(rv)
|
||||
if len(args) != 1:
|
||||
raise TypeError(
|
||||
f"format_map() takes exactly one argument ({len(args)} given)"
|
||||
)
|
||||
|
||||
kwargs = args[0]
|
||||
args = ()
|
||||
|
||||
return str_type(vformat(f_self, args, kwargs))
|
||||
|
||||
return update_wrapper(wrapper, value)
|
||||
|
||||
def call(
|
||||
__self, # noqa: B902
|
||||
@ -383,9 +393,6 @@ class SandboxedEnvironment(Environment):
|
||||
**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.
|
||||
|
||||
@ -18,8 +18,17 @@ if t.TYPE_CHECKING:
|
||||
|
||||
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"})()
|
||||
|
||||
class _MissingType:
|
||||
def __repr__(self) -> str:
|
||||
return "missing"
|
||||
|
||||
def __reduce__(self) -> str:
|
||||
return "missing"
|
||||
|
||||
|
||||
missing: t.Any = _MissingType()
|
||||
"""Special singleton representing missing values for the runtime."""
|
||||
|
||||
internal_code: t.MutableSet[CodeType] = set()
|
||||
|
||||
@ -324,6 +333,8 @@ def urlize(
|
||||
elif (
|
||||
"@" in middle
|
||||
and not middle.startswith("www.")
|
||||
# ignore values like `@a@b`
|
||||
and not middle.startswith("@")
|
||||
and ":" not in middle
|
||||
and _email_re.match(middle)
|
||||
):
|
||||
@ -453,7 +464,7 @@ class LRUCache:
|
||||
def __getnewargs__(self) -> t.Tuple[t.Any, ...]:
|
||||
return (self.capacity,)
|
||||
|
||||
def copy(self) -> "LRUCache":
|
||||
def copy(self) -> "te.Self":
|
||||
"""Return a shallow copy of the instance."""
|
||||
rv = self.__class__(self.capacity)
|
||||
rv._mapping.update(self._mapping)
|
||||
|
||||
@ -323,8 +323,6 @@ class TestUndefined:
|
||||
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)
|
||||
@ -335,8 +333,6 @@ class TestUndefined:
|
||||
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() == ""
|
||||
@ -368,8 +364,6 @@ class TestUndefined:
|
||||
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)
|
||||
@ -386,8 +380,6 @@ class TestUndefined:
|
||||
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):
|
||||
@ -433,3 +425,11 @@ class TestLowLevel:
|
||||
env = CustomEnvironment()
|
||||
tmpl = env.from_string("{{ foo }}")
|
||||
assert tmpl.render() == "resolve-foo"
|
||||
|
||||
|
||||
def test_overlay_enable_async(env):
|
||||
assert not env.is_async
|
||||
assert not env.overlay().is_async
|
||||
env_async = env.overlay(enable_async=True)
|
||||
assert env_async.is_async
|
||||
assert not env_async.overlay(enable_async=False).is_async
|
||||
|
||||
@ -266,6 +266,13 @@ def test_slice(env_async, items):
|
||||
)
|
||||
|
||||
|
||||
def test_unique_with_async_gen(env_async):
|
||||
items = ["a", "b", "c", "c", "a", "d", "z"]
|
||||
tmpl = env_async.from_string("{{ items|reject('==', 'z')|unique|list }}")
|
||||
out = tmpl.render(items=items)
|
||||
assert out == "['a', 'b', 'c', 'd']"
|
||||
|
||||
|
||||
def test_custom_async_filter(env_async, run_async_fn):
|
||||
async def customfilter(val):
|
||||
return str(val)
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2.environment import Environment
|
||||
from jinja2.loaders import DictLoader
|
||||
|
||||
@ -26,3 +29,80 @@ def test_import_as_with_context_deterministic(tmp_path):
|
||||
expect = [f"'bar{i}': " for i in range(10)]
|
||||
found = re.findall(r"'bar\d': ", content)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_top_level_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f"{{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" 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"context.vars.update({{'a{i}': l_0_a{i}, 'b{i}': l_0_b{i}, 'c{i}': l_0_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"context\.vars\.update\(\{'a\d': l_0_a\d, 'b\d': l_0_b\d, 'c\d': l_0_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
expect = [
|
||||
f"context.exported_vars.update(('a{i}', 'b{i}', 'c{i}'))" for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"context\.exported_vars\.update\(\('a\d', 'b\d', 'c\d'\)\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_loop_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f" {{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" for i in range(10))
|
||||
src = f"{{% for i in seq %}}\n{src}\n{{% endfor %}}"
|
||||
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"_loop_vars.update({{'a{i}': l_1_a{i}, 'b{i}': l_1_b{i}, 'c{i}': l_1_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"_loop_vars\.update\(\{'a\d': l_1_a\d, 'b\d': l_1_b\d, 'c\d': l_1_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_block_set_vars_unpacking_deterministic(tmp_path):
|
||||
src = "\n".join(f" {{% set a{i}, b{i}, c{i} = tuple_var{i} %}}" for i in range(10))
|
||||
src = f"{{% block test %}}\n{src}\n{{% endblock test %}}"
|
||||
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"_block_vars.update({{'a{i}': l_0_a{i}, 'b{i}': l_0_b{i}, 'c{i}': l_0_c{i}}})"
|
||||
for i in range(10)
|
||||
]
|
||||
found = re.findall(
|
||||
r"_block_vars\.update\(\{'a\d': l_0_a\d, 'b\d': l_0_b\d, 'c\d': l_0_c\d\}\)",
|
||||
content,
|
||||
)[:10]
|
||||
assert found == expect
|
||||
|
||||
|
||||
def test_undefined_import_curly_name():
|
||||
env = Environment(
|
||||
loader=DictLoader(
|
||||
{
|
||||
"{bad}": "{% from 'macro' import m %}{{ m() }}",
|
||||
"macro": "",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Must not raise `NameError: 'bad' is not defined`, as that would indicate
|
||||
# that `{bad}` is being interpreted as an f-string. It must be escaped.
|
||||
with pytest.raises(UndefinedError):
|
||||
env.get_template("{bad}").render()
|
||||
|
||||
@ -538,6 +538,14 @@ class TestSet:
|
||||
)
|
||||
assert tmpl.render() == "13|37"
|
||||
|
||||
def test_namespace_set_tuple(self, env_trim):
|
||||
tmpl = env_trim.from_string(
|
||||
"{% set ns = namespace(a=12, b=36) %}"
|
||||
"{% set ns.a, ns.b = ns.a + 1, ns.b + 1 %}"
|
||||
"{{ ns.a }}|{{ ns.b }}"
|
||||
)
|
||||
assert tmpl.render() == "13|37"
|
||||
|
||||
def test_block_escaping_filtered(self):
|
||||
env = Environment(autoescape=True)
|
||||
tmpl = env.from_string(
|
||||
|
||||
@ -196,6 +196,7 @@ class TestFilter:
|
||||
("abc", "0"),
|
||||
("32.32", "32"),
|
||||
("12345678901234567890", "12345678901234567890"),
|
||||
("1e10000", "0"),
|
||||
),
|
||||
)
|
||||
def test_int(self, env, value, expect):
|
||||
|
||||
@ -179,6 +179,24 @@ class TestFileSystemLoader:
|
||||
t = e.get_template("foo/test.html")
|
||||
assert t.filename == str(self.searchpath / "foo" / "test.html")
|
||||
|
||||
def test_error_includes_paths(self, env, filesystem_loader):
|
||||
env.loader = filesystem_loader
|
||||
|
||||
with pytest.raises(TemplateNotFound) as info:
|
||||
env.get_template("missing")
|
||||
|
||||
e_str = str(info.value)
|
||||
assert e_str.startswith("'missing' not found in search path: ")
|
||||
|
||||
filesystem_loader.searchpath.append("other")
|
||||
|
||||
with pytest.raises(TemplateNotFound) as info:
|
||||
env.get_template("missing")
|
||||
|
||||
e_str = str(info.value)
|
||||
assert e_str.startswith("'missing' not found in search paths: ")
|
||||
assert ", 'other'" in e_str
|
||||
|
||||
|
||||
class TestModuleLoader:
|
||||
archive = None
|
||||
@ -411,3 +429,8 @@ def test_pep_451_import_hook():
|
||||
assert "test.html" in package_loader.list_templates()
|
||||
finally:
|
||||
sys.meta_path[:] = before
|
||||
|
||||
|
||||
def test_package_loader_no_dir() -> None:
|
||||
with pytest.raises(ValueError, match="could not find a 'templates' directory"):
|
||||
PackageLoader("jinja2")
|
||||
|
||||
@ -177,3 +177,13 @@ def test_macro(env):
|
||||
result = t.render()
|
||||
assert result == 2
|
||||
assert isinstance(result, int)
|
||||
|
||||
|
||||
def test_block(env):
|
||||
t = env.from_string(
|
||||
"{% block b %}{% for i in range(1) %}{{ loop.index }}{% endfor %}"
|
||||
"{% endblock %}{{ self.b() }}"
|
||||
)
|
||||
result = t.render()
|
||||
assert result == 11
|
||||
assert isinstance(result, int)
|
||||
|
||||
@ -737,6 +737,28 @@ End"""
|
||||
)
|
||||
assert tmpl.render() == "hellohellohello"
|
||||
|
||||
def test_pass_context_with_select(self, env):
|
||||
@pass_context
|
||||
def is_foo(ctx, s):
|
||||
assert ctx is not None
|
||||
return s == "foo"
|
||||
|
||||
env.tests["foo"] = is_foo
|
||||
tmpl = env.from_string(
|
||||
"{% for x in ['one', 'foo'] | select('foo') %}{{ x }}{% endfor %}"
|
||||
)
|
||||
assert tmpl.render() == "foo"
|
||||
|
||||
|
||||
def test_load_parameter_when_set_in_all_if_branches(env):
|
||||
tmpl = env.from_string(
|
||||
"{% if True %}{{ a.b }}{% set a = 1 %}"
|
||||
"{% elif False %}{% set a = 2 %}"
|
||||
"{% else %}{% set a = 3 %}{% endif %}"
|
||||
"{{ a }}"
|
||||
)
|
||||
assert tmpl.render(a={"b": 0}) == "01"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unicode_char", ["\N{FORM FEED}", "\x85"])
|
||||
def test_unicode_whitespace(env, unicode_char):
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
import copy
|
||||
import itertools
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
||||
from jinja2 import ChainableUndefined
|
||||
from jinja2 import DebugUndefined
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import Undefined
|
||||
from jinja2.runtime import LoopContext
|
||||
|
||||
TEST_IDX_TEMPLATE_STR_1 = (
|
||||
@ -73,3 +82,44 @@ def test_mock_not_pass_arg_marker():
|
||||
out = t.render(calc=Calc())
|
||||
# Would be "1" if context argument was passed.
|
||||
assert out == "0"
|
||||
|
||||
|
||||
_undefined_types = (Undefined, ChainableUndefined, DebugUndefined, StrictUndefined)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_copy(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = copy.copy(undef)
|
||||
|
||||
assert copied is not undef
|
||||
assert copied._undefined_hint is undef._undefined_hint
|
||||
assert copied._undefined_obj is undef._undefined_obj
|
||||
assert copied._undefined_name is undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_deepcopy(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = copy.deepcopy(undef)
|
||||
|
||||
assert copied._undefined_hint is undef._undefined_hint
|
||||
assert copied._undefined_obj is not undef._undefined_obj
|
||||
assert copied._undefined_obj == undef._undefined_obj
|
||||
assert copied._undefined_name is undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
|
||||
@pytest.mark.parametrize("undefined_type", _undefined_types)
|
||||
def test_undefined_pickle(undefined_type):
|
||||
undef = undefined_type("a hint", ["foo"], "a name", TemplateRuntimeError)
|
||||
copied = pickle.loads(pickle.dumps(undef))
|
||||
|
||||
assert copied._undefined_hint is not undef._undefined_hint
|
||||
assert copied._undefined_hint == undef._undefined_hint
|
||||
assert copied._undefined_obj is not undef._undefined_obj
|
||||
assert copied._undefined_obj == undef._undefined_obj
|
||||
assert copied._undefined_name is not undef._undefined_name
|
||||
assert copied._undefined_name == undef._undefined_name
|
||||
assert copied._undefined_exception is undef._undefined_exception
|
||||
|
||||
@ -58,6 +58,8 @@ class TestSandbox:
|
||||
def test_immutable_environment(self, env):
|
||||
env = ImmutableSandboxedEnvironment()
|
||||
pytest.raises(SecurityError, env.from_string("{{ [].append(23) }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ [].clear() }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ [1].pop() }}").render)
|
||||
pytest.raises(SecurityError, env.from_string("{{ {1:2}.clear() }}").render)
|
||||
|
||||
def test_restricted(self, env):
|
||||
@ -171,3 +173,20 @@ class TestStringFormatMap:
|
||||
'{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}'
|
||||
)
|
||||
assert t.render() == "a42b<foo>"
|
||||
|
||||
def test_indirect_call(self):
|
||||
def run(value, arg):
|
||||
return value.run(arg)
|
||||
|
||||
env = SandboxedEnvironment()
|
||||
env.filters["run"] = run
|
||||
t = env.from_string(
|
||||
"""{% set
|
||||
ns = namespace(run="{0.__call__.__builtins__[__import__]}".format)
|
||||
%}
|
||||
{{ ns | run(not_here) }}
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(SecurityError):
|
||||
t.render()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import pickle
|
||||
import random
|
||||
from collections import deque
|
||||
@ -141,6 +142,14 @@ class TestEscapeUrlizeTarget:
|
||||
"http://example.org</a>"
|
||||
)
|
||||
|
||||
def test_urlize_mail_mastodon(self):
|
||||
fr = "nabijaczleweli@nabijaczleweli.xyz\n@eater@cijber.social\n"
|
||||
to = (
|
||||
'<a href="mailto:nabijaczleweli@nabijaczleweli.xyz">'
|
||||
"nabijaczleweli@nabijaczleweli.xyz</a>\n@eater@cijber.social\n"
|
||||
)
|
||||
assert urlize(fr) == to
|
||||
|
||||
|
||||
class TestLoremIpsum:
|
||||
def test_lorem_ipsum_markup(self):
|
||||
@ -183,3 +192,14 @@ def test_consume():
|
||||
consume(x)
|
||||
with pytest.raises(StopIteration):
|
||||
next(x)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
||||
def test_pickle_missing(protocol: int) -> None:
|
||||
"""Test that missing can be pickled while remaining a singleton."""
|
||||
assert pickle.loads(pickle.dumps(missing, protocol)) is missing
|
||||
|
||||
|
||||
def test_copy_missing() -> None:
|
||||
"""Test that missing can be copied while remaining a singleton."""
|
||||
assert copy.copy(missing) is missing
|
||||
|
||||
Loading…
Reference in New Issue
Block a user