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