@barefootjs/jinja 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +84 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +106 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189097 -0
- package/dist/adapter/jinja-adapter.d.ts +394 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/jinja-naming.d.ts +77 -0
- package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +81 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +61 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189117 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189118 -0
- package/package.json +66 -0
- package/python/VERSION +1 -0
- package/python/barefootjs/__init__.py +57 -0
- package/python/barefootjs/backend_jinja.py +125 -0
- package/python/barefootjs/evaluator.py +787 -0
- package/python/barefootjs/runtime.py +1334 -0
- package/python/barefootjs/search_params.py +89 -0
- package/python/pyproject.toml +32 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/test_eval_vectors.py +88 -0
- package/python/tests/test_evaluator.py +406 -0
- package/python/tests/test_helper_vectors.py +288 -0
- package/python/tests/test_omit.py +62 -0
- package/python/tests/test_props_attr.py +54 -0
- package/python/tests/test_query.py +41 -0
- package/python/tests/test_render.py +102 -0
- package/python/tests/test_render_child.py +96 -0
- package/python/tests/test_search_params.py +50 -0
- package/python/tests/test_spread_attrs.py +86 -0
- package/python/tests/test_template_primitives.py +347 -0
- package/python/tests/vector-divergences.json +42 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
- package/src/__tests__/jinja-adapter.test.ts +53 -0
- package/src/__tests__/jinja-counter.test.ts +62 -0
- package/src/__tests__/jinja-query-href.test.ts +99 -0
- package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +176 -0
- package/src/adapter/emit-context.ts +118 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/jinja-adapter.ts +1747 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/jinja-naming.ts +114 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +132 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +166 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +714 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""BarefootJS.SearchParams -- Python-specific concerns, ported from
|
|
2
|
+
packages/adapter-perl/t/search_params.t.
|
|
3
|
+
|
|
4
|
+
The cross-language VALUE semantics of `get` are owned by the
|
|
5
|
+
language-independent golden vectors (`search_params_get` in
|
|
6
|
+
`test_helper_vectors.py`), so Go/Perl/Python parity there is mechanical.
|
|
7
|
+
This file covers only what those value vectors can't: the lazy factory
|
|
8
|
+
seam, lenient parsing (never raises), and UTF-8 decoding.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
from barefootjs import BarefootJS, SearchParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SearchParamsTest(unittest.TestCase):
|
|
19
|
+
def test_lazy_factory(self):
|
|
20
|
+
sp = BarefootJS.search_params("sort=price")
|
|
21
|
+
self.assertIsInstance(sp, SearchParams)
|
|
22
|
+
self.assertEqual(sp.get("sort"), "price")
|
|
23
|
+
self.assertIsInstance(BarefootJS.search_params(), SearchParams)
|
|
24
|
+
|
|
25
|
+
def test_none_composition_coalesces_only_none(self):
|
|
26
|
+
# The adapters lower `searchParams().get(k) ?? d` to a Python
|
|
27
|
+
# expression that coalesces only `None` (not a bare `or`, which
|
|
28
|
+
# would also default a present-but-empty value) -- so an absent key
|
|
29
|
+
# falls back to the author's default while a present-but-empty
|
|
30
|
+
# value keeps ''.
|
|
31
|
+
absent = BarefootJS.search_params("other=x")
|
|
32
|
+
got = absent.get("sort")
|
|
33
|
+
self.assertEqual(got if got is not None else "none", "none")
|
|
34
|
+
|
|
35
|
+
empty = BarefootJS.search_params("sort=")
|
|
36
|
+
got = empty.get("sort")
|
|
37
|
+
self.assertEqual(got if got is not None else "none", "")
|
|
38
|
+
|
|
39
|
+
def test_utf8_percent_decoding(self):
|
|
40
|
+
sp = BarefootJS.search_params("q=%E2%9C%93")
|
|
41
|
+
self.assertEqual(sp.get("q"), "✓")
|
|
42
|
+
|
|
43
|
+
def test_lenient_parsing_never_raises(self):
|
|
44
|
+
BarefootJS.search_params(None) # should not raise
|
|
45
|
+
self.assertIsNone(BarefootJS.search_params("&&&").get("x"))
|
|
46
|
+
self.assertIsNone(BarefootJS.search_params("=novalue").get("x"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
unittest.main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""`BarefootJS.spread_attrs` -- ported from packages/adapter-perl/t/spread_attrs.t.
|
|
2
|
+
|
|
3
|
+
JSX intrinsic-element spread runtime helper (#1407 follow-up). Mirrors the
|
|
4
|
+
JS `spreadAttrs` runtime and the Go/Perl adapters' equivalents so SSR output
|
|
5
|
+
stays byte-equal across every adapter -- cross-adapter parity regressions
|
|
6
|
+
surface here first.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
from barefootjs import BarefootJS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _PureBackend:
|
|
17
|
+
"""`spread_attrs` only reaches the backend for `mark_raw`, which is the
|
|
18
|
+
identity here."""
|
|
19
|
+
|
|
20
|
+
def mark_raw(self, s):
|
|
21
|
+
return s
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(bag):
|
|
25
|
+
bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
26
|
+
return str(bf.spread_attrs(bag))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SpreadAttrsTest(unittest.TestCase):
|
|
30
|
+
def test_basic_shapes(self):
|
|
31
|
+
self.assertEqual(run(None), "")
|
|
32
|
+
self.assertEqual(run({}), "")
|
|
33
|
+
self.assertEqual(run("not a hash"), "")
|
|
34
|
+
self.assertEqual(run({"id": "a"}), 'id="a"')
|
|
35
|
+
|
|
36
|
+
def test_alphabetic_key_order(self):
|
|
37
|
+
self.assertEqual(run({"id": "a", "class": "on"}), 'class="on" id="a"')
|
|
38
|
+
|
|
39
|
+
def test_key_remapping(self):
|
|
40
|
+
self.assertEqual(run({"className": "foo"}), 'class="foo"')
|
|
41
|
+
self.assertEqual(run({"htmlFor": "x"}), 'for="x"')
|
|
42
|
+
self.assertEqual(run({"dataPriority": "high"}), 'data-priority="high"')
|
|
43
|
+
# SVG XML attrs are case-sensitive -- preserve verbatim.
|
|
44
|
+
self.assertEqual(run({"viewBox": "0 0 10 10"}), 'viewBox="0 0 10 10"')
|
|
45
|
+
self.assertEqual(run({"clipPathUnits": "userSpaceOnUse"}), 'clipPathUnits="userSpaceOnUse"')
|
|
46
|
+
# JS-reference parity (#1411): a leading uppercase letter emits a
|
|
47
|
+
# leading dash.
|
|
48
|
+
self.assertEqual(run({"XData": "x"}), '-x-data="x"')
|
|
49
|
+
|
|
50
|
+
def test_event_handlers_js_predicate_parity(self):
|
|
51
|
+
self.assertEqual(run({"onClick": "fn", "id": "a"}), 'id="a"')
|
|
52
|
+
self.assertEqual(run({"on_custom": "fn", "id": "a"}), 'id="a"')
|
|
53
|
+
self.assertEqual(run({"on0": "fn", "id": "a"}), 'id="a"')
|
|
54
|
+
self.assertEqual(run({"oncology": "x"}), 'oncology="x"')
|
|
55
|
+
|
|
56
|
+
def test_children_skipped_ref_passed_through(self):
|
|
57
|
+
self.assertEqual(run({"children": "x", "id": "a"}), 'id="a"')
|
|
58
|
+
# JS `spreadAttrs` does NOT filter `ref` (`applyRestAttrs` does --
|
|
59
|
+
# that's a separate divergence).
|
|
60
|
+
self.assertEqual(run({"ref": "x", "id": "a"}), 'id="a" ref="x"')
|
|
61
|
+
|
|
62
|
+
def test_boolean_values(self):
|
|
63
|
+
# Contract: callers MUST use a real Python bool for boolean
|
|
64
|
+
# attributes -- unlike Perl, no sentinel object is needed.
|
|
65
|
+
self.assertEqual(run({"hidden": True, "id": "a"}), 'hidden id="a"')
|
|
66
|
+
self.assertEqual(run({"hidden": False, "id": "a"}), 'id="a"')
|
|
67
|
+
# Plain numeric 0 renders as a value (matches `tabindex="0"`).
|
|
68
|
+
self.assertEqual(run({"tabindex": 0}), 'tabindex="0"')
|
|
69
|
+
|
|
70
|
+
def test_nullish_skip(self):
|
|
71
|
+
self.assertEqual(run({"a": None, "b": "x"}), 'b="x"')
|
|
72
|
+
|
|
73
|
+
def test_html_escape(self):
|
|
74
|
+
self.assertEqual(run({"title": '<b>"x"</b>'}), 'title="<b>"x"</b>"')
|
|
75
|
+
self.assertEqual(run({"alt": "tom & jerry"}), 'alt="tom & jerry"')
|
|
76
|
+
|
|
77
|
+
def test_style_object_lowering(self):
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
run({"style": {"backgroundColor": "red", "color": "white"}}),
|
|
80
|
+
'style="background-color:red;color:white"',
|
|
81
|
+
)
|
|
82
|
+
self.assertEqual(run({"style": "color:red"}), 'style="color:red"')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
unittest.main()
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""JS-compat helper coverage (#1189), ported from
|
|
2
|
+
packages/adapter-perl/t/template_primitives.t.
|
|
3
|
+
|
|
4
|
+
Covers the array/string method surface NOT already exercised byte-for-byte
|
|
5
|
+
by the shared golden vectors (`test_helper_vectors.py`) -- receiver-type
|
|
6
|
+
dispatch edge cases, mutation isolation (a helper must return a NEW list,
|
|
7
|
+
never alias the caller's), and the structured `sort` comparator dispatch.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
from barefootjs import BarefootJS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _PureBackend:
|
|
19
|
+
def encode_json(self, data):
|
|
20
|
+
return json.dumps(data, separators=(",", ":"), sort_keys=True)
|
|
21
|
+
|
|
22
|
+
def mark_raw(self, s):
|
|
23
|
+
return s
|
|
24
|
+
|
|
25
|
+
def materialize(self, value):
|
|
26
|
+
return value() if callable(value) else value
|
|
27
|
+
|
|
28
|
+
def render_named(self, *args, **kwargs):
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_nan(n):
|
|
33
|
+
return isinstance(n, float) and n != n
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TemplatePrimitivesTest(unittest.TestCase):
|
|
37
|
+
def setUp(self):
|
|
38
|
+
self.bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
39
|
+
|
|
40
|
+
def test_json(self):
|
|
41
|
+
self.assertEqual(self.bf.json({"a": 1}), '{"a":1}')
|
|
42
|
+
self.assertEqual(self.bf.json([1, 2, 3]), "[1,2,3]")
|
|
43
|
+
self.assertEqual(self.bf.json("hi"), '"hi"')
|
|
44
|
+
self.assertEqual(self.bf.json(None), "null")
|
|
45
|
+
|
|
46
|
+
def test_string(self):
|
|
47
|
+
self.assertEqual(self.bf.string(42), "42")
|
|
48
|
+
self.assertEqual(self.bf.string("hi"), "hi")
|
|
49
|
+
self.assertEqual(self.bf.string(None), "")
|
|
50
|
+
# Python has a real boolean type -- no Perl-style divergence.
|
|
51
|
+
self.assertEqual(self.bf.string(True), "true")
|
|
52
|
+
self.assertEqual(self.bf.string(False), "false")
|
|
53
|
+
self.assertEqual(self.bf.string(1.0), "1")
|
|
54
|
+
|
|
55
|
+
def test_number(self):
|
|
56
|
+
self.assertEqual(self.bf.number("3.14"), 3.14)
|
|
57
|
+
self.assertEqual(self.bf.number(42), 42)
|
|
58
|
+
self.assertTrue(_is_nan(self.bf.number("not a num")))
|
|
59
|
+
self.assertTrue(_is_nan(self.bf.number(None)))
|
|
60
|
+
|
|
61
|
+
def test_floor_ceil_round(self):
|
|
62
|
+
self.assertEqual(self.bf.floor(3.7), 3)
|
|
63
|
+
self.assertEqual(self.bf.floor(-3.2), -4)
|
|
64
|
+
self.assertTrue(_is_nan(self.bf.floor("not")))
|
|
65
|
+
|
|
66
|
+
self.assertEqual(self.bf.ceil(3.1), 4)
|
|
67
|
+
self.assertEqual(self.bf.ceil(-3.7), -3)
|
|
68
|
+
self.assertTrue(_is_nan(self.bf.ceil("not")))
|
|
69
|
+
|
|
70
|
+
self.assertEqual(self.bf.round(3.5), 4)
|
|
71
|
+
self.assertEqual(self.bf.round(3.4), 3)
|
|
72
|
+
# JS Math.round ties go toward +Infinity, not away from zero.
|
|
73
|
+
self.assertEqual(self.bf.round(-1.5), -1)
|
|
74
|
+
self.assertEqual(self.bf.round(-1.6), -2)
|
|
75
|
+
self.assertTrue(_is_nan(self.bf.round("not")))
|
|
76
|
+
|
|
77
|
+
def test_includes_dispatch(self):
|
|
78
|
+
# `Array.prototype.includes(x)` + `String.prototype.includes(sub)`
|
|
79
|
+
# lower to the same `bf.includes(recv, elem)` shape -- see #1448
|
|
80
|
+
# Tier A. `BarefootJS.includes` dispatches on `recv`'s Python type:
|
|
81
|
+
# a `list` scans elements via `evaluator._same_value_zero` (#2075 --
|
|
82
|
+
# SameValueZero, handling None/undefined parity, and no
|
|
83
|
+
# numeric/string coercion -- see the cross-type cases below);
|
|
84
|
+
# anything else (a `dict`, or a scalar) falls back to a substring
|
|
85
|
+
# search via `js_string` coercion.
|
|
86
|
+
self.assertTrue(self.bf.includes(["a", "b", "c"], "b"))
|
|
87
|
+
self.assertFalse(self.bf.includes(["a", "b", "c"], "z"))
|
|
88
|
+
self.assertTrue(self.bf.includes([1, 2, 3], 2))
|
|
89
|
+
self.assertFalse(self.bf.includes([], "a"))
|
|
90
|
+
self.assertTrue(self.bf.includes([None, "a"], None))
|
|
91
|
+
self.assertFalse(self.bf.includes(["a", "b"], None))
|
|
92
|
+
|
|
93
|
+
# SameValueZero never coerces across types.
|
|
94
|
+
self.assertFalse(self.bf.includes([2], "2"))
|
|
95
|
+
self.assertTrue(self.bf.includes([2], 2))
|
|
96
|
+
self.assertTrue(self.bf.includes(["2"], "2"))
|
|
97
|
+
|
|
98
|
+
self.assertTrue(self.bf.includes("hello world", "world"))
|
|
99
|
+
self.assertFalse(self.bf.includes("hello world", "earth"))
|
|
100
|
+
self.assertTrue(self.bf.includes("hello", ""))
|
|
101
|
+
self.assertFalse(self.bf.includes("", "x"))
|
|
102
|
+
self.assertFalse(self.bf.includes(None, "x"))
|
|
103
|
+
|
|
104
|
+
self.assertFalse(self.bf.includes({"a": 1}, "a"))
|
|
105
|
+
|
|
106
|
+
def test_index_of_last_index_of(self):
|
|
107
|
+
arr = ["a", "b", "c", "b", "d"]
|
|
108
|
+
self.assertEqual(self.bf.index_of(arr, "a"), 0)
|
|
109
|
+
self.assertEqual(self.bf.index_of(arr, "b"), 1)
|
|
110
|
+
self.assertEqual(self.bf.index_of(arr, "d"), 4)
|
|
111
|
+
self.assertEqual(self.bf.index_of(arr, "z"), -1)
|
|
112
|
+
self.assertEqual(self.bf.index_of([], "a"), -1)
|
|
113
|
+
self.assertEqual(self.bf.index_of("not an array", "a"), -1)
|
|
114
|
+
|
|
115
|
+
self.assertEqual(self.bf.last_index_of(arr, "b"), 3)
|
|
116
|
+
self.assertEqual(self.bf.last_index_of(arr, "a"), 0)
|
|
117
|
+
self.assertEqual(self.bf.last_index_of(arr, "z"), -1)
|
|
118
|
+
|
|
119
|
+
self.assertEqual(self.bf.index_of([None, "x", None], None), 0)
|
|
120
|
+
self.assertEqual(self.bf.last_index_of([None, "x", None], None), 2)
|
|
121
|
+
|
|
122
|
+
def test_at(self):
|
|
123
|
+
arr = ["a", "b", "c"]
|
|
124
|
+
self.assertEqual(self.bf.at(arr, 0), "a")
|
|
125
|
+
self.assertEqual(self.bf.at(arr, 2), "c")
|
|
126
|
+
self.assertEqual(self.bf.at(arr, -1), "c")
|
|
127
|
+
self.assertEqual(self.bf.at(arr, -3), "a")
|
|
128
|
+
self.assertIsNone(self.bf.at(arr, 3))
|
|
129
|
+
self.assertIsNone(self.bf.at(arr, -4))
|
|
130
|
+
self.assertIsNone(self.bf.at([], 0))
|
|
131
|
+
self.assertIsNone(self.bf.at(None, 0))
|
|
132
|
+
self.assertIsNone(self.bf.at({"a": 1}, 0))
|
|
133
|
+
|
|
134
|
+
def test_concat_mutation_isolation(self):
|
|
135
|
+
self.assertEqual(self.bf.concat(["a", "b"], ["c", "d"]), ["a", "b", "c", "d"])
|
|
136
|
+
self.assertEqual(self.bf.concat(None, ["a"]), ["a"])
|
|
137
|
+
self.assertEqual(self.bf.concat(["a"], None), ["a"])
|
|
138
|
+
|
|
139
|
+
left = ["a", "b"]
|
|
140
|
+
right = ["c", "d"]
|
|
141
|
+
out = self.bf.concat(left, right)
|
|
142
|
+
out.append("mutated")
|
|
143
|
+
self.assertEqual(left, ["a", "b"])
|
|
144
|
+
self.assertEqual(right, ["c", "d"])
|
|
145
|
+
|
|
146
|
+
def test_slice_mutation_isolation_and_clamping(self):
|
|
147
|
+
arr = ["a", "b", "c", "d", "e"]
|
|
148
|
+
self.assertEqual(self.bf.slice(arr, 1, 3), ["b", "c"])
|
|
149
|
+
self.assertEqual(self.bf.slice(arr, 2, None), ["c", "d", "e"])
|
|
150
|
+
self.assertEqual(self.bf.slice(arr, -2, None), ["d", "e"])
|
|
151
|
+
self.assertEqual(self.bf.slice(arr, 0, -1), ["a", "b", "c", "d"])
|
|
152
|
+
self.assertEqual(self.bf.slice(arr, 100, None), [])
|
|
153
|
+
self.assertEqual(self.bf.slice(arr, 3, 1), [])
|
|
154
|
+
self.assertEqual(self.bf.slice(None, 0, None), [])
|
|
155
|
+
|
|
156
|
+
src = ["a", "b", "c"]
|
|
157
|
+
out = self.bf.slice(src, 0, 2)
|
|
158
|
+
out.append("mutated")
|
|
159
|
+
self.assertEqual(src, ["a", "b", "c"])
|
|
160
|
+
|
|
161
|
+
def test_reverse_mutation_isolation(self):
|
|
162
|
+
self.assertEqual(self.bf.reverse(["a", "b", "c"]), ["c", "b", "a"])
|
|
163
|
+
self.assertEqual(self.bf.reverse([]), [])
|
|
164
|
+
|
|
165
|
+
src = ["a", "b", "c"]
|
|
166
|
+
out = self.bf.reverse(src)
|
|
167
|
+
out.append("mutated")
|
|
168
|
+
self.assertEqual(src, ["a", "b", "c"])
|
|
169
|
+
self.assertEqual(self.bf.reverse(None), [])
|
|
170
|
+
|
|
171
|
+
def test_trim(self):
|
|
172
|
+
self.assertEqual(self.bf.trim(" padded "), "padded")
|
|
173
|
+
self.assertEqual(self.bf.trim(""), "")
|
|
174
|
+
self.assertEqual(self.bf.trim(None), "")
|
|
175
|
+
self.assertEqual(self.bf.trim({"a": 1}), "")
|
|
176
|
+
self.assertEqual(self.bf.trim(42), "42")
|
|
177
|
+
|
|
178
|
+
def test_split(self):
|
|
179
|
+
self.assertEqual(self.bf.split("a,b,c", ","), ["a", "b", "c"])
|
|
180
|
+
self.assertEqual(self.bf.split("a.b.c", "."), ["a", "b", "c"])
|
|
181
|
+
self.assertEqual(self.bf.split("a,", ","), ["a", ""])
|
|
182
|
+
self.assertEqual(self.bf.split(",a", ","), ["", "a"])
|
|
183
|
+
self.assertEqual(self.bf.split("abc", ""), ["a", "b", "c"])
|
|
184
|
+
self.assertEqual(self.bf.split("", ""), [])
|
|
185
|
+
self.assertEqual(self.bf.split("abc", ","), ["abc"])
|
|
186
|
+
self.assertEqual(self.bf.split("a,b,c"), ["a,b,c"])
|
|
187
|
+
self.assertEqual(self.bf.split("a,b,c,d", ",", 2), ["a", "b"])
|
|
188
|
+
self.assertEqual(self.bf.split("a,b", ",", 0), [])
|
|
189
|
+
self.assertEqual(self.bf.split(None, ","), [""])
|
|
190
|
+
self.assertEqual(self.bf.split(42, ","), ["42"])
|
|
191
|
+
|
|
192
|
+
def test_starts_with_ends_with_positions(self):
|
|
193
|
+
self.assertTrue(self.bf.starts_with("hello world", "hello"))
|
|
194
|
+
self.assertTrue(self.bf.starts_with("anything", ""))
|
|
195
|
+
self.assertTrue(self.bf.ends_with("hello world", "world"))
|
|
196
|
+
self.assertTrue(self.bf.starts_with("abc", "b", 1))
|
|
197
|
+
self.assertFalse(self.bf.starts_with("abc", "a", 99))
|
|
198
|
+
self.assertTrue(self.bf.starts_with("abc", "a", -5))
|
|
199
|
+
self.assertTrue(self.bf.ends_with("abc", "b", 2))
|
|
200
|
+
self.assertTrue(self.bf.ends_with("abc", "c", 99))
|
|
201
|
+
self.assertFalse(self.bf.ends_with("abc", "a", -1))
|
|
202
|
+
|
|
203
|
+
def test_replace(self):
|
|
204
|
+
self.assertEqual(self.bf.replace("hello world", "o", "0"), "hell0 world")
|
|
205
|
+
self.assertEqual(self.bf.replace("abc", "", "X"), "Xabc")
|
|
206
|
+
self.assertEqual(self.bf.replace("ab", "a", "$&"), "$&b")
|
|
207
|
+
|
|
208
|
+
def test_repeat(self):
|
|
209
|
+
self.assertEqual(self.bf.repeat("ab", 3), "ababab")
|
|
210
|
+
self.assertEqual(self.bf.repeat("ab", 0), "")
|
|
211
|
+
self.assertEqual(self.bf.repeat("ab", -2), "")
|
|
212
|
+
self.assertEqual(self.bf.repeat("ab", 2.9), "abab")
|
|
213
|
+
|
|
214
|
+
def test_pad_start_pad_end(self):
|
|
215
|
+
self.assertEqual(self.bf.pad_start("42", 5, "0"), "00042")
|
|
216
|
+
self.assertEqual(self.bf.pad_end("42", 5, "."), "42...")
|
|
217
|
+
self.assertEqual(self.bf.pad_start("42", 5), " 42")
|
|
218
|
+
self.assertEqual(self.bf.pad_start("x", 5, "ab"), "ababx")
|
|
219
|
+
self.assertEqual(self.bf.pad_start("hello", 3, "0"), "hello")
|
|
220
|
+
self.assertEqual(self.bf.pad_start("42", 5, ""), "42")
|
|
221
|
+
self.assertEqual(self.bf.pad_start("7", 4.9, "0"), "0007")
|
|
222
|
+
|
|
223
|
+
def test_sort_structured_comparator_dispatch(self):
|
|
224
|
+
items = [
|
|
225
|
+
{"name": "c", "price": 30},
|
|
226
|
+
{"name": "a", "price": 10},
|
|
227
|
+
{"name": "b", "price": 20},
|
|
228
|
+
]
|
|
229
|
+
self.assertEqual(
|
|
230
|
+
self.bf.sort(
|
|
231
|
+
items,
|
|
232
|
+
{"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "asc"}]},
|
|
233
|
+
),
|
|
234
|
+
[{"name": "a", "price": 10}, {"name": "b", "price": 20}, {"name": "c", "price": 30}],
|
|
235
|
+
)
|
|
236
|
+
self.assertEqual(
|
|
237
|
+
self.bf.sort(
|
|
238
|
+
items,
|
|
239
|
+
{"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "desc"}]},
|
|
240
|
+
),
|
|
241
|
+
[{"name": "c", "price": 30}, {"name": "b", "price": 20}, {"name": "a", "price": 10}],
|
|
242
|
+
)
|
|
243
|
+
self.assertEqual(
|
|
244
|
+
self.bf.sort([3, 1, 2], {"keys": [{"key_kind": "self", "compare_type": "numeric", "direction": "asc"}]}),
|
|
245
|
+
[1, 2, 3],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Mutation isolation.
|
|
249
|
+
src = [{"price": 3}, {"price": 1}, {"price": 2}]
|
|
250
|
+
out = self.bf.sort(
|
|
251
|
+
src, {"keys": [{"key_kind": "field", "key": "price", "compare_type": "numeric", "direction": "asc"}]}
|
|
252
|
+
)
|
|
253
|
+
out.append({"price": 99})
|
|
254
|
+
self.assertEqual(src, [{"price": 3}, {"price": 1}, {"price": 2}])
|
|
255
|
+
|
|
256
|
+
self.assertEqual(
|
|
257
|
+
self.bf.sort(None, {"keys": [{"key_kind": "self", "compare_type": "numeric", "direction": "asc"}]}), []
|
|
258
|
+
)
|
|
259
|
+
self.assertEqual(self.bf.sort([], {"keys": [{"key_kind": "field", "key": "price"}]}), [])
|
|
260
|
+
|
|
261
|
+
def test_sort_multi_key_tie_break(self):
|
|
262
|
+
items = [{"p": 1, "name": "b"}, {"p": 1, "name": "a"}, {"p": 0, "name": "c"}]
|
|
263
|
+
self.assertEqual(
|
|
264
|
+
self.bf.sort(
|
|
265
|
+
items,
|
|
266
|
+
{
|
|
267
|
+
"keys": [
|
|
268
|
+
{"key_kind": "field", "key": "p", "compare_type": "numeric", "direction": "asc"},
|
|
269
|
+
{"key_kind": "field", "key": "name", "compare_type": "string", "direction": "asc"},
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
[{"p": 0, "name": "c"}, {"p": 1, "name": "a"}, {"p": 1, "name": "b"}],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def test_sort_auto_compare(self):
|
|
277
|
+
self.assertEqual(
|
|
278
|
+
self.bf.sort([3, 1, 2], {"keys": [{"key_kind": "self", "compare_type": "auto", "direction": "asc"}]}),
|
|
279
|
+
[1, 2, 3],
|
|
280
|
+
)
|
|
281
|
+
self.assertEqual(
|
|
282
|
+
self.bf.sort(
|
|
283
|
+
["charlie", "alice", "bob"],
|
|
284
|
+
{"keys": [{"key_kind": "self", "compare_type": "auto", "direction": "asc"}]},
|
|
285
|
+
),
|
|
286
|
+
["alice", "bob", "charlie"],
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class EvalDelegationTest(unittest.TestCase):
|
|
291
|
+
"""Verifies `bf.*_eval` methods correctly delegate to the `evaluator`
|
|
292
|
+
module (JSON-string seam wiring), not just the underlying evaluator
|
|
293
|
+
functions tested directly in test_evaluator.py / test_eval_vectors.py."""
|
|
294
|
+
|
|
295
|
+
def setUp(self):
|
|
296
|
+
self.bf = BarefootJS(None, {"backend": _PureBackend()})
|
|
297
|
+
|
|
298
|
+
def _node(self, **kwargs):
|
|
299
|
+
return kwargs
|
|
300
|
+
|
|
301
|
+
def test_sort_eval(self):
|
|
302
|
+
cmp = self._node(
|
|
303
|
+
kind="binary",
|
|
304
|
+
op="-",
|
|
305
|
+
left={"kind": "member", "object": {"kind": "identifier", "name": "a"}, "property": "v"},
|
|
306
|
+
right={"kind": "member", "object": {"kind": "identifier", "name": "b"}, "property": "v"},
|
|
307
|
+
)
|
|
308
|
+
out = self.bf.sort_eval([{"v": 3}, {"v": 1}, {"v": 2}], json.dumps(cmp), "a", "b")
|
|
309
|
+
self.assertEqual([x["v"] for x in out], [1, 2, 3])
|
|
310
|
+
|
|
311
|
+
def test_reduce_eval(self):
|
|
312
|
+
body = self._node(
|
|
313
|
+
kind="binary",
|
|
314
|
+
op="+",
|
|
315
|
+
left={"kind": "identifier", "name": "acc"},
|
|
316
|
+
right={"kind": "identifier", "name": "item"},
|
|
317
|
+
)
|
|
318
|
+
out = self.bf.reduce_eval([1, 2, 3], json.dumps(body), "acc", "item", 0)
|
|
319
|
+
self.assertEqual(out, 6)
|
|
320
|
+
|
|
321
|
+
def test_filter_every_some_find_eval(self):
|
|
322
|
+
pred = self._node(
|
|
323
|
+
kind="binary",
|
|
324
|
+
op=">=",
|
|
325
|
+
left={"kind": "member", "object": {"kind": "identifier", "name": "u"}, "property": "age"},
|
|
326
|
+
right={"kind": "literal", "value": 18},
|
|
327
|
+
)
|
|
328
|
+
pred_json = json.dumps(pred)
|
|
329
|
+
rows = [{"age": 15}, {"age": 30}, {"age": 18}]
|
|
330
|
+
self.assertEqual([r["age"] for r in self.bf.filter_eval(rows, pred_json, "u")], [30, 18])
|
|
331
|
+
self.assertFalse(self.bf.every_eval(rows, pred_json, "u"))
|
|
332
|
+
self.assertTrue(self.bf.some_eval(rows, pred_json, "u"))
|
|
333
|
+
self.assertEqual(self.bf.find_eval(rows, pred_json, "u")["age"], 30)
|
|
334
|
+
self.assertEqual(self.bf.find_index_eval(rows, pred_json, "u", forward=False), 2)
|
|
335
|
+
|
|
336
|
+
def test_flat_map_eval_and_map_eval(self):
|
|
337
|
+
field = self._node(kind="member", object={"kind": "identifier", "name": "i"}, property="tags")
|
|
338
|
+
rows = [{"tags": ["a", "b"]}, {"tags": ["c"]}]
|
|
339
|
+
self.assertEqual(self.bf.flat_map_eval(rows, json.dumps(field), "i"), ["a", "b", "c"])
|
|
340
|
+
|
|
341
|
+
name_field = self._node(kind="member", object={"kind": "identifier", "name": "u"}, property="name")
|
|
342
|
+
users = [{"name": "Ada"}, {"name": "Grace"}]
|
|
343
|
+
self.assertEqual(self.bf.map_eval(users, json.dumps(name_field), "u"), ["Ada", "Grace"])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
unittest.main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"backend": "python",
|
|
4
|
+
"runner": "packages/adapter-jinja/python/tests/test_helper_vectors.py",
|
|
5
|
+
"spec": "spec/template-helpers.md",
|
|
6
|
+
"divergences": {
|
|
7
|
+
"add/beyond the safe-integer edge rounds as a double": {
|
|
8
|
+
"expect": 9007199254740993,
|
|
9
|
+
"reason": "Python int arithmetic is exact (arbitrary precision), not double-rounded"
|
|
10
|
+
},
|
|
11
|
+
"div/zero divisor yields Infinity": {
|
|
12
|
+
"throws": true,
|
|
13
|
+
"exception": "ZeroDivisionError",
|
|
14
|
+
"reason": "Python native / raises ZeroDivisionError on a zero divisor"
|
|
15
|
+
},
|
|
16
|
+
"number/empty string coerces to 0": {
|
|
17
|
+
"expect": { "$num": "NaN" },
|
|
18
|
+
"reason": "deliberate: empty input must not silently zero downstream arithmetic (matches the Perl port)"
|
|
19
|
+
},
|
|
20
|
+
"number/null coerces to 0": {
|
|
21
|
+
"expect": { "$num": "NaN" },
|
|
22
|
+
"reason": "deliberate: unset props must not silently zero downstream arithmetic (matches the Perl port)"
|
|
23
|
+
},
|
|
24
|
+
"string/null renders as the string \"null\"": {
|
|
25
|
+
"expect": "",
|
|
26
|
+
"reason": "deliberate: an unset prop must not surface a literal \"null\" in HTML (matches the Perl port)"
|
|
27
|
+
},
|
|
28
|
+
"sort/localeCompare orders case-insensitively (ICU collation)": {
|
|
29
|
+
"expect": ["B", "a"],
|
|
30
|
+
"reason": "Python str comparison is codepoint order, not ICU collation"
|
|
31
|
+
},
|
|
32
|
+
"sort/relational compare on numeric strings is lexical": {
|
|
33
|
+
"expect": ["9", "10"],
|
|
34
|
+
"reason": "the \"auto\" compare goes numeric when both keys look_like_number (Perl/Go parity)"
|
|
35
|
+
},
|
|
36
|
+
"reduce/numeric-string items concatenate under JS +": {
|
|
37
|
+
"expect": 11.0,
|
|
38
|
+
"reason": "numeric folds parse numeric strings instead of concatenating (Perl/Go parity)"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"unsupported": {}
|
|
42
|
+
}
|