From 3fd92f070433a6d938189e8e50ae6c687e540a0b Mon Sep 17 00:00:00 2001 From: James Prior Date: Fri, 16 Aug 2024 08:47:00 +0100 Subject: [PATCH] Handle interrupts in tablerow tags --- CHANGES.md | 4 +++ liquid/builtin/tags/tablerow_tag.py | 43 +++++++++++++++++++--- liquid/future/environment.py | 2 ++ liquid/future/tags/__init__.py | 2 ++ liquid/future/tags/_tablerow_tag.py | 14 ++++++++ liquid/golden/tablerow_tag.py | 55 +++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 liquid/future/tags/_tablerow_tag.py diff --git a/CHANGES.md b/CHANGES.md index 53fd57d6..d56f9f28 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ - Fixed `{% case %}` / `{% when %}` behavior. When using [`liquid.future.Environment`](https://jg-rp.github.io/liquid/api/future-environment), we now render any number of `{% else %}` blocks and allow `{% when %}` tags to appear after `{% else %}` tags. The default `Environment` continues to raise a `LiquidSyntaxError` in such cases. +**Changed** + +- Changed `{% break %}` and `{% continue %}` tag handling when they appear inside a `{% tablerow %}` tag. Now, when using `liquid.future.Environment`, interrupts follow Shopify/Liquid behavior introduced in [#1818](https://github.com/Shopify/liquid/pull/1818). Python Liquid's default environment is unchanged. + ## Version 1.12.1 **Fixes** diff --git a/liquid/builtin/tags/tablerow_tag.py b/liquid/builtin/tags/tablerow_tag.py index 1dd68827..228dbb60 100644 --- a/liquid/builtin/tags/tablerow_tag.py +++ b/liquid/builtin/tags/tablerow_tag.py @@ -12,6 +12,8 @@ from liquid.ast import ChildNode from liquid.ast import Node from liquid.context import Context +from liquid.exceptions import BreakLoop +from liquid.exceptions import ContinueLoop from liquid.expression import NIL from liquid.expression import LoopExpression from liquid.limits import to_int @@ -156,6 +158,9 @@ def step(self) -> None: class TablerowNode(Node): """Parse tree node for the built-in "tablerow" tag.""" + interrupts = False + """If _true_, handle `break` and `continue` interrupts inside a tablerow loop.""" + __slots__ = ("tok", "expression", "block") def __init__( @@ -194,18 +199,33 @@ def render_to_output(self, context: Context, buffer: TextIO) -> Optional[bool]: } buffer.write('\n') + _break = False with context.extend(namespace): for item in tablerow: namespace[name] = item buffer.write(f'') - self.block.render(context=context, buffer=buffer) + + try: + self.block.render(context=context, buffer=buffer) + except BreakLoop: + if self.interrupts: + _break = True + else: + raise + except ContinueLoop: + if not self.interrupts: + raise + buffer.write("") if tablerow.col_last and not tablerow.last: buffer.write(f'\n') - buffer.write("\n") + if _break: + break + + buffer.write("\n") return True async def render_to_output_async( @@ -227,18 +247,33 @@ async def render_to_output_async( } buffer.write('\n') + _break = False with context.extend(namespace): for item in tablerow: namespace[name] = item buffer.write(f'') - await self.block.render_async(context=context, buffer=buffer) + + try: + await self.block.render_async(context=context, buffer=buffer) + except BreakLoop: + if self.interrupts: + _break = True + else: + raise + except ContinueLoop: + if not self.interrupts: + raise + buffer.write("") if tablerow.col_last and not tablerow.last: buffer.write(f'\n') - buffer.write("\n") + if _break: + break + + buffer.write("\n") return True def children(self) -> List[ChildNode]: diff --git a/liquid/future/environment.py b/liquid/future/environment.py index e4807d52..6bd3dbd0 100644 --- a/liquid/future/environment.py +++ b/liquid/future/environment.py @@ -6,6 +6,7 @@ from ..environment import Environment as DefaultEnvironment from ..template import FutureBoundTemplate from .filters import split +from .tags import InterruptingTablerowTag from .tags import LaxCaseTag from .tags import LaxIfTag from .tags import LaxUnlessTag @@ -31,3 +32,4 @@ def setup_tags_and_filters(self) -> None: self.add_tag(LaxCaseTag) self.add_tag(LaxIfTag) self.add_tag(LaxUnlessTag) + self.add_tag(InterruptingTablerowTag) diff --git a/liquid/future/tags/__init__.py b/liquid/future/tags/__init__.py index f33af1f1..d3b2b08e 100644 --- a/liquid/future/tags/__init__.py +++ b/liquid/future/tags/__init__.py @@ -1,8 +1,10 @@ from ._case_tag import LaxCaseTag # noqa: D104 from ._if_tag import LaxIfTag +from ._tablerow_tag import InterruptingTablerowTag from ._unless_tag import LaxUnlessTag __all__ = ( + "InterruptingTablerowTag", "LaxCaseTag", "LaxIfTag", "LaxUnlessTag", diff --git a/liquid/future/tags/_tablerow_tag.py b/liquid/future/tags/_tablerow_tag.py new file mode 100644 index 00000000..47ed5708 --- /dev/null +++ b/liquid/future/tags/_tablerow_tag.py @@ -0,0 +1,14 @@ +from liquid.builtin.tags.tablerow_tag import TablerowNode +from liquid.builtin.tags.tablerow_tag import TablerowTag + + +class InterruptingTablerowNode(TablerowNode): + """A _tablerow_ node with interrupt handling enabled.""" + + interrupts = True + + +class InterruptingTablerowTag(TablerowTag): + """A _tablerow_ tag that handles `break` and `continue` tags.""" + + node_class = InterruptingTablerowNode diff --git a/liquid/golden/tablerow_tag.py b/liquid/golden/tablerow_tag.py index b2648aa9..96aeb64f 100644 --- a/liquid/golden/tablerow_tag.py +++ b/liquid/golden/tablerow_tag.py @@ -282,6 +282,61 @@ "\n" ), ), + Case( + description="break from a tablerow loop", + template=( + r"{% tablerow n in (1..3) cols:2 %}" + r"{{n}}{% break %}{{n}}" + r"{% endtablerow %}" + ), + expect='\n1\n', + future=True, + ), + Case( + description="continue from a tablerow loop", + template=( + r"{% tablerow n in (1..3) cols:2 %}" + r"{{n}}{% continue %}{{n}}" + r"{% endtablerow %}" + ), + expect=( + '\n' + '1' + '2' + "\n" + '' + '3' + "\n" + ), + future=True, + ), + Case( + description="break from a tablerow loop inside a for loop", + template=( + r"{% for i in (1..2) -%}\n" + r"{% for j in (1..2) -%}\n" + r"{% tablerow k in (1..3) %}{% break %}{% endtablerow -%}\n" + r"loop j={{ j }}\n" + r"{% endfor -%}\n" + r"loop i={{ i }}\n" + r"{% endfor -%}\n" + r"after loop\n" + ), + expect="\n".join( + [ + r'\n\n', + r'', + r'\nloop j=1\n\n', + r'', + r'\nloop j=2\n\nloop i=1\n\n\n', + r'', + r'\nloop j=1\n\n', + r'', + r"\nloop j=2\n\nloop i=2\n\nafter loop\n", + ] + ), + future=True, + ), # Case( # description="cols is non number string", # template=(