rapydscript-ns 0.8.3 → 0.9.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/CHANGELOG.md +26 -0
- package/README.md +1351 -141
- package/TODO.md +12 -6
- package/language-service/index.js +184 -26
- package/package.json +1 -1
- package/release/baselib-plain-pretty.js +5895 -1928
- package/release/baselib-plain-ugly.js +140 -3
- package/release/compiler.js +16282 -5408
- package/release/signatures.json +25 -22
- package/src/ast.pyj +94 -1
- package/src/baselib-builtins.pyj +362 -3
- package/src/baselib-bytes.pyj +664 -0
- package/src/baselib-containers.pyj +99 -0
- package/src/baselib-errors.pyj +45 -1
- package/src/baselib-internal.pyj +346 -49
- package/src/baselib-itertools.pyj +17 -4
- package/src/baselib-str.pyj +46 -4
- package/src/lib/abc.pyj +317 -0
- package/src/lib/copy.pyj +120 -0
- package/src/lib/dataclasses.pyj +532 -0
- package/src/lib/enum.pyj +125 -0
- package/src/lib/pythonize.pyj +1 -1
- package/src/lib/re.pyj +35 -1
- package/src/lib/react.pyj +74 -0
- package/src/lib/typing.pyj +577 -0
- package/src/monaco-language-service/builtins.js +19 -4
- package/src/monaco-language-service/diagnostics.js +40 -19
- package/src/output/classes.pyj +161 -25
- package/src/output/codegen.pyj +16 -2
- package/src/output/exceptions.pyj +97 -1
- package/src/output/functions.pyj +87 -5
- package/src/output/jsx.pyj +164 -0
- package/src/output/literals.pyj +28 -2
- package/src/output/loops.pyj +5 -2
- package/src/output/modules.pyj +1 -1
- package/src/output/operators.pyj +108 -36
- package/src/output/statements.pyj +2 -2
- package/src/output/stream.pyj +1 -0
- package/src/parse.pyj +496 -128
- package/src/tokenizer.pyj +38 -4
- package/test/abc.pyj +291 -0
- package/test/arithmetic_nostrict.pyj +88 -0
- package/test/arithmetic_types.pyj +169 -0
- package/test/baselib.pyj +91 -0
- package/test/bytes.pyj +467 -0
- package/test/classes.pyj +1 -0
- package/test/comparison_ops.pyj +173 -0
- package/test/dataclasses.pyj +253 -0
- package/test/enum.pyj +134 -0
- package/test/eval_exec.pyj +56 -0
- package/test/format.pyj +148 -0
- package/test/object.pyj +64 -0
- package/test/python_compat.pyj +17 -15
- package/test/python_features.pyj +89 -21
- package/test/regexp.pyj +29 -1
- package/test/tuples.pyj +96 -0
- package/test/typing.pyj +469 -0
- package/test/unit/index.js +2292 -70
- package/test/unit/language-service.js +674 -4
- package/test/unit/web-repl.js +1106 -0
- package/test/vars_locals_globals.pyj +94 -0
- package/tools/cli.js +11 -0
- package/tools/compile.js +5 -0
- package/tools/embedded_compiler.js +15 -4
- package/tools/lint.js +16 -19
- package/tools/repl.js +1 -1
- package/web-repl/env.js +122 -0
- package/web-repl/main.js +1 -3
- package/web-repl/rapydscript.js +125 -3
- package/PYTHON_DIFFERENCES_REPORT.md +0 -291
- package/PYTHON_FEATURE_COVERAGE.md +0 -200
- package/hack_demo.pyj +0 -112
package/test/unit/index.js
CHANGED
|
@@ -98,6 +98,16 @@ function compile_with_flags(src, flags_obj) {
|
|
|
98
98
|
return output.toString();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// compile_python_mode simulates the default (legacy_rapydscript=false) behavior:
|
|
102
|
+
// all Python compatibility flags are enabled globally.
|
|
103
|
+
var PYTHON_MODE_FLAGS = ['dict_literals', 'overload_getitem', 'bound_methods',
|
|
104
|
+
'hash_literals', 'overload_operators', 'truthiness', 'jsx'];
|
|
105
|
+
function compile_python_mode(src) {
|
|
106
|
+
var flags = {};
|
|
107
|
+
PYTHON_MODE_FLAGS.forEach(function(f) { flags[f] = true; });
|
|
108
|
+
return compile_with_flags(src, flags);
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
function compile_virtual(src, virtual_files) {
|
|
102
112
|
compiler_module.set_virtual_files(virtual_files);
|
|
103
113
|
try {
|
|
@@ -682,6 +692,119 @@ var TESTS = [
|
|
|
682
692
|
].join("\n"),
|
|
683
693
|
},
|
|
684
694
|
|
|
695
|
+
// ── __new__ constructor hook ───────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
{
|
|
698
|
+
name: "new_basic",
|
|
699
|
+
description: "__new__ is called before __init__; returns an instance of the class",
|
|
700
|
+
src: [
|
|
701
|
+
"# globals: assrt",
|
|
702
|
+
"order = []",
|
|
703
|
+
"class Foo:",
|
|
704
|
+
" def __new__(cls):",
|
|
705
|
+
" order.append('new')",
|
|
706
|
+
" return super().__new__(cls)",
|
|
707
|
+
" def __init__(self):",
|
|
708
|
+
" order.append('init')",
|
|
709
|
+
" self.x = 42",
|
|
710
|
+
"f = Foo()",
|
|
711
|
+
"assrt.deepEqual(order, ['new', 'init'])",
|
|
712
|
+
"assrt.equal(f.x, 42)",
|
|
713
|
+
"assrt.ok(isinstance(f, Foo))",
|
|
714
|
+
].join("\n"),
|
|
715
|
+
js_checks: [
|
|
716
|
+
"Foo.__new__(Foo, ...arguments)",
|
|
717
|
+
"ρσ_instance instanceof Foo",
|
|
718
|
+
],
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
{
|
|
722
|
+
name: "new_singleton",
|
|
723
|
+
description: "__new__ can implement the singleton pattern",
|
|
724
|
+
src: [
|
|
725
|
+
"# globals: assrt",
|
|
726
|
+
"class Singleton:",
|
|
727
|
+
" _instance = None",
|
|
728
|
+
" def __new__(cls):",
|
|
729
|
+
" if cls._instance is None:",
|
|
730
|
+
" cls._instance = super().__new__(cls)",
|
|
731
|
+
" return cls._instance",
|
|
732
|
+
" def __init__(self):",
|
|
733
|
+
" pass",
|
|
734
|
+
"a = Singleton()",
|
|
735
|
+
"b = Singleton()",
|
|
736
|
+
"assrt.ok(a is b)",
|
|
737
|
+
"assrt.ok(isinstance(a, Singleton))",
|
|
738
|
+
].join("\n"),
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
{
|
|
742
|
+
name: "new_returns_other_type",
|
|
743
|
+
description: "__new__ returning a non-class instance skips __init__",
|
|
744
|
+
src: [
|
|
745
|
+
"# globals: assrt",
|
|
746
|
+
"init_called = [False]",
|
|
747
|
+
"class MyInt:",
|
|
748
|
+
" def __new__(cls, val):",
|
|
749
|
+
" return val * 2",
|
|
750
|
+
" def __init__(self, val):",
|
|
751
|
+
" init_called[0] = True",
|
|
752
|
+
"result = MyInt(21)",
|
|
753
|
+
"assrt.equal(result, 42)",
|
|
754
|
+
"assrt.equal(init_called[0], False)",
|
|
755
|
+
].join("\n"),
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
{
|
|
759
|
+
name: "new_with_args",
|
|
760
|
+
description: "__new__ receives the same args as __init__",
|
|
761
|
+
src: [
|
|
762
|
+
"# globals: assrt",
|
|
763
|
+
"class Point:",
|
|
764
|
+
" def __new__(cls, x, y):",
|
|
765
|
+
" instance = super().__new__(cls)",
|
|
766
|
+
" instance._raw_x = x",
|
|
767
|
+
" return instance",
|
|
768
|
+
" def __init__(self, x, y):",
|
|
769
|
+
" self.x = x",
|
|
770
|
+
" self.y = y",
|
|
771
|
+
"p = Point(3, 4)",
|
|
772
|
+
"assrt.equal(p.x, 3)",
|
|
773
|
+
"assrt.equal(p.y, 4)",
|
|
774
|
+
"assrt.equal(p._raw_x, 3)",
|
|
775
|
+
"assrt.ok(isinstance(p, Point))",
|
|
776
|
+
].join("\n"),
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
{
|
|
780
|
+
name: "new_subclass_inherits",
|
|
781
|
+
description: "__new__ in parent class with subclass override",
|
|
782
|
+
src: [
|
|
783
|
+
"# globals: assrt",
|
|
784
|
+
"class Base:",
|
|
785
|
+
" def __new__(cls):",
|
|
786
|
+
" instance = super().__new__(cls)",
|
|
787
|
+
" instance.created_by = 'Base.__new__'",
|
|
788
|
+
" return instance",
|
|
789
|
+
" def __init__(self):",
|
|
790
|
+
" pass",
|
|
791
|
+
"class Child(Base):",
|
|
792
|
+
" def __new__(cls):",
|
|
793
|
+
" instance = super().__new__(cls)",
|
|
794
|
+
" instance.child_attr = 'set'",
|
|
795
|
+
" return instance",
|
|
796
|
+
" def __init__(self):",
|
|
797
|
+
" pass",
|
|
798
|
+
"b = Base()",
|
|
799
|
+
"assrt.equal(b.created_by, 'Base.__new__')",
|
|
800
|
+
"c = Child()",
|
|
801
|
+
"assrt.equal(c.created_by, 'Base.__new__')",
|
|
802
|
+
"assrt.equal(c.child_attr, 'set')",
|
|
803
|
+
"assrt.ok(isinstance(c, Child))",
|
|
804
|
+
"assrt.ok(isinstance(c, Base))",
|
|
805
|
+
].join("\n"),
|
|
806
|
+
},
|
|
807
|
+
|
|
685
808
|
// ── Verbatim JS ───────────────────────────────────────────────────────
|
|
686
809
|
|
|
687
810
|
{
|
|
@@ -692,8 +815,8 @@ var TESTS = [
|
|
|
692
815
|
'result = v"typeof undefined"',
|
|
693
816
|
'assrt.equal(result, "undefined")',
|
|
694
817
|
"arr = [1, 2, 3]",
|
|
695
|
-
'
|
|
696
|
-
"assrt.equal(
|
|
818
|
+
'leng = v"arr.length"',
|
|
819
|
+
"assrt.equal(leng, 3)",
|
|
697
820
|
'assrt.equal(v"Math.max(4, 7)", 7)',
|
|
698
821
|
].join("\n"),
|
|
699
822
|
js_checks: ["typeof undefined", "arr.length", "Math.max(4, 7)"],
|
|
@@ -775,6 +898,102 @@ assrt.equal(fib(15), 610)
|
|
|
775
898
|
js_checks: ["Circle.prototype", "function double(x)"],
|
|
776
899
|
},
|
|
777
900
|
|
|
901
|
+
// ── __import__() ─────────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
{
|
|
904
|
+
name: "__import__-basic",
|
|
905
|
+
description: "__import__(name) returns the module object for an already-imported module",
|
|
906
|
+
src: [
|
|
907
|
+
"# globals: assrt",
|
|
908
|
+
"from mymodule import square",
|
|
909
|
+
"m = __import__('mymodule')",
|
|
910
|
+
"assrt.equal(m.square(4), 16)",
|
|
911
|
+
"assrt.equal(m.square(7), 49)",
|
|
912
|
+
].join("\n"),
|
|
913
|
+
virtual_files: {
|
|
914
|
+
mymodule: [
|
|
915
|
+
"def square(n):",
|
|
916
|
+
" return n * n",
|
|
917
|
+
].join("\n"),
|
|
918
|
+
},
|
|
919
|
+
js_checks: ["__import__"],
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
{
|
|
923
|
+
name: "__import__-via-import-stmt",
|
|
924
|
+
description: "__import__(name) also works when module was loaded via 'import x'",
|
|
925
|
+
src: [
|
|
926
|
+
"# globals: assrt",
|
|
927
|
+
"import myutils",
|
|
928
|
+
"m = __import__('myutils')",
|
|
929
|
+
"assrt.equal(m.add(3, 4), 7)",
|
|
930
|
+
].join("\n"),
|
|
931
|
+
virtual_files: {
|
|
932
|
+
myutils: [
|
|
933
|
+
"def add(a, b):",
|
|
934
|
+
" return a + b",
|
|
935
|
+
].join("\n"),
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
|
|
939
|
+
{
|
|
940
|
+
name: "__import__-dotted-no-fromlist",
|
|
941
|
+
description: "__import__('pkg.sub') without fromlist returns the top-level package",
|
|
942
|
+
src: [
|
|
943
|
+
"# globals: assrt",
|
|
944
|
+
"from pkg.utils import helper",
|
|
945
|
+
"top = __import__('pkg.utils')",
|
|
946
|
+
"assrt.equal(top.name, 'pkg')",
|
|
947
|
+
].join("\n"),
|
|
948
|
+
virtual_files: {
|
|
949
|
+
"pkg": "name = 'pkg'", // pkg/__init__.pyj content; key is "pkg"
|
|
950
|
+
"pkg/utils": [
|
|
951
|
+
"def helper():",
|
|
952
|
+
" return 99",
|
|
953
|
+
].join("\n"),
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
{
|
|
958
|
+
name: "__import__-dotted-with-fromlist",
|
|
959
|
+
description: "__import__('pkg.sub', fromlist=['fn']) returns the submodule",
|
|
960
|
+
src: [
|
|
961
|
+
"# globals: assrt",
|
|
962
|
+
"from pkg.utils import helper",
|
|
963
|
+
"sub = __import__('pkg.utils', None, None, ['helper'])",
|
|
964
|
+
"assrt.equal(sub.helper(), 99)",
|
|
965
|
+
].join("\n"),
|
|
966
|
+
virtual_files: {
|
|
967
|
+
"pkg": "", // pkg/__init__.pyj; key is "pkg"
|
|
968
|
+
"pkg/utils": [
|
|
969
|
+
"def helper():",
|
|
970
|
+
" return 99",
|
|
971
|
+
].join("\n"),
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
{
|
|
976
|
+
name: "__import__-error-on-missing",
|
|
977
|
+
description: "__import__ raises ModuleNotFoundError for a module not in ρσ_modules",
|
|
978
|
+
src: [
|
|
979
|
+
"# globals: assrt",
|
|
980
|
+
"from mymod import fn",
|
|
981
|
+
"caught = False",
|
|
982
|
+
"try:",
|
|
983
|
+
" __import__('does_not_exist')",
|
|
984
|
+
"except ModuleNotFoundError as e:",
|
|
985
|
+
" caught = True",
|
|
986
|
+
" assrt.equal(e.message, \"No module named 'does_not_exist'\")",
|
|
987
|
+
"assrt.ok(caught)",
|
|
988
|
+
].join("\n"),
|
|
989
|
+
virtual_files: {
|
|
990
|
+
mymod: [
|
|
991
|
+
"def fn():",
|
|
992
|
+
" return 1",
|
|
993
|
+
].join("\n"),
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
|
|
778
997
|
// ── Walrus operator (:=) ──────────────────────────────────────────────
|
|
779
998
|
|
|
780
999
|
{
|
|
@@ -2979,10 +3198,11 @@ assrt.equal(fib(15), 610)
|
|
|
2979
3198
|
var js_with = ec_with.compile(src, { python_flags: "truthiness" });
|
|
2980
3199
|
assert.ok(/if\s*\(ρσ_bool\(/.test(js_with),
|
|
2981
3200
|
"python_flags='truthiness': expected if(ρσ_bool( in: " + js_with);
|
|
2982
|
-
|
|
2983
|
-
var
|
|
2984
|
-
|
|
2985
|
-
|
|
3201
|
+
// Use legacy_rapydscript: true to get legacy mode (no python flags by default)
|
|
3202
|
+
var ec_legacy = make_ec(RapydScript, baselib, null);
|
|
3203
|
+
var js_legacy = ec_legacy.compile(src, { legacy_rapydscript: true });
|
|
3204
|
+
assert.ok(!/if\s*\(ρσ_bool\(/.test(js_legacy),
|
|
3205
|
+
"legacy mode: if(ρσ_bool( should NOT appear in: " + js_legacy);
|
|
2986
3206
|
},
|
|
2987
3207
|
},
|
|
2988
3208
|
|
|
@@ -3008,80 +3228,2082 @@ assrt.equal(fib(15), 610)
|
|
|
3008
3228
|
js_checks: ["ρσ_op_add", "__getitem__"],
|
|
3009
3229
|
},
|
|
3010
3230
|
|
|
3011
|
-
|
|
3231
|
+
// ── JSX ───────────────────────────────────────────────────────────────
|
|
3012
3232
|
|
|
3013
|
-
|
|
3233
|
+
{
|
|
3234
|
+
name: "jsx_basic_element",
|
|
3235
|
+
description: "JSX: basic element compiles to React.createElement",
|
|
3236
|
+
src: [
|
|
3237
|
+
"from __python__ import jsx",
|
|
3238
|
+
"def render():",
|
|
3239
|
+
" return <div>Hello</div>",
|
|
3240
|
+
].join("\n"),
|
|
3241
|
+
js_checks: ["React.createElement", '"div"', '"Hello"'],
|
|
3242
|
+
skip_run: true,
|
|
3243
|
+
},
|
|
3014
3244
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
:
|
|
3245
|
+
{
|
|
3246
|
+
name: "jsx_self_closing",
|
|
3247
|
+
description: "JSX: self-closing element compiles to React.createElement",
|
|
3248
|
+
src: [
|
|
3249
|
+
"from __python__ import jsx",
|
|
3250
|
+
"def render():",
|
|
3251
|
+
" return <input type='text' />",
|
|
3252
|
+
].join("\n"),
|
|
3253
|
+
js_checks: ["React.createElement", '"input"', "type"],
|
|
3254
|
+
skip_run: true,
|
|
3255
|
+
},
|
|
3019
3256
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3257
|
+
{
|
|
3258
|
+
name: "jsx_string_attribute",
|
|
3259
|
+
description: "JSX: string attributes become object properties",
|
|
3260
|
+
src: [
|
|
3261
|
+
"from __python__ import jsx",
|
|
3262
|
+
"def render():",
|
|
3263
|
+
' return <div className="app" id="root">content</div>',
|
|
3264
|
+
].join("\n"),
|
|
3265
|
+
js_checks: ["React.createElement", "className", '"app"', "id", '"root"'],
|
|
3266
|
+
skip_run: true,
|
|
3267
|
+
},
|
|
3024
3268
|
|
|
3025
|
-
|
|
3269
|
+
{
|
|
3270
|
+
name: "jsx_expression_attribute",
|
|
3271
|
+
description: "JSX: expression attributes compile Python to JS",
|
|
3272
|
+
src: [
|
|
3273
|
+
"from __python__ import jsx",
|
|
3274
|
+
"def render(isActive):",
|
|
3275
|
+
" return <div disabled={not isActive}>content</div>",
|
|
3276
|
+
].join("\n"),
|
|
3277
|
+
js_checks: ["React.createElement", "disabled", "isActive"],
|
|
3278
|
+
skip_run: true,
|
|
3279
|
+
},
|
|
3026
3280
|
|
|
3027
|
-
|
|
3281
|
+
{
|
|
3282
|
+
name: "jsx_boolean_attribute",
|
|
3283
|
+
description: "JSX: boolean attributes (no value) compile to true",
|
|
3284
|
+
src: [
|
|
3285
|
+
"from __python__ import jsx",
|
|
3286
|
+
"def render():",
|
|
3287
|
+
" return <input disabled />",
|
|
3288
|
+
].join("\n"),
|
|
3289
|
+
js_checks: ["React.createElement", "disabled", "true"],
|
|
3290
|
+
skip_run: true,
|
|
3291
|
+
},
|
|
3028
3292
|
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
console.log(colored("PASS " + test.name, "green") +
|
|
3041
|
-
" – " + test.description);
|
|
3042
|
-
return;
|
|
3043
|
-
}
|
|
3293
|
+
{
|
|
3294
|
+
name: "jsx_hyphenated_attribute",
|
|
3295
|
+
description: "JSX: hyphenated attribute names are quoted as object keys",
|
|
3296
|
+
src: [
|
|
3297
|
+
"from __python__ import jsx",
|
|
3298
|
+
"def render():",
|
|
3299
|
+
' return <button aria-label="Close">X</button>',
|
|
3300
|
+
].join("\n"),
|
|
3301
|
+
js_checks: ["React.createElement", '"aria-label"', '"Close"'],
|
|
3302
|
+
skip_run: true,
|
|
3303
|
+
},
|
|
3044
3304
|
|
|
3045
|
-
|
|
3305
|
+
{
|
|
3306
|
+
name: "jsx_expression_child",
|
|
3307
|
+
description: "JSX: expression children compile Python expressions to JS",
|
|
3308
|
+
src: [
|
|
3309
|
+
"from __python__ import jsx",
|
|
3310
|
+
"def render(count):",
|
|
3311
|
+
" return <h1>Count: {count * 2}</h1>",
|
|
3312
|
+
].join("\n"),
|
|
3313
|
+
js_checks: ["React.createElement", "count * 2"],
|
|
3314
|
+
skip_run: true,
|
|
3315
|
+
},
|
|
3046
3316
|
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3317
|
+
{
|
|
3318
|
+
name: "jsx_nested_elements",
|
|
3319
|
+
description: "JSX: nested elements produce nested React.createElement calls",
|
|
3320
|
+
src: [
|
|
3321
|
+
"from __python__ import jsx",
|
|
3322
|
+
"def render():",
|
|
3323
|
+
" return <div><span>inner</span></div>",
|
|
3324
|
+
].join("\n"),
|
|
3325
|
+
js_checks: ["React.createElement", '"div"', '"span"', '"inner"'],
|
|
3326
|
+
skip_run: true,
|
|
3327
|
+
},
|
|
3056
3328
|
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
console.log(colored("FAIL " + test.name, "red") +
|
|
3072
|
-
" [JS pattern mismatch]\n " + e.message + "\n");
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
3329
|
+
{
|
|
3330
|
+
name: "jsx_fragment",
|
|
3331
|
+
description: "JSX: fragments (<>...</>) compile to React.Fragment",
|
|
3332
|
+
src: [
|
|
3333
|
+
"from __python__ import jsx",
|
|
3334
|
+
"def render():",
|
|
3335
|
+
" return <>",
|
|
3336
|
+
" <span>First</span>",
|
|
3337
|
+
" <span>Second</span>",
|
|
3338
|
+
" </>",
|
|
3339
|
+
].join("\n"),
|
|
3340
|
+
js_checks: ["React.createElement", "React.Fragment", '"First"', '"Second"'],
|
|
3341
|
+
skip_run: true,
|
|
3342
|
+
},
|
|
3075
3343
|
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3344
|
+
{
|
|
3345
|
+
name: "jsx_component",
|
|
3346
|
+
description: "JSX: component tags (uppercase) are passed as references",
|
|
3347
|
+
src: [
|
|
3348
|
+
"from __python__ import jsx",
|
|
3349
|
+
"def render():",
|
|
3350
|
+
" return <MyComponent name='test' />",
|
|
3351
|
+
].join("\n"),
|
|
3352
|
+
js_checks: ["React.createElement", "MyComponent", "name"],
|
|
3353
|
+
skip_run: true,
|
|
3354
|
+
},
|
|
3355
|
+
|
|
3356
|
+
{
|
|
3357
|
+
name: "jsx_dot_component",
|
|
3358
|
+
description: "JSX: dot-notation component tags compile as member expressions",
|
|
3359
|
+
src: [
|
|
3360
|
+
"from __python__ import jsx",
|
|
3361
|
+
"def render():",
|
|
3362
|
+
" return <Router.Route path='/home' />",
|
|
3363
|
+
].join("\n"),
|
|
3364
|
+
js_checks: ["React.createElement", "Router.Route"],
|
|
3365
|
+
skip_run: true,
|
|
3366
|
+
},
|
|
3367
|
+
|
|
3368
|
+
{
|
|
3369
|
+
name: "jsx_spread_attr",
|
|
3370
|
+
description: "JSX: spread attributes {...props} compile to object spread",
|
|
3371
|
+
src: [
|
|
3372
|
+
"from __python__ import jsx",
|
|
3373
|
+
"def render(props):",
|
|
3374
|
+
" return <div {...props}>content</div>",
|
|
3375
|
+
].join("\n"),
|
|
3376
|
+
js_checks: ["React.createElement", "...props"],
|
|
3377
|
+
skip_run: true,
|
|
3378
|
+
},
|
|
3379
|
+
|
|
3380
|
+
{
|
|
3381
|
+
name: "jsx_multiline",
|
|
3382
|
+
description: "JSX: multi-line JSX compiles to nested React.createElement calls",
|
|
3383
|
+
src: [
|
|
3384
|
+
"from __python__ import jsx",
|
|
3385
|
+
"def render(title, body):",
|
|
3386
|
+
" return (",
|
|
3387
|
+
" <article>",
|
|
3388
|
+
" <h2>{title}</h2>",
|
|
3389
|
+
" <p>{body}</p>",
|
|
3390
|
+
" </article>",
|
|
3391
|
+
" )",
|
|
3392
|
+
].join("\n"),
|
|
3393
|
+
js_checks: ["React.createElement", '"article"', '"h2"', '"p"', "title", "body"],
|
|
3394
|
+
skip_run: true,
|
|
3395
|
+
},
|
|
3396
|
+
|
|
3397
|
+
{
|
|
3398
|
+
name: "jsx_no_jsx_without_flag",
|
|
3399
|
+
description: "JSX: < in expression is a comparison without the jsx flag",
|
|
3400
|
+
src: [
|
|
3401
|
+
"# globals: assrt",
|
|
3402
|
+
"x = 5",
|
|
3403
|
+
"assrt.equal(x < 10, True)",
|
|
3404
|
+
].join("\n"),
|
|
3405
|
+
js_checks: ["x < 10"],
|
|
3406
|
+
},
|
|
3407
|
+
|
|
3408
|
+
// ── JSX whitespace and HTML entity handling ──────────────────────────────
|
|
3409
|
+
|
|
3410
|
+
{
|
|
3411
|
+
name: "jsx_nbsp_entity",
|
|
3412
|
+
description: "JSX: is decoded to a non-breaking space (U+00A0) in the JS string",
|
|
3413
|
+
src: [
|
|
3414
|
+
"from __python__ import jsx",
|
|
3415
|
+
"def render():",
|
|
3416
|
+
" return <p>Hello World</p>",
|
|
3417
|
+
].join("\n"),
|
|
3418
|
+
js_checks: [/Hello[\u00a0]World/],
|
|
3419
|
+
skip_run: true,
|
|
3420
|
+
},
|
|
3421
|
+
|
|
3422
|
+
{
|
|
3423
|
+
name: "jsx_amp_entity",
|
|
3424
|
+
description: "JSX: & is decoded to & in the JS string",
|
|
3425
|
+
src: [
|
|
3426
|
+
"from __python__ import jsx",
|
|
3427
|
+
"def render():",
|
|
3428
|
+
" return <p>a & b</p>",
|
|
3429
|
+
].join("\n"),
|
|
3430
|
+
js_checks: [/"a & b"/],
|
|
3431
|
+
skip_run: true,
|
|
3432
|
+
},
|
|
3433
|
+
|
|
3434
|
+
{
|
|
3435
|
+
name: "jsx_lt_gt_entities",
|
|
3436
|
+
description: "JSX: < and > are decoded to < and > in the JS string",
|
|
3437
|
+
src: [
|
|
3438
|
+
"from __python__ import jsx",
|
|
3439
|
+
"def render():",
|
|
3440
|
+
" return <p>1 < 2 > 0</p>",
|
|
3441
|
+
].join("\n"),
|
|
3442
|
+
js_checks: [/"1 < 2 > 0"/],
|
|
3443
|
+
skip_run: true,
|
|
3444
|
+
},
|
|
3445
|
+
|
|
3446
|
+
{
|
|
3447
|
+
name: "jsx_quot_entity",
|
|
3448
|
+
description: "JSX: " is decoded to \" in the JS string",
|
|
3449
|
+
src: [
|
|
3450
|
+
"from __python__ import jsx",
|
|
3451
|
+
"def render():",
|
|
3452
|
+
" return <p>"quoted"</p>",
|
|
3453
|
+
].join("\n"),
|
|
3454
|
+
js_checks: [/null, "\\\"quoted\\\""\)/],
|
|
3455
|
+
skip_run: true,
|
|
3456
|
+
},
|
|
3457
|
+
|
|
3458
|
+
{
|
|
3459
|
+
name: "jsx_numeric_entity_decimal",
|
|
3460
|
+
description: "JSX: decimal numeric entity   is decoded to U+00A0",
|
|
3461
|
+
src: [
|
|
3462
|
+
"from __python__ import jsx",
|
|
3463
|
+
"def render():",
|
|
3464
|
+
" return <p>a b</p>",
|
|
3465
|
+
].join("\n"),
|
|
3466
|
+
js_checks: [/a[\u00a0]b/],
|
|
3467
|
+
skip_run: true,
|
|
3468
|
+
},
|
|
3469
|
+
|
|
3470
|
+
{
|
|
3471
|
+
name: "jsx_numeric_entity_hex",
|
|
3472
|
+
description: "JSX: hex numeric entity   is decoded to U+00A0",
|
|
3473
|
+
src: [
|
|
3474
|
+
"from __python__ import jsx",
|
|
3475
|
+
"def render():",
|
|
3476
|
+
" return <p>a b</p>",
|
|
3477
|
+
].join("\n"),
|
|
3478
|
+
js_checks: [/a[\u00a0]b/],
|
|
3479
|
+
skip_run: true,
|
|
3480
|
+
},
|
|
3481
|
+
|
|
3482
|
+
{
|
|
3483
|
+
name: "jsx_no_double_decode",
|
|
3484
|
+
description: "JSX: &lt; decodes to < (not <), entities decoded in one pass",
|
|
3485
|
+
src: [
|
|
3486
|
+
"from __python__ import jsx",
|
|
3487
|
+
"def render():",
|
|
3488
|
+
" return <p>&lt;</p>",
|
|
3489
|
+
].join("\n"),
|
|
3490
|
+
js_checks: [/"<"/],
|
|
3491
|
+
skip_run: true,
|
|
3492
|
+
},
|
|
3493
|
+
|
|
3494
|
+
{
|
|
3495
|
+
name: "jsx_inline_spaces_preserved",
|
|
3496
|
+
description: "JSX: spaces within inline text are preserved",
|
|
3497
|
+
src: [
|
|
3498
|
+
"from __python__ import jsx",
|
|
3499
|
+
"def render():",
|
|
3500
|
+
" return <p>Hello World</p>",
|
|
3501
|
+
].join("\n"),
|
|
3502
|
+
js_checks: [/"Hello World"/],
|
|
3503
|
+
skip_run: true,
|
|
3504
|
+
},
|
|
3505
|
+
|
|
3506
|
+
{
|
|
3507
|
+
name: "jsx_multiline_whitespace_collapsed",
|
|
3508
|
+
description: "JSX: whitespace-only lines between tags are dropped; text lines are preserved",
|
|
3509
|
+
src: [
|
|
3510
|
+
"from __python__ import jsx",
|
|
3511
|
+
"def render():",
|
|
3512
|
+
" return (",
|
|
3513
|
+
" <p>",
|
|
3514
|
+
" Hello World",
|
|
3515
|
+
" </p>",
|
|
3516
|
+
" )",
|
|
3517
|
+
].join("\n"),
|
|
3518
|
+
js_checks: [/"Hello World"/],
|
|
3519
|
+
skip_run: true,
|
|
3520
|
+
},
|
|
3521
|
+
|
|
3522
|
+
{
|
|
3523
|
+
name: "jsx_single_space_same_line",
|
|
3524
|
+
description: "JSX: a single space on its own between same-line tags is preserved",
|
|
3525
|
+
src: [
|
|
3526
|
+
"from __python__ import jsx",
|
|
3527
|
+
"def render():",
|
|
3528
|
+
" return <span>a </span>",
|
|
3529
|
+
].join("\n"),
|
|
3530
|
+
js_checks: [/"a "/],
|
|
3531
|
+
skip_run: true,
|
|
3532
|
+
},
|
|
3533
|
+
|
|
3534
|
+
// ── React standard library ───────────────────────────────────────────────
|
|
3535
|
+
|
|
3536
|
+
{
|
|
3537
|
+
name: "react_import_usestate",
|
|
3538
|
+
description: "react lib: from react import useState compiles to React.useState reference",
|
|
3539
|
+
src: [
|
|
3540
|
+
"from react import useState",
|
|
3541
|
+
"def Counter():",
|
|
3542
|
+
" count, setCount = useState(0)",
|
|
3543
|
+
" return count",
|
|
3544
|
+
].join("\n"),
|
|
3545
|
+
js_checks: ["React.useState", "useState"],
|
|
3546
|
+
skip_run: true,
|
|
3547
|
+
},
|
|
3548
|
+
|
|
3549
|
+
{
|
|
3550
|
+
name: "react_import_multiple_hooks",
|
|
3551
|
+
description: "react lib: multiple hook imports each resolve to React.*",
|
|
3552
|
+
src: [
|
|
3553
|
+
"from react import useState, useEffect, useMemo, useRef, useCallback",
|
|
3554
|
+
"def Component(items):",
|
|
3555
|
+
" count, setCount = useState(0)",
|
|
3556
|
+
" ref = useRef(None)",
|
|
3557
|
+
" result = useMemo(def(): return items.length;, [items])",
|
|
3558
|
+
" def cb():",
|
|
3559
|
+
" setCount(count + 1)",
|
|
3560
|
+
" fn = useCallback(cb, [count])",
|
|
3561
|
+
" useEffect(def(): pass;, [])",
|
|
3562
|
+
" return count",
|
|
3563
|
+
].join("\n"),
|
|
3564
|
+
js_checks: [
|
|
3565
|
+
"React.useState", "React.useEffect", "React.useMemo",
|
|
3566
|
+
"React.useRef", "React.useCallback",
|
|
3567
|
+
],
|
|
3568
|
+
skip_run: true,
|
|
3569
|
+
},
|
|
3570
|
+
|
|
3571
|
+
{
|
|
3572
|
+
name: "react_jsx_functional_component",
|
|
3573
|
+
description: "react lib: functional component with useState and JSX",
|
|
3574
|
+
src: [
|
|
3575
|
+
"from __python__ import jsx",
|
|
3576
|
+
"from react import useState",
|
|
3577
|
+
"def Counter():",
|
|
3578
|
+
" count, setCount = useState(0)",
|
|
3579
|
+
" def increment():",
|
|
3580
|
+
" setCount(count + 1)",
|
|
3581
|
+
" return <button onClick={increment}>{count}</button>",
|
|
3582
|
+
].join("\n"),
|
|
3583
|
+
js_checks: [
|
|
3584
|
+
"React.useState", "React.createElement",
|
|
3585
|
+
'"button"', "increment", "count",
|
|
3586
|
+
],
|
|
3587
|
+
skip_run: true,
|
|
3588
|
+
},
|
|
3589
|
+
|
|
3590
|
+
{
|
|
3591
|
+
name: "react_use_effect",
|
|
3592
|
+
description: "react lib: useEffect with deps array compiles correctly",
|
|
3593
|
+
src: [
|
|
3594
|
+
"from react import useState, useEffect",
|
|
3595
|
+
"def Timer():",
|
|
3596
|
+
" count, setCount = useState(0)",
|
|
3597
|
+
" def tick():",
|
|
3598
|
+
" setCount(count + 1)",
|
|
3599
|
+
" useEffect(tick, [count])",
|
|
3600
|
+
" return count",
|
|
3601
|
+
].join("\n"),
|
|
3602
|
+
js_checks: ["React.useState", "React.useEffect", "tick", "count"],
|
|
3603
|
+
skip_run: true,
|
|
3604
|
+
},
|
|
3605
|
+
|
|
3606
|
+
{
|
|
3607
|
+
name: "react_use_context",
|
|
3608
|
+
description: "react lib: createContext and useContext compile correctly",
|
|
3609
|
+
src: [
|
|
3610
|
+
"from react import createContext, useContext",
|
|
3611
|
+
"ThemeContext = createContext('light')",
|
|
3612
|
+
"def ThemedButton():",
|
|
3613
|
+
" theme = useContext(ThemeContext)",
|
|
3614
|
+
" return theme",
|
|
3615
|
+
].join("\n"),
|
|
3616
|
+
js_checks: [
|
|
3617
|
+
"React.createContext", "React.useContext",
|
|
3618
|
+
"ThemeContext", "light",
|
|
3619
|
+
],
|
|
3620
|
+
skip_run: true,
|
|
3621
|
+
},
|
|
3622
|
+
|
|
3623
|
+
{
|
|
3624
|
+
name: "react_use_reducer",
|
|
3625
|
+
description: "react lib: useReducer with action dispatch compiles correctly",
|
|
3626
|
+
src: [
|
|
3627
|
+
"from react import useReducer",
|
|
3628
|
+
"def reducer(state, action):",
|
|
3629
|
+
" if action.type == 'increment':",
|
|
3630
|
+
" return state + 1",
|
|
3631
|
+
" return state",
|
|
3632
|
+
"def Counter():",
|
|
3633
|
+
" state, dispatch = useReducer(reducer, 0)",
|
|
3634
|
+
" return state",
|
|
3635
|
+
].join("\n"),
|
|
3636
|
+
js_checks: ["React.useReducer", "reducer", "dispatch"],
|
|
3637
|
+
skip_run: true,
|
|
3638
|
+
},
|
|
3639
|
+
|
|
3640
|
+
{
|
|
3641
|
+
name: "react_use_ref",
|
|
3642
|
+
description: "react lib: useRef for DOM reference compiles correctly",
|
|
3643
|
+
src: [
|
|
3644
|
+
"from __python__ import jsx",
|
|
3645
|
+
"from react import useRef",
|
|
3646
|
+
"def FocusInput():",
|
|
3647
|
+
" inputRef = useRef(None)",
|
|
3648
|
+
" def handleClick():",
|
|
3649
|
+
" inputRef.current.focus()",
|
|
3650
|
+
" return <input ref={inputRef} />",
|
|
3651
|
+
].join("\n"),
|
|
3652
|
+
js_checks: [
|
|
3653
|
+
"React.useRef", "inputRef", "current",
|
|
3654
|
+
"React.createElement", '"input"',
|
|
3655
|
+
],
|
|
3656
|
+
skip_run: true,
|
|
3657
|
+
},
|
|
3658
|
+
|
|
3659
|
+
{
|
|
3660
|
+
name: "react_memo_wrapper",
|
|
3661
|
+
description: "react lib: memo() wraps a component to prevent re-renders",
|
|
3662
|
+
src: [
|
|
3663
|
+
"from __python__ import jsx",
|
|
3664
|
+
"from react import memo",
|
|
3665
|
+
"def Row(props):",
|
|
3666
|
+
" return <div>{props.label}</div>",
|
|
3667
|
+
"MemoRow = memo(Row)",
|
|
3668
|
+
].join("\n"),
|
|
3669
|
+
js_checks: ["React.memo", "Row", "MemoRow"],
|
|
3670
|
+
skip_run: true,
|
|
3671
|
+
},
|
|
3672
|
+
|
|
3673
|
+
{
|
|
3674
|
+
name: "react_create_context",
|
|
3675
|
+
description: "react lib: createContext creates a context object",
|
|
3676
|
+
src: [
|
|
3677
|
+
"from react import createContext",
|
|
3678
|
+
"UserContext = createContext(None)",
|
|
3679
|
+
].join("\n"),
|
|
3680
|
+
js_checks: ["React.createContext", "UserContext"],
|
|
3681
|
+
skip_run: true,
|
|
3682
|
+
},
|
|
3683
|
+
|
|
3684
|
+
{
|
|
3685
|
+
name: "react_forward_ref",
|
|
3686
|
+
description: "react lib: forwardRef passes ref to child component",
|
|
3687
|
+
src: [
|
|
3688
|
+
"from __python__ import jsx",
|
|
3689
|
+
"from react import forwardRef",
|
|
3690
|
+
"def FancyInput(props, ref):",
|
|
3691
|
+
" return <input ref={ref} />",
|
|
3692
|
+
"FancyInputWithRef = forwardRef(FancyInput)",
|
|
3693
|
+
].join("\n"),
|
|
3694
|
+
js_checks: ["React.forwardRef", "FancyInput", "FancyInputWithRef"],
|
|
3695
|
+
skip_run: true,
|
|
3696
|
+
},
|
|
3697
|
+
|
|
3698
|
+
{
|
|
3699
|
+
name: "react_fragment_import",
|
|
3700
|
+
description: "react lib: Fragment import can be used as a component tag",
|
|
3701
|
+
src: [
|
|
3702
|
+
"from __python__ import jsx",
|
|
3703
|
+
"from react import Fragment",
|
|
3704
|
+
"def TwoItems():",
|
|
3705
|
+
" return <Fragment><span>A</span><span>B</span></Fragment>",
|
|
3706
|
+
].join("\n"),
|
|
3707
|
+
js_checks: ["React.Fragment", "React.createElement", '"span"'],
|
|
3708
|
+
skip_run: true,
|
|
3709
|
+
},
|
|
3710
|
+
|
|
3711
|
+
{
|
|
3712
|
+
name: "react_use_id",
|
|
3713
|
+
description: "react lib: useId (React 18) compiles correctly",
|
|
3714
|
+
src: [
|
|
3715
|
+
"from react import useId",
|
|
3716
|
+
"def LabeledInput():",
|
|
3717
|
+
" id = useId()",
|
|
3718
|
+
" return id",
|
|
3719
|
+
].join("\n"),
|
|
3720
|
+
js_checks: ["React.useId", "useId"],
|
|
3721
|
+
skip_run: true,
|
|
3722
|
+
},
|
|
3723
|
+
|
|
3724
|
+
{
|
|
3725
|
+
name: "react_use_transition",
|
|
3726
|
+
description: "react lib: useTransition (React 18) compiles correctly",
|
|
3727
|
+
src: [
|
|
3728
|
+
"from react import useState, useTransition",
|
|
3729
|
+
"def SearchInput():",
|
|
3730
|
+
" isPending, startTransition = useTransition()",
|
|
3731
|
+
" query, setQuery = useState('')",
|
|
3732
|
+
" def handleChange(e):",
|
|
3733
|
+
" startTransition(def(): setQuery(e.target.value);)",
|
|
3734
|
+
" return isPending",
|
|
3735
|
+
].join("\n"),
|
|
3736
|
+
js_checks: ["React.useTransition", "React.useState", "startTransition"],
|
|
3737
|
+
skip_run: true,
|
|
3738
|
+
},
|
|
3739
|
+
|
|
3740
|
+
{
|
|
3741
|
+
name: "react_class_component",
|
|
3742
|
+
description: "react lib: class component importing Component base class",
|
|
3743
|
+
src: [
|
|
3744
|
+
"from __python__ import jsx",
|
|
3745
|
+
"from react import Component",
|
|
3746
|
+
"class Greeter(Component):",
|
|
3747
|
+
" def render(self):",
|
|
3748
|
+
" return <h1>Hello, {self.props.name}</h1>",
|
|
3749
|
+
].join("\n"),
|
|
3750
|
+
js_checks: [
|
|
3751
|
+
"React.Component", "Greeter", "render",
|
|
3752
|
+
"React.createElement", '"h1"',
|
|
3753
|
+
],
|
|
3754
|
+
skip_run: true,
|
|
3755
|
+
},
|
|
3756
|
+
|
|
3757
|
+
{
|
|
3758
|
+
name: "react_jsx_list_rendering",
|
|
3759
|
+
description: "react lib: list comprehension renders JSX list of elements",
|
|
3760
|
+
src: [
|
|
3761
|
+
"from __python__ import jsx",
|
|
3762
|
+
"from react import useState",
|
|
3763
|
+
"def TodoList():",
|
|
3764
|
+
" items, setItems = useState(['a', 'b', 'c'])",
|
|
3765
|
+
" return (",
|
|
3766
|
+
" <ul>",
|
|
3767
|
+
" {[<li key={i}>{item}</li> for i, item in enumerate(items)]}",
|
|
3768
|
+
" </ul>",
|
|
3769
|
+
" )",
|
|
3770
|
+
].join("\n"),
|
|
3771
|
+
js_checks: [
|
|
3772
|
+
"React.useState", "React.createElement",
|
|
3773
|
+
'"ul"', '"li"', "enumerate",
|
|
3774
|
+
],
|
|
3775
|
+
skip_run: true,
|
|
3776
|
+
},
|
|
3777
|
+
|
|
3778
|
+
// Binding-pattern tests: verify that imported names are wired to React.* in
|
|
3779
|
+
// the compiled output via both the module export and the var binding.
|
|
3780
|
+
|
|
3781
|
+
{
|
|
3782
|
+
name: "react_binding_memo",
|
|
3783
|
+
description: "react lib: memo is exported from module as React.memo and bound via var",
|
|
3784
|
+
src: [
|
|
3785
|
+
"from react import memo",
|
|
3786
|
+
"def A(): return 1",
|
|
3787
|
+
"B = memo(A)",
|
|
3788
|
+
].join("\n"),
|
|
3789
|
+
// module init: ρσ_modules.react.memo = <something>; and React.memo assignment
|
|
3790
|
+
// import binding: var memo = ρσ_modules.react.memo
|
|
3791
|
+
js_checks: [
|
|
3792
|
+
"React.memo",
|
|
3793
|
+
"ρσ_modules.react.memo",
|
|
3794
|
+
"var memo",
|
|
3795
|
+
"memo(A)",
|
|
3796
|
+
],
|
|
3797
|
+
skip_run: true,
|
|
3798
|
+
},
|
|
3799
|
+
|
|
3800
|
+
{
|
|
3801
|
+
name: "react_binding_usestate",
|
|
3802
|
+
description: "react lib: useState is exported from module as React.useState and bound via var",
|
|
3803
|
+
src: [
|
|
3804
|
+
"from react import useState",
|
|
3805
|
+
"def C():",
|
|
3806
|
+
" n, setN = useState(0)",
|
|
3807
|
+
" return n",
|
|
3808
|
+
].join("\n"),
|
|
3809
|
+
js_checks: [
|
|
3810
|
+
"React.useState",
|
|
3811
|
+
"ρσ_modules.react.useState",
|
|
3812
|
+
"var useState",
|
|
3813
|
+
"useState(0)",
|
|
3814
|
+
],
|
|
3815
|
+
skip_run: true,
|
|
3816
|
+
},
|
|
3817
|
+
|
|
3818
|
+
{
|
|
3819
|
+
name: "react_binding_useeffect",
|
|
3820
|
+
description: "react lib: useEffect is exported from module as React.useEffect and bound via var",
|
|
3821
|
+
src: [
|
|
3822
|
+
"from react import useEffect",
|
|
3823
|
+
"def D():",
|
|
3824
|
+
" def run(): pass",
|
|
3825
|
+
" useEffect(run, [])",
|
|
3826
|
+
].join("\n"),
|
|
3827
|
+
js_checks: [
|
|
3828
|
+
"React.useEffect",
|
|
3829
|
+
"ρσ_modules.react.useEffect",
|
|
3830
|
+
"var useEffect",
|
|
3831
|
+
"useEffect(run",
|
|
3832
|
+
],
|
|
3833
|
+
skip_run: true,
|
|
3834
|
+
},
|
|
3835
|
+
|
|
3836
|
+
{
|
|
3837
|
+
name: "react_binding_forwardref",
|
|
3838
|
+
description: "react lib: forwardRef is exported from module as React.forwardRef and bound via var",
|
|
3839
|
+
src: [
|
|
3840
|
+
"from __python__ import jsx",
|
|
3841
|
+
"from react import forwardRef",
|
|
3842
|
+
"FancyInput = forwardRef(def(props, ref): return <input ref={ref}/>;)",
|
|
3843
|
+
].join("\n"),
|
|
3844
|
+
js_checks: [
|
|
3845
|
+
"React.forwardRef",
|
|
3846
|
+
"ρσ_modules.react.forwardRef",
|
|
3847
|
+
"var forwardRef",
|
|
3848
|
+
"forwardRef(",
|
|
3849
|
+
],
|
|
3850
|
+
skip_run: true,
|
|
3851
|
+
},
|
|
3852
|
+
|
|
3853
|
+
{
|
|
3854
|
+
name: "react_binding_all_hooks",
|
|
3855
|
+
description: "react lib: every imported hook produces a var binding to ρσ_modules.react.*",
|
|
3856
|
+
src: [
|
|
3857
|
+
"from react import useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useLayoutEffect, useId",
|
|
3858
|
+
].join("\n"),
|
|
3859
|
+
js_checks: [
|
|
3860
|
+
"React.useState", "React.useEffect", "React.useContext",
|
|
3861
|
+
"React.useReducer", "React.useCallback", "React.useMemo",
|
|
3862
|
+
"React.useRef", "React.useLayoutEffect", "React.useId",
|
|
3863
|
+
"var useState", "var useEffect", "var useContext",
|
|
3864
|
+
"var useReducer", "var useCallback", "var useMemo",
|
|
3865
|
+
"var useRef", "var useLayoutEffect", "var useId",
|
|
3866
|
+
],
|
|
3867
|
+
skip_run: true,
|
|
3868
|
+
},
|
|
3869
|
+
|
|
3870
|
+
{
|
|
3871
|
+
name: "react_counter_example",
|
|
3872
|
+
description: "react lib: full counter component from TODO example compiles correctly",
|
|
3873
|
+
src: [
|
|
3874
|
+
"from __python__ import jsx",
|
|
3875
|
+
"from react import useState, useEffect, memo",
|
|
3876
|
+
"def Counter(props):",
|
|
3877
|
+
" count, setCount = useState(props.initial or 0)",
|
|
3878
|
+
" def increment():",
|
|
3879
|
+
" setCount(count + 1)",
|
|
3880
|
+
" def decrement():",
|
|
3881
|
+
" setCount(count - 1)",
|
|
3882
|
+
" def log_change():",
|
|
3883
|
+
" pass",
|
|
3884
|
+
" useEffect(log_change, [count])",
|
|
3885
|
+
" return (",
|
|
3886
|
+
" <div className='counter'>",
|
|
3887
|
+
" <h2>{props.title}</h2>",
|
|
3888
|
+
" <button onClick={decrement}>-</button>",
|
|
3889
|
+
" <span>{count}</span>",
|
|
3890
|
+
" <button onClick={increment}>+</button>",
|
|
3891
|
+
" </div>",
|
|
3892
|
+
" )",
|
|
3893
|
+
"Counter = memo(Counter)",
|
|
3894
|
+
].join("\n"),
|
|
3895
|
+
js_checks: [
|
|
3896
|
+
// hooks are wired correctly
|
|
3897
|
+
"React.useState", "React.useEffect", "React.memo",
|
|
3898
|
+
"var useState", "var useEffect", "var memo",
|
|
3899
|
+
// JSX output
|
|
3900
|
+
"React.createElement", '"div"', '"button"', '"span"', '"h2"',
|
|
3901
|
+
// logic
|
|
3902
|
+
"increment", "decrement", "count",
|
|
3903
|
+
],
|
|
3904
|
+
skip_run: true,
|
|
3905
|
+
},
|
|
3906
|
+
|
|
3907
|
+
{
|
|
3908
|
+
name: "react_lazy_suspense",
|
|
3909
|
+
description: "react lib: lazy and Suspense compile correctly",
|
|
3910
|
+
src: [
|
|
3911
|
+
"from __python__ import jsx",
|
|
3912
|
+
"from react import lazy, Suspense",
|
|
3913
|
+
"def Fallback():",
|
|
3914
|
+
" return <div>Loading...</div>",
|
|
3915
|
+
"def App():",
|
|
3916
|
+
" return <Suspense fallback={<Fallback/>}></Suspense>",
|
|
3917
|
+
].join("\n"),
|
|
3918
|
+
js_checks: [
|
|
3919
|
+
"React.lazy", "React.Suspense",
|
|
3920
|
+
"var lazy", "var Suspense",
|
|
3921
|
+
"React.createElement",
|
|
3922
|
+
],
|
|
3923
|
+
skip_run: true,
|
|
3924
|
+
},
|
|
3925
|
+
|
|
3926
|
+
{
|
|
3927
|
+
name: "react_use_callback_deps",
|
|
3928
|
+
description: "react lib: useCallback dependency array passes through correctly",
|
|
3929
|
+
src: [
|
|
3930
|
+
"from react import useState, useCallback",
|
|
3931
|
+
"def Form():",
|
|
3932
|
+
" value, setValue = useState('')",
|
|
3933
|
+
" def onChange(e):",
|
|
3934
|
+
" setValue(e.target.value)",
|
|
3935
|
+
" handler = useCallback(onChange, [value])",
|
|
3936
|
+
" return handler",
|
|
3937
|
+
].join("\n"),
|
|
3938
|
+
js_checks: [
|
|
3939
|
+
"React.useCallback", "React.useState",
|
|
3940
|
+
"var useCallback", "var useState",
|
|
3941
|
+
"useCallback(onChange",
|
|
3942
|
+
],
|
|
3943
|
+
skip_run: true,
|
|
3944
|
+
},
|
|
3945
|
+
|
|
3946
|
+
{
|
|
3947
|
+
name: "react_use_memo_deps",
|
|
3948
|
+
description: "react lib: useMemo dependency array passes through correctly",
|
|
3949
|
+
src: [
|
|
3950
|
+
"from react import useState, useMemo",
|
|
3951
|
+
"def Expensive(items):",
|
|
3952
|
+
" count, setCount = useState(0)",
|
|
3953
|
+
" def compute(): return items.length * count",
|
|
3954
|
+
" result = useMemo(compute, [items, count])",
|
|
3955
|
+
" return result",
|
|
3956
|
+
].join("\n"),
|
|
3957
|
+
js_checks: [
|
|
3958
|
+
"React.useMemo", "React.useState",
|
|
3959
|
+
"var useMemo", "var useState",
|
|
3960
|
+
"useMemo(compute",
|
|
3961
|
+
],
|
|
3962
|
+
skip_run: true,
|
|
3963
|
+
},
|
|
3964
|
+
|
|
3965
|
+
// ── JSON support ─────────────────────────────────────────────────────────
|
|
3966
|
+
|
|
3967
|
+
{
|
|
3968
|
+
name: "json_stringify_dict",
|
|
3969
|
+
description: "JSON.stringify on a Python dict produces valid JSON",
|
|
3970
|
+
src: [
|
|
3971
|
+
"# globals: assrt",
|
|
3972
|
+
"from __python__ import dict_literals",
|
|
3973
|
+
"d = {'key': 'value', 'num': 42}",
|
|
3974
|
+
"s = JSON.stringify(d)",
|
|
3975
|
+
"assrt.equal(jstype(s), 'string')",
|
|
3976
|
+
"parsed = JSON.parse(s)",
|
|
3977
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
3978
|
+
"assrt.equal(parsed.get('key'), 'value')",
|
|
3979
|
+
"assrt.equal(parsed.get('num'), 42)",
|
|
3980
|
+
].join("\n"),
|
|
3981
|
+
},
|
|
3982
|
+
|
|
3983
|
+
{
|
|
3984
|
+
name: "json_parse_returns_dict",
|
|
3985
|
+
description: "JSON.parse returns ρσ_dict instances for objects",
|
|
3986
|
+
src: [
|
|
3987
|
+
"# globals: assrt",
|
|
3988
|
+
"parsed = JSON.parse('{\"a\": 1, \"b\": 2}')",
|
|
3989
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
3990
|
+
"assrt.equal(parsed.get('a'), 1)",
|
|
3991
|
+
"assrt.equal(parsed.get('b'), 2)",
|
|
3992
|
+
"assrt.equal(len(parsed), 2)",
|
|
3993
|
+
].join("\n"),
|
|
3994
|
+
// JSON.parse in RapydScript must compile to ρσ_json_parse (not the native global)
|
|
3995
|
+
js_checks: ["ρσ_json_parse"],
|
|
3996
|
+
},
|
|
3997
|
+
|
|
3998
|
+
{
|
|
3999
|
+
name: "json_stringify_nested_dict",
|
|
4000
|
+
description: "JSON.stringify and parse handle nested dicts correctly",
|
|
4001
|
+
src: [
|
|
4002
|
+
"# globals: assrt",
|
|
4003
|
+
"from __python__ import dict_literals",
|
|
4004
|
+
"outer = {'inner': {'x': 1, 'y': 2}}",
|
|
4005
|
+
"s = JSON.stringify(outer)",
|
|
4006
|
+
"parsed = JSON.parse(s)",
|
|
4007
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
4008
|
+
"inner = parsed.get('inner')",
|
|
4009
|
+
"assrt.ok(isinstance(inner, dict))",
|
|
4010
|
+
"assrt.equal(inner.get('x'), 1)",
|
|
4011
|
+
"assrt.equal(inner.get('y'), 2)",
|
|
4012
|
+
].join("\n"),
|
|
4013
|
+
},
|
|
4014
|
+
|
|
4015
|
+
{
|
|
4016
|
+
name: "json_dict_with_list_values",
|
|
4017
|
+
description: "JSON.stringify/parse handles dicts with list values",
|
|
4018
|
+
src: [
|
|
4019
|
+
"# globals: assrt",
|
|
4020
|
+
"from __python__ import dict_literals",
|
|
4021
|
+
"d = {'items': [1, 2, 3], 'name': 'test'}",
|
|
4022
|
+
"s = JSON.stringify(d)",
|
|
4023
|
+
"parsed = JSON.parse(s)",
|
|
4024
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
4025
|
+
"items = parsed.get('items')",
|
|
4026
|
+
"assrt.ok(Array.isArray(items))",
|
|
4027
|
+
"assrt.equal(items[0], 1)",
|
|
4028
|
+
"assrt.equal(items[2], 3)",
|
|
4029
|
+
].join("\n"),
|
|
4030
|
+
},
|
|
4031
|
+
|
|
4032
|
+
{
|
|
4033
|
+
name: "json_dict_null_bool_values",
|
|
4034
|
+
description: "JSON.stringify/parse handles None, True, False values",
|
|
4035
|
+
src: [
|
|
4036
|
+
"# globals: assrt",
|
|
4037
|
+
"from __python__ import dict_literals",
|
|
4038
|
+
"d = {'a': None, 'b': True, 'c': False}",
|
|
4039
|
+
"s = JSON.stringify(d)",
|
|
4040
|
+
"parsed = JSON.parse(s)",
|
|
4041
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
4042
|
+
"assrt.equal(parsed.get('a'), None)",
|
|
4043
|
+
"assrt.equal(parsed.get('b'), True)",
|
|
4044
|
+
"assrt.equal(parsed.get('c'), False)",
|
|
4045
|
+
].join("\n"),
|
|
4046
|
+
},
|
|
4047
|
+
|
|
4048
|
+
{
|
|
4049
|
+
name: "json_parse_array_of_dicts",
|
|
4050
|
+
description: "JSON.parse converts objects inside arrays to dicts",
|
|
4051
|
+
src: [
|
|
4052
|
+
"# globals: assrt",
|
|
4053
|
+
"arr = JSON.parse('[{\"x\": 1}, {\"y\": 2}]')",
|
|
4054
|
+
"assrt.ok(Array.isArray(arr))",
|
|
4055
|
+
"assrt.ok(isinstance(arr[0], dict))",
|
|
4056
|
+
"assrt.ok(isinstance(arr[1], dict))",
|
|
4057
|
+
"assrt.equal(arr[0].get('x'), 1)",
|
|
4058
|
+
"assrt.equal(arr[1].get('y'), 2)",
|
|
4059
|
+
].join("\n"),
|
|
4060
|
+
},
|
|
4061
|
+
|
|
4062
|
+
{
|
|
4063
|
+
name: "json_roundtrip_dict_comprehension",
|
|
4064
|
+
description: "JSON round-trip works with dict comprehensions",
|
|
4065
|
+
src: [
|
|
4066
|
+
"# globals: assrt",
|
|
4067
|
+
"from __python__ import dict_literals",
|
|
4068
|
+
"d = {str(i): i * i for i in range(4)}",
|
|
4069
|
+
"s = JSON.stringify(d)",
|
|
4070
|
+
"parsed = JSON.parse(s)",
|
|
4071
|
+
"assrt.ok(isinstance(parsed, dict))",
|
|
4072
|
+
"assrt.equal(parsed.get('0'), 0)",
|
|
4073
|
+
"assrt.equal(parsed.get('2'), 4)",
|
|
4074
|
+
"assrt.equal(parsed.get('3'), 9)",
|
|
4075
|
+
].join("\n"),
|
|
4076
|
+
},
|
|
4077
|
+
|
|
4078
|
+
// ── __hash__ dunder ───────────────────────────────────────────────────
|
|
4079
|
+
|
|
4080
|
+
{
|
|
4081
|
+
name: "hash_basic",
|
|
4082
|
+
description: "def __hash__ in a class is dispatched by hash() builtin",
|
|
4083
|
+
src: [
|
|
4084
|
+
"# globals: assrt",
|
|
4085
|
+
"class Point:",
|
|
4086
|
+
" def __init__(self, x, y):",
|
|
4087
|
+
" self.x = x",
|
|
4088
|
+
" self.y = y",
|
|
4089
|
+
" def __hash__(self):",
|
|
4090
|
+
" return hash(self.x) ^ hash(self.y)",
|
|
4091
|
+
"p1 = Point(1, 2)",
|
|
4092
|
+
"p2 = Point(1, 2)",
|
|
4093
|
+
"p3 = Point(3, 4)",
|
|
4094
|
+
"assrt.equal(hash(p1), hash(p2))",
|
|
4095
|
+
"assrt.notEqual(hash(p1), hash(p3))",
|
|
4096
|
+
].join("\n"),
|
|
4097
|
+
js_checks: ["Point.prototype.__hash__"],
|
|
4098
|
+
},
|
|
4099
|
+
|
|
4100
|
+
{
|
|
4101
|
+
name: "hash_identity",
|
|
4102
|
+
description: "class without __hash__ gets a stable identity hash",
|
|
4103
|
+
src: [
|
|
4104
|
+
"# globals: assrt",
|
|
4105
|
+
"class Foo:",
|
|
4106
|
+
" def __init__(self, x):",
|
|
4107
|
+
" self.x = x",
|
|
4108
|
+
"a = Foo(1)",
|
|
4109
|
+
"b = Foo(1)",
|
|
4110
|
+
"h_a1 = hash(a)",
|
|
4111
|
+
"h_a2 = hash(a)",
|
|
4112
|
+
"h_b = hash(b)",
|
|
4113
|
+
"assrt.equal(h_a1, h_a2)",
|
|
4114
|
+
"assrt.notEqual(h_a1, h_b)",
|
|
4115
|
+
].join("\n"),
|
|
4116
|
+
},
|
|
4117
|
+
|
|
4118
|
+
{
|
|
4119
|
+
name: "hash_unhashable_via_eq",
|
|
4120
|
+
description: "class that defines __eq__ without __hash__ becomes unhashable (TypeError)",
|
|
4121
|
+
src: [
|
|
4122
|
+
"# globals: assrt",
|
|
4123
|
+
"class Bar:",
|
|
4124
|
+
" def __init__(self, v):",
|
|
4125
|
+
" self.v = v",
|
|
4126
|
+
" def __eq__(self, other):",
|
|
4127
|
+
" return self.v == other.v",
|
|
4128
|
+
"b = Bar(1)",
|
|
4129
|
+
"caught = False",
|
|
4130
|
+
"try:",
|
|
4131
|
+
" hash(b)",
|
|
4132
|
+
"except TypeError:",
|
|
4133
|
+
" caught = True",
|
|
4134
|
+
"assrt.ok(caught, 'hash(Bar()) should raise TypeError')",
|
|
4135
|
+
].join("\n"),
|
|
4136
|
+
js_checks: [".prototype.__hash__ = null"],
|
|
4137
|
+
},
|
|
4138
|
+
|
|
4139
|
+
{
|
|
4140
|
+
name: "hash_explicit_eq_and_hash",
|
|
4141
|
+
description: "class that defines both __eq__ and __hash__ is hashable",
|
|
4142
|
+
src: [
|
|
4143
|
+
"# globals: assrt",
|
|
4144
|
+
"class Key:",
|
|
4145
|
+
" def __init__(self, v):",
|
|
4146
|
+
" self.v = v",
|
|
4147
|
+
" def __eq__(self, other):",
|
|
4148
|
+
" return self.v == other.v",
|
|
4149
|
+
" def __hash__(self):",
|
|
4150
|
+
" return hash(self.v)",
|
|
4151
|
+
"k1 = Key('x')",
|
|
4152
|
+
"k2 = Key('x')",
|
|
4153
|
+
"assrt.equal(hash(k1), hash(k2))",
|
|
4154
|
+
].join("\n"),
|
|
4155
|
+
js_checks: ["Key.prototype.__hash__"],
|
|
4156
|
+
},
|
|
4157
|
+
|
|
4158
|
+
{
|
|
4159
|
+
name: "hash_primitives",
|
|
4160
|
+
description: "hash() of primitives follows Python semantics",
|
|
4161
|
+
src: [
|
|
4162
|
+
"# globals: assrt",
|
|
4163
|
+
"assrt.equal(hash(None), 0)",
|
|
4164
|
+
"assrt.equal(hash(True), 1)",
|
|
4165
|
+
"assrt.equal(hash(False), 0)",
|
|
4166
|
+
"assrt.equal(hash(42), 42)",
|
|
4167
|
+
"assrt.equal(hash(42.0), 42)",
|
|
4168
|
+
"assrt.equal(jstype(hash('hello')), 'number')",
|
|
4169
|
+
"assrt.equal(hash('hello'), hash('hello'))",
|
|
4170
|
+
].join("\n"),
|
|
4171
|
+
},
|
|
4172
|
+
|
|
4173
|
+
// ── __getattr__ / __setattr__ / __delattr__ / __getattribute__ ────────────
|
|
4174
|
+
|
|
4175
|
+
{
|
|
4176
|
+
name: "getattr_dunder_basic",
|
|
4177
|
+
description: "__getattr__ is called as fallback when an attribute is not found",
|
|
4178
|
+
src: [
|
|
4179
|
+
"# globals: assrt",
|
|
4180
|
+
"class Bag:",
|
|
4181
|
+
" def __getattr__(self, name):",
|
|
4182
|
+
" return 'missing_' + name",
|
|
4183
|
+
"b = Bag()",
|
|
4184
|
+
"# Missing attributes fall back to __getattr__.",
|
|
4185
|
+
"assrt.equal(b.foo, 'missing_foo')",
|
|
4186
|
+
"assrt.equal(b.bar, 'missing_bar')",
|
|
4187
|
+
].join("\n"),
|
|
4188
|
+
js_checks: ["ρσ_attr_proxy_handler"],
|
|
4189
|
+
},
|
|
4190
|
+
|
|
4191
|
+
{
|
|
4192
|
+
name: "setattr_dunder_basic",
|
|
4193
|
+
description: "__setattr__ intercepts all attribute assignments including those in __init__",
|
|
4194
|
+
src: [
|
|
4195
|
+
"# globals: assrt",
|
|
4196
|
+
"class Recorder:",
|
|
4197
|
+
" def __init__(self):",
|
|
4198
|
+
" # Each assignment goes through __setattr__.",
|
|
4199
|
+
" self.x = 10",
|
|
4200
|
+
" def __setattr__(self, name, value):",
|
|
4201
|
+
" # Store doubled numeric values via bypass.",
|
|
4202
|
+
" if jstype(value) is 'number':",
|
|
4203
|
+
" object.__setattr__(self, name, value * 2)",
|
|
4204
|
+
" else:",
|
|
4205
|
+
" object.__setattr__(self, name, value)",
|
|
4206
|
+
"r = Recorder()",
|
|
4207
|
+
"# x was doubled by __setattr__ during __init__.",
|
|
4208
|
+
"assrt.equal(r.x, 20, 'x doubled by __setattr__ in __init__')",
|
|
4209
|
+
"r.y = 7",
|
|
4210
|
+
"assrt.equal(r.y, 14, 'y doubled by __setattr__')",
|
|
4211
|
+
"r.name = 'hello'",
|
|
4212
|
+
"assrt.equal(r.name, 'hello', 'string stored as-is')",
|
|
4213
|
+
].join("\n"),
|
|
4214
|
+
js_checks: ["ρσ_attr_proxy_handler", "ρσ_object_setattr"],
|
|
4215
|
+
},
|
|
4216
|
+
|
|
4217
|
+
{
|
|
4218
|
+
name: "delattr_dunder_basic",
|
|
4219
|
+
description: "__delattr__ intercepts del obj.attr",
|
|
4220
|
+
src: [
|
|
4221
|
+
"# globals: assrt",
|
|
4222
|
+
"class Guarded:",
|
|
4223
|
+
" def __init__(self):",
|
|
4224
|
+
" # Track deleted names.",
|
|
4225
|
+
" object.__setattr__(self, 'deleted', [])",
|
|
4226
|
+
" def __setattr__(self, name, value):",
|
|
4227
|
+
" object.__setattr__(self, name, value)",
|
|
4228
|
+
" def __delattr__(self, name):",
|
|
4229
|
+
" self.deleted.append(name)",
|
|
4230
|
+
" object.__delattr__(self, name)",
|
|
4231
|
+
"g = Guarded()",
|
|
4232
|
+
"g.x = 5",
|
|
4233
|
+
"assrt.equal(g.x, 5)",
|
|
4234
|
+
"del g.x",
|
|
4235
|
+
"assrt.ok(g.deleted.indexOf('x') >= 0, 'x recorded as deleted by __delattr__')",
|
|
4236
|
+
"# After deletion the attribute is gone (returns undefined).",
|
|
4237
|
+
"assrt.equal(g.x, undefined, 'x is gone after del')",
|
|
4238
|
+
].join("\n"),
|
|
4239
|
+
js_checks: ["ρσ_attr_proxy_handler"],
|
|
4240
|
+
},
|
|
4241
|
+
|
|
4242
|
+
{
|
|
4243
|
+
name: "getattribute_dunder_basic",
|
|
4244
|
+
description: "__getattribute__ overrides ALL attribute access",
|
|
4245
|
+
src: [
|
|
4246
|
+
"# globals: assrt",
|
|
4247
|
+
"class AllCaps:",
|
|
4248
|
+
" def __init__(self):",
|
|
4249
|
+
" object.__setattr__(self, 'value', 'hello')",
|
|
4250
|
+
" def __getattribute__(self, name):",
|
|
4251
|
+
" # Use object.__getattribute__ (compiles to ρσ_object_getattr) to bypass the hook.",
|
|
4252
|
+
" raw = object.__getattribute__(self, name)",
|
|
4253
|
+
" if jstype(raw) is 'string':",
|
|
4254
|
+
" return raw.toUpperCase()",
|
|
4255
|
+
" return raw",
|
|
4256
|
+
"a = AllCaps()",
|
|
4257
|
+
"assrt.equal(a.value, 'HELLO', '__getattribute__ transforms string values')",
|
|
4258
|
+
].join("\n"),
|
|
4259
|
+
js_checks: ["ρσ_attr_proxy_handler"],
|
|
4260
|
+
},
|
|
4261
|
+
|
|
4262
|
+
{
|
|
4263
|
+
name: "getattribute_with_getattr_fallback",
|
|
4264
|
+
description: "__getattribute__ raising AttributeError falls back to __getattr__",
|
|
4265
|
+
src: [
|
|
4266
|
+
"# globals: assrt",
|
|
4267
|
+
"class Fallback:",
|
|
4268
|
+
" def __init__(self):",
|
|
4269
|
+
" object.__setattr__(self, 'real', 1)",
|
|
4270
|
+
" def __getattribute__(self, name):",
|
|
4271
|
+
" if name is 'real':",
|
|
4272
|
+
" return object.__getattribute__(self, name)",
|
|
4273
|
+
" raise AttributeError(name)",
|
|
4274
|
+
" def __getattr__(self, name):",
|
|
4275
|
+
" return 'fallback'",
|
|
4276
|
+
"f = Fallback()",
|
|
4277
|
+
"assrt.equal(f.real, 1, 'real attribute via __getattribute__')",
|
|
4278
|
+
"assrt.equal(f.anything, 'fallback', 'unknown attribute via __getattr__ fallback')",
|
|
4279
|
+
].join("\n"),
|
|
4280
|
+
js_checks: ["ρσ_attr_proxy_handler"],
|
|
4281
|
+
},
|
|
4282
|
+
|
|
4283
|
+
{
|
|
4284
|
+
name: "setattr_object_setattr_bypass",
|
|
4285
|
+
description: "object.__setattr__ bypasses __setattr__ to avoid infinite recursion",
|
|
4286
|
+
src: [
|
|
4287
|
+
"# globals: assrt",
|
|
4288
|
+
"class Doubler:",
|
|
4289
|
+
" def __init__(self):",
|
|
4290
|
+
" self.x = 5",
|
|
4291
|
+
" def __setattr__(self, name, value):",
|
|
4292
|
+
" # Double all numeric values, then store directly.",
|
|
4293
|
+
" if jstype(value) is 'number':",
|
|
4294
|
+
" object.__setattr__(self, name, value * 2)",
|
|
4295
|
+
" else:",
|
|
4296
|
+
" object.__setattr__(self, name, value)",
|
|
4297
|
+
"d = Doubler()",
|
|
4298
|
+
"assrt.equal(d.x, 10, 'value doubled by __setattr__')",
|
|
4299
|
+
"d.y = 3",
|
|
4300
|
+
"assrt.equal(d.y, 6)",
|
|
4301
|
+
"d.label = 'alice'",
|
|
4302
|
+
"assrt.equal(d.label, 'alice', 'string value stored as-is')",
|
|
4303
|
+
].join("\n"),
|
|
4304
|
+
},
|
|
4305
|
+
|
|
4306
|
+
{
|
|
4307
|
+
name: "attr_dunders_inheritance",
|
|
4308
|
+
description: "__getattr__ is inherited by subclasses",
|
|
4309
|
+
src: [
|
|
4310
|
+
"# globals: assrt",
|
|
4311
|
+
"class Base:",
|
|
4312
|
+
" def __getattr__(self, name):",
|
|
4313
|
+
" return 'from_base'",
|
|
4314
|
+
"class Child(Base):",
|
|
4315
|
+
" def __init__(self):",
|
|
4316
|
+
" self.own = 'child_own'",
|
|
4317
|
+
"c = Child()",
|
|
4318
|
+
"assrt.equal(c.own, 'child_own', 'own attribute still direct')",
|
|
4319
|
+
"assrt.equal(c.missing, 'from_base', '__getattr__ inherited from Base')",
|
|
4320
|
+
].join("\n"),
|
|
4321
|
+
js_checks: ["ρσ_attr_proxy_handler"],
|
|
4322
|
+
},
|
|
4323
|
+
|
|
4324
|
+
{
|
|
4325
|
+
name: "attr_dunders_getattr_with_setattr",
|
|
4326
|
+
description: "__setattr__ stores via bypass, __getattr__ reads back",
|
|
4327
|
+
src: [
|
|
4328
|
+
"# globals: assrt",
|
|
4329
|
+
"class AttrStore:",
|
|
4330
|
+
" def __init__(self):",
|
|
4331
|
+
" object.__setattr__(self, '_store', {})",
|
|
4332
|
+
" def __setattr__(self, name, value):",
|
|
4333
|
+
" self._store[name] = value",
|
|
4334
|
+
" def __getattr__(self, name):",
|
|
4335
|
+
" if name in self._store:",
|
|
4336
|
+
" return self._store[name]",
|
|
4337
|
+
" raise AttributeError(name)",
|
|
4338
|
+
"d = AttrStore()",
|
|
4339
|
+
"d.x = 1",
|
|
4340
|
+
"d.y = 2",
|
|
4341
|
+
"assrt.equal(d.x, 1)",
|
|
4342
|
+
"assrt.equal(d.y, 2)",
|
|
4343
|
+
"caught = False",
|
|
4344
|
+
"try:",
|
|
4345
|
+
" _ = d.z",
|
|
4346
|
+
"except AttributeError:",
|
|
4347
|
+
" caught = True",
|
|
4348
|
+
"assrt.ok(caught, 'missing attr raises AttributeError')",
|
|
4349
|
+
].join("\n"),
|
|
4350
|
+
},
|
|
4351
|
+
|
|
4352
|
+
// ── __class_getitem__ ─────────────────────────────────────────────────
|
|
4353
|
+
|
|
4354
|
+
{
|
|
4355
|
+
name: "class_getitem_basic",
|
|
4356
|
+
description: "Class[item] calls __class_getitem__(cls, item) and returns the result",
|
|
4357
|
+
src: [
|
|
4358
|
+
"# globals: assrt",
|
|
4359
|
+
"class Box:",
|
|
4360
|
+
" def __class_getitem__(cls, item):",
|
|
4361
|
+
" return cls.__name__ + '[' + str(item) + ']'",
|
|
4362
|
+
"assrt.equal(Box[42], 'Box[42]')",
|
|
4363
|
+
"assrt.equal(Box['x'], 'Box[x]')",
|
|
4364
|
+
].join("\n"),
|
|
4365
|
+
js_checks: ["Box.__class_getitem__("],
|
|
4366
|
+
},
|
|
4367
|
+
|
|
4368
|
+
{
|
|
4369
|
+
name: "class_getitem_cls_is_class",
|
|
4370
|
+
description: "__class_getitem__ receives the class as cls; can return it",
|
|
4371
|
+
src: [
|
|
4372
|
+
"# globals: assrt",
|
|
4373
|
+
"class Stack:",
|
|
4374
|
+
" def __class_getitem__(cls, item):",
|
|
4375
|
+
" return cls",
|
|
4376
|
+
"assrt.ok(Stack[int] is Stack)",
|
|
4377
|
+
"assrt.ok(Stack[str] is Stack)",
|
|
4378
|
+
].join("\n"),
|
|
4379
|
+
},
|
|
4380
|
+
|
|
4381
|
+
{
|
|
4382
|
+
name: "class_getitem_subclass_inherits",
|
|
4383
|
+
description: "subclass without __class_getitem__ inherits it from parent; cls is the subclass",
|
|
4384
|
+
src: [
|
|
4385
|
+
"# globals: assrt",
|
|
4386
|
+
"class Base:",
|
|
4387
|
+
" def __class_getitem__(cls, item):",
|
|
4388
|
+
" return cls.__name__ + '<' + str(item) + '>'",
|
|
4389
|
+
"class Child(Base):",
|
|
4390
|
+
" pass",
|
|
4391
|
+
"assrt.equal(Base[42], 'Base<42>')",
|
|
4392
|
+
"assrt.equal(Child[42], 'Child<42>')",
|
|
4393
|
+
].join("\n"),
|
|
4394
|
+
},
|
|
4395
|
+
|
|
4396
|
+
{
|
|
4397
|
+
name: "class_getitem_subclass_overrides",
|
|
4398
|
+
description: "subclass can override __class_getitem__",
|
|
4399
|
+
src: [
|
|
4400
|
+
"# globals: assrt",
|
|
4401
|
+
"class Base:",
|
|
4402
|
+
" def __class_getitem__(cls, item):",
|
|
4403
|
+
" return 'base'",
|
|
4404
|
+
"class Child(Base):",
|
|
4405
|
+
" def __class_getitem__(cls, item):",
|
|
4406
|
+
" return 'child'",
|
|
4407
|
+
"assrt.equal(Base[1], 'base')",
|
|
4408
|
+
"assrt.equal(Child[1], 'child')",
|
|
4409
|
+
].join("\n"),
|
|
4410
|
+
},
|
|
4411
|
+
|
|
4412
|
+
{
|
|
4413
|
+
name: "class_getitem_classvar",
|
|
4414
|
+
description: "__class_getitem__ can access class variables via cls",
|
|
4415
|
+
src: [
|
|
4416
|
+
"# globals: assrt",
|
|
4417
|
+
"class Tagged:",
|
|
4418
|
+
" prefix = 'Tag'",
|
|
4419
|
+
" def __class_getitem__(cls, item):",
|
|
4420
|
+
" return cls.prefix + ':' + str(item)",
|
|
4421
|
+
"assrt.equal(Tagged['int'], 'Tag:int')",
|
|
4422
|
+
].join("\n"),
|
|
4423
|
+
},
|
|
4424
|
+
|
|
4425
|
+
{
|
|
4426
|
+
name: "class_getitem_builtin_name",
|
|
4427
|
+
description: "Built-in types int/str/float/bool have .__name__ so they work as __class_getitem__ arguments",
|
|
4428
|
+
src: [
|
|
4429
|
+
"# globals: assrt",
|
|
4430
|
+
"class TypedList:",
|
|
4431
|
+
" prefix = 'TypedList'",
|
|
4432
|
+
" def __class_getitem__(cls, item):",
|
|
4433
|
+
" return cls.prefix + '[' + item.__name__ + ']'",
|
|
4434
|
+
"assrt.equal(TypedList[int], 'TypedList[int]')",
|
|
4435
|
+
"assrt.equal(TypedList[str], 'TypedList[str]')",
|
|
4436
|
+
"assrt.equal(TypedList[float], 'TypedList[float]')",
|
|
4437
|
+
"assrt.equal(TypedList[bool], 'TypedList[bool]')",
|
|
4438
|
+
].join("\n"),
|
|
4439
|
+
},
|
|
4440
|
+
|
|
4441
|
+
// ── __init_subclass__ hook ────────────────────────────────────────────
|
|
4442
|
+
|
|
4443
|
+
{
|
|
4444
|
+
name: "init_subclass_basic",
|
|
4445
|
+
description: "__init_subclass__ is called when a subclass is created",
|
|
4446
|
+
src: [
|
|
4447
|
+
"# globals: assrt",
|
|
4448
|
+
"log = []",
|
|
4449
|
+
"class Base:",
|
|
4450
|
+
" def __init_subclass__(cls, **kwargs):",
|
|
4451
|
+
" log.append(cls.__name__)",
|
|
4452
|
+
"class Child(Base):",
|
|
4453
|
+
" pass",
|
|
4454
|
+
"class GrandChild(Child):",
|
|
4455
|
+
" pass",
|
|
4456
|
+
"assrt.deepEqual(log, ['Child', 'GrandChild'])",
|
|
4457
|
+
].join("\n"),
|
|
4458
|
+
js_checks: ['.__init_subclass__.call(Child)', '.__init_subclass__.call(GrandChild)'],
|
|
4459
|
+
},
|
|
4460
|
+
|
|
4461
|
+
{
|
|
4462
|
+
name: "init_subclass_cls_is_subclass",
|
|
4463
|
+
description: "__init_subclass__ receives the subclass as cls",
|
|
4464
|
+
src: [
|
|
4465
|
+
"# globals: assrt",
|
|
4466
|
+
"received = []",
|
|
4467
|
+
"class Base:",
|
|
4468
|
+
" def __init_subclass__(cls, **kwargs):",
|
|
4469
|
+
" received.append(cls)",
|
|
4470
|
+
"class Child(Base):",
|
|
4471
|
+
" pass",
|
|
4472
|
+
"assrt.equal(received.length, 1)",
|
|
4473
|
+
"assrt.equal(received[0], Child)",
|
|
4474
|
+
].join("\n"),
|
|
4475
|
+
},
|
|
4476
|
+
|
|
4477
|
+
{
|
|
4478
|
+
name: "init_subclass_kwargs",
|
|
4479
|
+
description: "keyword arguments from class header are passed to __init_subclass__",
|
|
4480
|
+
src: [
|
|
4481
|
+
"# globals: assrt",
|
|
4482
|
+
"log = []",
|
|
4483
|
+
"class Base:",
|
|
4484
|
+
" def __init_subclass__(cls, tag=None, **kwargs):",
|
|
4485
|
+
" log.append(tag)",
|
|
4486
|
+
"class Child(Base, tag='alpha'):",
|
|
4487
|
+
" pass",
|
|
4488
|
+
"class Other(Base, tag='beta'):",
|
|
4489
|
+
" pass",
|
|
4490
|
+
"assrt.deepEqual(log, ['alpha', 'beta'])",
|
|
4491
|
+
].join("\n"),
|
|
4492
|
+
js_checks: ["ρσ_isc_kw"],
|
|
4493
|
+
},
|
|
4494
|
+
|
|
4495
|
+
{
|
|
4496
|
+
name: "init_subclass_super_chain",
|
|
4497
|
+
description: "super().__init_subclass__ propagates to grandparent",
|
|
4498
|
+
src: [
|
|
4499
|
+
"# globals: assrt",
|
|
4500
|
+
"calls = []",
|
|
4501
|
+
"class GrandParent:",
|
|
4502
|
+
" def __init_subclass__(cls, **kwargs):",
|
|
4503
|
+
" calls.append('GrandParent:' + cls.__name__)",
|
|
4504
|
+
"class Parent(GrandParent):",
|
|
4505
|
+
" def __init_subclass__(cls, **kwargs):",
|
|
4506
|
+
" super().__init_subclass__(**kwargs)",
|
|
4507
|
+
" calls.append('Parent:' + cls.__name__)",
|
|
4508
|
+
"class Child(Parent):",
|
|
4509
|
+
" pass",
|
|
4510
|
+
"assrt.deepEqual(calls, ['GrandParent:Parent', 'GrandParent:Child', 'Parent:Child'])",
|
|
4511
|
+
].join("\n"),
|
|
4512
|
+
},
|
|
4513
|
+
|
|
4514
|
+
{
|
|
4515
|
+
name: "init_subclass_set_classvar",
|
|
4516
|
+
description: "__init_subclass__ can set class variables on the subclass",
|
|
4517
|
+
src: [
|
|
4518
|
+
"# globals: assrt",
|
|
4519
|
+
"class Registry:",
|
|
4520
|
+
" _registry = []",
|
|
4521
|
+
" def __init_subclass__(cls, **kwargs):",
|
|
4522
|
+
" cls._registered = True",
|
|
4523
|
+
" Registry._registry.append(cls.__name__)",
|
|
4524
|
+
"class A(Registry):",
|
|
4525
|
+
" pass",
|
|
4526
|
+
"class B(Registry):",
|
|
4527
|
+
" pass",
|
|
4528
|
+
"assrt.equal(A._registered, True)",
|
|
4529
|
+
"assrt.equal(B._registered, True)",
|
|
4530
|
+
"assrt.deepEqual(Registry._registry, ['A', 'B'])",
|
|
4531
|
+
].join("\n"),
|
|
4532
|
+
},
|
|
4533
|
+
|
|
4534
|
+
{
|
|
4535
|
+
name: "init_subclass_no_hook_no_call",
|
|
4536
|
+
description: "no __init_subclass__ defined: class definition works normally",
|
|
4537
|
+
src: [
|
|
4538
|
+
"# globals: assrt",
|
|
4539
|
+
"class Base:",
|
|
4540
|
+
" pass",
|
|
4541
|
+
"class Child(Base):",
|
|
4542
|
+
" pass",
|
|
4543
|
+
"c = Child()",
|
|
4544
|
+
"assrt.ok(isinstance(c, Child))",
|
|
4545
|
+
].join("\n"),
|
|
4546
|
+
},
|
|
4547
|
+
|
|
4548
|
+
// ── except* / ExceptionGroup ──────────────────────────────────────────
|
|
4549
|
+
|
|
4550
|
+
{
|
|
4551
|
+
name: "except_star_basic",
|
|
4552
|
+
description: "except* catches matching exceptions from an ExceptionGroup",
|
|
4553
|
+
src: [
|
|
4554
|
+
"# globals: assrt",
|
|
4555
|
+
'eg = ExceptionGroup("errors", [ValueError("bad"), ValueError("again")])',
|
|
4556
|
+
"caught_ve = []",
|
|
4557
|
+
"try:",
|
|
4558
|
+
" raise eg",
|
|
4559
|
+
"except* ValueError as g:",
|
|
4560
|
+
" for e in g.exceptions:",
|
|
4561
|
+
" caught_ve.append(str(e))",
|
|
4562
|
+
"assrt.equal(len(caught_ve), 2)",
|
|
4563
|
+
'assrt.ok(caught_ve[0].indexOf("bad") >= 0)',
|
|
4564
|
+
'assrt.ok(caught_ve[1].indexOf("again") >= 0)',
|
|
4565
|
+
].join("\n"),
|
|
4566
|
+
js_checks: ["ExceptionGroup", "ρσ_eg_exceptions"],
|
|
4567
|
+
},
|
|
4568
|
+
|
|
4569
|
+
{
|
|
4570
|
+
name: "except_star_multiple_handlers",
|
|
4571
|
+
description: "multiple except* clauses each receive their matching sub-group",
|
|
4572
|
+
src: [
|
|
4573
|
+
"# globals: assrt",
|
|
4574
|
+
'eg = ExceptionGroup("mixed", [ValueError("v1"), TypeError("t1"), ValueError("v2")])',
|
|
4575
|
+
"val_count = 0",
|
|
4576
|
+
"type_count = 0",
|
|
4577
|
+
"try:",
|
|
4578
|
+
" raise eg",
|
|
4579
|
+
"except* ValueError as g:",
|
|
4580
|
+
" val_count = len(g.exceptions)",
|
|
4581
|
+
"except* TypeError as g:",
|
|
4582
|
+
" type_count = len(g.exceptions)",
|
|
4583
|
+
"assrt.equal(val_count, 2)",
|
|
4584
|
+
"assrt.equal(type_count, 1)",
|
|
4585
|
+
].join("\n"),
|
|
4586
|
+
},
|
|
4587
|
+
|
|
4588
|
+
{
|
|
4589
|
+
name: "except_star_unmatched_reraise",
|
|
4590
|
+
description: "unmatched exceptions from an ExceptionGroup are re-raised",
|
|
4591
|
+
src: [
|
|
4592
|
+
"# globals: assrt",
|
|
4593
|
+
'eg = ExceptionGroup("mixed", [ValueError("v"), KeyError("k")])',
|
|
4594
|
+
"caught = False",
|
|
4595
|
+
"reraised = False",
|
|
4596
|
+
"try:",
|
|
4597
|
+
" try:",
|
|
4598
|
+
" raise eg",
|
|
4599
|
+
" except* ValueError as g:",
|
|
4600
|
+
" caught = True",
|
|
4601
|
+
"except ExceptionGroup as outer:",
|
|
4602
|
+
" reraised = True",
|
|
4603
|
+
" assrt.equal(len(outer.exceptions), 1)",
|
|
4604
|
+
" assrt.ok(isinstance(outer.exceptions[0], KeyError))",
|
|
4605
|
+
"assrt.ok(caught)",
|
|
4606
|
+
"assrt.ok(reraised)",
|
|
4607
|
+
].join("\n"),
|
|
4608
|
+
},
|
|
4609
|
+
|
|
4610
|
+
{
|
|
4611
|
+
name: "except_star_non_group",
|
|
4612
|
+
description: "except* also handles a plain (non-ExceptionGroup) exception",
|
|
4613
|
+
src: [
|
|
4614
|
+
"# globals: assrt",
|
|
4615
|
+
"caught = False",
|
|
4616
|
+
"try:",
|
|
4617
|
+
' raise ValueError("plain")',
|
|
4618
|
+
"except* ValueError as g:",
|
|
4619
|
+
" caught = True",
|
|
4620
|
+
" assrt.ok(isinstance(g, ValueError))",
|
|
4621
|
+
"assrt.ok(caught)",
|
|
4622
|
+
].join("\n"),
|
|
4623
|
+
},
|
|
4624
|
+
|
|
4625
|
+
{
|
|
4626
|
+
name: "except_star_bare",
|
|
4627
|
+
description: "bare except* catches all remaining exceptions",
|
|
4628
|
+
src: [
|
|
4629
|
+
"# globals: assrt",
|
|
4630
|
+
'eg = ExceptionGroup("all", [ValueError("v"), TypeError("t")])',
|
|
4631
|
+
"total = 0",
|
|
4632
|
+
"try:",
|
|
4633
|
+
" raise eg",
|
|
4634
|
+
"except* as g:",
|
|
4635
|
+
" total = len(g.exceptions)",
|
|
4636
|
+
"assrt.equal(total, 2)",
|
|
4637
|
+
].join("\n"),
|
|
4638
|
+
},
|
|
4639
|
+
|
|
4640
|
+
{
|
|
4641
|
+
name: "except_star_exception_group_class",
|
|
4642
|
+
description: "ExceptionGroup class has correct attributes and subgroup/split methods",
|
|
4643
|
+
src: [
|
|
4644
|
+
"# globals: assrt",
|
|
4645
|
+
'eg = ExceptionGroup("demo", [ValueError("v"), TypeError("t"), ValueError("v2")])',
|
|
4646
|
+
"assrt.equal(eg.message, 'demo')",
|
|
4647
|
+
"assrt.equal(len(eg.exceptions), 3)",
|
|
4648
|
+
"sub = eg.subgroup(ValueError)",
|
|
4649
|
+
"assrt.equal(len(sub.exceptions), 2)",
|
|
4650
|
+
"parts = eg.split(ValueError)",
|
|
4651
|
+
"assrt.equal(len(parts[0].exceptions), 2)",
|
|
4652
|
+
"assrt.equal(len(parts[1].exceptions), 1)",
|
|
4653
|
+
].join("\n"),
|
|
4654
|
+
},
|
|
4655
|
+
|
|
4656
|
+
// ── * and ** unpacking operators ──────────────────────────────────────────
|
|
4657
|
+
|
|
4658
|
+
{
|
|
4659
|
+
name: "list_spread_basic",
|
|
4660
|
+
description: "list spread: [*a, 1, 2] flattens iterable into a new list",
|
|
4661
|
+
src: [
|
|
4662
|
+
"# globals: assrt",
|
|
4663
|
+
"a = [1, 2, 3]",
|
|
4664
|
+
"b = [*a, 4, 5]",
|
|
4665
|
+
"assrt.deepEqual(b, [1, 2, 3, 4, 5])",
|
|
4666
|
+
].join("\n"),
|
|
4667
|
+
js_checks: [/\.\.\./],
|
|
4668
|
+
},
|
|
4669
|
+
|
|
4670
|
+
{
|
|
4671
|
+
name: "list_spread_middle",
|
|
4672
|
+
description: "list spread: spread in the middle and at both ends",
|
|
4673
|
+
src: [
|
|
4674
|
+
"# globals: assrt",
|
|
4675
|
+
"a = [2, 3]",
|
|
4676
|
+
"b = [1, *a, 4]",
|
|
4677
|
+
"assrt.deepEqual(b, [1, 2, 3, 4])",
|
|
4678
|
+
"x = [10, 20]",
|
|
4679
|
+
"y = [30, 40]",
|
|
4680
|
+
"z = [*x, *y]",
|
|
4681
|
+
"assrt.deepEqual(z, [10, 20, 30, 40])",
|
|
4682
|
+
].join("\n"),
|
|
4683
|
+
},
|
|
4684
|
+
|
|
4685
|
+
{
|
|
4686
|
+
name: "list_spread_string",
|
|
4687
|
+
description: "list spread: *string unpacks characters",
|
|
4688
|
+
src: [
|
|
4689
|
+
"# globals: assrt",
|
|
4690
|
+
"chars = [*'abc', 'd']",
|
|
4691
|
+
"assrt.deepEqual(chars, ['a', 'b', 'c', 'd'])",
|
|
4692
|
+
].join("\n"),
|
|
4693
|
+
},
|
|
4694
|
+
|
|
4695
|
+
{
|
|
4696
|
+
name: "list_spread_first",
|
|
4697
|
+
description: "list spread: spread as the very first element",
|
|
4698
|
+
src: [
|
|
4699
|
+
"# globals: assrt",
|
|
4700
|
+
"a = [1, 2]",
|
|
4701
|
+
"b = [*a, 3]",
|
|
4702
|
+
"assrt.deepEqual(b, [1, 2, 3])",
|
|
4703
|
+
"c = [*a]",
|
|
4704
|
+
"assrt.deepEqual(c, [1, 2])",
|
|
4705
|
+
].join("\n"),
|
|
4706
|
+
},
|
|
4707
|
+
|
|
4708
|
+
{
|
|
4709
|
+
name: "set_spread_basic",
|
|
4710
|
+
description: "set spread: {*a, 1} builds a set from iterable and literals",
|
|
4711
|
+
src: [
|
|
4712
|
+
"# globals: assrt",
|
|
4713
|
+
"a = [1, 2, 3]",
|
|
4714
|
+
"s = {*a, 4}",
|
|
4715
|
+
"assrt.ok(isinstance(s, set))",
|
|
4716
|
+
"assrt.equal(len(s), 4)",
|
|
4717
|
+
"assrt.ok(s.has(1))",
|
|
4718
|
+
"assrt.ok(s.has(4))",
|
|
4719
|
+
].join("\n"),
|
|
4720
|
+
js_checks: ["ρσ_set(["],
|
|
4721
|
+
},
|
|
4722
|
+
|
|
4723
|
+
{
|
|
4724
|
+
name: "set_spread_multiple",
|
|
4725
|
+
description: "set spread: multiple spreads merge iterables into a set",
|
|
4726
|
+
src: [
|
|
4727
|
+
"# globals: assrt",
|
|
4728
|
+
"a = [1, 2]",
|
|
4729
|
+
"b = [3, 4]",
|
|
4730
|
+
"s = {*a, *b}",
|
|
4731
|
+
"assrt.equal(len(s), 4)",
|
|
4732
|
+
"assrt.ok(s.has(2))",
|
|
4733
|
+
"assrt.ok(s.has(3))",
|
|
4734
|
+
].join("\n"),
|
|
4735
|
+
},
|
|
4736
|
+
|
|
4737
|
+
{
|
|
4738
|
+
name: "kwargs_spread_expr",
|
|
4739
|
+
description: "**expr in function call accepts arbitrary expression, not just symbol",
|
|
4740
|
+
src: [
|
|
4741
|
+
"# globals: assrt",
|
|
4742
|
+
"def f(a=0, b=0, c=0):",
|
|
4743
|
+
" return a + b + c",
|
|
4744
|
+
"opts = {'a': 1, 'b': 2, 'c': 3}",
|
|
4745
|
+
"assrt.equal(f(**opts), 6)",
|
|
4746
|
+
].join("\n"),
|
|
4747
|
+
},
|
|
4748
|
+
|
|
4749
|
+
{
|
|
4750
|
+
name: "kwargs_spread_getattr",
|
|
4751
|
+
description: "**obj.attr in function call spreads attribute access result",
|
|
4752
|
+
src: [
|
|
4753
|
+
"# globals: assrt",
|
|
4754
|
+
"class Cfg:",
|
|
4755
|
+
" params = {'x': 10, 'y': 20}",
|
|
4756
|
+
"def add(x=0, y=0):",
|
|
4757
|
+
" return x + y",
|
|
4758
|
+
"assrt.equal(add(**Cfg.params), 30)",
|
|
4759
|
+
].join("\n"),
|
|
4760
|
+
},
|
|
4761
|
+
|
|
4762
|
+
{
|
|
4763
|
+
name: "star_in_call_existing",
|
|
4764
|
+
description: "*args in function call: existing behaviour still works",
|
|
4765
|
+
src: [
|
|
4766
|
+
"# globals: assrt",
|
|
4767
|
+
"def f(a, b, c):",
|
|
4768
|
+
" return a + b + c",
|
|
4769
|
+
"args = [1, 2, 3]",
|
|
4770
|
+
"assrt.equal(f(*args), 6)",
|
|
4771
|
+
].join("\n"),
|
|
4772
|
+
},
|
|
4773
|
+
|
|
4774
|
+
// ── tuple type ────────────────────────────────────────────────────────────
|
|
4775
|
+
|
|
4776
|
+
{
|
|
4777
|
+
name: "tuple_from_list",
|
|
4778
|
+
description: "tuple() converts a list to a plain array",
|
|
4779
|
+
src: [
|
|
4780
|
+
"# globals: assrt",
|
|
4781
|
+
"t = tuple([1, 2, 3])",
|
|
4782
|
+
"assrt.equal(t[0], 1)",
|
|
4783
|
+
"assrt.equal(t[1], 2)",
|
|
4784
|
+
"assrt.equal(t[2], 3)",
|
|
4785
|
+
"assrt.equal(len(t), 3)",
|
|
4786
|
+
].join("\n"),
|
|
4787
|
+
},
|
|
4788
|
+
|
|
4789
|
+
{
|
|
4790
|
+
name: "tuple_from_string",
|
|
4791
|
+
description: "tuple() converts a string to an array of characters",
|
|
4792
|
+
src: [
|
|
4793
|
+
"# globals: assrt",
|
|
4794
|
+
"t = tuple('abc')",
|
|
4795
|
+
"assrt.equal(t[0], 'a')",
|
|
4796
|
+
"assrt.equal(t[1], 'b')",
|
|
4797
|
+
"assrt.equal(t[2], 'c')",
|
|
4798
|
+
"assrt.equal(len(t), 3)",
|
|
4799
|
+
].join("\n"),
|
|
4800
|
+
},
|
|
4801
|
+
|
|
4802
|
+
{
|
|
4803
|
+
name: "tuple_empty",
|
|
4804
|
+
description: "tuple() with no args returns an empty array",
|
|
4805
|
+
src: [
|
|
4806
|
+
"# globals: assrt",
|
|
4807
|
+
"t = tuple()",
|
|
4808
|
+
"assrt.equal(len(t), 0)",
|
|
4809
|
+
].join("\n"),
|
|
4810
|
+
},
|
|
4811
|
+
|
|
4812
|
+
{
|
|
4813
|
+
name: "tuple_annotation_variable",
|
|
4814
|
+
description: "tuple used as a variable type annotation with paren notation compiles and runs",
|
|
4815
|
+
src: [
|
|
4816
|
+
"# globals: assrt",
|
|
4817
|
+
"coords: tuple = (10, 20)",
|
|
4818
|
+
"assrt.equal(coords[0], 10)",
|
|
4819
|
+
"assrt.equal(coords[1], 20)",
|
|
4820
|
+
].join("\n"),
|
|
4821
|
+
js_checks: [/coords\s*=\s*ρσ_list_decorate\(\s*\[\s*10,\s*20\s*\]/],
|
|
4822
|
+
},
|
|
4823
|
+
|
|
4824
|
+
{
|
|
4825
|
+
name: "tuple_annotation_function_arg",
|
|
4826
|
+
description: "tuple used as a function argument type annotation works",
|
|
4827
|
+
src: [
|
|
4828
|
+
"# globals: assrt",
|
|
4829
|
+
"def first(t: tuple):",
|
|
4830
|
+
" return t[0]",
|
|
4831
|
+
"assrt.equal(first([7, 8, 9]), 7)",
|
|
4832
|
+
].join("\n"),
|
|
4833
|
+
},
|
|
4834
|
+
|
|
4835
|
+
{
|
|
4836
|
+
name: "tuple_iterable",
|
|
4837
|
+
description: "tuple() result is iterable with for-in",
|
|
4838
|
+
src: [
|
|
4839
|
+
"# globals: assrt",
|
|
4840
|
+
"t = tuple([10, 20, 30])",
|
|
4841
|
+
"total = 0",
|
|
4842
|
+
"for v in t:",
|
|
4843
|
+
" total += v",
|
|
4844
|
+
"assrt.equal(total, 60)",
|
|
4845
|
+
].join("\n"),
|
|
4846
|
+
},
|
|
4847
|
+
|
|
4848
|
+
{
|
|
4849
|
+
name: "list_spread_is_list",
|
|
4850
|
+
description: "result of [*a] is a proper Python list with list methods",
|
|
4851
|
+
src: [
|
|
4852
|
+
"# globals: assrt",
|
|
4853
|
+
"a = [1, 2]",
|
|
4854
|
+
"b = [*a, 3]",
|
|
4855
|
+
"assrt.ok(isinstance(b, list))",
|
|
4856
|
+
"b.append(4)",
|
|
4857
|
+
"assrt.equal(len(b), 4)",
|
|
4858
|
+
].join("\n"),
|
|
4859
|
+
},
|
|
4860
|
+
|
|
4861
|
+
// ── copy ──────────────────────────────────────────────────────────────
|
|
4862
|
+
|
|
4863
|
+
{
|
|
4864
|
+
name: "copy_primitives",
|
|
4865
|
+
description: "copy.copy returns primitives unchanged",
|
|
4866
|
+
src: [
|
|
4867
|
+
"# globals: assrt",
|
|
4868
|
+
"from copy import copy",
|
|
4869
|
+
"assrt.equal(copy(42), 42)",
|
|
4870
|
+
"assrt.equal(copy('hello'), 'hello')",
|
|
4871
|
+
"assrt.equal(copy(True), True)",
|
|
4872
|
+
"assrt.equal(copy(None), None)",
|
|
4873
|
+
].join("\n"),
|
|
4874
|
+
},
|
|
4875
|
+
|
|
4876
|
+
{
|
|
4877
|
+
name: "copy_list_shallow",
|
|
4878
|
+
description: "copy.copy of a list returns a shallow copy",
|
|
4879
|
+
src: [
|
|
4880
|
+
"# globals: assrt",
|
|
4881
|
+
"from copy import copy",
|
|
4882
|
+
"orig = [1, [2, 3], 4]",
|
|
4883
|
+
"c = copy(orig)",
|
|
4884
|
+
"assrt.equal(len(c), 3)",
|
|
4885
|
+
"assrt.equal(c[0], 1)",
|
|
4886
|
+
// shallow: inner list is the same object
|
|
4887
|
+
"assrt.ok(c[1] is orig[1])",
|
|
4888
|
+
// modifying the copy does not affect the original
|
|
4889
|
+
"c.append(5)",
|
|
4890
|
+
"assrt.equal(len(orig), 3)",
|
|
4891
|
+
"assrt.equal(len(c), 4)",
|
|
4892
|
+
].join("\n"),
|
|
4893
|
+
},
|
|
4894
|
+
|
|
4895
|
+
{
|
|
4896
|
+
name: "copy_dict_shallow",
|
|
4897
|
+
description: "copy.copy of a dict returns a shallow copy",
|
|
4898
|
+
src: [
|
|
4899
|
+
"# globals: assrt",
|
|
4900
|
+
"from __python__ import dict_literals, overload_getitem",
|
|
4901
|
+
"from copy import copy",
|
|
4902
|
+
"inner = [99]",
|
|
4903
|
+
"orig = {'a': 1, 'b': inner}",
|
|
4904
|
+
"c = copy(orig)",
|
|
4905
|
+
"assrt.equal(c['a'], 1)",
|
|
4906
|
+
// shallow: inner list is the same object
|
|
4907
|
+
"assrt.ok(c['b'] is orig['b'])",
|
|
4908
|
+
"c['x'] = 100",
|
|
4909
|
+
"assrt.ok(not ('x' in orig))",
|
|
4910
|
+
].join("\n"),
|
|
4911
|
+
},
|
|
4912
|
+
|
|
4913
|
+
{
|
|
4914
|
+
name: "copy_set_shallow",
|
|
4915
|
+
description: "copy.copy of a set returns an independent copy",
|
|
4916
|
+
src: [
|
|
4917
|
+
"# globals: assrt",
|
|
4918
|
+
"from copy import copy",
|
|
4919
|
+
"orig = {1, 2, 3}",
|
|
4920
|
+
"c = copy(orig)",
|
|
4921
|
+
"assrt.equal(len(c), 3)",
|
|
4922
|
+
"c.add(4)",
|
|
4923
|
+
"assrt.equal(len(orig), 3)",
|
|
4924
|
+
"assrt.equal(len(c), 4)",
|
|
4925
|
+
].join("\n"),
|
|
4926
|
+
},
|
|
4927
|
+
|
|
4928
|
+
{
|
|
4929
|
+
name: "copy_class_instance_shallow",
|
|
4930
|
+
description: "copy.copy of a class instance is shallow",
|
|
4931
|
+
src: [
|
|
4932
|
+
"# globals: assrt",
|
|
4933
|
+
"from copy import copy",
|
|
4934
|
+
"class Point:",
|
|
4935
|
+
" def __init__(self, x, y):",
|
|
4936
|
+
" self.x = x",
|
|
4937
|
+
" self.y = y",
|
|
4938
|
+
"p = Point(1, 2)",
|
|
4939
|
+
"p.data = [10, 20]",
|
|
4940
|
+
"q = copy(p)",
|
|
4941
|
+
"assrt.equal(q.x, 1)",
|
|
4942
|
+
"assrt.equal(q.y, 2)",
|
|
4943
|
+
"assrt.ok(q is not p)",
|
|
4944
|
+
// shallow: mutable attribute is the same object
|
|
4945
|
+
"assrt.ok(q.data is p.data)",
|
|
4946
|
+
].join("\n"),
|
|
4947
|
+
},
|
|
4948
|
+
|
|
4949
|
+
{
|
|
4950
|
+
name: "copy_custom_copy_hook",
|
|
4951
|
+
description: "__copy__ method is called by copy.copy",
|
|
4952
|
+
src: [
|
|
4953
|
+
"# globals: assrt",
|
|
4954
|
+
"from copy import copy",
|
|
4955
|
+
"class MyObj:",
|
|
4956
|
+
" def __init__(self, val):",
|
|
4957
|
+
" self.val = val",
|
|
4958
|
+
" self.copy_called = False",
|
|
4959
|
+
" def __copy__(self):",
|
|
4960
|
+
" result = MyObj(self.val * 2)",
|
|
4961
|
+
" return result",
|
|
4962
|
+
"obj = MyObj(5)",
|
|
4963
|
+
"c = copy(obj)",
|
|
4964
|
+
"assrt.equal(c.val, 10)",
|
|
4965
|
+
].join("\n"),
|
|
4966
|
+
},
|
|
4967
|
+
|
|
4968
|
+
{
|
|
4969
|
+
name: "deepcopy_list_nested",
|
|
4970
|
+
description: "copy.deepcopy of a list with nested lists returns independent copies",
|
|
4971
|
+
src: [
|
|
4972
|
+
"# globals: assrt",
|
|
4973
|
+
"from copy import deepcopy",
|
|
4974
|
+
"orig = [1, [2, 3], [4, [5, 6]]]",
|
|
4975
|
+
"d = deepcopy(orig)",
|
|
4976
|
+
"assrt.equal(d[0], 1)",
|
|
4977
|
+
"assrt.equal(d[1][0], 2)",
|
|
4978
|
+
"assrt.equal(d[2][1][0], 5)",
|
|
4979
|
+
// deep: inner lists are different objects
|
|
4980
|
+
"assrt.ok(d[1] is not orig[1])",
|
|
4981
|
+
"assrt.ok(d[2][1] is not orig[2][1])",
|
|
4982
|
+
// mutating copy does not affect original
|
|
4983
|
+
"d[1].append(99)",
|
|
4984
|
+
"assrt.equal(len(orig[1]), 2)",
|
|
4985
|
+
].join("\n"),
|
|
4986
|
+
},
|
|
4987
|
+
|
|
4988
|
+
{
|
|
4989
|
+
name: "deepcopy_dict_nested",
|
|
4990
|
+
description: "copy.deepcopy of a dict with nested dicts returns independent copies",
|
|
4991
|
+
src: [
|
|
4992
|
+
"# globals: assrt",
|
|
4993
|
+
"from __python__ import dict_literals, overload_getitem",
|
|
4994
|
+
"from copy import deepcopy",
|
|
4995
|
+
"orig = {'a': {'x': 1}, 'b': [2, 3]}",
|
|
4996
|
+
"d = deepcopy(orig)",
|
|
4997
|
+
"assrt.equal(d['a']['x'], 1)",
|
|
4998
|
+
"assrt.ok(d['a'] is not orig['a'])",
|
|
4999
|
+
"assrt.ok(d['b'] is not orig['b'])",
|
|
5000
|
+
"d['a']['x'] = 99",
|
|
5001
|
+
"assrt.equal(orig['a']['x'], 1)",
|
|
5002
|
+
].join("\n"),
|
|
5003
|
+
},
|
|
5004
|
+
|
|
5005
|
+
{
|
|
5006
|
+
name: "deepcopy_circular",
|
|
5007
|
+
description: "copy.deepcopy handles circular references without infinite recursion",
|
|
5008
|
+
src: [
|
|
5009
|
+
"# globals: assrt",
|
|
5010
|
+
"from copy import deepcopy",
|
|
5011
|
+
"a = [1, 2]",
|
|
5012
|
+
"a.push(a) # circular reference",
|
|
5013
|
+
"b = deepcopy(a)",
|
|
5014
|
+
"assrt.equal(b[0], 1)",
|
|
5015
|
+
"assrt.equal(b[1], 2)",
|
|
5016
|
+
"assrt.ok(b[2] is b)", // circularity preserved in the copy
|
|
5017
|
+
"assrt.ok(b is not a)",
|
|
5018
|
+
].join("\n"),
|
|
5019
|
+
},
|
|
5020
|
+
|
|
5021
|
+
{
|
|
5022
|
+
name: "deepcopy_custom_hook",
|
|
5023
|
+
description: "__deepcopy__(memo) method is called by copy.deepcopy",
|
|
5024
|
+
src: [
|
|
5025
|
+
"# globals: assrt",
|
|
5026
|
+
"from copy import deepcopy",
|
|
5027
|
+
"class Node:",
|
|
5028
|
+
" def __init__(self, val):",
|
|
5029
|
+
" self.val = val",
|
|
5030
|
+
" self.children = []",
|
|
5031
|
+
" def __deepcopy__(self, memo):",
|
|
5032
|
+
" result = Node(self.val * 10)",
|
|
5033
|
+
" return result",
|
|
5034
|
+
"n = Node(7)",
|
|
5035
|
+
"m = deepcopy(n)",
|
|
5036
|
+
"assrt.equal(m.val, 70)",
|
|
5037
|
+
"assrt.ok(m is not n)",
|
|
5038
|
+
].join("\n"),
|
|
5039
|
+
},
|
|
5040
|
+
|
|
5041
|
+
{
|
|
5042
|
+
name: "deepcopy_class_instance",
|
|
5043
|
+
description: "copy.deepcopy of a class instance deeply copies instance attributes",
|
|
5044
|
+
src: [
|
|
5045
|
+
"# globals: assrt",
|
|
5046
|
+
"from copy import deepcopy",
|
|
5047
|
+
"class Box:",
|
|
5048
|
+
" def __init__(self, items):",
|
|
5049
|
+
" self.items = items",
|
|
5050
|
+
"b = Box([1, 2, 3])",
|
|
5051
|
+
"c = deepcopy(b)",
|
|
5052
|
+
"assrt.ok(c is not b)",
|
|
5053
|
+
"assrt.ok(c.items is not b.items)",
|
|
5054
|
+
"c.items.append(4)",
|
|
5055
|
+
"assrt.equal(len(b.items), 3)",
|
|
5056
|
+
].join("\n"),
|
|
5057
|
+
},
|
|
5058
|
+
|
|
5059
|
+
// ── str.expandtabs ────────────────────────────────────────────────────
|
|
5060
|
+
|
|
5061
|
+
{
|
|
5062
|
+
name: "expandtabs_default",
|
|
5063
|
+
description: "str.expandtabs() with default tabsize=8 replaces tabs with spaces",
|
|
5064
|
+
src: [
|
|
5065
|
+
"# globals: assrt",
|
|
5066
|
+
'assrt.equal(str.expandtabs("\\t"), " ")',
|
|
5067
|
+
'assrt.equal(str.expandtabs("a\\tb"), "a b")',
|
|
5068
|
+
'assrt.equal(str.expandtabs("ab\\tc"), "ab c")',
|
|
5069
|
+
].join("\n"),
|
|
5070
|
+
js_checks: ["expandtabs"],
|
|
5071
|
+
},
|
|
5072
|
+
|
|
5073
|
+
{
|
|
5074
|
+
name: "expandtabs_custom_tabsize",
|
|
5075
|
+
description: "str.expandtabs(tabsize) respects a custom tabsize",
|
|
5076
|
+
src: [
|
|
5077
|
+
"# globals: assrt",
|
|
5078
|
+
'assrt.equal(str.expandtabs("\\t", 4), " ")',
|
|
5079
|
+
'assrt.equal(str.expandtabs("a\\tb", 4), "a b")',
|
|
5080
|
+
'assrt.equal(str.expandtabs("abc\\td", 4), "abc d")',
|
|
5081
|
+
'assrt.equal(str.expandtabs("ab\\tcd", 4), "ab cd")',
|
|
5082
|
+
].join("\n"),
|
|
5083
|
+
js_checks: [],
|
|
5084
|
+
},
|
|
5085
|
+
|
|
5086
|
+
{
|
|
5087
|
+
name: "expandtabs_tabsize_zero",
|
|
5088
|
+
description: "str.expandtabs(0) removes all tab characters",
|
|
5089
|
+
src: [
|
|
5090
|
+
"# globals: assrt",
|
|
5091
|
+
'assrt.equal(str.expandtabs("a\\tb\\tc", 0), "abc")',
|
|
5092
|
+
'assrt.equal(str.expandtabs("\\t\\t", 0), "")',
|
|
5093
|
+
].join("\n"),
|
|
5094
|
+
js_checks: [],
|
|
5095
|
+
},
|
|
5096
|
+
|
|
5097
|
+
{
|
|
5098
|
+
name: "expandtabs_newline_resets_column",
|
|
5099
|
+
description: "str.expandtabs() resets column counter at newlines",
|
|
5100
|
+
src: [
|
|
5101
|
+
"# globals: assrt",
|
|
5102
|
+
'assrt.equal(str.expandtabs("a\\n\\tb", 4), "a\\n b")',
|
|
5103
|
+
'assrt.equal(str.expandtabs("abc\\n\\td", 4), "abc\\n d")',
|
|
5104
|
+
].join("\n"),
|
|
5105
|
+
js_checks: [],
|
|
5106
|
+
},
|
|
5107
|
+
|
|
5108
|
+
{
|
|
5109
|
+
name: "expandtabs_instance_method",
|
|
5110
|
+
description: "expandtabs works as an instance method via str.prototype",
|
|
5111
|
+
src: [
|
|
5112
|
+
"# globals: assrt",
|
|
5113
|
+
"from pythonize import strings",
|
|
5114
|
+
"strings()",
|
|
5115
|
+
'assrt.equal("\\t".expandtabs(), " ")',
|
|
5116
|
+
'assrt.equal("a\\tb".expandtabs(4), "a b")',
|
|
5117
|
+
].join("\n"),
|
|
5118
|
+
js_checks: [],
|
|
5119
|
+
},
|
|
5120
|
+
|
|
5121
|
+
{
|
|
5122
|
+
name: "expandtabs_no_tabs",
|
|
5123
|
+
description: "str.expandtabs() returns string unchanged when no tabs present",
|
|
5124
|
+
src: [
|
|
5125
|
+
"# globals: assrt",
|
|
5126
|
+
'assrt.equal(str.expandtabs("hello world"), "hello world")',
|
|
5127
|
+
'assrt.equal(str.expandtabs(""), "")',
|
|
5128
|
+
].join("\n"),
|
|
5129
|
+
js_checks: [],
|
|
5130
|
+
},
|
|
5131
|
+
|
|
5132
|
+
// ── legacy_rapydscript default ────────────────────────────────────────
|
|
5133
|
+
|
|
5134
|
+
{
|
|
5135
|
+
name: "legacy_rapydscript_false_enables_overload_operators",
|
|
5136
|
+
description: "default (legacy_rapydscript=false): overload_operators active; a+b uses ρσ_op_add",
|
|
5137
|
+
run: function() {
|
|
5138
|
+
// Use unique variable names so the pattern is only from user code, not baselib
|
|
5139
|
+
var js = compile_python_mode("myval = myarg_a + myarg_b");
|
|
5140
|
+
assert.ok(js.indexOf("ρσ_op_add(myarg_a, myarg_b)") !== -1,
|
|
5141
|
+
"expected ρσ_op_add(myarg_a, myarg_b) in python mode; got:\n" + js);
|
|
5142
|
+
},
|
|
5143
|
+
},
|
|
5144
|
+
|
|
5145
|
+
{
|
|
5146
|
+
name: "legacy_rapydscript_true_no_overload_operators",
|
|
5147
|
+
description: "legacy mode (no flags): a+b uses ρσ_list_add, not ρσ_op_add",
|
|
5148
|
+
run: function() {
|
|
5149
|
+
// compile() uses no scoped_flags → no overload_operators
|
|
5150
|
+
var js = compile("myval = myarg_a + myarg_b");
|
|
5151
|
+
assert.ok(js.indexOf("ρσ_op_add(myarg_a") === -1,
|
|
5152
|
+
"unexpected ρσ_op_add(myarg_a in legacy mode; got:\n" + js);
|
|
5153
|
+
assert.ok(js.indexOf("ρσ_list_add(myarg_a") !== -1,
|
|
5154
|
+
"expected ρσ_list_add(myarg_a in legacy mode; got:\n" + js);
|
|
5155
|
+
},
|
|
5156
|
+
},
|
|
5157
|
+
|
|
5158
|
+
{
|
|
5159
|
+
name: "legacy_rapydscript_false_enables_dict_literals",
|
|
5160
|
+
description: "default (legacy_rapydscript=false): dict_literals active; {} compiles to ρσ_dict()",
|
|
5161
|
+
run: function() {
|
|
5162
|
+
var js = compile_python_mode("mydict = {}");
|
|
5163
|
+
assert.ok(js.indexOf("dict_literal") !== -1 || js.indexOf("ρσ_dict") !== -1,
|
|
5164
|
+
"expected dict() wrapper with dict_literals enabled (python mode); got:\n" + js);
|
|
5165
|
+
},
|
|
5166
|
+
},
|
|
5167
|
+
|
|
5168
|
+
{
|
|
5169
|
+
name: "legacy_rapydscript_true_no_dict_literals",
|
|
5170
|
+
description: "legacy mode (no flags): {} compiles to a plain JS object literal",
|
|
5171
|
+
run: function() {
|
|
5172
|
+
var js = compile("mydict = {}");
|
|
5173
|
+
// The assignment line should not contain dict_literal or ρσ_dict
|
|
5174
|
+
var lines = js.split("\n").filter(function(l) { return l.indexOf("mydict") !== -1; });
|
|
5175
|
+
var userline = lines.join("\n");
|
|
5176
|
+
assert.ok(userline.indexOf("dict_literal") === -1 && userline.indexOf("ρσ_dict") === -1,
|
|
5177
|
+
"unexpected dict() wrapper in legacy mode; got:\n" + userline);
|
|
5178
|
+
},
|
|
5179
|
+
},
|
|
5180
|
+
|
|
5181
|
+
{
|
|
5182
|
+
name: "legacy_rapydscript_false_enables_truthiness",
|
|
5183
|
+
description: "default (legacy_rapydscript=false): truthiness active; if-condition wrapped in ρσ_bool()",
|
|
5184
|
+
run: function() {
|
|
5185
|
+
var js = compile_python_mode("if mycond:\n pass");
|
|
5186
|
+
assert.ok(/if\s*\(ρσ_bool\(mycond/.test(js),
|
|
5187
|
+
"expected if(ρσ_bool(mycond in python mode; got:\n" + js);
|
|
5188
|
+
},
|
|
5189
|
+
},
|
|
5190
|
+
|
|
5191
|
+
{
|
|
5192
|
+
name: "legacy_rapydscript_true_no_truthiness",
|
|
5193
|
+
description: "legacy mode (no flags): if-condition is not wrapped in ρσ_bool()",
|
|
5194
|
+
run: function() {
|
|
5195
|
+
var js = compile("if mycond:\n pass");
|
|
5196
|
+
assert.ok(!/if\s*\(ρσ_bool\(mycond/.test(js),
|
|
5197
|
+
"unexpected ρσ_bool(mycond wrapping in legacy mode; got:\n" + js);
|
|
5198
|
+
},
|
|
5199
|
+
},
|
|
5200
|
+
|
|
5201
|
+
{
|
|
5202
|
+
name: "legacy_rapydscript_false_enables_bound_methods",
|
|
5203
|
+
description: "default (legacy_rapydscript=false): bound_methods active; methods are .bind()-ed",
|
|
5204
|
+
run: function() {
|
|
5205
|
+
var js = compile_python_mode([
|
|
5206
|
+
"class MyFoo:",
|
|
5207
|
+
" def mybar(self):",
|
|
5208
|
+
" return 1",
|
|
5209
|
+
].join("\n"));
|
|
5210
|
+
// Bound methods produce a .bind(this) call in the class prototype setup
|
|
5211
|
+
assert.ok(/mybar.*bind/.test(js) || /bind.*mybar/.test(js),
|
|
5212
|
+
"expected .bind() for mybar with bound_methods enabled (python mode); got:\n" + js);
|
|
5213
|
+
},
|
|
5214
|
+
},
|
|
5215
|
+
|
|
5216
|
+
{
|
|
5217
|
+
name: "legacy_rapydscript_true_no_bound_methods",
|
|
5218
|
+
description: "legacy mode (no flags): methods are not .bind()-ed",
|
|
5219
|
+
run: function() {
|
|
5220
|
+
var js = compile([
|
|
5221
|
+
"class MyFoo:",
|
|
5222
|
+
" def mybar(self):",
|
|
5223
|
+
" return 1",
|
|
5224
|
+
].join("\n"));
|
|
5225
|
+
assert.ok(!/mybar.*bind/.test(js) && !/bind.*mybar/.test(js),
|
|
5226
|
+
"unexpected .bind() for mybar in legacy mode; got:\n" + js);
|
|
5227
|
+
},
|
|
5228
|
+
},
|
|
5229
|
+
|
|
5230
|
+
];
|
|
5231
|
+
|
|
5232
|
+
// ── Runner ───────────────────────────────────────────────────────────────────
|
|
5233
|
+
|
|
5234
|
+
function run_tests(filter) {
|
|
5235
|
+
var tests = filter
|
|
5236
|
+
? TESTS.filter(function (t) { return t.name === filter; })
|
|
5237
|
+
: TESTS;
|
|
5238
|
+
|
|
5239
|
+
if (tests.length === 0) {
|
|
5240
|
+
console.error(colored("No test found: " + filter, "red"));
|
|
5241
|
+
process.exit(1);
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
var failures = [];
|
|
5245
|
+
|
|
5246
|
+
tests.forEach(function (test) {
|
|
5247
|
+
|
|
5248
|
+
// Custom run function (for tests that need direct JS-level control)
|
|
5249
|
+
if (typeof test.run === "function") {
|
|
5250
|
+
try {
|
|
5251
|
+
test.run();
|
|
5252
|
+
} catch (e) {
|
|
5253
|
+
failures.push(test.name);
|
|
5254
|
+
var msg = e.stack || String(e);
|
|
5255
|
+
console.log(colored("FAIL " + test.name, "red") +
|
|
5256
|
+
" [run]\n " + msg + "\n");
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
console.log(colored("PASS " + test.name, "green") +
|
|
5260
|
+
" – " + test.description);
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
var js;
|
|
5265
|
+
|
|
5266
|
+
// 1 – compile RapydScript → JS
|
|
5267
|
+
try {
|
|
5268
|
+
js = test.virtual_files ? compile_virtual(test.src, test.virtual_files) : compile(test.src);
|
|
5269
|
+
} catch (e) {
|
|
5270
|
+
failures.push(test.name);
|
|
5271
|
+
console.log(colored("FAIL " + test.name, "red") +
|
|
5272
|
+
" [compile error]\n " + e + "\n");
|
|
5273
|
+
return;
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
// 2 – verify expected patterns appear in the JS output
|
|
5277
|
+
try {
|
|
5278
|
+
check_js_patterns(test.name, js, test.js_checks);
|
|
5279
|
+
// also check patterns that must NOT appear
|
|
5280
|
+
(test.js_not_checks || []).forEach(function (pat) {
|
|
5281
|
+
var found = (pat instanceof RegExp) ? pat.test(js) : js.indexOf(pat) !== -1;
|
|
5282
|
+
if (found) {
|
|
5283
|
+
var desc = (pat instanceof RegExp) ? String(pat) : JSON.stringify(pat);
|
|
5284
|
+
throw new Error("compiled JS unexpectedly contains " + desc + "\n in test: " + test.name);
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
} catch (e) {
|
|
5288
|
+
failures.push(test.name);
|
|
5289
|
+
console.debug("Emitted JS:\n" + js + "\n");
|
|
5290
|
+
console.log(colored("FAIL " + test.name, "red") +
|
|
5291
|
+
" [JS pattern mismatch]\n " + e.message + "\n");
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
// 3 – run the JS; assertions embedded in src catch wrong values
|
|
5296
|
+
// (skipped for tests that produce JSX or other non-executable output)
|
|
5297
|
+
if (!test.skip_run) {
|
|
5298
|
+
try {
|
|
5299
|
+
run_js(js);
|
|
5300
|
+
} catch (e) {
|
|
5301
|
+
failures.push(test.name);
|
|
5302
|
+
var msg = e.stack || String(e);
|
|
5303
|
+
console.log(colored("FAIL " + test.name, "red") +
|
|
5304
|
+
" [runtime]\n " + msg + "\n");
|
|
5305
|
+
return;
|
|
5306
|
+
}
|
|
3085
5307
|
}
|
|
3086
5308
|
|
|
3087
5309
|
console.log(colored("PASS " + test.name, "green") +
|