diff --git a/.gitignore b/.gitignore index 470e62a..69cace2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ var/ .installed.cfg *.egg +# Python environment +.venv + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/README.md b/README.md index 076e1e4..e4332f1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is a fork of the original [`airspeed`](https://github.com/purcell/airspeed) ⚠️ Note: This fork of `airspeed` focuses on providing maximum parity with AWS' implementation of Velocity templates (used in, e.g., API Gateway or AppSync). In some cases, the behavior may diverge from the VTL spec, or from the Velocity [reference implementation](https://velocity.apache.org/download.cgi). ## Change Log: +* v0.6.7: fix support for floating point starting with a decimal; Implement `REPLACE_FORMAL_TEXT` to allow bypassing silent behavior of `FormalReference` element. * v0.6.6: add support for `$string.matches( $pattern )`; fix bug where some escaped character would prevent string matching * v0.6.5: handle `$map.put('key', null)` correctly * v0.6.4: add support for string.indexOf, string.substring and array.isEmpty diff --git a/airspeed/operators.py b/airspeed/operators.py index 7b614c5..2625762 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -12,6 +12,10 @@ LOG = logging.getLogger(__name__) +# This can be used by a function to bypass the silent behavior of a FormalReference. +# If a function returns this value instead of None, the origial text of the reference will be returned instead. +REPLACE_FORMAL_TEXT = "__FORMAL_REFERENCE__REPLACE_TEXT__" + # A dict that maps classes to dicts of additional methods. # This allows support for methods that are available in Java-based Velocity # implementations, e.g., .size() of a list or .length() of a string. @@ -434,7 +438,7 @@ def calculate(self, namespace, loader): class FloatingPointLiteral(_Element): - FLOAT = re.compile(r"(-?\d+\.\d+)(.*)", re.S) + FLOAT = re.compile(r"(-?\d*\.\d+)(.*)", re.S) def parse(self): (self.value,) = self.identity_match(self.FLOAT) @@ -795,7 +799,9 @@ def evaluate_raw(self, stream, namespace, loader): value = None if self.expression is not None: value = self.expression.calculate(namespace, loader) - if value is None: + if value == REPLACE_FORMAL_TEXT: + value = self.my_text() + elif value is None: if self.alternate is not None: value = self.alternate.calculate(namespace, loader) or "" elif self.silent and self.expression is not None: diff --git a/setup.py b/setup.py index b8cc254..366b9e3 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="airspeed-ext", - version="0.6.6", + version="0.6.7", description=( "Airspeed is a powerful and easy-to-use templating engine " "for Python that aims for a high level of compatibility " diff --git a/tests/test_templating.py b/tests/test_templating.py index 452d0d1..54756d6 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -879,6 +879,10 @@ def test_string_with_escaped_char(self, test_render): template = '"\{\n\t\r\%\5"' # noqa test_render(template) + def test_floating_point_starting_with_decimal(self, test_render): + template = """#set($x = .5*.1)$x""" + test_render(template) + class TestInternals: """ @@ -1206,15 +1210,37 @@ def test_template_cannot_modify_its_args(self, test_render): def test_doesnt_blow_stack(self, test_render): template = airspeed.Template( - """ -#foreach($i in [1..$end]) - $assembly## -#end -""" + textwrap.dedent( + """ + #foreach($i in [1..$end]) + $assembly## + #end + """ + ) ) ns = {"end": 400} template.merge(ns) + def test_formal_reference_test_bypass(self, test_render_locally): + test_render = test_render_locally( + f'#set($bypass = "{airspeed.operators.REPLACE_FORMAL_TEXT}")$bypass' + ) + assert test_render == "$bypass" + + test_render_map = test_render_locally( + textwrap.dedent( + f""" + #set( $map = {{}}) + #set($ignore = $map.put('bypass', "{airspeed.operators.REPLACE_FORMAL_TEXT}")) + #set($ignore = $map.put('no-bypass', "value")) + $map.bypass + $map.no-bypass + """ + ) + ) + + assert test_render_map == "\n$map.bypass\nvalue\n" + class TestMacros: """ diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 58db179..e7ba4a6 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1104,5 +1104,12 @@ "render-result-2-cli": "true", "render-result-2": "true" } + }, + "tests/test_templating.py::TestTemplating::test_floating_point_starting_with_decimal": { + "recorded-date": "10-11-2024, 04:53:47", + "recorded-content": { + "render-result-1-cli": "0.05", + "render-result-1": "0.05" + } } }