rapydscript-ns 0.8.2 → 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 +39 -0
- package/HACKING.md +103 -103
- package/LICENSE +24 -24
- package/PYTHON_DIFFERENCES_REPORT.md +291 -0
- package/PYTHON_FEATURE_COVERAGE.md +106 -15
- package/README.md +831 -52
- package/TODO.md +4 -286
- 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 +4623 -0
- package/language-service/language-service.d.ts +40 -0
- package/package.json +9 -7
- 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 +99 -2
- package/src/baselib-containers.pyj +107 -4
- package/src/baselib-errors.pyj +44 -0
- package/src/baselib-internal.pyj +124 -5
- package/src/baselib-itertools.pyj +97 -97
- package/src/baselib-str.pyj +32 -1
- package/src/compiler.pyj +36 -36
- package/src/errors.pyj +30 -30
- package/src/lib/aes.pyj +646 -646
- package/src/lib/collections.pyj +1 -1
- 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/numpy.pyj +10 -10
- 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/analyzer.js +131 -9
- package/src/monaco-language-service/builtins.js +17 -2
- package/src/monaco-language-service/completions.js +170 -1
- package/src/monaco-language-service/diagnostics.js +25 -3
- package/src/monaco-language-service/dts.js +550 -550
- package/src/monaco-language-service/index.js +17 -0
- package/src/monaco-language-service/scope.js +3 -0
- package/src/output/classes.pyj +128 -11
- package/src/output/codegen.pyj +17 -3
- package/src/output/comments.pyj +45 -45
- package/src/output/exceptions.pyj +201 -105
- package/src/output/functions.pyj +13 -16
- package/src/output/jsx.pyj +164 -0
- package/src/output/literals.pyj +28 -2
- package/src/output/loops.pyj +0 -9
- package/src/output/modules.pyj +2 -5
- package/src/output/operators.pyj +22 -2
- package/src/output/statements.pyj +2 -2
- package/src/output/stream.pyj +1 -13
- package/src/output/treeshake.pyj +182 -182
- package/src/output/utils.pyj +72 -72
- package/src/parse.pyj +434 -114
- 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/baselib.pyj +4 -4
- package/test/classes.pyj +56 -17
- package/test/collections.pyj +5 -5
- 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_compat.pyj +326 -0
- package/test/python_features.pyj +129 -29
- package/test/regexp.pyj +55 -55
- package/test/repl.pyj +121 -121
- package/test/scoped_flags.pyj +76 -76
- package/test/slice.pyj +105 -0
- package/test/str.pyj +25 -0
- package/test/unit/fixtures/fibonacci_expected.js +1 -1
- package/test/unit/index.js +2296 -71
- package/test/unit/language-service-builtins.js +70 -0
- package/test/unit/language-service-bundle.js +5 -5
- package/test/unit/language-service-completions.js +180 -0
- package/test/unit/language-service-dts.js +543 -543
- package/test/unit/language-service-hover.js +455 -455
- package/test/unit/language-service-index.js +350 -0
- package/test/unit/language-service-scope.js +255 -0
- package/test/unit/language-service.js +625 -4
- package/test/unit/run-language-service.js +1 -0
- package/test/unit/web-repl.js +437 -0
- package/tools/build-language-service.js +2 -2
- package/tools/cli.js +547 -547
- package/tools/compile.js +219 -219
- package/tools/compiler.js +0 -24
- package/tools/completer.js +131 -131
- package/tools/embedded_compiler.js +251 -251
- package/tools/export.js +3 -37
- 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 +227 -139
- package/web-repl/sha1.js +25 -25
- package/hack_demo.pyj +0 -112
- package/web-repl/language-service.js +0 -4187
package/test/unit/index.js
CHANGED
|
@@ -185,6 +185,84 @@ var TESTS = [
|
|
|
185
185
|
js_checks: ["Math.pow(2, 10)", "Math.pow(3, 3)", "Math.pow(10, 0)"],
|
|
186
186
|
},
|
|
187
187
|
|
|
188
|
+
// ── List concatenation ────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
name: "list_concatenation_literals",
|
|
192
|
+
description: "list + list returns a new concatenated list (literal operands)",
|
|
193
|
+
src: [
|
|
194
|
+
"# globals: assrt",
|
|
195
|
+
"result = [1, 2] + [3, 4]",
|
|
196
|
+
"assrt.deepEqual(result, [1, 2, 3, 4])",
|
|
197
|
+
"assrt.equal(result.length, 4)",
|
|
198
|
+
"# originals not mutated",
|
|
199
|
+
"a = [1, 2]",
|
|
200
|
+
"b = [3, 4]",
|
|
201
|
+
"c = a + b",
|
|
202
|
+
"assrt.deepEqual(c, [1, 2, 3, 4])",
|
|
203
|
+
"assrt.deepEqual(a, [1, 2])",
|
|
204
|
+
"assrt.deepEqual(b, [3, 4])",
|
|
205
|
+
].join("\n"),
|
|
206
|
+
js_checks: ["ρσ_list_add("],
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
{
|
|
210
|
+
name: "list_concatenation_variables",
|
|
211
|
+
description: "list + list works with variable references",
|
|
212
|
+
src: [
|
|
213
|
+
"# globals: assrt",
|
|
214
|
+
"a = [10, 20]",
|
|
215
|
+
"b = [30]",
|
|
216
|
+
"assrt.deepEqual(a + b, [10, 20, 30])",
|
|
217
|
+
"assrt.deepEqual([] + [1], [1])",
|
|
218
|
+
"assrt.deepEqual([1] + [], [1])",
|
|
219
|
+
"assrt.deepEqual([] + [], [])",
|
|
220
|
+
].join("\n"),
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
name: "list_iadd_extends_in_place",
|
|
225
|
+
description: "list += list extends the list in-place (same object)",
|
|
226
|
+
src: [
|
|
227
|
+
"# globals: assrt",
|
|
228
|
+
"a = [1, 2]",
|
|
229
|
+
"ref = a",
|
|
230
|
+
"a += [3, 4]",
|
|
231
|
+
"assrt.deepEqual(a, [1, 2, 3, 4])",
|
|
232
|
+
"# ref still points to same object, now extended",
|
|
233
|
+
"assrt.deepEqual(ref, [1, 2, 3, 4])",
|
|
234
|
+
"assrt.ok(a is ref)",
|
|
235
|
+
].join("\n"),
|
|
236
|
+
js_checks: ["ρσ_list_iadd("],
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
name: "list_concat_does_not_break_number_add",
|
|
241
|
+
description: "numbers still add correctly after list-concat helpers are introduced",
|
|
242
|
+
src: [
|
|
243
|
+
"# globals: assrt",
|
|
244
|
+
"assrt.equal(1 + 2, 3)",
|
|
245
|
+
"assrt.equal(3 + 4, 7)",
|
|
246
|
+
"assrt.equal(0 + 0, 0)",
|
|
247
|
+
"x = 10",
|
|
248
|
+
"x += 5",
|
|
249
|
+
"assrt.equal(x, 15)",
|
|
250
|
+
].join("\n"),
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
name: "list_concat_does_not_break_string_concat",
|
|
255
|
+
description: "string concatenation still works correctly",
|
|
256
|
+
src: [
|
|
257
|
+
"# globals: assrt",
|
|
258
|
+
"assrt.equal('hello' + ' world', 'hello world')",
|
|
259
|
+
"assrt.equal('a' + 'b' + 'c', 'abc')",
|
|
260
|
+
"s = 'foo'",
|
|
261
|
+
"s += 'bar'",
|
|
262
|
+
"assrt.equal(s, 'foobar')",
|
|
263
|
+
].join("\n"),
|
|
264
|
+
},
|
|
265
|
+
|
|
188
266
|
{
|
|
189
267
|
name: "not_operator",
|
|
190
268
|
description: '"not" compiles to "!"',
|
|
@@ -424,8 +502,8 @@ var TESTS = [
|
|
|
424
502
|
"increment()",
|
|
425
503
|
"assrt.equal(counter, 3)",
|
|
426
504
|
].join("\n"),
|
|
427
|
-
// nonlocal → the outer variable is accessed/modified directly
|
|
428
|
-
js_checks: ["counter
|
|
505
|
+
// nonlocal → the outer variable is accessed/modified directly via ρσ_list_iadd
|
|
506
|
+
js_checks: ["ρσ_list_iadd(counter,"],
|
|
429
507
|
},
|
|
430
508
|
|
|
431
509
|
// ── Classes ───────────────────────────────────────────────────────────
|
|
@@ -604,6 +682,119 @@ var TESTS = [
|
|
|
604
682
|
].join("\n"),
|
|
605
683
|
},
|
|
606
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
|
+
|
|
607
798
|
// ── Verbatim JS ───────────────────────────────────────────────────────
|
|
608
799
|
|
|
609
800
|
{
|
|
@@ -697,6 +888,102 @@ assrt.equal(fib(15), 610)
|
|
|
697
888
|
js_checks: ["Circle.prototype", "function double(x)"],
|
|
698
889
|
},
|
|
699
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
|
+
|
|
700
987
|
// ── Walrus operator (:=) ──────────────────────────────────────────────
|
|
701
988
|
|
|
702
989
|
{
|
|
@@ -1093,7 +1380,7 @@ assrt.equal(fib(15), 610)
|
|
|
1093
1380
|
" return fn(x)",
|
|
1094
1381
|
"assrt.equal(apply(lambda x: x * x, 5), 25)",
|
|
1095
1382
|
"nums = [3, 1, 2]",
|
|
1096
|
-
"nums.sort(
|
|
1383
|
+
"nums.sort()",
|
|
1097
1384
|
"assrt.deepEqual(nums, [1, 2, 3])",
|
|
1098
1385
|
].join("\n"),
|
|
1099
1386
|
js_checks: [],
|
|
@@ -2664,15 +2951,14 @@ assrt.equal(fib(15), 610)
|
|
|
2664
2951
|
|
|
2665
2952
|
{
|
|
2666
2953
|
name: "operator_overloading_no_flag",
|
|
2667
|
-
description: "Without overload_operators flag
|
|
2954
|
+
description: "Without overload_operators flag: + uses ρσ_list_add (not ρσ_op_add), * is native",
|
|
2668
2955
|
src: [
|
|
2669
2956
|
"# globals: assrt",
|
|
2670
2957
|
"assrt.equal(2 + 3, 5)",
|
|
2671
2958
|
"assrt.equal(3 * 4, 12)",
|
|
2672
2959
|
].join("\n"),
|
|
2673
|
-
//
|
|
2674
|
-
js_checks: [
|
|
2675
|
-
js_not_checks: [],
|
|
2960
|
+
// + should compile to ρσ_list_add (lightweight list/number/string helper)
|
|
2961
|
+
js_checks: ["ρσ_list_add(2, 3)", /assrt\.equal\(3 \* 4, 12\)/],
|
|
2676
2962
|
},
|
|
2677
2963
|
|
|
2678
2964
|
{
|
|
@@ -2874,6 +3160,41 @@ assrt.equal(fib(15), 610)
|
|
|
2874
3160
|
},
|
|
2875
3161
|
},
|
|
2876
3162
|
|
|
3163
|
+
{
|
|
3164
|
+
name: "python_flag_truthiness_via_scoped_flags",
|
|
3165
|
+
description: "truthiness flag via scoped_flags wraps if-conditions with ρσ_bool(; plain code does not",
|
|
3166
|
+
run: function() {
|
|
3167
|
+
var src = "if x:\n pass";
|
|
3168
|
+
var src_inline = "from __python__ import truthiness\n" + src;
|
|
3169
|
+
var js_inline = compile(src_inline);
|
|
3170
|
+
var js_flagged = compile_with_flags(src, { truthiness: true });
|
|
3171
|
+
var js_plain = compile(src);
|
|
3172
|
+
assert.ok(/if\s*\(ρσ_bool\(/.test(js_inline),
|
|
3173
|
+
"inline import: expected if(ρσ_bool( in: " + js_inline);
|
|
3174
|
+
assert.ok(/if\s*\(ρσ_bool\(/.test(js_flagged),
|
|
3175
|
+
"scoped_flags: expected if(ρσ_bool( in: " + js_flagged);
|
|
3176
|
+
assert.ok(!/if\s*\(ρσ_bool\(/.test(js_plain),
|
|
3177
|
+
"no flag: if(ρσ_bool( should NOT appear in: " + js_plain);
|
|
3178
|
+
},
|
|
3179
|
+
},
|
|
3180
|
+
|
|
3181
|
+
{
|
|
3182
|
+
name: "python_flag_truthiness_via_python_flags_option",
|
|
3183
|
+
description: "truthiness passed as python_flags string to embedded_compiler wraps if-conditions",
|
|
3184
|
+
run: function() {
|
|
3185
|
+
var make_ec = require("../../tools/embedded_compiler.js");
|
|
3186
|
+
var src = "if x:\n pass";
|
|
3187
|
+
var ec_with = make_ec(RapydScript, baselib, null);
|
|
3188
|
+
var js_with = ec_with.compile(src, { python_flags: "truthiness" });
|
|
3189
|
+
assert.ok(/if\s*\(ρσ_bool\(/.test(js_with),
|
|
3190
|
+
"python_flags='truthiness': expected if(ρσ_bool( in: " + js_with);
|
|
3191
|
+
var ec_without = make_ec(RapydScript, baselib, null);
|
|
3192
|
+
var js_without = ec_without.compile(src, {});
|
|
3193
|
+
assert.ok(!/if\s*\(ρσ_bool\(/.test(js_without),
|
|
3194
|
+
"no python_flags: if(ρσ_bool( should NOT appear in: " + js_without);
|
|
3195
|
+
},
|
|
3196
|
+
},
|
|
3197
|
+
|
|
2877
3198
|
{
|
|
2878
3199
|
name: "python_flag_all_flags_runtime",
|
|
2879
3200
|
description: "overload_operators + overload_getitem work correctly at runtime via scoped_flags",
|
|
@@ -2896,80 +3217,1984 @@ assrt.equal(fib(15), 610)
|
|
|
2896
3217
|
js_checks: ["ρσ_op_add", "__getitem__"],
|
|
2897
3218
|
},
|
|
2898
3219
|
|
|
2899
|
-
|
|
3220
|
+
// ── JSX ───────────────────────────────────────────────────────────────
|
|
2900
3221
|
|
|
2901
|
-
|
|
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
|
+
},
|
|
2902
3233
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
:
|
|
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
|
+
},
|
|
2907
3245
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
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
|
+
},
|
|
2912
3257
|
|
|
2913
|
-
|
|
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
|
+
},
|
|
2914
3269
|
|
|
2915
|
-
|
|
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
|
+
},
|
|
2916
3281
|
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
console.log(colored("PASS " + test.name, "green") +
|
|
2929
|
-
" – " + test.description);
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
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
|
+
},
|
|
2932
3293
|
|
|
2933
|
-
|
|
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
|
+
},
|
|
2934
3305
|
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
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
|
+
},
|
|
2944
3317
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
console.log(colored("FAIL " + test.name, "red") +
|
|
2960
|
-
" [JS pattern mismatch]\n " + e.message + "\n");
|
|
2961
|
-
return;
|
|
2962
|
-
}
|
|
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
|
+
},
|
|
2963
3332
|
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
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
|
+
}
|
|
2973
5198
|
}
|
|
2974
5199
|
|
|
2975
5200
|
console.log(colored("PASS " + test.name, "green") +
|