From 2d88607c8054bc1a5fe455faf841fe8a2112ba01750afdd188121a5ee637e283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Schr=C3=B6ter?= Date: Tue, 24 Dec 2024 17:14:52 +0100 Subject: [PATCH] Sync from SUSE:SLFO:Main python-Jinja2 revision 18277625f48ada76e409c23ff521f708 --- fix-ftbfs-with-python313.patch | 581 +++++++++++++++++++++++++++++++++ python-Jinja2.changes | 12 + python-Jinja2.spec | 9 + 3 files changed, 602 insertions(+) create mode 100644 fix-ftbfs-with-python313.patch diff --git a/fix-ftbfs-with-python313.patch b/fix-ftbfs-with-python313.patch new file mode 100644 index 0000000..9ec6b21 --- /dev/null +++ b/fix-ftbfs-with-python313.patch @@ -0,0 +1,581 @@ +From d44af7635fa97e980673f29c6192d9fc5cbfc85a Mon Sep 17 00:00:00 2001 +From: Thomas Grainger +Date: Thu, 23 May 2024 15:30:36 +0200 +Subject: [PATCH] Python 3.13 fixes + +Combined from: + - https://github.com/pallets/jinja/pull/1960 + - https://github.com/pallets/jinja/pull/1977 + +Co-Authored-By: David Lord +--- + src/jinja2/async_utils.py | 25 ++++++-- + src/jinja2/compiler.py | 46 +++++++++----- + src/jinja2/environment.py | 12 +++- + tests/test_async.py | 122 +++++++++++++++++++++++++++++------- + tests/test_async_filters.py | 67 ++++++++++++++++---- + tests/test_loader.py | 5 +- + 6 files changed, 214 insertions(+), 63 deletions(-) + +diff --git a/src/jinja2/async_utils.py b/src/jinja2/async_utils.py +index e65219e..b0d277d 100644 +--- a/src/jinja2/async_utils.py ++++ b/src/jinja2/async_utils.py +@@ -6,6 +6,9 @@ from functools import wraps + from .utils import _PassArg + from .utils import pass_eval_context + ++if t.TYPE_CHECKING: ++ import typing_extensions as te ++ + V = t.TypeVar("V") + + +@@ -67,15 +70,27 @@ async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V": + return t.cast("V", value) + + +-async def auto_aiter( ++class _IteratorToAsyncIterator(t.Generic[V]): ++ def __init__(self, iterator: "t.Iterator[V]"): ++ self._iterator = iterator ++ ++ def __aiter__(self) -> "te.Self": ++ return self ++ ++ async def __anext__(self) -> V: ++ try: ++ return next(self._iterator) ++ except StopIteration as e: ++ raise StopAsyncIteration(e.value) from e ++ ++ ++def auto_aiter( + iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", + ) -> "t.AsyncIterator[V]": + if hasattr(iterable, "__aiter__"): +- async for item in t.cast("t.AsyncIterable[V]", iterable): +- yield item ++ return iterable.__aiter__() + else: +- for item in iterable: +- yield item ++ return _IteratorToAsyncIterator(iter(iterable)) + + + async def auto_to_list( +diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py +index 2740717..91720c5 100644 +--- a/src/jinja2/compiler.py ++++ b/src/jinja2/compiler.py +@@ -55,7 +55,7 @@ def optimizeconst(f: F) -> F: + + return f(self, node, frame, **kwargs) + +- return update_wrapper(t.cast(F, new_func), f) ++ return update_wrapper(new_func, f) # type: ignore[return-value] + + + def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]: +@@ -902,12 +902,15 @@ class CodeGenerator(NodeVisitor): + if not self.environment.is_async: + self.writeline("yield from parent_template.root_render_func(context)") + else: +- self.writeline( +- "async for event in parent_template.root_render_func(context):" +- ) ++ self.writeline("agen = parent_template.root_render_func(context)") ++ self.writeline("try:") ++ self.indent() ++ self.writeline("async for event in agen:") + self.indent() + self.writeline("yield event") + self.outdent() ++ self.outdent() ++ self.writeline("finally: await agen.aclose()") + self.outdent(1 + (not self.has_known_extends)) + + # at this point we now have the blocks collected and can visit them too. +@@ -977,14 +980,20 @@ class CodeGenerator(NodeVisitor): + f"yield from context.blocks[{node.name!r}][0]({context})", node + ) + else: ++ self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})") ++ self.writeline("try:") ++ self.indent() + self.writeline( +- f"{self.choose_async()}for event in" +- f" context.blocks[{node.name!r}][0]({context}):", ++ f"{self.choose_async()}for event in gen:", + node, + ) + self.indent() + self.simple_write("event", frame) + self.outdent() ++ self.outdent() ++ self.writeline( ++ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" ++ ) + + self.outdent(level) + +@@ -1057,26 +1066,33 @@ class CodeGenerator(NodeVisitor): + self.writeline("else:") + self.indent() + +- skip_event_yield = False ++ def loop_body() -> None: ++ self.indent() ++ self.simple_write("event", frame) ++ self.outdent() ++ + if node.with_context: + self.writeline( +- f"{self.choose_async()}for event in template.root_render_func(" ++ f"gen = template.root_render_func(" + "template.new_context(context.get_all(), True," +- f" {self.dump_local_context(frame)})):" ++ f" {self.dump_local_context(frame)}))" ++ ) ++ self.writeline("try:") ++ self.indent() ++ self.writeline(f"{self.choose_async()}for event in gen:") ++ loop_body() ++ self.outdent() ++ self.writeline( ++ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" + ) + elif self.environment.is_async: + self.writeline( + "for event in (await template._get_default_module_async())" + "._body_stream:" + ) ++ loop_body() + else: + self.writeline("yield from template._get_default_module()._body_stream") +- skip_event_yield = True +- +- if not skip_event_yield: +- self.indent() +- self.simple_write("event", frame) +- self.outdent() + + if node.ignore_missing: + self.outdent() +diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py +index 1d3be0b..bdd6a2b 100644 +--- a/src/jinja2/environment.py ++++ b/src/jinja2/environment.py +@@ -1358,7 +1358,7 @@ class Template: + + async def generate_async( + self, *args: t.Any, **kwargs: t.Any +- ) -> t.AsyncIterator[str]: ++ ) -> t.AsyncGenerator[str, object]: + """An async version of :meth:`generate`. Works very similarly but + returns an async iterator instead. + """ +@@ -1370,8 +1370,14 @@ class Template: + ctx = self.new_context(dict(*args, **kwargs)) + + try: +- async for event in self.root_render_func(ctx): # type: ignore +- yield event ++ agen = self.root_render_func(ctx) ++ try: ++ async for event in agen: # type: ignore ++ yield event ++ finally: ++ # we can't use async with aclosing(...) because that's only ++ # in 3.10+ ++ await agen.aclose() # type: ignore + except Exception: + yield self.environment.handle_exception() + +diff --git a/tests/test_async.py b/tests/test_async.py +index c9ba70c..4edced9 100644 +--- a/tests/test_async.py ++++ b/tests/test_async.py +@@ -1,6 +1,7 @@ + import asyncio + + import pytest ++import trio + + from jinja2 import ChainableUndefined + from jinja2 import DictLoader +@@ -13,7 +14,16 @@ from jinja2.exceptions import UndefinedError + from jinja2.nativetypes import NativeEnvironment + + +-def test_basic_async(): ++def _asyncio_run(async_fn, *args): ++ return asyncio.run(async_fn(*args)) ++ ++ ++@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"]) ++def run_async_fn(request): ++ return request.param ++ ++ ++def test_basic_async(run_async_fn): + t = Template( + "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True + ) +@@ -21,11 +31,11 @@ def test_basic_async(): + async def func(): + return await t.render_async() + +- rv = asyncio.run(func()) ++ rv = run_async_fn(func) + assert rv == "[1][2][3]" + + +-def test_await_on_calls(): ++def test_await_on_calls(run_async_fn): + t = Template("{{ async_func() + normal_func() }}", enable_async=True) + + async def async_func(): +@@ -37,7 +47,7 @@ def test_await_on_calls(): + async def func(): + return await t.render_async(async_func=async_func, normal_func=normal_func) + +- rv = asyncio.run(func()) ++ rv = run_async_fn(func) + assert rv == "65" + + +@@ -54,7 +64,7 @@ def test_await_on_calls_normal_render(): + assert rv == "65" + + +-def test_await_and_macros(): ++def test_await_and_macros(run_async_fn): + t = Template( + "{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}", + enable_async=True, +@@ -66,11 +76,11 @@ def test_await_and_macros(): + async def func(): + return await t.render_async(async_func=async_func) + +- rv = asyncio.run(func()) ++ rv = run_async_fn(func) + assert rv == "[42][42]" + + +-def test_async_blocks(): ++def test_async_blocks(run_async_fn): + t = Template( + "{% block foo %}{% endblock %}{{ self.foo() }}", + enable_async=True, +@@ -80,7 +90,7 @@ def test_async_blocks(): + async def func(): + return await t.render_async() + +- rv = asyncio.run(func()) ++ rv = run_async_fn(func) + assert rv == "" + + +@@ -156,8 +166,8 @@ class TestAsyncImports: + test_env_async.from_string('{% from "foo" import bar, with, context %}') + test_env_async.from_string('{% from "foo" import bar, with with context %}') + +- def test_exports(self, test_env_async): +- coro = test_env_async.from_string( ++ def test_exports(self, test_env_async, run_async_fn): ++ coro_fn = test_env_async.from_string( + """ + {% macro toplevel() %}...{% endmacro %} + {% macro __private() %}...{% endmacro %} +@@ -166,9 +176,9 @@ class TestAsyncImports: + {% macro notthere() %}{% endmacro %} + {% endfor %} + """ +- )._get_default_module_async() +- m = asyncio.run(coro) +- assert asyncio.run(m.toplevel()) == "..." ++ )._get_default_module_async ++ m = run_async_fn(coro_fn) ++ assert run_async_fn(m.toplevel) == "..." + assert not hasattr(m, "__missing") + assert m.variable == 42 + assert not hasattr(m, "notthere") +@@ -457,17 +467,19 @@ class TestAsyncForLoop: + ) + assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3" + +- def test_loop_errors(self, test_env_async): ++ def test_loop_errors(self, test_env_async, run_async_fn): + tmpl = test_env_async.from_string( + """{% for item in [1] if loop.index + == 0 %}...{% endfor %}""" + ) +- pytest.raises(UndefinedError, tmpl.render) ++ with pytest.raises(UndefinedError): ++ run_async_fn(tmpl.render_async) ++ + tmpl = test_env_async.from_string( + """{% for item in [] %}...{% else + %}{{ loop }}{% endfor %}""" + ) +- assert tmpl.render() == "" ++ assert run_async_fn(tmpl.render_async) == "" + + def test_loop_filter(self, test_env_async): + tmpl = test_env_async.from_string( +@@ -597,7 +609,7 @@ class TestAsyncForLoop: + assert t.render(a=dict(b=[1, 2, 3])) == "1" + + +-def test_namespace_awaitable(test_env_async): ++def test_namespace_awaitable(test_env_async, run_async_fn): + async def _test(): + t = test_env_async.from_string( + '{% set ns = namespace(foo="Bar") %}{{ ns.foo }}' +@@ -605,10 +617,10 @@ def test_namespace_awaitable(test_env_async): + actual = await t.render_async() + assert actual == "Bar" + +- asyncio.run(_test()) ++ run_async_fn(_test) + + +-def test_chainable_undefined_aiter(): ++def test_chainable_undefined_aiter(run_async_fn): + async def _test(): + t = Template( + "{% for x in a['b']['c'] %}{{ x }}{% endfor %}", +@@ -618,7 +630,7 @@ def test_chainable_undefined_aiter(): + rv = await t.render_async(a={}) + assert rv == "" + +- asyncio.run(_test()) ++ run_async_fn(_test) + + + @pytest.fixture +@@ -626,22 +638,22 @@ def async_native_env(): + return NativeEnvironment(enable_async=True) + + +-def test_native_async(async_native_env): ++def test_native_async(async_native_env, run_async_fn): + async def _test(): + t = async_native_env.from_string("{{ x }}") + rv = await t.render_async(x=23) + assert rv == 23 + +- asyncio.run(_test()) ++ run_async_fn(_test) + + +-def test_native_list_async(async_native_env): ++def test_native_list_async(async_native_env, run_async_fn): + async def _test(): + t = async_native_env.from_string("{{ x }}") + rv = await t.render_async(x=list(range(3))) + assert rv == [0, 1, 2] + +- asyncio.run(_test()) ++ run_async_fn(_test) + + + def test_getitem_after_filter(): +@@ -658,3 +670,65 @@ def test_getitem_after_call(): + t = env.from_string("{{ add_each(a, 2)[1:] }}") + out = t.render(a=range(3)) + assert out == "[3, 4]" ++ ++ ++def test_basic_generate_async(run_async_fn): ++ t = Template( ++ "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True ++ ) ++ ++ async def func(): ++ agen = t.generate_async() ++ try: ++ return await agen.__anext__() ++ finally: ++ await agen.aclose() ++ ++ rv = run_async_fn(func) ++ assert rv == "[" ++ ++ ++def test_include_generate_async(run_async_fn, test_env_async): ++ t = test_env_async.from_string('{% include "header" %}') ++ ++ async def func(): ++ agen = t.generate_async() ++ try: ++ return await agen.__anext__() ++ finally: ++ await agen.aclose() ++ ++ rv = run_async_fn(func) ++ assert rv == "[" ++ ++ ++def test_blocks_generate_async(run_async_fn): ++ t = Template( ++ "{% block foo %}{% endblock %}{{ self.foo() }}", ++ enable_async=True, ++ autoescape=True, ++ ) ++ ++ async def func(): ++ agen = t.generate_async() ++ try: ++ return await agen.__anext__() ++ finally: ++ await agen.aclose() ++ ++ rv = run_async_fn(func) ++ assert rv == "" ++ ++ ++def test_async_extend(run_async_fn, test_env_async): ++ t = test_env_async.from_string('{% extends "header" %}') ++ ++ async def func(): ++ agen = t.generate_async() ++ try: ++ return await agen.__anext__() ++ finally: ++ await agen.aclose() ++ ++ rv = run_async_fn(func) ++ assert rv == "[" +diff --git a/tests/test_async_filters.py b/tests/test_async_filters.py +index f5b2627..e8cc350 100644 +--- a/tests/test_async_filters.py ++++ b/tests/test_async_filters.py +@@ -1,6 +1,9 @@ ++import asyncio ++import contextlib + from collections import namedtuple + + import pytest ++import trio + from markupsafe import Markup + + from jinja2 import Environment +@@ -26,10 +29,39 @@ def env_async(): + return Environment(enable_async=True) + + ++def _asyncio_run(async_fn, *args): ++ return asyncio.run(async_fn(*args)) ++ ++ ++@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"]) ++def run_async_fn(request): ++ return request.param ++ ++ ++@contextlib.asynccontextmanager ++async def closing_factory(): ++ async with contextlib.AsyncExitStack() as stack: ++ ++ def closing(maybe_agen): ++ try: ++ aclose = maybe_agen.aclose ++ except AttributeError: ++ pass ++ else: ++ stack.push_async_callback(aclose) ++ return maybe_agen ++ ++ yield closing ++ ++ + @mark_dualiter("foo", lambda: range(10)) +-def test_first(env_async, foo): +- tmpl = env_async.from_string("{{ foo()|first }}") +- out = tmpl.render(foo=foo) ++def test_first(env_async, foo, run_async_fn): ++ async def test(): ++ async with closing_factory() as closing: ++ tmpl = env_async.from_string("{{ closing(foo())|first }}") ++ return await tmpl.render_async(foo=foo, closing=closing) ++ ++ out = run_async_fn(test) + assert out == "0" + + +@@ -245,18 +277,23 @@ def test_slice(env_async, items): + ) + + +-def test_custom_async_filter(env_async): ++def test_custom_async_filter(env_async, run_async_fn): + async def customfilter(val): + return str(val) + +- env_async.filters["customfilter"] = customfilter +- tmpl = env_async.from_string("{{ 'static'|customfilter }} {{ arg|customfilter }}") +- out = tmpl.render(arg="dynamic") ++ async def test(): ++ env_async.filters["customfilter"] = customfilter ++ tmpl = env_async.from_string( ++ "{{ 'static'|customfilter }} {{ arg|customfilter }}" ++ ) ++ return await tmpl.render_async(arg="dynamic") ++ ++ out = run_async_fn(test) + assert out == "static dynamic" + + + @mark_dualiter("items", lambda: range(10)) +-def test_custom_async_iteratable_filter(env_async, items): ++def test_custom_async_iteratable_filter(env_async, items, run_async_fn): + async def customfilter(iterable): + items = [] + async for item in auto_aiter(iterable): +@@ -265,9 +302,13 @@ def test_custom_async_iteratable_filter(env_async, items): + break + return ",".join(items) + +- env_async.filters["customfilter"] = customfilter +- tmpl = env_async.from_string( +- "{{ items()|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}" +- ) +- out = tmpl.render(items=items) ++ async def test(): ++ async with closing_factory() as closing: ++ env_async.filters["customfilter"] = customfilter ++ tmpl = env_async.from_string( ++ "{{ closing(items())|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}" ++ ) ++ return await tmpl.render_async(items=items, closing=closing) ++ ++ out = run_async_fn(test) + assert out == "0,1,2 .. 3,4,5" +diff --git a/tests/test_loader.py b/tests/test_loader.py +index 77d686e..3e64f62 100644 +--- a/tests/test_loader.py ++++ b/tests/test_loader.py +@@ -2,7 +2,6 @@ import importlib.abc + import importlib.machinery + import importlib.util + import os +-import platform + import shutil + import sys + import tempfile +@@ -364,8 +363,8 @@ def test_package_zip_source(package_zip_loader, template, expect): + + + @pytest.mark.xfail( +- platform.python_implementation() == "PyPy", +- reason="PyPy's zipimporter doesn't have a '_files' attribute.", ++ sys.implementation.name == "pypy" or sys.version_info > (3, 13), ++ reason="zipimporter doesn't have a '_files' attribute", + raises=TypeError, + ) + def test_package_zip_list(package_zip_loader): +-- +2.45.0 + diff --git a/python-Jinja2.changes b/python-Jinja2.changes index 559f166..5088b6c 100644 --- a/python-Jinja2.changes +++ b/python-Jinja2.changes @@ -1,3 +1,15 @@ +------------------------------------------------------------------- +Tue Sep 24 12:48:03 UTC 2024 - ecsos + +- Fix build error under Leap. + +------------------------------------------------------------------- +Tue Jul 30 10:44:01 UTC 2024 - John Paul Adrian Glaubitz + +- Cherry-pick patch from Fedora to fix FTBFS with Python 3.13 + * fix-ftbfs-with-python313.patch +- Add new build dependency python-trio to BuildRequires + ------------------------------------------------------------------- Mon May 6 18:10:40 UTC 2024 - Dirk Müller diff --git a/python-Jinja2.spec b/python-Jinja2.spec index 4027981..0ad9610 100644 --- a/python-Jinja2.spec +++ b/python-Jinja2.spec @@ -29,11 +29,14 @@ Summary: A template engine written in pure Python License: BSD-3-Clause URL: https://jinja.palletsprojects.com Source: https://files.pythonhosted.org/packages/source/J/Jinja2/jinja2-%{version}.tar.gz +# PATCH-FIX-UPSTREAM - gh/pallets/jinja#1960 and gh/pallets/jinja#1977 - Fix FTBFS with Python 3.13 +Patch1: https://src.fedoraproject.org/rpms/python-jinja2/raw/rawhide/f/python3.13.patch#/fix-ftbfs-with-python313.patch BuildRequires: %{python_module MarkupSafe >= 0.23} BuildRequires: %{python_module base >= 3.7} BuildRequires: %{python_module flit-core} BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest} +BuildRequires: %{python_module trio} BuildRequires: %{python_module wheel} BuildRequires: dos2unix BuildRequires: fdupes @@ -53,12 +56,18 @@ sandboxed environment. %prep %setup -q -n jinja2-%{version} +%patch -P 1 -p1 %build %pyproject_wheel %install %pyproject_install +# Fix python-bytecode-inconsistent-mtime +pushd %{buildroot}%{python_sitelib} +find . -name '*.pyc' -exec rm -f '{}' ';' +python%python_bin_suffix -m compileall *.py ';' +popd %python_expand %fdupes %{buildroot}%{$python_sitelib} %check