New upstream version 3.1.3

This commit is contained in:
tangtingting 2025-06-24 13:48:06 +08:00
parent 65adae8035
commit 9bcffab58c
32 changed files with 236 additions and 180 deletions

View File

@ -1,5 +1,17 @@
.. currentmodule:: jinja2
Version 3.1.3
-------------
Released 2024-01-10
- Fix compiler error when checking if required blocks in parent templates are
empty. :pr:`1858`
- ``xmlattr`` filter does not allow keys with spaces. GHSA-h5c8-rqwp-cp95
- Make error messages stemming from invalid nesting of ``{% trans %}`` blocks
more helpful. :pr:`1916`
Version 3.1.2
-------------

View File

@ -1,10 +1,8 @@
Metadata-Version: 2.1
Name: Jinja2
Version: 3.1.2
Version: 3.1.3
Summary: A very fast and expressive template engine.
Home-page: https://palletsprojects.com/p/jinja/
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
Maintainer: Pallets
Maintainer-email: contact@palletsprojects.com
License: BSD-3-Clause
@ -13,9 +11,7 @@ Project-URL: Documentation, https://jinja.palletsprojects.com/
Project-URL: Changes, https://jinja.palletsprojects.com/changes/
Project-URL: Source Code, https://github.com/pallets/jinja/
Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/
Project-URL: Twitter, https://twitter.com/PalletsTeam
Project-URL: Chat, https://discord.gg/pallets
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
@ -26,8 +22,10 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
Provides-Extra: i18n
License-File: LICENSE.rst
Requires-Dist: MarkupSafe>=2.0
Provides-Extra: i18n
Requires-Dist: Babel>=2.7; extra == "i18n"
Jinja
=====
@ -104,8 +102,4 @@ Links
- PyPI Releases: https://pypi.org/project/Jinja2/
- Source Code: https://github.com/pallets/jinja/
- Issue Tracker: https://github.com/pallets/jinja/issues/
- Website: https://palletsprojects.com/p/jinja/
- Twitter: https://twitter.com/PalletsTeam
- Chat: https://discord.gg/pallets

View File

@ -73,6 +73,4 @@ Links
- PyPI Releases: https://pypi.org/project/Jinja2/
- Source Code: https://github.com/pallets/jinja/
- Issue Tracker: https://github.com/pallets/jinja/issues/
- Website: https://palletsprojects.com/p/jinja/
- Twitter: https://twitter.com/PalletsTeam
- Chat: https://discord.gg/pallets

View File

@ -751,8 +751,8 @@ Now it can be used in templates:
{% endif %}
Some decorators are available to tell Jinja to pass extra information to
the filter. The object is passed as the first argument, making the value
being filtered the second argument.
the test. The object is passed as the first argument, making the value
being tested the second argument.
- :func:`pass_environment` passes the :class:`Environment`.
- :func:`pass_eval_context` passes the :ref:`eval-context`.

View File

@ -32,8 +32,6 @@ html_context = {
ProjectLink("PyPI Releases", "https://pypi.org/project/Jinja2/"),
ProjectLink("Source Code", "https://github.com/pallets/jinja/"),
ProjectLink("Issue Tracker", "https://github.com/pallets/jinja/issues/"),
ProjectLink("Website", "https://palletsprojects.com/p/jinja/"),
ProjectLink("Twitter", "https://twitter.com/PalletsTeam"),
ProjectLink("Chat", "https://discord.gg/pallets"),
]
}

View File

@ -91,4 +91,4 @@ this add this to ``config/environment.py``:
config['pylons.strict_c'] = True
.. _Pylons: https://pylonshq.com/
.. _Pylons: https://pylonsproject.org/

View File

@ -84,7 +84,7 @@ In Django, the special variable for the loop context is called
.. code-block:: django
{% for item in items %}
{{ item }}
{{ forloop.counter }}. {{ item }}
{% empty %}
No items!
{% endfor %}
@ -95,7 +95,7 @@ and the ``else`` block is used for no loop items.
.. code-block:: jinja
{% for item in items %}
{{ loop.index}}. {{ item }}
{{ loop.index }}. {{ item }}
{% else %}
No items!
{% endfor %}

View File

@ -599,7 +599,8 @@ first and pass it in to ``render``.
else:
layout = env.get_template("layout.html")
user_detail = env.get_template("user/detail.html", layout=layout)
user_detail = env.get_template("user/detail.html")
return user_detail.render(layout=layout)
.. code-block:: jinja
@ -1014,6 +1015,9 @@ template data. Just wrap the code in the special `filter` section::
This text becomes uppercase
{% endfilter %}
Filters that accept arguments can be called like this::
{% filter center(100) %}Center this{% endfilter %}
.. _assignments:
@ -1168,7 +1172,7 @@ none of the templates exist.
{% include ['special_sidebar.html', 'sidebar.html'] ignore missing %}
A variable, with either a template name or template object, can also be
passed to the statment.
passed to the statement.
.. _import:
@ -1607,8 +1611,7 @@ The following functions are available in the global scope by default:
.. versionadded:: 2.1
.. method:: current
:property:
.. property:: current
Return the current item. Equivalent to the item that will be
returned next time :meth:`next` is called.

15
requirements/build.txt Normal file
View File

@ -0,0 +1,15 @@
# SHA1:80754af91bfb6d1073585b046fe0a474ce868509
#
# This file is autogenerated by pip-compile-multi
# To update, run:
#
# pip-compile-multi
#
build==0.10.0
# via -r requirements/build.in
packaging==23.1
# via build
pyproject-hooks==1.0.0
# via build
tomli==2.0.1
# via build

View File

@ -8,51 +8,55 @@
-r docs.txt
-r tests.txt
-r typing.txt
build==0.10.0
# via pip-tools
cachetools==5.3.1
# via tox
cfgv==3.3.1
# via pre-commit
click==8.1.2
chardet==5.1.0
# via tox
click==8.1.3
# via
# pip-compile-multi
# pip-tools
distlib==0.3.4
colorama==0.4.6
# via tox
distlib==0.3.6
# via virtualenv
filelock==3.6.0
filelock==3.12.2
# via
# tox
# virtualenv
identify==2.5.0
identify==2.5.24
# via pre-commit
nodeenv==1.6.0
nodeenv==1.8.0
# via pre-commit
pep517==0.12.0
# via pip-tools
pip-compile-multi==2.4.5
pip-compile-multi==2.6.3
# via -r requirements/dev.in
pip-tools==6.6.0
pip-tools==6.13.0
# via pip-compile-multi
platformdirs==2.5.2
# via virtualenv
pre-commit==2.18.1
platformdirs==3.8.0
# via
# tox
# virtualenv
pre-commit==3.3.3
# via -r requirements/dev.in
pyproject-api==1.5.2
# via tox
pyproject-hooks==1.0.0
# via build
pyyaml==6.0
# via pre-commit
six==1.16.0
# via
# tox
# virtualenv
toml==0.10.2
# via
# pre-commit
# tox
toposort==1.7
toposort==1.10
# via pip-compile-multi
tox==3.25.0
tox==4.6.3
# via -r requirements/dev.in
virtualenv==20.14.1
virtualenv==20.23.1
# via
# pre-commit
# tox
wheel==0.37.1
wheel==0.40.0
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:

View File

@ -5,41 +5,37 @@
#
# pip-compile-multi
#
alabaster==0.7.12
alabaster==0.7.13
# via sphinx
babel==2.10.1
babel==2.12.1
# via sphinx
certifi==2021.10.8
certifi==2023.5.7
# via requests
charset-normalizer==2.0.12
charset-normalizer==3.1.0
# via requests
docutils==0.17.1
docutils==0.20.1
# via sphinx
idna==3.3
idna==3.4
# via requests
imagesize==1.3.0
imagesize==1.4.1
# via sphinx
jinja2==3.1.1
jinja2==3.1.2
# via sphinx
markupsafe==2.1.1
markupsafe==2.1.3
# via jinja2
packaging==21.3
packaging==23.1
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.0.2
pallets-sphinx-themes==2.1.1
# via -r requirements/docs.in
pygments==2.12.0
pygments==2.15.1
# via sphinx
pyparsing==3.0.8
# via packaging
pytz==2022.1
# via babel
requests==2.27.1
requests==2.31.0
# via sphinx
snowballstemmer==2.2.0
# via sphinx
sphinx==4.5.0
sphinx==7.0.1
# via
# -r requirements/docs.in
# pallets-sphinx-themes
@ -47,11 +43,11 @@ sphinx==4.5.0
# sphinxcontrib-log-cabinet
sphinx-issues==3.0.1
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-applehelp==1.0.4
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-htmlhelp==2.0.1
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
@ -61,5 +57,5 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
urllib3==1.26.9
urllib3==2.0.3
# via requests

View File

@ -5,19 +5,15 @@
#
# pip-compile-multi
#
attrs==21.4.0
exceptiongroup==1.1.1
# via pytest
iniconfig==1.1.1
iniconfig==2.0.0
# via pytest
packaging==21.3
packaging==23.1
# via pytest
pluggy==1.0.0
pluggy==1.2.0
# via pytest
py==1.11.0
# via pytest
pyparsing==3.0.8
# via packaging
pytest==7.1.2
pytest==7.4.0
# via -r requirements/tests.in
tomli==2.0.1
# via pytest

View File

@ -5,11 +5,11 @@
#
# pip-compile-multi
#
mypy==0.950
mypy==1.4.1
# via -r requirements/typing.in
mypy-extensions==0.4.3
mypy-extensions==1.0.0
# via mypy
tomli==2.0.1
# via mypy
typing-extensions==4.2.0
typing-extensions==4.6.3
# via mypy

View File

@ -8,12 +8,9 @@ project_urls =
Changes = https://jinja.palletsprojects.com/changes/
Source Code = https://github.com/pallets/jinja/
Issue Tracker = https://github.com/pallets/jinja/issues/
Twitter = https://twitter.com/PalletsTeam
Chat = https://discord.gg/pallets
license = BSD-3-Clause
license_files = LICENSE.rst
author = Armin Ronacher
author_email = armin.ronacher@active-4.com
maintainer = Pallets
maintainer_email = contact@palletsprojects.com
description = A very fast and expressive template engine.
@ -46,7 +43,6 @@ babel.extractors =
testpaths = tests
filterwarnings =
error
ignore:The loop argument:DeprecationWarning:asyncio[.]base_events:542
[coverage:run]
branch = True
@ -59,17 +55,6 @@ source =
src
*/site-packages
[flake8]
select = B, E, F, W, B9, ISC
ignore =
E203
E501
E722
W503
max-line-length = 80
per-file-ignores =
src/jinja2/__init__.py: F401
[mypy]
files = src/jinja2
python_version = 3.7

View File

@ -1,10 +1,8 @@
Metadata-Version: 2.1
Name: Jinja2
Version: 3.1.2
Version: 3.1.3
Summary: A very fast and expressive template engine.
Home-page: https://palletsprojects.com/p/jinja/
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
Maintainer: Pallets
Maintainer-email: contact@palletsprojects.com
License: BSD-3-Clause
@ -13,9 +11,7 @@ Project-URL: Documentation, https://jinja.palletsprojects.com/
Project-URL: Changes, https://jinja.palletsprojects.com/changes/
Project-URL: Source Code, https://github.com/pallets/jinja/
Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/
Project-URL: Twitter, https://twitter.com/PalletsTeam
Project-URL: Chat, https://discord.gg/pallets
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
@ -26,8 +22,10 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
Provides-Extra: i18n
License-File: LICENSE.rst
Requires-Dist: MarkupSafe>=2.0
Provides-Extra: i18n
Requires-Dist: Babel>=2.7; extra == "i18n"
Jinja
=====
@ -104,8 +102,4 @@ Links
- PyPI Releases: https://pypi.org/project/Jinja2/
- Source Code: https://github.com/pallets/jinja/
- Issue Tracker: https://github.com/pallets/jinja/issues/
- Website: https://palletsprojects.com/p/jinja/
- Twitter: https://twitter.com/PalletsTeam
- Chat: https://discord.gg/pallets

View File

@ -35,6 +35,7 @@ examples/basic/test_loop_filter.py
examples/basic/translate.py
examples/basic/templates/broken.html
examples/basic/templates/subbroken.html
requirements/build.txt
requirements/dev.txt
requirements/docs.txt
requirements/tests.txt

View File

@ -34,4 +34,4 @@ from .utils import pass_environment as pass_environment
from .utils import pass_eval_context as pass_eval_context
from .utils import select_autoescape as select_autoescape
__version__ = "3.1.2"
__version__ = "3.1.3"

View File

@ -74,7 +74,7 @@ async def auto_aiter(
async for item in t.cast("t.AsyncIterable[V]", iterable):
yield item
else:
for item in t.cast("t.Iterable[V]", iterable):
for item in iterable:
yield item

View File

@ -993,7 +993,6 @@ class CodeGenerator(NodeVisitor):
# far, we don't have to add a check if something extended
# the template before this one.
if self.extends_so_far > 0:
# if we have a known extends we just add a template runtime
# error into the generated code. We could catch that at compile
# time too, but i welcome it not to confuse users by throwing the
@ -1407,7 +1406,7 @@ class CodeGenerator(NodeVisitor):
if pass_arg is None:
def finalize(value: t.Any) -> t.Any:
def finalize(value: t.Any) -> t.Any: # noqa: F811
return default(env_finalize(value))
else:
@ -1415,7 +1414,7 @@ class CodeGenerator(NodeVisitor):
if pass_arg == "environment":
def finalize(value: t.Any) -> t.Any:
def finalize(value: t.Any) -> t.Any: # noqa: F811
return default(env_finalize(self.environment, value))
self._finalize = self._FinalizeInfo(finalize, src)

View File

@ -701,7 +701,7 @@ class Environment:
.. versionadded:: 2.5
"""
return compile(source, filename, "exec") # type: ignore
return compile(source, filename, "exec")
@typing.overload
def compile( # type: ignore
@ -920,7 +920,7 @@ class Environment:
)
def filter_func(x: str) -> bool:
return "." in x and x.rsplit(".", 1)[1] in extensions # type: ignore
return "." in x and x.rsplit(".", 1)[1] in extensions
if filter_func is not None:
names = [name for name in names if filter_func(name)]
@ -1253,7 +1253,7 @@ class Template:
t.blocks = namespace["blocks"]
# render function and module
t.root_render_func = namespace["root"] # type: ignore
t.root_render_func = namespace["root"]
t._module = None
# debug and loader helpers
@ -1349,7 +1349,7 @@ class Template:
ctx = self.new_context(dict(*args, **kwargs))
try:
yield from self.root_render_func(ctx) # type: ignore
yield from self.root_render_func(ctx)
except Exception:
yield self.environment.handle_exception()
@ -1532,7 +1532,7 @@ class TemplateModule:
" API you are using."
)
body_stream = list(template.root_render_func(context)) # type: ignore
body_stream = list(template.root_render_func(context))
self._body_stream = body_stream
self.__dict__.update(context.get_exported())
@ -1564,7 +1564,7 @@ class TemplateExpression:
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Optional[t.Any]:
context = self._template.new_context(dict(*args, **kwargs))
consume(self._template.root_render_func(context)) # type: ignore
consume(self._template.root_render_func(context))
rv = context.vars["result"]
if self._undefined_to_none and isinstance(rv, Undefined):
rv = None

View File

@ -291,14 +291,14 @@ class InternationalizationExtension(Extension):
if hasattr(translations, "pgettext"):
# Python < 3.8
pgettext = translations.pgettext # type: ignore
pgettext = translations.pgettext
else:
def pgettext(c: str, s: str) -> str:
return s
if hasattr(translations, "npgettext"):
npgettext = translations.npgettext # type: ignore
npgettext = translations.npgettext
else:
def npgettext(c: str, s: str, p: str, n: int) -> str:
@ -495,16 +495,26 @@ class InternationalizationExtension(Extension):
parser.stream.expect("variable_end")
elif parser.stream.current.type == "block_begin":
next(parser.stream)
if parser.stream.current.test("name:endtrans"):
block_name = (
parser.stream.current.value
if parser.stream.current.type == "name"
else None
)
if block_name == "endtrans":
break
elif parser.stream.current.test("name:pluralize"):
elif block_name == "pluralize":
if allow_pluralize:
break
parser.fail(
"a translatable section can have only one pluralize section"
)
elif block_name == "trans":
parser.fail(
"trans blocks can't be nested; did you mean `endtrans`?"
)
parser.fail(
"control structures in translatable sections are not allowed"
f"control structures in translatable sections are not allowed; "
f"saw `{block_name}`"
)
elif parser.stream.eos:
parser.fail("unclosed translation block")

View File

@ -248,13 +248,17 @@ def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K
yield from value.items()
_space_re = re.compile(r"\s", flags=re.ASCII)
@pass_eval_context
def do_xmlattr(
eval_ctx: "EvalContext", d: t.Mapping[str, t.Any], autospace: bool = True
) -> str:
"""Create an SGML/XML attribute string based on the items in a dict.
All values that are neither `none` nor `undefined` are automatically
escaped:
If any key contains a space, this fails with a ``ValueError``. Values that
are neither ``none`` nor ``undefined`` are automatically escaped.
.. sourcecode:: html+jinja
@ -273,12 +277,22 @@ def do_xmlattr(
As you can see it automatically prepends a space in front of the item
if the filter returned something unless the second parameter is false.
.. versionchanged:: 3.1.3
Keys with spaces are not allowed.
"""
rv = " ".join(
f'{escape(key)}="{escape(value)}"'
for key, value in d.items()
if value is not None and not isinstance(value, Undefined)
)
items = []
for key, value in d.items():
if value is None or isinstance(value, Undefined):
continue
if _space_re.search(key) is not None:
raise ValueError(f"Spaces are not allowed in attributes: '{key}'")
items.append(f'{escape(key)}="{escape(value)}"')
rv = " ".join(items)
if autospace and rv:
rv = " " + rv

View File

@ -15,7 +15,6 @@ from types import ModuleType
from .exceptions import TemplateNotFound
from .utils import internalcode
from .utils import open_if_exists
if t.TYPE_CHECKING:
from .environment import Environment
@ -193,29 +192,30 @@ class FileSystemLoader(BaseLoader):
self, environment: "Environment", template: str
) -> t.Tuple[str, str, t.Callable[[], bool]]:
pieces = split_template_path(template)
for searchpath in self.searchpath:
# Use posixpath even on Windows to avoid "drive:" or UNC
# segments breaking out of the search directory.
filename = posixpath.join(searchpath, *pieces)
f = open_if_exists(filename)
if f is None:
continue
if os.path.isfile(filename):
break
else:
raise TemplateNotFound(template)
with open(filename, encoding=self.encoding) as f:
contents = f.read()
mtime = os.path.getmtime(filename)
def uptodate() -> bool:
try:
contents = f.read().decode(self.encoding)
finally:
f.close()
return os.path.getmtime(filename) == mtime
except OSError:
return False
mtime = os.path.getmtime(filename)
def uptodate() -> bool:
try:
return os.path.getmtime(filename) == mtime
except OSError:
return False
# Use normpath to convert Windows altsep to sep.
return contents, os.path.normpath(filename), uptodate
raise TemplateNotFound(template)
# Use normpath to convert Windows altsep to sep.
return contents, os.path.normpath(filename), uptodate
def list_templates(self) -> t.List[str]:
found = set()
@ -392,7 +392,7 @@ class PackageLoader(BaseLoader):
)
offset = len(prefix)
for name in self._loader._files.keys(): # type: ignore
for name in self._loader._files.keys():
# Find names under the templates directory that aren't directories.
if name.startswith(prefix) and name[-1] != os.path.sep:
results.append(name[offset:].replace(os.path.sep, "/"))

View File

@ -106,7 +106,7 @@ class NativeTemplate(Template):
try:
return self.environment_class.concat( # type: ignore
self.root_render_func(ctx) # type: ignore
self.root_render_func(ctx)
)
except Exception:
return self.environment.handle_exception()

View File

@ -311,12 +311,14 @@ class Parser:
# enforce that required blocks only contain whitespace or comments
# by asserting that the body, if not empty, is just TemplateData nodes
# with whitespace data
if node.required and not all(
isinstance(child, nodes.TemplateData) and child.data.isspace()
for body in node.body
for child in body.nodes # type: ignore
):
self.fail("Required blocks can only contain comments or whitespace")
if node.required:
for body_node in node.body:
if not isinstance(body_node, nodes.Output) or any(
not isinstance(output_node, nodes.TemplateData)
or not output_node.data.isspace()
for output_node in body_node.nodes
):
self.fail("Required blocks can only contain comments or whitespace")
self.stream.skip_if("name:" + node.name)
return node
@ -857,7 +859,7 @@ class Parser:
else:
args.append(None)
return nodes.Slice(lineno=lineno, *args)
return nodes.Slice(lineno=lineno, *args) # noqa: B026
def parse_call_args(self) -> t.Tuple:
token = self.stream.expect("lparen")

View File

@ -272,9 +272,9 @@ class Context:
# Allow callable classes to take a context
if (
hasattr(__obj, "__call__") # noqa: B004
and _PassArg.from_obj(__obj.__call__) is not None # type: ignore
and _PassArg.from_obj(__obj.__call__) is not None
):
__obj = __obj.__call__ # type: ignore
__obj = __obj.__call__
pass_arg = _PassArg.from_obj(__obj)
@ -927,9 +927,7 @@ def make_logging_undefined(
logger.addHandler(logging.StreamHandler(sys.stderr))
def _log_message(undef: Undefined) -> None:
logger.warning( # type: ignore
"Template variable warning: %s", undef._undefined_message
)
logger.warning("Template variable warning: %s", undef._undefined_message)
class LoggingUndefined(base): # type: ignore
__slots__ = ()

View File

@ -182,7 +182,7 @@ def object_type_repr(obj: t.Any) -> str:
def pformat(obj: t.Any) -> str:
"""Format an object using :func:`pprint.pformat`."""
from pprint import pformat # type: ignore
from pprint import pformat
return pformat(obj)
@ -259,7 +259,7 @@ def urlize(
if trim_url_limit is not None:
def trim_url(x: str) -> str:
if len(x) > trim_url_limit: # type: ignore
if len(x) > trim_url_limit:
return f"{x[:trim_url_limit]}..."
return x

View File

@ -7,6 +7,7 @@ from jinja2 import DictLoader
from jinja2 import Environment
from jinja2 import nodes
from jinja2 import pass_context
from jinja2 import TemplateSyntaxError
from jinja2.exceptions import TemplateAssertionError
from jinja2.ext import Extension
from jinja2.lexer import count_newlines
@ -468,6 +469,18 @@ class TestInternationalization:
(3, "npgettext", ("babel", "%(users)s user", "%(users)s users", None), []),
]
def test_nested_trans_error(self):
s = "{% trans %}foo{% trans %}{% endtrans %}"
with pytest.raises(TemplateSyntaxError) as excinfo:
i18n_env.from_string(s)
assert "trans blocks can't be nested" in str(excinfo.value)
def test_trans_block_error(self):
s = "{% trans %}foo{% wibble bar %}{% endwibble %}{% endtrans %}"
with pytest.raises(TemplateSyntaxError) as excinfo:
i18n_env.from_string(s)
assert "saw `wibble`" in str(excinfo.value)
class TestScope:
def test_basic_scope_behavior(self):

View File

@ -474,6 +474,12 @@ class TestFilter:
assert 'bar="23"' in out
assert 'blub:blub="&lt;?&gt;"' in out
def test_xmlattr_key_with_spaces(self, env):
with pytest.raises(ValueError, match="Spaces are not allowed"):
env.from_string(
"{{ {'src=1 onerror=alert(1)': 'my_class'}|xmlattr }}"
).render()
def test_sort1(self, env):
tmpl = env.from_string("{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}")
assert tmpl.render() == "[1, 2, 3]|[3, 2, 1]"
@ -870,4 +876,6 @@ class TestFilter:
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
t1.render(x=42)
with pytest.raises(TemplateRuntimeError, match="No filter named 'f'"):
t2.render(x=42)

View File

@ -287,26 +287,34 @@ class TestInheritance:
env = Environment(
loader=DictLoader(
{
"default": "{% block x required %}data {# #}{% endblock %}",
"default1": "{% block x required %}{% block y %}"
"{% endblock %} {% endblock %}",
"default2": "{% block x required %}{% if true %}"
"{% endif %} {% endblock %}",
"level1": "{% if default %}{% extends default %}"
"{% else %}{% extends 'default' %}{% endif %}"
"{%- block x %}CHILD{% endblock %}",
"empty": "{% block x required %}{% endblock %}",
"blank": "{% block x required %} {# c #}{% endblock %}",
"text": "{% block x required %}data {# c #}{% endblock %}",
"block": "{% block x required %}{% block y %}"
"{% endblock %}{% endblock %}",
"if": "{% block x required %}{% if true %}"
"{% endif %}{% endblock %}",
"top": "{% extends t %}{% block x %}CHILD{% endblock %}",
}
)
)
t = env.get_template("level1")
t = env.get_template("top")
assert t.render(t="empty") == "CHILD"
assert t.render(t="blank") == "CHILD"
with pytest.raises(
required_block_check = pytest.raises(
TemplateSyntaxError,
match="Required blocks can only contain comments or whitespace",
):
assert t.render(default="default")
assert t.render(default="default2")
assert t.render(default="default3")
)
with required_block_check:
t.render(t="text")
with required_block_check:
t.render(t="block")
with required_block_check:
t.render(t="if")
def test_required_with_scope(self, env):
env = Environment(
@ -347,8 +355,11 @@ class TestInheritance:
)
)
tmpl = env.get_template("child")
with pytest.raises(TemplateSyntaxError):
tmpl.render(default="default1", seq=list(range(3)))
with pytest.raises(TemplateSyntaxError):
tmpl.render(default="default2", seq=list(range(3)))

View File

@ -183,6 +183,7 @@ class TestFileSystemLoader:
class TestModuleLoader:
archive = None
mod_env = None
def compile_down(self, prefix_loader, zip="deflated"):
log = []
@ -196,13 +197,14 @@ class TestModuleLoader:
self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive))
return "".join(log)
def teardown(self):
if hasattr(self, "mod_env"):
def teardown_method(self):
if self.archive is not None:
if os.path.isfile(self.archive):
os.remove(self.archive)
else:
shutil.rmtree(self.archive)
self.archive = None
self.mod_env = None
def test_log(self, prefix_loader):
log = self.compile_down(prefix_loader)

View File

@ -1,19 +1,22 @@
[tox]
envlist =
py3{11,10,9,8,7},pypy3{8,7}
py3{12,11,10,9,8,7}
pypy310
style
typing
docs
skip_missing_interpreters = true
[testenv]
package = wheel
wheel_build_env = .pkg
deps = -r requirements/tests.txt
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
[testenv:style]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files --show-diff-on-failure
commands = pre-commit run --all-files
[testenv:typing]
deps = -r requirements/typing.txt