@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,89 @@
|
|
|
1
|
+
"""Python port of packages/adapter-perl/lib/BarefootJS/SearchParams.pm.
|
|
2
|
+
|
|
3
|
+
Request-scoped SSR view of the query string behind the reactive
|
|
4
|
+
`searchParams()` environment signal. The framework integration builds one
|
|
5
|
+
per request from the request URL and threads it into the template scope as
|
|
6
|
+
`searchParams` (the camelCase JS name the adapters keep, like every other
|
|
7
|
+
signal/prop var); the compiled template reads it via
|
|
8
|
+
`{{ searchParams.get('key') }}`.
|
|
9
|
+
|
|
10
|
+
This runtime is template-engine- and framework-agnostic (stdlib only),
|
|
11
|
+
matching the rest of the `barefootjs` package.
|
|
12
|
+
|
|
13
|
+
Semantics mirror the browser's `URLSearchParams.get` exactly under the
|
|
14
|
+
adapters' `?? -> or` lowering: `get()` returns the first value for a key, or
|
|
15
|
+
`None` when the key is absent. The Jinja/Python lowering of `??` should
|
|
16
|
+
coalesce only `None` (Jinja's `x.get(k) or default` would ALSO coalesce a
|
|
17
|
+
present-but-empty string, which is wrong -- the emitter must use
|
|
18
|
+
`x.get(k); ... if v is not None else default` shaped logic, not a bare
|
|
19
|
+
`or`), preserving the distinction JS `??` draws between `null` and `''`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _decode(s: Optional[str]) -> str:
|
|
29
|
+
"""Percent/`+`-decode a query-string component, mirroring
|
|
30
|
+
`URLSearchParams`'s `application/x-www-form-urlencoded` parsing. Never
|
|
31
|
+
raises on malformed input (lenient parsing, matching the browser)."""
|
|
32
|
+
if s is None:
|
|
33
|
+
s = ""
|
|
34
|
+
s = s.replace("+", " ")
|
|
35
|
+
raw = bytearray()
|
|
36
|
+
i = 0
|
|
37
|
+
n = len(s)
|
|
38
|
+
while i < n:
|
|
39
|
+
ch = s[i]
|
|
40
|
+
if ch == "%" and i + 2 < n and _is_hex_pair(s[i + 1], s[i + 2]):
|
|
41
|
+
raw.append(int(s[i + 1 : i + 3], 16))
|
|
42
|
+
i += 3
|
|
43
|
+
continue
|
|
44
|
+
raw.extend(ch.encode("utf-8"))
|
|
45
|
+
i += 1
|
|
46
|
+
try:
|
|
47
|
+
return raw.decode("utf-8")
|
|
48
|
+
except UnicodeDecodeError:
|
|
49
|
+
# Lenient: a byte run that isn't valid UTF-8 is kept (with
|
|
50
|
+
# replacement chars for the invalid bytes) rather than raising --
|
|
51
|
+
# mirrors Perl's `utf8::decode`, which never dies.
|
|
52
|
+
return raw.decode("utf-8", errors="replace")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_hex_pair(a: str, b: str) -> bool:
|
|
56
|
+
return a in "0123456789ABCDEFabcdef" and b in "0123456789ABCDEFabcdef"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_PAIR_SPLIT_RE = re.compile(r"[&;]")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SearchParams:
|
|
63
|
+
"""new(query=''): parse a raw query string into the reader. A leading
|
|
64
|
+
'?' is tolerated, '+' decodes to a space, and %XX escapes are decoded."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, query: str = ""):
|
|
67
|
+
query = query or ""
|
|
68
|
+
if query.startswith("?"):
|
|
69
|
+
query = query[1:]
|
|
70
|
+
values: dict[str, list[str]] = {}
|
|
71
|
+
for pair in _PAIR_SPLIT_RE.split(query):
|
|
72
|
+
if pair == "":
|
|
73
|
+
continue
|
|
74
|
+
if "=" in pair:
|
|
75
|
+
key, val = pair.split("=", 1)
|
|
76
|
+
else:
|
|
77
|
+
key, val = pair, None
|
|
78
|
+
key = _decode(key)
|
|
79
|
+
val_decoded = _decode(val) if val is not None else ""
|
|
80
|
+
values.setdefault(key, []).append(val_decoded)
|
|
81
|
+
self._values = values
|
|
82
|
+
|
|
83
|
+
def get(self, key: str) -> Optional[str]:
|
|
84
|
+
"""First value for `key`, or `None` when the key is absent. A
|
|
85
|
+
present-but-empty value returns ''."""
|
|
86
|
+
vals = self._values.get(key)
|
|
87
|
+
if not vals:
|
|
88
|
+
return None
|
|
89
|
+
return vals[0]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "barefootjs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python runtime for the @barefootjs/jinja adapter"
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "kobaken", email = "kentafly88@gmail.com" }]
|
|
12
|
+
keywords = ["jinja", "jinja2", "barefoot", "ssr", "templates"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["jinja2>=3.1"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/piconic-ai/barefootjs"
|
|
26
|
+
Repository = "https://github.com/piconic-ai/barefootjs"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
include = ["barefootjs*"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.dynamic]
|
|
32
|
+
version = {file = "VERSION"}
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Golden ParsedExpr-evaluator vectors, ported from
|
|
2
|
+
packages/adapter-perl/t/eval_vectors.t.
|
|
3
|
+
|
|
4
|
+
Runs `packages/adapter-tests/vectors/eval-vectors.json` -- generated
|
|
5
|
+
from the JS reference evaluator, shared with the Go and Perl evaluators --
|
|
6
|
+
against `barefootjs.evaluator.evaluate`. The evaluator is JS-faithful by
|
|
7
|
+
contract, so unlike the helper vectors there are NO Python-side
|
|
8
|
+
divergences here: each case's real ParsedExpr tree, evaluated against its
|
|
9
|
+
environment, must reproduce the JS-computed expect exactly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
from barefootjs import evaluator
|
|
19
|
+
|
|
20
|
+
VECTORS_PATH = os.path.join(
|
|
21
|
+
os.path.dirname(__file__), "..", "..", "..", "adapter-tests", "vectors", "eval-vectors.json"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _match(got, expect):
|
|
26
|
+
"""Spec value-compat comparison -- non-finite sentinel hashes, booleans
|
|
27
|
+
by truthiness (but the evaluator result must ITSELF be a real bool, not
|
|
28
|
+
a truthy int -- matching a boolean-valued JS operator must return a real
|
|
29
|
+
boolean), numbers numerically, arrays/hashes recursively, strings by
|
|
30
|
+
equality."""
|
|
31
|
+
if expect is None:
|
|
32
|
+
return got is None
|
|
33
|
+
if isinstance(expect, dict) and "$num" in expect:
|
|
34
|
+
kind = expect["$num"]
|
|
35
|
+
if isinstance(got, bool) or not isinstance(got, (int, float)):
|
|
36
|
+
return False
|
|
37
|
+
g = float(got)
|
|
38
|
+
if kind == "NaN":
|
|
39
|
+
return g != g
|
|
40
|
+
inf = float("inf")
|
|
41
|
+
return g == (inf if kind == "Infinity" else -inf)
|
|
42
|
+
if isinstance(expect, bool):
|
|
43
|
+
return isinstance(got, bool) and got == expect
|
|
44
|
+
if isinstance(expect, list):
|
|
45
|
+
if not isinstance(got, list) or len(got) != len(expect):
|
|
46
|
+
return False
|
|
47
|
+
return all(_match(g, e) for g, e in zip(got, expect))
|
|
48
|
+
if isinstance(expect, dict):
|
|
49
|
+
if not isinstance(got, dict) or len(got) != len(expect):
|
|
50
|
+
return False
|
|
51
|
+
return all(k in got and _match(got[k], v) for k, v in expect.items())
|
|
52
|
+
if got is None or isinstance(got, (list, dict)):
|
|
53
|
+
return False
|
|
54
|
+
# Numeric comparison only when BOTH are real numbers (not
|
|
55
|
+
# numeric-looking strings) -- e.g. String(42) must return the string
|
|
56
|
+
# "42", and evaluating it as the number 42 must NOT pass.
|
|
57
|
+
want_num = isinstance(expect, (int, float)) and not isinstance(expect, bool)
|
|
58
|
+
got_num = isinstance(got, (int, float)) and not isinstance(got, bool)
|
|
59
|
+
if want_num != got_num:
|
|
60
|
+
return False
|
|
61
|
+
if want_num:
|
|
62
|
+
if isinstance(got, int) and isinstance(expect, int):
|
|
63
|
+
return got == expect
|
|
64
|
+
return float(got) == float(expect)
|
|
65
|
+
return got == expect
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@unittest.skipUnless(os.path.exists(VECTORS_PATH), "eval vectors not available outside the monorepo checkout")
|
|
69
|
+
class EvalVectorsTest(unittest.TestCase):
|
|
70
|
+
def test_vectors(self):
|
|
71
|
+
with open(VECTORS_PATH, encoding="utf-8") as fh:
|
|
72
|
+
doc = json.load(fh)
|
|
73
|
+
|
|
74
|
+
self.assertTrue(doc["cases"], "eval-vectors.json contains no cases")
|
|
75
|
+
|
|
76
|
+
for case in doc["cases"]:
|
|
77
|
+
note, expr, env, expect = case["note"], case["expr"], case["env"], case["expect"]
|
|
78
|
+
with self.subTest(note=note):
|
|
79
|
+
try:
|
|
80
|
+
got = evaluator.evaluate(expr, env)
|
|
81
|
+
except Exception as exc: # noqa: BLE001
|
|
82
|
+
self.fail(f"{note} raised: {exc!r}")
|
|
83
|
+
continue
|
|
84
|
+
self.assertTrue(_match(got, expect), f"{note}: got {got!r}, want {expect!r}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Hand-built ParsedExpr evaluator demonstrations, ported from
|
|
2
|
+
packages/adapter-perl/t/evaluator.t.
|
|
3
|
+
|
|
4
|
+
Mirrors the Go/Perl `eval_test.go` / `evaluator.t` demonstrations so all
|
|
5
|
+
three backends prove the SAME restriction-lifting on the SAME shapes (a
|
|
6
|
+
reducer/comparator/predicate body the fixed bf_reduce/bf_sort/bf_filter
|
|
7
|
+
catalogues can't express, but the evaluator handles as just another pure
|
|
8
|
+
expression).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
from barefootjs import evaluator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def nid(name):
|
|
20
|
+
return {"kind": "identifier", "name": name}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def nmem(obj, prop):
|
|
24
|
+
return {"kind": "member", "object": obj, "property": prop, "computed": False}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def nbin(op, left, right):
|
|
28
|
+
return {"kind": "binary", "op": op, "left": left, "right": right}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def nstr(value):
|
|
32
|
+
return {"kind": "literal", "value": value, "literalType": "string"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def nnum(value):
|
|
36
|
+
return {"kind": "literal", "value": value, "literalType": "number"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ncall_math(fn, arg):
|
|
40
|
+
return {"kind": "call", "callee": nmem(nid("Math"), fn), "args": [arg]}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def nincludes(obj, needle):
|
|
44
|
+
return {"kind": "array-method", "method": "includes", "object": obj, "args": [needle]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EvaluatorTest(unittest.TestCase):
|
|
48
|
+
def test_fold_arbitrary_reducer_body(self):
|
|
49
|
+
# acc + item.price * item.qty
|
|
50
|
+
body = nbin("+", nid("acc"), nbin("*", nmem(nid("item"), "price"), nmem(nid("item"), "qty")))
|
|
51
|
+
items = [{"price": 5, "qty": 3}, {"price": 2, "qty": 4}]
|
|
52
|
+
self.assertEqual(evaluator.fold(items, body, "acc", "item", 0, "left"), 23)
|
|
53
|
+
|
|
54
|
+
def test_fold_direction_observable_for_string_concat(self):
|
|
55
|
+
body = nbin("+", nid("acc"), nid("item"))
|
|
56
|
+
items = ["a", "b", "c"]
|
|
57
|
+
self.assertEqual(evaluator.fold(items, body, "acc", "item", "", "left"), "abc")
|
|
58
|
+
self.assertEqual(evaluator.fold(items, body, "acc", "item", "", "right"), "cba")
|
|
59
|
+
|
|
60
|
+
def test_sort_by_arbitrary_comparator_abs_of_field_difference(self):
|
|
61
|
+
cmp = nbin("-", ncall_math("abs", nmem(nid("a"), "v")), ncall_math("abs", nmem(nid("b"), "v")))
|
|
62
|
+
items = [{"v": -5}, {"v": 3}, {"v": -1}]
|
|
63
|
+
sorted_items = evaluator.sort_by(items, cmp, "a", "b")
|
|
64
|
+
self.assertEqual([x["v"] for x in sorted_items], [-1, 3, -5])
|
|
65
|
+
|
|
66
|
+
def test_sort_by_descending_via_reversed_comparator(self):
|
|
67
|
+
cmp = nbin("-", nmem(nid("b"), "x"), nmem(nid("a"), "x"))
|
|
68
|
+
items = [{"x": 10}, {"x": 30}, {"x": 20}]
|
|
69
|
+
sorted_items = evaluator.sort_by(items, cmp, "a", "b")
|
|
70
|
+
self.assertEqual([x["x"] for x in sorted_items], [30, 20, 10])
|
|
71
|
+
|
|
72
|
+
def test_nonfinite_division_and_js_stringification(self):
|
|
73
|
+
def div(a, b):
|
|
74
|
+
return evaluator.evaluate(nbin("/", nid("a"), nid("b")), {"a": a, "b": b})
|
|
75
|
+
|
|
76
|
+
inf = float("inf")
|
|
77
|
+
self.assertEqual(div(1, 0), inf)
|
|
78
|
+
self.assertEqual(div(-1, 0), -inf)
|
|
79
|
+
nan = div(0, 0)
|
|
80
|
+
self.assertNotEqual(nan, nan)
|
|
81
|
+
|
|
82
|
+
self.assertEqual(evaluator._to_string(inf), "Infinity")
|
|
83
|
+
self.assertEqual(evaluator._to_string(-inf), "-Infinity")
|
|
84
|
+
self.assertEqual(evaluator._to_string(inf - inf), "NaN")
|
|
85
|
+
|
|
86
|
+
def test_captured_free_vars_via_base_env(self):
|
|
87
|
+
body = nbin("+", nid("acc"), nbin("*", nid("item"), nid("factor")))
|
|
88
|
+
total = evaluator.fold([1, 2, 3], body, "acc", "item", 0, "left", {"factor": 10})
|
|
89
|
+
self.assertEqual(total, 60)
|
|
90
|
+
|
|
91
|
+
cmp = nbin(
|
|
92
|
+
"-",
|
|
93
|
+
ncall_math("abs", nbin("-", nid("a"), nid("pivot"))),
|
|
94
|
+
ncall_math("abs", nbin("-", nid("b"), nid("pivot"))),
|
|
95
|
+
)
|
|
96
|
+
sorted_items = evaluator.sort_by([1, 8, 4], cmp, "a", "b", {"pivot": 5})
|
|
97
|
+
self.assertEqual(sorted_items, [4, 8, 1])
|
|
98
|
+
|
|
99
|
+
def test_boolean_valued_ops_return_real_booleans(self):
|
|
100
|
+
lt = evaluator.evaluate(nbin("<", nid("a"), nid("b")), {"a": 1, "b": 2})
|
|
101
|
+
self.assertIsInstance(lt, bool)
|
|
102
|
+
self.assertEqual(evaluator._to_string(lt), "true")
|
|
103
|
+
|
|
104
|
+
cat = evaluator.evaluate(nbin("+", nstr("x"), nbin("<", nid("a"), nid("b"))), {"a": 1, "b": 2})
|
|
105
|
+
self.assertEqual(cat, "xtrue")
|
|
106
|
+
|
|
107
|
+
eq = evaluator.evaluate(nbin("===", nid("a"), nid("b")), {"a": 1, "b": 1})
|
|
108
|
+
self.assertIsInstance(eq, bool)
|
|
109
|
+
|
|
110
|
+
not_ = evaluator.evaluate({"kind": "unary", "op": "!", "argument": nstr("")}, {})
|
|
111
|
+
self.assertEqual(evaluator._to_string(not_), "true")
|
|
112
|
+
|
|
113
|
+
b = evaluator.evaluate({"kind": "call", "callee": nid("Boolean"), "args": [nstr("")]}, {})
|
|
114
|
+
self.assertIsInstance(b, bool)
|
|
115
|
+
self.assertEqual(evaluator._to_string(b), "false")
|
|
116
|
+
|
|
117
|
+
# `.length` is a string/array property only; a numeric scalar has none.
|
|
118
|
+
length = evaluator.evaluate(nmem(nid("n"), "length"), {"n": 123})
|
|
119
|
+
self.assertIsNone(length)
|
|
120
|
+
|
|
121
|
+
def test_array_method_includes(self):
|
|
122
|
+
# `.includes` (#2075) is the one `array-method` in the evaluator
|
|
123
|
+
# subset, dispatching on the receiver type like the SSR template
|
|
124
|
+
# lowering does at runtime (`bf.includes`): array -> SameValueZero
|
|
125
|
+
# membership (the same value rules as `===`, so a numeric 2 does NOT
|
|
126
|
+
# match the string "2"); string -> substring search; anything else
|
|
127
|
+
# degrades to false rather than raising.
|
|
128
|
+
hit = evaluator.evaluate(nincludes(nid("tags"), nstr("go")), {"tags": ["perl", "go"]})
|
|
129
|
+
self.assertIsInstance(hit, bool)
|
|
130
|
+
self.assertTrue(hit)
|
|
131
|
+
|
|
132
|
+
miss = evaluator.evaluate(nincludes(nid("tags"), nstr("rust")), {"tags": ["perl", "go"]})
|
|
133
|
+
self.assertIsInstance(miss, bool)
|
|
134
|
+
self.assertFalse(miss)
|
|
135
|
+
|
|
136
|
+
# SameValueZero, not loose equality: the numeric element 2 matches
|
|
137
|
+
# the numeric needle 2, but the string needle "2" (a different JS
|
|
138
|
+
# type) does not -- mirroring `===`'s type-sensitivity.
|
|
139
|
+
num_hit = evaluator.evaluate(nincludes(nid("nums"), nnum(2)), {"nums": [1, 2, 3]})
|
|
140
|
+
self.assertTrue(num_hit)
|
|
141
|
+
num_vs_string = evaluator.evaluate(nincludes(nid("nums"), nstr("2")), {"nums": [1, 2, 3]})
|
|
142
|
+
self.assertFalse(num_vs_string)
|
|
143
|
+
|
|
144
|
+
sub = evaluator.evaluate(nincludes(nid("name"), nstr("ar")), {"name": "bare"})
|
|
145
|
+
self.assertTrue(sub)
|
|
146
|
+
|
|
147
|
+
# A non-array, non-string receiver (number, null, object) is not a
|
|
148
|
+
# JS `.includes` target; the evaluator degrades to false rather than
|
|
149
|
+
# raising.
|
|
150
|
+
scalar_recv = evaluator.evaluate(nincludes(nid("n"), nnum(1)), {"n": 42})
|
|
151
|
+
self.assertFalse(scalar_recv)
|
|
152
|
+
null_recv = evaluator.evaluate(nincludes(nid("n"), nstr("x")), {"n": None})
|
|
153
|
+
self.assertFalse(null_recv)
|
|
154
|
+
|
|
155
|
+
def test_sort_by_non_array_receiver_returns_empty_list(self):
|
|
156
|
+
cmp = nbin("-", nid("a"), nid("b"))
|
|
157
|
+
self.assertEqual(evaluator.sort_by(None, cmp, "a", "b"), [])
|
|
158
|
+
self.assertEqual(evaluator.sort_by(42, cmp, "a", "b"), [])
|
|
159
|
+
|
|
160
|
+
def test_sort_by_is_stable_for_equal_keys(self):
|
|
161
|
+
cmp = nbin("-", nmem(nid("a"), "k"), nmem(nid("b"), "k"))
|
|
162
|
+
eq = evaluator.sort_by(
|
|
163
|
+
[{"k": 1, "id": "a"}, {"k": 1, "id": "b"}, {"k": 1, "id": "c"}], cmp, "a", "b"
|
|
164
|
+
)
|
|
165
|
+
self.assertEqual([x["id"] for x in eq], ["a", "b", "c"])
|
|
166
|
+
|
|
167
|
+
mixed = evaluator.sort_by(
|
|
168
|
+
[{"k": 2, "id": "x"}, {"k": 1, "id": "y"}, {"k": 2, "id": "z"}], cmp, "a", "b"
|
|
169
|
+
)
|
|
170
|
+
self.assertEqual([x["id"] for x in mixed], ["y", "x", "z"])
|
|
171
|
+
|
|
172
|
+
def test_fold_json_and_sort_by_json_decode_and_evaluate(self):
|
|
173
|
+
rows = [{"duration": 95}, {"duration": 213}, {"duration": 185}]
|
|
174
|
+
|
|
175
|
+
reduce_body = json.dumps(nbin("+", nid("sum"), nmem(nid("t"), "duration")))
|
|
176
|
+
self.assertEqual(evaluator.fold_json(rows, reduce_body, "sum", "t", 0, "left", {}), 493)
|
|
177
|
+
|
|
178
|
+
labels = [{"label": "a"}, {"label": "b"}, {"label": "c"}]
|
|
179
|
+
concat_body = json.dumps(nbin("+", nid("acc"), nmem(nid("x"), "label")))
|
|
180
|
+
self.assertEqual(evaluator.fold_json(labels, concat_body, "acc", "x", "", "left", {}), "abc")
|
|
181
|
+
self.assertEqual(evaluator.fold_json(labels, concat_body, "acc", "x", "", "right", {}), "cba")
|
|
182
|
+
|
|
183
|
+
cmp_json = json.dumps(nbin("-", nmem(nid("a"), "duration"), nmem(nid("b"), "duration")))
|
|
184
|
+
sorted_rows = evaluator.sort_by_json(rows, cmp_json, "a", "b", {})
|
|
185
|
+
self.assertEqual([r["duration"] for r in sorted_rows], [95, 185, 213])
|
|
186
|
+
|
|
187
|
+
def test_filter_every_some_find_find_index_over_predicate(self):
|
|
188
|
+
rows = [{"age": 15}, {"age": 30}, {"age": 18}]
|
|
189
|
+
pred = nbin(">=", nmem(nid("u"), "age"), nnum(18))
|
|
190
|
+
|
|
191
|
+
f = evaluator.filter(rows, pred, "u")
|
|
192
|
+
self.assertEqual([r["age"] for r in f], [30, 18])
|
|
193
|
+
|
|
194
|
+
self.assertTrue(evaluator.some(rows, pred, "u"))
|
|
195
|
+
self.assertFalse(evaluator.every(rows, pred, "u"))
|
|
196
|
+
|
|
197
|
+
self.assertEqual(evaluator.find(rows, pred, "u", True)["age"], 30)
|
|
198
|
+
self.assertEqual(evaluator.find(rows, pred, "u", False)["age"], 18)
|
|
199
|
+
self.assertEqual(evaluator.find_index(rows, pred, "u", True), 1)
|
|
200
|
+
self.assertEqual(evaluator.find_index(rows, pred, "u", False), 2)
|
|
201
|
+
|
|
202
|
+
self.assertTrue(evaluator.every([], pred, "u"))
|
|
203
|
+
self.assertFalse(evaluator.some([], pred, "u"))
|
|
204
|
+
self.assertIsNone(evaluator.find([], pred, "u"))
|
|
205
|
+
self.assertEqual(evaluator.find_index([], pred, "u"), -1)
|
|
206
|
+
|
|
207
|
+
pred_json = json.dumps(pred)
|
|
208
|
+
fj = evaluator.filter_json(rows, pred_json, "u")
|
|
209
|
+
self.assertEqual([r["age"] for r in fj], [30, 18])
|
|
210
|
+
self.assertFalse(evaluator.every_json(rows, pred_json, "u"))
|
|
211
|
+
self.assertTrue(evaluator.some_json(rows, pred_json, "u"))
|
|
212
|
+
self.assertEqual(evaluator.find_json(rows, pred_json, "u", True)["age"], 30)
|
|
213
|
+
self.assertEqual(evaluator.find_index_json(rows, pred_json, "u", False), 2)
|
|
214
|
+
|
|
215
|
+
cap = nbin(">=", nmem(nid("u"), "age"), nid("threshold"))
|
|
216
|
+
hi = evaluator.filter(rows, cap, "u", {"threshold": 18})
|
|
217
|
+
lo = evaluator.filter(rows, cap, "u", {"threshold": 100})
|
|
218
|
+
self.assertEqual(len(hi), 2)
|
|
219
|
+
self.assertEqual(len(lo), 0)
|
|
220
|
+
self.assertEqual(evaluator.find_index(rows, cap, "u", True, {"threshold": 100}), -1)
|
|
221
|
+
|
|
222
|
+
def test_flat_map_projects_and_flattens_one_level(self):
|
|
223
|
+
rows = [{"tags": ["a", "b"]}, {"tags": ["c"]}]
|
|
224
|
+
field = nmem(nid("i"), "tags")
|
|
225
|
+
self.assertEqual(evaluator.flat_map(rows, field, "i"), ["a", "b", "c"])
|
|
226
|
+
|
|
227
|
+
pts = [{"x": 1, "y": 2}, {"x": 3, "y": 4}]
|
|
228
|
+
tuple_proj = {
|
|
229
|
+
"kind": "array-literal",
|
|
230
|
+
"elements": [nmem(nid("p"), "x"), nmem(nid("p"), "y")],
|
|
231
|
+
}
|
|
232
|
+
self.assertEqual(evaluator.flat_map(pts, tuple_proj, "p"), [1, 2, 3, 4])
|
|
233
|
+
|
|
234
|
+
fj = evaluator.flat_map_json(rows, json.dumps(field), "i")
|
|
235
|
+
self.assertEqual(fj, ["a", "b", "c"])
|
|
236
|
+
|
|
237
|
+
def test_map_items_projects_one_result_per_element_no_flatten(self):
|
|
238
|
+
tmpl = {
|
|
239
|
+
"kind": "template-literal",
|
|
240
|
+
"parts": [
|
|
241
|
+
{"type": "string", "value": "#"},
|
|
242
|
+
{"type": "expression", "expr": nid("t")},
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
self.assertEqual(evaluator.map_items(["perl", "go"], tmpl, "t"), ["#perl", "#go"])
|
|
246
|
+
|
|
247
|
+
users = [{"name": "Ada"}, {"name": "Grace"}]
|
|
248
|
+
field = nmem(nid("u"), "name")
|
|
249
|
+
self.assertEqual(evaluator.map_items(users, field, "u"), ["Ada", "Grace"])
|
|
250
|
+
|
|
251
|
+
rows = [{"tags": ["a", "b"]}]
|
|
252
|
+
self.assertEqual(evaluator.map_items(rows, nmem(nid("i"), "tags"), "i"), [["a", "b"]])
|
|
253
|
+
|
|
254
|
+
mj = evaluator.map_json(users, json.dumps(field), "u")
|
|
255
|
+
self.assertEqual(mj, ["Ada", "Grace"])
|
|
256
|
+
|
|
257
|
+
def test_nested_map_call_inside_a_callback_body(self):
|
|
258
|
+
# #2094: a `.map(cb)` call nested INSIDE a callback body the
|
|
259
|
+
# evaluator is already given, e.g. a `.flatMap(p => p.tags.map(t =>
|
|
260
|
+
# '#'+t))` projection (the #1938 blog-showcase shape). The inner
|
|
261
|
+
# `.map` is a `call` node whose callee is `{kind: 'member', property:
|
|
262
|
+
# 'map', computed: false}` and whose first arg is an `arrow` node.
|
|
263
|
+
prefix_tmpl = {
|
|
264
|
+
"kind": "template-literal",
|
|
265
|
+
"parts": [
|
|
266
|
+
{"type": "string", "value": "#"},
|
|
267
|
+
{"type": "expression", "expr": nid("t")},
|
|
268
|
+
],
|
|
269
|
+
}
|
|
270
|
+
inner_map = {
|
|
271
|
+
"kind": "call",
|
|
272
|
+
"callee": nmem(nmem(nid("p"), "tags"), "map"),
|
|
273
|
+
"args": [{"kind": "arrow", "params": ["t"], "body": prefix_tmpl}],
|
|
274
|
+
}
|
|
275
|
+
self.assertEqual(
|
|
276
|
+
evaluator.evaluate(inner_map, {"p": {"tags": ["a", "b"]}}),
|
|
277
|
+
["#a", "#b"],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# 2-param arrow (value, index).
|
|
281
|
+
idx_body = nbin("+", nid("t"), ncall_math("abs", nid("i")))
|
|
282
|
+
inner_map_idx = {
|
|
283
|
+
"kind": "call",
|
|
284
|
+
"callee": nmem(nid("xs"), "map"),
|
|
285
|
+
"args": [{"kind": "arrow", "params": ["t", "i"], "body": idx_body}],
|
|
286
|
+
}
|
|
287
|
+
self.assertEqual(evaluator.evaluate(inner_map_idx, {"xs": [10, 20]}), [10, 21])
|
|
288
|
+
|
|
289
|
+
# doubly-nested .map + .join: posts.flatMap(p => p.tags.map(t =>
|
|
290
|
+
# '#'+t).join(', ')) -- the #1938 blog-showcase shape, one level
|
|
291
|
+
# flattened by the outer `flat_map` composition.
|
|
292
|
+
join_of_map = {
|
|
293
|
+
"kind": "array-method",
|
|
294
|
+
"method": "join",
|
|
295
|
+
"object": inner_map,
|
|
296
|
+
"args": [nstr(", ")],
|
|
297
|
+
}
|
|
298
|
+
self.assertEqual(
|
|
299
|
+
evaluator.evaluate(join_of_map, {"p": {"tags": ["a", "b"]}}), "#a, #b"
|
|
300
|
+
)
|
|
301
|
+
posts = [{"tags": ["a", "b"]}, {"tags": ["c"]}]
|
|
302
|
+
proj = {
|
|
303
|
+
"kind": "array-method",
|
|
304
|
+
"method": "join",
|
|
305
|
+
"object": {
|
|
306
|
+
"kind": "call",
|
|
307
|
+
"callee": nmem(nmem(nid("p"), "tags"), "map"),
|
|
308
|
+
"args": [{"kind": "arrow", "params": ["t"], "body": prefix_tmpl}],
|
|
309
|
+
},
|
|
310
|
+
"args": [nstr(", ")],
|
|
311
|
+
}
|
|
312
|
+
self.assertEqual(evaluator.flat_map(posts, proj, "p"), ["#a, #b", "#c"])
|
|
313
|
+
|
|
314
|
+
def test_nested_callback_non_list_receiver_evaluates_to_none(self):
|
|
315
|
+
# Cross-runtime contract (Copilot review on #2095): a nested
|
|
316
|
+
# `.map`/`.filter` whose receiver is not an array evaluates to
|
|
317
|
+
# None, matching Go nil / Perl undef / Rust Null / PHP null /
|
|
318
|
+
# Ruby nil -- never an empty list, so "missing" stays
|
|
319
|
+
# distinguishable from "empty array".
|
|
320
|
+
inner_map = {
|
|
321
|
+
"kind": "call",
|
|
322
|
+
"callee": nmem(nmem(nid("p"), "tags"), "map"),
|
|
323
|
+
"args": [{"kind": "arrow", "params": ["t"], "body": nid("t")}],
|
|
324
|
+
}
|
|
325
|
+
self.assertIsNone(evaluator.evaluate(inner_map, {"p": {}}))
|
|
326
|
+
self.assertIsNone(evaluator.evaluate(inner_map, {"p": {"tags": "not-a-list"}}))
|
|
327
|
+
self.assertEqual(evaluator.evaluate(inner_map, {"p": {"tags": []}}), [])
|
|
328
|
+
|
|
329
|
+
def test_nested_filter_call_inside_a_callback_body(self):
|
|
330
|
+
# #2094: a `.filter(cb)` call nested inside a callback body,
|
|
331
|
+
# composed with `.length` and a relational comparison (the doc /
|
|
332
|
+
# #2038 motivating shape): `.filter(u => u.active).length > 0`.
|
|
333
|
+
inner_filter = {
|
|
334
|
+
"kind": "call",
|
|
335
|
+
"callee": nmem(nid("users"), "filter"),
|
|
336
|
+
"args": [
|
|
337
|
+
{
|
|
338
|
+
"kind": "arrow",
|
|
339
|
+
"params": ["u"],
|
|
340
|
+
"body": nmem(nid("u"), "active"),
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
}
|
|
344
|
+
composed = nbin(">", nmem(inner_filter, "length"), nnum(0))
|
|
345
|
+
self.assertTrue(
|
|
346
|
+
evaluator.evaluate(
|
|
347
|
+
composed,
|
|
348
|
+
{"users": [{"active": True}, {"active": False}]},
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
self.assertFalse(
|
|
352
|
+
evaluator.evaluate(
|
|
353
|
+
composed,
|
|
354
|
+
{"users": [{"active": False}, {"active": False}]},
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# predicate with comparison + logical operators.
|
|
359
|
+
pred_body = {
|
|
360
|
+
"kind": "logical",
|
|
361
|
+
"op": "&&",
|
|
362
|
+
"left": nbin(">=", nmem(nid("u"), "age"), nnum(18)),
|
|
363
|
+
"right": nmem(nid("u"), "active"),
|
|
364
|
+
}
|
|
365
|
+
inner_filter2 = {
|
|
366
|
+
"kind": "call",
|
|
367
|
+
"callee": nmem(nid("users"), "filter"),
|
|
368
|
+
"args": [{"kind": "arrow", "params": ["u"], "body": pred_body}],
|
|
369
|
+
}
|
|
370
|
+
rows = [{"age": 20, "active": True}, {"age": 15, "active": True}, {"age": 40, "active": False}]
|
|
371
|
+
self.assertEqual(
|
|
372
|
+
[r["age"] for r in evaluator.evaluate(inner_filter2, {"users": rows})], [20]
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def test_array_method_join(self):
|
|
376
|
+
# #2094: `.join(sep?)` as an `array-method` node, sharing the shape
|
|
377
|
+
# with `.includes` above. Default separator is `,`; a `None`
|
|
378
|
+
# (null/undefined) element joins as `''`, NOT the string "null"
|
|
379
|
+
# (which is what a bare ToString call on `None` produces).
|
|
380
|
+
def njoin(obj, sep=None):
|
|
381
|
+
node = {"kind": "array-method", "method": "join", "object": obj}
|
|
382
|
+
if sep is not None:
|
|
383
|
+
node["args"] = [sep]
|
|
384
|
+
return node
|
|
385
|
+
|
|
386
|
+
self.assertEqual(
|
|
387
|
+
evaluator.evaluate(njoin(nid("xs"), nstr("-")), {"xs": ["a", "b", "c"]}), "a-b-c"
|
|
388
|
+
)
|
|
389
|
+
self.assertEqual(
|
|
390
|
+
evaluator.evaluate(njoin(nid("xs")), {"xs": ["a", "b"]}), "a,b"
|
|
391
|
+
)
|
|
392
|
+
self.assertEqual(evaluator.evaluate(njoin(nid("xs")), {"xs": []}), "")
|
|
393
|
+
self.assertEqual(
|
|
394
|
+
evaluator.evaluate(njoin(nid("xs")), {"xs": ["a", None, "b"]}), "a,,b"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def test_length_member_read_on_arrays_and_strings(self):
|
|
398
|
+
# `.length` on both arrays and strings, needed for a composed case
|
|
399
|
+
# (`.filter(...).length > 0`); already handled by `_read_property`.
|
|
400
|
+
self.assertEqual(evaluator.evaluate(nmem(nid("xs"), "length"), {"xs": [1, 2, 3]}), 3)
|
|
401
|
+
self.assertEqual(evaluator.evaluate(nmem(nid("s"), "length"), {"s": "abc"}), 3)
|
|
402
|
+
self.assertEqual(evaluator.evaluate(nmem(nid("xs"), "length"), {"xs": []}), 0)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
if __name__ == "__main__":
|
|
406
|
+
unittest.main()
|