Merge branch 'stable'

This commit is contained in:
David Lord 2024-12-21 10:47:46 -08:00
commit 6aeab5d1da
No known key found for this signature in database
GPG Key ID: 43368A7AA8CC5926
38 changed files with 584 additions and 213 deletions

View File

@ -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/

View File

@ -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') }}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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/

View File

@ -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
---

View File

@ -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.

View File

@ -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::

View File

@ -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() }}

View File

@ -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

View File

@ -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:

View 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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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())

View File

@ -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__)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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`.
"""

View File

@ -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

View File

@ -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__,
)

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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(

View File

@ -196,6 +196,7 @@ class TestFilter:
("abc", "0"),
("32.32", "32"),
("12345678901234567890", "12345678901234567890"),
("1e10000", "0"),
),
)
def test_int(self, env, value, expect):

View File

@ -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")

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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&lt;foo&gt;"
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()

View File

@ -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