rapydscript-ns 0.9.2 → 0.9.3

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.
Files changed (151) hide show
  1. package/.agignore +1 -1
  2. package/.github/workflows/ci.yml +38 -38
  3. package/=template.pyj +5 -5
  4. package/CHANGELOG.md +19 -0
  5. package/HACKING.md +103 -103
  6. package/LICENSE +24 -24
  7. package/PYTHON_GAPS.md +420 -0
  8. package/README.md +153 -29
  9. package/TODO.md +16 -118
  10. package/add-toc-to-readme +2 -2
  11. package/bin/export +75 -75
  12. package/bin/rapydscript +70 -70
  13. package/bin/web-repl-export +102 -102
  14. package/build +2 -2
  15. package/language-service/index.js +237 -8
  16. package/memory/project_string_impl.md +43 -0
  17. package/package.json +1 -1
  18. package/publish.py +37 -37
  19. package/release/baselib-plain-pretty.js +248 -38
  20. package/release/baselib-plain-ugly.js +8 -8
  21. package/release/compiler.js +778 -277
  22. package/release/signatures.json +30 -30
  23. package/session.vim +4 -4
  24. package/setup.cfg +2 -2
  25. package/src/ast.pyj +4 -1
  26. package/src/baselib-builtins.pyj +56 -2
  27. package/src/baselib-containers.pyj +2 -0
  28. package/src/baselib-errors.pyj +7 -3
  29. package/src/baselib-internal.pyj +51 -6
  30. package/src/baselib-str.pyj +5 -3
  31. package/src/compiler.pyj +36 -36
  32. package/src/errors.pyj +30 -30
  33. package/src/lib/aes.pyj +646 -646
  34. package/src/lib/asyncio.pyj +534 -0
  35. package/src/lib/base64.pyj +399 -0
  36. package/src/lib/bisect.pyj +73 -0
  37. package/src/lib/collections.pyj +1 -1
  38. package/src/lib/copy.pyj +120 -120
  39. package/src/lib/csv.pyj +494 -0
  40. package/src/lib/elementmaker.pyj +83 -83
  41. package/src/lib/encodings.pyj +126 -126
  42. package/src/lib/gettext.pyj +569 -569
  43. package/src/lib/heapq.pyj +98 -0
  44. package/src/lib/html.pyj +382 -0
  45. package/src/lib/http/__init__.pyj +98 -0
  46. package/src/lib/http/client.pyj +304 -0
  47. package/src/lib/http/cookies.pyj +236 -0
  48. package/src/lib/itertools.pyj +580 -580
  49. package/src/lib/logging.pyj +672 -0
  50. package/src/lib/math.pyj +193 -193
  51. package/src/lib/operator.pyj +11 -11
  52. package/src/lib/pythonize.pyj +20 -20
  53. package/src/lib/random.pyj +118 -118
  54. package/src/lib/react.pyj +74 -74
  55. package/src/lib/string.pyj +357 -0
  56. package/src/lib/textwrap.pyj +329 -0
  57. package/src/lib/traceback.pyj +63 -63
  58. package/src/lib/urllib/__init__.pyj +14 -0
  59. package/src/lib/urllib/error.pyj +66 -0
  60. package/src/lib/urllib/parse.pyj +475 -0
  61. package/src/lib/urllib/request.pyj +86 -0
  62. package/src/lib/uuid.pyj +77 -77
  63. package/src/monaco-language-service/analyzer.js +5 -2
  64. package/src/monaco-language-service/completions.js +26 -0
  65. package/src/monaco-language-service/diagnostics.js +202 -3
  66. package/src/monaco-language-service/dts.js +550 -550
  67. package/src/monaco-language-service/scope.js +1 -0
  68. package/src/output/comments.pyj +45 -45
  69. package/src/output/exceptions.pyj +201 -201
  70. package/src/output/functions.pyj +152 -6
  71. package/src/output/jsx.pyj +164 -164
  72. package/src/output/loops.pyj +17 -2
  73. package/src/output/modules.pyj +1 -1
  74. package/src/output/operators.pyj +15 -0
  75. package/src/output/stream.pyj +0 -1
  76. package/src/output/treeshake.pyj +182 -182
  77. package/src/output/utils.pyj +72 -72
  78. package/src/parse.pyj +80 -17
  79. package/src/string_interpolation.pyj +72 -72
  80. package/src/tokenizer.pyj +1 -1
  81. package/src/unicode_aliases.pyj +576 -576
  82. package/src/utils.pyj +192 -192
  83. package/test/_import_one.pyj +37 -37
  84. package/test/_import_two/__init__.pyj +11 -11
  85. package/test/_import_two/level2/deep.pyj +4 -4
  86. package/test/_import_two/other.pyj +6 -6
  87. package/test/_import_two/sub.pyj +13 -13
  88. package/test/aes_vectors.pyj +421 -421
  89. package/test/annotations.pyj +80 -80
  90. package/test/async_generators.pyj +144 -0
  91. package/test/asyncio.pyj +307 -0
  92. package/test/base64.pyj +202 -0
  93. package/test/bisect.pyj +178 -0
  94. package/test/csv.pyj +405 -0
  95. package/test/decorators.pyj +77 -77
  96. package/test/docstrings.pyj +39 -39
  97. package/test/elementmaker_test.pyj +45 -45
  98. package/test/float_special.pyj +64 -0
  99. package/test/functions.pyj +151 -151
  100. package/test/generators.pyj +41 -41
  101. package/test/generic.pyj +370 -370
  102. package/test/heapq.pyj +174 -0
  103. package/test/html.pyj +212 -0
  104. package/test/http.pyj +259 -0
  105. package/test/imports.pyj +79 -72
  106. package/test/internationalization.pyj +73 -73
  107. package/test/lint.pyj +164 -164
  108. package/test/logging.pyj +356 -0
  109. package/test/long.pyj +130 -0
  110. package/test/loops.pyj +85 -85
  111. package/test/numpy.pyj +734 -734
  112. package/test/parenthesized_with.pyj +141 -0
  113. package/test/python_compat.pyj +3 -5
  114. package/test/python_modulo.pyj +76 -0
  115. package/test/python_modulo_off.pyj +21 -0
  116. package/test/repl.pyj +121 -121
  117. package/test/scoped_flags.pyj +76 -76
  118. package/test/str.pyj +14 -0
  119. package/test/string.pyj +245 -0
  120. package/test/textwrap.pyj +172 -0
  121. package/test/type_display.pyj +48 -0
  122. package/test/type_enforcement.pyj +164 -0
  123. package/test/unit/index.js +14 -6
  124. package/test/unit/language-service-completions.js +119 -0
  125. package/test/unit/language-service-dts.js +543 -543
  126. package/test/unit/language-service-hover.js +455 -455
  127. package/test/unit/language-service-scope.js +32 -0
  128. package/test/unit/language-service.js +127 -3
  129. package/test/unit/run-language-service.js +17 -3
  130. package/test/unit/web-repl.js +2094 -29
  131. package/test/urllib.pyj +193 -0
  132. package/tools/compile.js +1 -1
  133. package/tools/compiler.d.ts +367 -367
  134. package/tools/completer.js +131 -131
  135. package/tools/embedded_compiler.js +7 -7
  136. package/tools/gettext.js +185 -185
  137. package/tools/ini.js +65 -65
  138. package/tools/msgfmt.js +187 -187
  139. package/tools/repl.js +223 -223
  140. package/tools/test.js +118 -118
  141. package/tools/utils.js +128 -128
  142. package/tools/web_repl.js +95 -95
  143. package/try +41 -41
  144. package/web-repl/env.js +196 -196
  145. package/web-repl/index.html +163 -163
  146. package/web-repl/main.js +1 -1
  147. package/web-repl/prism.css +139 -139
  148. package/web-repl/prism.js +113 -113
  149. package/web-repl/rapydscript.js +224 -224
  150. package/web-repl/sha1.js +25 -25
  151. package/test/omit_function_metadata.pyj +0 -20
@@ -103,7 +103,6 @@ assert.deepEqual = function (a, b, message) {
103
103
  // Compile RapydScript using the web-repl's own compile() path (same as browser)
104
104
  function bundle_compile(repl, src) {
105
105
  return repl.compile(src, {
106
- omit_function_metadata: false,
107
106
  tree_shake: false,
108
107
  keep_baselib: true,
109
108
  });
@@ -1233,6 +1232,71 @@ var TESTS = [
1233
1232
  },
1234
1233
  },
1235
1234
 
1235
+ // ── f-string {x=} debugging format ───────────────────────────────────────
1236
+
1237
+ {
1238
+ name: "bundle_fstring_debug_simple",
1239
+ description: "f'{x=}' produces 'x=<value>' for simple variable",
1240
+ run: function () {
1241
+ var repl = RS.web_repl();
1242
+ var js = bundle_compile(repl, [
1243
+ "x = 42",
1244
+ "name = 'Alice'",
1245
+ "flag = True",
1246
+ "assrt.equal(f'{x=}', 'x=42')",
1247
+ "assrt.equal(f'{name=}', \"name=Alice\")",
1248
+ "assrt.equal(f'{flag=}', 'flag=true')",
1249
+ ].join("\n"));
1250
+ run_js(js);
1251
+ },
1252
+ },
1253
+
1254
+ {
1255
+ name: "bundle_fstring_debug_expr",
1256
+ description: "f'{expr=}' preserves the full expression text as prefix",
1257
+ run: function () {
1258
+ var repl = RS.web_repl();
1259
+ var js = bundle_compile(repl, [
1260
+ "x = 5",
1261
+ "nums = [10, 20, 30]",
1262
+ "assrt.equal(f'{x*2=}', 'x*2=10')",
1263
+ "assrt.equal(f'{nums[1]=}', 'nums[1]=20')",
1264
+ "assrt.equal(f'{x=} and {x*2=}', 'x=5 and x*2=10')",
1265
+ ].join("\n"));
1266
+ run_js(js);
1267
+ },
1268
+ },
1269
+
1270
+ {
1271
+ name: "bundle_fstring_debug_format_spec",
1272
+ description: "f'{x=:.2f}' combines debugging prefix with format spec",
1273
+ run: function () {
1274
+ var repl = RS.web_repl();
1275
+ var js = bundle_compile(repl, [
1276
+ "pi = 3.14159",
1277
+ "n = 1234567",
1278
+ "assrt.equal(f'{pi=:.2f}', 'pi=3.14')",
1279
+ "assrt.equal(f'{n=:,}', 'n=' + (1234567).toLocaleString())",
1280
+ ].join("\n"));
1281
+ run_js(js);
1282
+ },
1283
+ },
1284
+
1285
+ {
1286
+ name: "bundle_fstring_debug_repr",
1287
+ description: "f'{x=!r}' combines debugging prefix with !r repr conversion",
1288
+ run: function () {
1289
+ var repl = RS.web_repl();
1290
+ var js = bundle_compile(repl, [
1291
+ "msg = 'hello'",
1292
+ "nums = [1, 2, 3]",
1293
+ "assrt.equal(f'{msg=!r}', 'msg=\"hello\"')",
1294
+ "assrt.equal(f'{nums=!r}', 'nums=[1, 2, 3]')",
1295
+ ].join("\n"));
1296
+ run_js(js);
1297
+ },
1298
+ },
1299
+
1236
1300
  // ── object() builtin ──────────────────────────────────────────────────────
1237
1301
 
1238
1302
  {
@@ -2198,6 +2262,522 @@ var TESTS = [
2198
2262
  },
2199
2263
  },
2200
2264
 
2265
+ // ── float() special string values ────────────────────────────────────────
2266
+
2267
+ {
2268
+ name: "bundle_float_special_inf_nan",
2269
+ description: "float() accepts 'inf', '-inf', 'infinity', 'nan' (and variants) in the web-repl bundle",
2270
+ run: function () {
2271
+ var repl = RS.web_repl();
2272
+ var js = bundle_compile(repl, [
2273
+ // positive infinity
2274
+ "assrt.equal(float('inf'), Infinity)",
2275
+ "assrt.equal(float('+inf'), Infinity)",
2276
+ "assrt.equal(float('INF'), Infinity)",
2277
+ "assrt.equal(float('infinity'), Infinity)",
2278
+ "assrt.equal(float('+infinity'), Infinity)",
2279
+ "assrt.equal(float('Infinity'), Infinity)",
2280
+ "assrt.equal(float('INFINITY'), Infinity)",
2281
+ // negative infinity
2282
+ "assrt.equal(float('-inf'), -Infinity)",
2283
+ "assrt.equal(float('-infinity'), -Infinity)",
2284
+ "assrt.equal(float('-Infinity'), -Infinity)",
2285
+ // nan
2286
+ "assrt.ok(isNaN(float('nan')))",
2287
+ "assrt.ok(isNaN(float('NaN')))",
2288
+ "assrt.ok(isNaN(float('NAN')))",
2289
+ "assrt.ok(isNaN(float('+nan')))",
2290
+ "assrt.ok(isNaN(float('-nan')))",
2291
+ // whitespace stripped
2292
+ "assrt.equal(float(' inf '), Infinity)",
2293
+ "assrt.equal(float(' -inf '), -Infinity)",
2294
+ "assrt.ok(isNaN(float(' nan ')))",
2295
+ // numeric strings still work
2296
+ "assrt.equal(float('3.14'), 3.14)",
2297
+ "assrt.equal(float('-2.5'), -2.5)",
2298
+ // real Infinity passes through
2299
+ "assrt.equal(float(Infinity), Infinity)",
2300
+ "assrt.equal(float(-Infinity), -Infinity)",
2301
+ // ValueError still raised for bad strings
2302
+ "_err = False",
2303
+ "try:",
2304
+ " float('bad')",
2305
+ "except ValueError:",
2306
+ " _err = True",
2307
+ "assrt.ok(_err)",
2308
+ ].join("\n"));
2309
+ run_js(js);
2310
+ },
2311
+ },
2312
+
2313
+ // ── base64 stdlib ────────────────────────────────────────────────────────
2314
+
2315
+ {
2316
+ name: "bundle_base64_encode_decode",
2317
+ description: "base64 stdlib: b64encode/b64decode round-trips in the web-repl bundle",
2318
+ run: function () {
2319
+ var repl = RS.web_repl();
2320
+ var js = bundle_compile(repl, [
2321
+ "from base64 import b64encode, b64decode",
2322
+ // basic encode
2323
+ "enc = b64encode(bytes([77, 97, 110]))",
2324
+ "assrt.equal(enc.decode('ascii'), 'TWFu')",
2325
+ // empty
2326
+ "assrt.equal(len(b64encode(bytes([]))), 0)",
2327
+ // padding variants
2328
+ "assrt.equal(b64encode(bytes([0])).decode('ascii'), 'AA==')",
2329
+ "assrt.equal(b64encode(bytes([0, 0])).decode('ascii'), 'AAA=')",
2330
+ "assrt.equal(b64encode(bytes([0, 0, 0])).decode('ascii'), 'AAAA')",
2331
+ // decode round-trip
2332
+ "msg = bytes([104, 101, 108, 108, 111])", // hello
2333
+ "assrt.deepEqual(list(b64decode(b64encode(msg))), list(msg))",
2334
+ // decode accepts string input
2335
+ "d = b64decode('TWFu')",
2336
+ "assrt.equal(d[0], 77)",
2337
+ "assrt.equal(d[1], 97)",
2338
+ "assrt.equal(d[2], 110)",
2339
+ // decode strips whitespace
2340
+ "assrt.deepEqual(list(b64decode('aGVs bG8=')), list(msg))",
2341
+ // missing padding accepted
2342
+ "assrt.deepEqual(list(b64decode('aGVsbG8')), list(msg))",
2343
+ // isinstance check
2344
+ "assrt.ok(isinstance(b64encode(bytes([1,2,3])), bytes))",
2345
+ ].join("\n"));
2346
+ run_js(js);
2347
+ },
2348
+ },
2349
+
2350
+ {
2351
+ name: "bundle_base64_urlsafe",
2352
+ description: "base64 stdlib: URL-safe encoding and altchars in the web-repl bundle",
2353
+ run: function () {
2354
+ var repl = RS.web_repl();
2355
+ var js = bundle_compile(repl, [
2356
+ "from base64 import urlsafe_b64encode, urlsafe_b64decode, b64encode, b64decode",
2357
+ // URL-safe produces no + or /
2358
+ "enc = urlsafe_b64encode(bytes([251, 239, 190]))",
2359
+ "s = enc.decode('ascii')",
2360
+ "assrt.ok(s.indexOf('+') < 0, 'no + in URL-safe')",
2361
+ "assrt.ok(s.indexOf('/') < 0, 'no / in URL-safe')",
2362
+ // -_7- decodes to [251, 254, 254]
2363
+ "dec = urlsafe_b64decode('-_7-')",
2364
+ "assrt.equal(dec[0], 251)",
2365
+ "assrt.equal(dec[1], 254)",
2366
+ "assrt.equal(dec[2], 254)",
2367
+ // round-trip
2368
+ "data = bytes([0, 127, 128, 255])",
2369
+ "assrt.deepEqual(list(urlsafe_b64decode(urlsafe_b64encode(data))), list(data))",
2370
+ // altchars
2371
+ "enc_alt = b64encode(bytes([251, 254, 254]), altchars=bytes([45, 95]))",
2372
+ "assrt.equal(enc_alt.decode('ascii'), '-_7-')",
2373
+ "dec_alt = b64decode('-_7-', altchars=bytes([45, 95]))",
2374
+ "assrt.deepEqual(list(dec_alt), [251, 254, 254])",
2375
+ ].join("\n"));
2376
+ run_js(js);
2377
+ },
2378
+ },
2379
+
2380
+ {
2381
+ name: "bundle_base64_b32_b16",
2382
+ description: "base64 stdlib: b32encode/b32decode and b16encode/b16decode in the web-repl bundle",
2383
+ run: function () {
2384
+ var repl = RS.web_repl();
2385
+ var js = bundle_compile(repl, [
2386
+ "from base64 import b32encode, b32decode, b16encode, b16decode",
2387
+ // b32 round-trips
2388
+ "b32_msg = bytes([102, 111, 111])", // 'foo'
2389
+ "b32_enc = b32encode(b32_msg)",
2390
+ "assrt.equal(b32_enc.decode('ascii'), 'MZXW6===')",
2391
+ "assrt.deepEqual(list(b32decode(b32_enc)), list(b32_msg))",
2392
+ // casefold
2393
+ "assrt.deepEqual(list(b32decode('mzxw6===', casefold=True)), list(b32_msg))",
2394
+ // b16 (hex) encoding
2395
+ "b16_msg = bytes([0, 1, 254, 255])",
2396
+ "b16_enc = b16encode(b16_msg)",
2397
+ "assrt.equal(b16_enc.decode('ascii'), '0001FEFF')",
2398
+ "assrt.deepEqual(list(b16decode(b16_enc)), list(b16_msg))",
2399
+ // b16 casefold
2400
+ "assrt.deepEqual(list(b16decode('0001feff', casefold=True)), list(b16_msg))",
2401
+ // b16 Error on bad input
2402
+ "b16_err = False",
2403
+ "try:",
2404
+ " b16decode('ZZ')",
2405
+ "except ValueError:",
2406
+ " b16_err = True",
2407
+ "assrt.ok(b16_err, 'b16decode of non-hex raises ValueError')",
2408
+ ].join("\n"));
2409
+ run_js(js);
2410
+ },
2411
+ },
2412
+
2413
+ {
2414
+ name: "bundle_base64_encodebytes",
2415
+ description: "base64 stdlib: encodebytes/decodebytes and validate= flag in the web-repl bundle",
2416
+ run: function () {
2417
+ var repl = RS.web_repl();
2418
+ var js = bundle_compile(repl, [
2419
+ "from base64 import encodebytes, decodebytes, b64decode",
2420
+ // Note: catch ValueError (base64.Error subclasses ValueError).
2421
+ // Importing 'Error' from base64 in the web-repl context would
2422
+ // shadow the native JS Error constructor, causing issues.
2423
+ // encodebytes wraps at 76 chars per line
2424
+ "data = bytes(list(range(57)))",
2425
+ "enc = encodebytes(data)",
2426
+ "s = enc.decode('ascii')",
2427
+ "assrt.equal(s.charCodeAt(s.length - 1), 10, 'encodebytes ends with newline')",
2428
+ // decodebytes round-trip
2429
+ "assrt.deepEqual(list(decodebytes(enc)), list(data))",
2430
+ // validate=True raises on bad input (caught as ValueError)
2431
+ "b64_err = False",
2432
+ "try:",
2433
+ " b64decode('aGVs!G8=', validate=True)",
2434
+ "except ValueError:",
2435
+ " b64_err = True",
2436
+ "assrt.ok(b64_err, 'validate=True should raise ValueError on non-base64 char')",
2437
+ // validate=True passes on good input
2438
+ "good = b64decode('aGVsbG8=', validate=True)",
2439
+ "assrt.equal(good[0], 104)",
2440
+ ].join("\n"));
2441
+ run_js(js);
2442
+ },
2443
+ },
2444
+
2445
+ {
2446
+ name: "bundle_multiline_paren_import",
2447
+ description: "multi-line parenthesized import with trailing comma works in the web-repl bundle",
2448
+ run: function () {
2449
+ var repl = RS.web_repl();
2450
+ var js = bundle_compile(repl, [
2451
+ "from math import (",
2452
+ " floor,",
2453
+ " ceil,",
2454
+ " sqrt,",
2455
+ ")",
2456
+ "assrt.equal(floor(3.9), 3)",
2457
+ "assrt.equal(ceil(3.1), 4)",
2458
+ "assrt.equal(sqrt(9), 3)",
2459
+ ].join("\n"));
2460
+ run_js(js);
2461
+ },
2462
+ },
2463
+
2464
+ {
2465
+ name: "bundle_multiline_paren_import_alias",
2466
+ description: "multi-line parenthesized import with aliases and trailing comma works in the web-repl bundle",
2467
+ run: function () {
2468
+ var repl = RS.web_repl();
2469
+ var js = bundle_compile(repl, [
2470
+ "from math import (",
2471
+ " floor as fl,",
2472
+ " ceil as cl,",
2473
+ ")",
2474
+ "assrt.equal(fl(2.9), 2)",
2475
+ "assrt.equal(cl(2.1), 3)",
2476
+ ].join("\n"));
2477
+ run_js(js);
2478
+ },
2479
+ },
2480
+
2481
+ // ── string stdlib ────────────────────────────────────────────────────────
2482
+
2483
+ {
2484
+ name: "bundle_string_constants",
2485
+ description: "string stdlib: character constants in the web-repl bundle",
2486
+ run: function () {
2487
+ var repl = RS.web_repl();
2488
+ var js = bundle_compile(repl, [
2489
+ "from string import (",
2490
+ " ascii_lowercase, ascii_uppercase, ascii_letters,",
2491
+ " digits, hexdigits, octdigits,",
2492
+ " punctuation, whitespace, printable,",
2493
+ ")",
2494
+ // lengths
2495
+ "assrt.equal(len(ascii_lowercase), 26)",
2496
+ "assrt.equal(len(ascii_uppercase), 26)",
2497
+ "assrt.equal(len(ascii_letters), 52)",
2498
+ "assrt.equal(len(digits), 10)",
2499
+ "assrt.equal(len(hexdigits), 22)",
2500
+ "assrt.equal(len(octdigits), 8)",
2501
+ "assrt.equal(len(punctuation), 32)",
2502
+ "assrt.equal(len(whitespace), 6)",
2503
+ "assrt.equal(len(printable), len(digits) + len(ascii_letters) + len(punctuation) + len(whitespace))",
2504
+ // spot-checks
2505
+ "assrt.equal(ascii_letters, ascii_lowercase + ascii_uppercase)",
2506
+ "assrt.ok(digits.indexOf('5') >= 0)",
2507
+ "assrt.ok(hexdigits.indexOf('f') >= 0)",
2508
+ "assrt.ok(punctuation.indexOf('!') >= 0)",
2509
+ "assrt.ok(whitespace.indexOf(' ') >= 0)",
2510
+ "assrt.ok(printable.indexOf('A') >= 0)",
2511
+ ].join("\n"));
2512
+ run_js(js);
2513
+ },
2514
+ },
2515
+
2516
+ {
2517
+ name: "bundle_string_template",
2518
+ description: "string stdlib: Template class in the web-repl bundle",
2519
+ run: function () {
2520
+ var repl = RS.web_repl();
2521
+ var js = bundle_compile(repl, [
2522
+ "from string import Template",
2523
+ // basic substitution
2524
+ "t1 = Template('Hello $name!')",
2525
+ "assrt.equal(t1.substitute({'name': 'World'}), 'Hello World!')",
2526
+ // $$ → $
2527
+ "assrt.equal(Template('Price: $$5').substitute({}), 'Price: $5')",
2528
+ // brace form
2529
+ "assrt.equal(Template('${x}bar').substitute({'x': 'foo'}), 'foobar')",
2530
+ // multiple fields
2531
+ "assrt.equal(Template('$a and $b').substitute({'a': '1', 'b': '2'}), '1 and 2')",
2532
+ // numeric value
2533
+ "assrt.equal(Template('n=$n').substitute({'n': 42}), 'n=42')",
2534
+ // safe_substitute leaves missing intact
2535
+ "assrt.equal(Template('$x $y').safe_substitute({'x': 'hi'}), 'hi $y')",
2536
+ // safe_substitute with all keys
2537
+ "assrt.equal(Template('$a').safe_substitute({'a': 'z'}), 'z')",
2538
+ // substitute raises KeyError for missing key
2539
+ "_t_err = False",
2540
+ "try:",
2541
+ " Template('Hello $missing').substitute({})",
2542
+ "except KeyError:",
2543
+ " _t_err = True",
2544
+ "assrt.ok(_t_err)",
2545
+ // template attribute
2546
+ "assrt.equal(Template('hello $x').template, 'hello $x')",
2547
+ // class attribute
2548
+ "assrt.equal(Template.delimiter, '$')",
2549
+ ].join("\n"));
2550
+ run_js(js);
2551
+ },
2552
+ },
2553
+
2554
+ {
2555
+ name: "bundle_string_formatter",
2556
+ description: "string stdlib: Formatter class in the web-repl bundle",
2557
+ run: function () {
2558
+ var repl = RS.web_repl();
2559
+ var js = bundle_compile(repl, [
2560
+ "from string import Formatter",
2561
+ "f = Formatter()",
2562
+ // positional args
2563
+ "assrt.equal(f.format('Hello {}!', 'World'), 'Hello World!')",
2564
+ "assrt.equal(f.format('{0} {1}', 'a', 'b'), 'a b')",
2565
+ "assrt.equal(f.format('{1} {0}', 'a', 'b'), 'b a')",
2566
+ "assrt.equal(f.format('no fields'), 'no fields')",
2567
+ // format specs
2568
+ "assrt.equal(f.format('{:.2f}', 3.14159), '3.14')",
2569
+ "assrt.equal(f.format('{:d}', 42), '42')",
2570
+ "assrt.equal(f.format('{:x}', 255), 'ff')",
2571
+ // {{ }} escaping
2572
+ "assrt.equal(f.format('{{ }}'), '{ }')",
2573
+ // vformat with named kwargs
2574
+ "assrt.equal(f.vformat('{name}', [], {'name': 'Alice'}), 'Alice')",
2575
+ "assrt.equal(f.vformat('{0} {name}', ['hi'], {'name': 'Bob'}), 'hi Bob')",
2576
+ // convert_field
2577
+ "assrt.equal(f.convert_field(42, 's'), '42')",
2578
+ "assrt.equal(f.convert_field('x', None), 'x')",
2579
+ // format_field
2580
+ "assrt.equal(f.format_field(3.14159, '.2f'), '3.14')",
2581
+ "assrt.equal(f.format_field('hi', ''), 'hi')",
2582
+ // get_value
2583
+ "assrt.equal(f.get_value(0, ['a', 'b'], {}), 'a')",
2584
+ "assrt.equal(f.get_value('x', [], {'x': 99}), 99)",
2585
+ // parse
2586
+ "_p = f.parse('lit {0:.2f} end')",
2587
+ "assrt.equal(_p[0][0], 'lit ')",
2588
+ "assrt.equal(_p[0][1], '0')",
2589
+ "assrt.equal(_p[0][2], '.2f')",
2590
+ "assrt.equal(_p[1][0], ' end')",
2591
+ "assrt.equal(_p[1][1], None)",
2592
+ ].join("\n"));
2593
+ run_js(js);
2594
+ },
2595
+ },
2596
+
2597
+ {
2598
+ name: "bundle_html_escape_unescape",
2599
+ description: "html stdlib: escape and unescape functions in the web-repl bundle",
2600
+ run: function () {
2601
+ var repl = RS.web_repl();
2602
+ var js = bundle_compile(repl, [
2603
+ "from html import escape, unescape",
2604
+ // escape basics
2605
+ "assrt.equal(escape('<'), '&lt;')",
2606
+ "assrt.equal(escape('>'), '&gt;')",
2607
+ "assrt.equal(escape('&'), '&amp;')",
2608
+ "assrt.equal(escape('\"'), '&quot;')",
2609
+ "assrt.equal(escape(\"'\"), '&#x27;')",
2610
+ // quote=False
2611
+ "assrt.equal(escape('A & B', quote=False), 'A &amp; B')",
2612
+ "assrt.equal(escape('\"hi\"', quote=False), '\"hi\"')",
2613
+ // unescape named entities
2614
+ "assrt.equal(unescape('&amp;'), '&')",
2615
+ "assrt.equal(unescape('&lt;'), '<')",
2616
+ "assrt.equal(unescape('&gt;'), '>')",
2617
+ "assrt.equal(unescape('&quot;'), '\"')",
2618
+ "assrt.equal(unescape('&copy;'), '\\u00a9')",
2619
+ "assrt.equal(unescape('&euro;'), '\\u20ac')",
2620
+ // numeric references
2621
+ "assrt.equal(unescape('&#65;'), 'A')",
2622
+ "assrt.equal(unescape('&#x41;'), 'A')",
2623
+ // unknown entity left intact
2624
+ "assrt.equal(unescape('&nosuchentity;'), '&nosuchentity;')",
2625
+ // round-trip
2626
+ "s = '<Hello & \"World\">'",
2627
+ "assrt.equal(unescape(escape(s)), s)",
2628
+ ].join("\n"));
2629
+ run_js(js);
2630
+ },
2631
+ },
2632
+
2633
+ {
2634
+ name: "bundle_html_parser_basic",
2635
+ description: "html stdlib: HTMLParser start/end/data callbacks in the web-repl bundle",
2636
+ run: function () {
2637
+ var repl = RS.web_repl();
2638
+ var js = bundle_compile(repl, [
2639
+ "from html import HTMLParser",
2640
+ "class _P(HTMLParser):",
2641
+ " def __init__(self):",
2642
+ " HTMLParser.__init__(self)",
2643
+ " self.events = []",
2644
+ " def handle_starttag(self, tag, attrs):",
2645
+ " self.events.push(['start', tag])",
2646
+ " def handle_endtag(self, tag):",
2647
+ " self.events.push(['end', tag])",
2648
+ " def handle_data(self, data):",
2649
+ " self.events.push(['data', data])",
2650
+ "p = _P()",
2651
+ "p.feed('<p>Hello, World!</p>')",
2652
+ "assrt.equal(p.events[0][0], 'start')",
2653
+ "assrt.equal(p.events[0][1], 'p')",
2654
+ "assrt.equal(p.events[1][0], 'data')",
2655
+ "assrt.equal(p.events[1][1], 'Hello, World!')",
2656
+ "assrt.equal(p.events[2][0], 'end')",
2657
+ "assrt.equal(p.events[2][1], 'p')",
2658
+ // entity conversion in data
2659
+ "p2 = _P()",
2660
+ "p2.feed('<p>A &amp; B</p>')",
2661
+ "assrt.equal(p2.events[1][1], 'A & B')",
2662
+ // tag names lowercased
2663
+ "p3 = _P()",
2664
+ "p3.feed('<DIV></DIV>')",
2665
+ "assrt.equal(p3.events[0][1], 'div')",
2666
+ ].join("\n"));
2667
+ run_js(js);
2668
+ },
2669
+ },
2670
+
2671
+ {
2672
+ name: "bundle_html_parser_attrs",
2673
+ description: "html stdlib: HTMLParser attribute parsing in the web-repl bundle",
2674
+ run: function () {
2675
+ var repl = RS.web_repl();
2676
+ var js = bundle_compile(repl, [
2677
+ "from html import HTMLParser",
2678
+ "class _PA(HTMLParser):",
2679
+ " def __init__(self):",
2680
+ " HTMLParser.__init__(self)",
2681
+ " self.last_attrs = None",
2682
+ " def handle_starttag(self, tag, attrs):",
2683
+ " self.last_attrs = attrs",
2684
+ // multiple attrs
2685
+ "p = _PA()",
2686
+ "p.feed('<a href=\"http://example.com\" target=\"_blank\">')",
2687
+ "assrt.equal(p.last_attrs.length, 2)",
2688
+ "assrt.equal(p.last_attrs[0][0], 'href')",
2689
+ "assrt.equal(p.last_attrs[0][1], 'http://example.com')",
2690
+ "assrt.equal(p.last_attrs[1][0], 'target')",
2691
+ "assrt.equal(p.last_attrs[1][1], '_blank')",
2692
+ // valueless attribute
2693
+ "p2 = _PA()",
2694
+ "p2.feed('<input disabled>')",
2695
+ "assrt.equal(p2.last_attrs[0][0], 'disabled')",
2696
+ "assrt.equal(p2.last_attrs[0][1], None)",
2697
+ // self-closing
2698
+ "class _PSC(HTMLParser):",
2699
+ " def __init__(self):",
2700
+ " HTMLParser.__init__(self)",
2701
+ " self.events = []",
2702
+ " def handle_starttag(self, tag, attrs):",
2703
+ " self.events.push('start:' + tag)",
2704
+ " def handle_endtag(self, tag):",
2705
+ " self.events.push('end:' + tag)",
2706
+ "psc = _PSC()",
2707
+ "psc.feed('<br/>')",
2708
+ "assrt.equal(psc.events.length, 2)",
2709
+ "assrt.equal(psc.events[0], 'start:br')",
2710
+ "assrt.equal(psc.events[1], 'end:br')",
2711
+ ].join("\n"));
2712
+ run_js(js);
2713
+ },
2714
+ },
2715
+
2716
+ {
2717
+ name: "bundle_html_parser_special",
2718
+ description: "html stdlib: HTMLParser comments, DOCTYPE, get_starttag_text in the web-repl bundle",
2719
+ run: function () {
2720
+ var repl = RS.web_repl();
2721
+ var js = bundle_compile(repl, [
2722
+ "from html import HTMLParser",
2723
+ // comment
2724
+ "class _PC(HTMLParser):",
2725
+ " def __init__(self):",
2726
+ " HTMLParser.__init__(self)",
2727
+ " self.comments = []",
2728
+ " def handle_comment(self, data):",
2729
+ " self.comments.push(data)",
2730
+ "pc = _PC()",
2731
+ "pc.feed('<!-- hello -->')",
2732
+ "assrt.equal(pc.comments.length, 1)",
2733
+ "assrt.equal(pc.comments[0], ' hello ')",
2734
+ // doctype
2735
+ "class _PD(HTMLParser):",
2736
+ " def __init__(self):",
2737
+ " HTMLParser.__init__(self)",
2738
+ " self.decls = []",
2739
+ " def handle_decl(self, decl):",
2740
+ " self.decls.push(decl)",
2741
+ "pd = _PD()",
2742
+ "pd.feed('<!DOCTYPE html>')",
2743
+ "assrt.equal(pd.decls[0], 'DOCTYPE html')",
2744
+ // get_starttag_text
2745
+ "class _PR(HTMLParser):",
2746
+ " def __init__(self):",
2747
+ " HTMLParser.__init__(self)",
2748
+ " self.raw = None",
2749
+ " def handle_starttag(self, tag, attrs):",
2750
+ " self.raw = self.get_starttag_text()",
2751
+ "pr = _PR()",
2752
+ "pr.feed('<img src=\"pic.png\">')",
2753
+ "assrt.equal(pr.raw, '<img src=\"pic.png\">')",
2754
+ ].join("\n"));
2755
+ run_js(js);
2756
+ },
2757
+ },
2758
+
2759
+ {
2760
+ name: "bundle_python_modulo",
2761
+ description: "% operator gives Python-style modulo (sign of divisor) in the web-repl bundle",
2762
+ run: function () {
2763
+ var repl = RS.web_repl();
2764
+ var js = bundle_compile(repl, [
2765
+ "assrt.equal(-7 % 3, 2)",
2766
+ "assrt.equal(7 % -3, -2)",
2767
+ "assrt.equal(-7 % -3, -1)",
2768
+ "assrt.equal(7 % 3, 1)",
2769
+ "assrt.equal(0 % 5, 0)",
2770
+ "assrt.equal(-6 % 3, 0)",
2771
+ "assrt.equal(-7.5 % 2, 0.5)",
2772
+ "assrt.equal(7.5 % -2, -0.5)",
2773
+ "x = -7",
2774
+ "x %= 3",
2775
+ "assrt.equal(x, 2)",
2776
+ ].join("\n"));
2777
+ run_js(js);
2778
+ },
2779
+ },
2780
+
2201
2781
  {
2202
2782
  name: "repl_exists_persistence",
2203
2783
  description: "ρσ_exists accessible after baselib init — existential operator on non-SymbolRef in web-repl context",
@@ -2218,39 +2798,1524 @@ var TESTS = [
2218
2798
  },
2219
2799
  },
2220
2800
 
2221
- ];
2222
-
2223
- // ---------------------------------------------------------------------------
2224
- // Runner
2225
- // ---------------------------------------------------------------------------
2801
+ {
2802
+ name: "bundle_asyncio_exceptions_and_primitives",
2803
+ description: "asyncio stdlib: exception classes, Queue/Lock/Event/Semaphore synchronous state in the web-repl bundle",
2804
+ run: function () {
2805
+ var repl = RS.web_repl();
2806
+ var js = bundle_compile(repl, [
2807
+ "from asyncio import (",
2808
+ " CancelledError, TimeoutError, InvalidStateError, RuntimeError,",
2809
+ " QueueEmpty, QueueFull,",
2810
+ " Lock, Event, Semaphore, BoundedSemaphore, Queue",
2811
+ ")",
2812
+ // Exception classes
2813
+ "try:",
2814
+ " raise CancelledError('c')",
2815
+ "except CancelledError as e:",
2816
+ " assrt.equal(e.message, 'c')",
2817
+ "try:",
2818
+ " raise TimeoutError('t')",
2819
+ "except TimeoutError as e:",
2820
+ " assrt.equal(e.message, 't')",
2821
+ "try:",
2822
+ " raise QueueEmpty('empty')",
2823
+ "except QueueEmpty as e:",
2824
+ " assrt.equal(e.message, 'empty')",
2825
+ "try:",
2826
+ " raise QueueFull('full')",
2827
+ "except QueueFull as e:",
2828
+ " assrt.equal(e.message, 'full')",
2829
+ // Lock
2830
+ "lock = Lock()",
2831
+ "assrt.equal(lock.locked(), False)",
2832
+ "try:",
2833
+ " lock.release()",
2834
+ " assrt.ok(False)",
2835
+ "except RuntimeError:",
2836
+ " assrt.ok(True)",
2837
+ // Event
2838
+ "ev = Event()",
2839
+ "assrt.equal(ev.is_set(), False)",
2840
+ "ev.set()",
2841
+ "assrt.equal(ev.is_set(), True)",
2842
+ "ev.clear()",
2843
+ "assrt.equal(ev.is_set(), False)",
2844
+ // Semaphore
2845
+ "sem = Semaphore(2)",
2846
+ "assrt.equal(sem.locked(), False)",
2847
+ "sem.release()",
2848
+ "assrt.equal(sem.locked(), False)",
2849
+ // BoundedSemaphore
2850
+ "bsem = BoundedSemaphore(1)",
2851
+ "try:",
2852
+ " bsem.release()",
2853
+ " assrt.ok(False)",
2854
+ "except ValueError:",
2855
+ " assrt.ok(True)",
2856
+ // Queue synchronous ops
2857
+ "q = Queue()",
2858
+ "assrt.equal(q.empty(), True)",
2859
+ "q.put_nowait('a')",
2860
+ "q.put_nowait('b')",
2861
+ "assrt.equal(q.qsize(), 2)",
2862
+ "assrt.equal(q.get_nowait(), 'a')",
2863
+ "q.task_done()",
2864
+ "assrt.equal(q.get_nowait(), 'b')",
2865
+ "q.task_done()",
2866
+ "assrt.equal(q.empty(), True)",
2867
+ ].join("\n"));
2868
+ run_js(js);
2869
+ },
2870
+ },
2226
2871
 
2227
- function run_tests(filter) {
2228
- var tests = filter
2229
- ? TESTS.filter(function (t) { return t.name === filter; })
2230
- : TESTS;
2872
+ {
2873
+ name: "bundle_asyncio_coroutine_helpers",
2874
+ description: "asyncio stdlib: iscoroutine, iscoroutinefunction, sleep/gather/run return Promises in the web-repl bundle",
2875
+ run: function () {
2876
+ var repl = RS.web_repl();
2877
+ var js = bundle_compile(repl, [
2878
+ "from asyncio import iscoroutine, iscoroutinefunction, sleep, gather, create_task, run, shield",
2879
+ // sleep returns a thenable
2880
+ "p = sleep(0)",
2881
+ "assrt.ok(iscoroutine(p))",
2882
+ // gather returns a thenable
2883
+ "p2 = gather(Promise.resolve(1), Promise.resolve(2))",
2884
+ "assrt.ok(iscoroutine(p2))",
2885
+ // create_task / run pass through
2886
+ "p3 = Promise.resolve(99)",
2887
+ "assrt.ok(iscoroutine(create_task(p3)))",
2888
+ "assrt.ok(iscoroutine(run(p3)))",
2889
+ "assrt.ok(iscoroutine(shield(p3)))",
2890
+ // iscoroutine
2891
+ "assrt.ok(not iscoroutine(42))",
2892
+ "assrt.ok(not iscoroutine(None))",
2893
+ "assrt.ok(iscoroutine(sleep(0)))",
2894
+ // iscoroutinefunction
2895
+ "async def _af():",
2896
+ " return 1",
2897
+ "assrt.ok(iscoroutinefunction(_af))",
2898
+ "def _sf():",
2899
+ " return 1",
2900
+ "assrt.ok(not iscoroutinefunction(_sf))",
2901
+ // async functions return Promises
2902
+ "assrt.ok(iscoroutine(_af()))",
2903
+ ].join("\n"));
2904
+ run_js(js);
2905
+ },
2906
+ },
2231
2907
 
2232
- if (tests.length === 0) {
2233
- console.error(colored("No test found: " + filter, "red"));
2234
- process.exit(1);
2235
- }
2908
+ {
2909
+ name: "bundle_asyncio_queue_variants",
2910
+ description: "asyncio stdlib: LifoQueue and PriorityQueue ordering in the web-repl bundle",
2911
+ run: function () {
2912
+ var repl = RS.web_repl();
2913
+ var js = bundle_compile(repl, [
2914
+ "from asyncio import Queue, LifoQueue, PriorityQueue, QueueFull, QueueEmpty",
2915
+ // Bounded Queue — QueueFull
2916
+ "q2 = Queue(2)",
2917
+ "q2.put_nowait(1)",
2918
+ "q2.put_nowait(2)",
2919
+ "assrt.equal(q2.full(), True)",
2920
+ "try:",
2921
+ " q2.put_nowait(3)",
2922
+ " assrt.ok(False)",
2923
+ "except QueueFull:",
2924
+ " assrt.ok(True)",
2925
+ // LifoQueue ordering
2926
+ "lq = LifoQueue()",
2927
+ "lq.put_nowait(1)",
2928
+ "lq.put_nowait(2)",
2929
+ "lq.put_nowait(3)",
2930
+ "assrt.equal(lq.get_nowait(), 3)",
2931
+ "assrt.equal(lq.get_nowait(), 2)",
2932
+ "assrt.equal(lq.get_nowait(), 1)",
2933
+ // PriorityQueue ordering (lowest value first)
2934
+ "pq = PriorityQueue()",
2935
+ "pq.put_nowait(30)",
2936
+ "pq.put_nowait(10)",
2937
+ "pq.put_nowait(20)",
2938
+ "assrt.equal(pq.get_nowait(), 10)",
2939
+ "assrt.equal(pq.get_nowait(), 20)",
2940
+ "assrt.equal(pq.get_nowait(), 30)",
2941
+ // QueueEmpty
2942
+ "q3 = Queue()",
2943
+ "try:",
2944
+ " q3.get_nowait()",
2945
+ " assrt.ok(False)",
2946
+ "except QueueEmpty:",
2947
+ " assrt.ok(True)",
2948
+ ].join("\n"));
2949
+ run_js(js);
2950
+ },
2951
+ },
2236
2952
 
2237
- var failures = [];
2238
- tests.forEach(function (test) {
2239
- try {
2240
- test.run();
2241
- console.log(colored("PASS " + test.name, "green") + " – " + test.description);
2242
- } catch (e) {
2243
- failures.push(test.name);
2244
- console.log(colored("FAIL " + test.name, "red") + "\n " + (e.stack || String(e)) + "\n");
2245
- }
2246
- });
2953
+ {
2954
+ name: "bundle_asyncio_async_await",
2955
+ description: "asyncio stdlib: async def / await compiles and returns Promise in the web-repl bundle",
2956
+ run: function () {
2957
+ var repl = RS.web_repl();
2958
+ var js = bundle_compile(repl, [
2959
+ "from asyncio import sleep, gather, iscoroutine, iscoroutinefunction, get_event_loop",
2960
+ // async functions compile and return Promises
2961
+ "async def _add(a, b):",
2962
+ " return a + b",
2963
+ "assrt.ok(iscoroutinefunction(_add))",
2964
+ "p = _add(2, 3)",
2965
+ "assrt.ok(iscoroutine(p))",
2966
+ // chained awaits produce a Promise
2967
+ "async def _chain():",
2968
+ " x = await Promise.resolve(10)",
2969
+ " y = await Promise.resolve(20)",
2970
+ " return x + y",
2971
+ "assrt.ok(iscoroutine(_chain()))",
2972
+ // gather of resolved Promises
2973
+ "async def _g():",
2974
+ " results = await gather(Promise.resolve(1), Promise.resolve(2))",
2975
+ " return results",
2976
+ "assrt.ok(iscoroutine(_g()))",
2977
+ // event loop stub
2978
+ "loop = get_event_loop()",
2979
+ "assrt.ok(loop is not None)",
2980
+ "assrt.ok(not loop.is_closed())",
2981
+ "assrt.ok(loop.is_running())",
2982
+ ].join("\n"));
2983
+ run_js(js);
2984
+ },
2985
+ },
2247
2986
 
2248
- console.log("");
2249
- if (failures.length) {
2250
- console.log(colored(failures.length + " test(s) failed.", "red"));
2251
- } else {
2252
- console.log(colored("All " + tests.length + " web-repl tests passed!", "green"));
2987
+ {
2988
+ name: "bundle_async_generator_shape",
2989
+ description: "async def with yield returns an async iterator with .next/.send/.asend (web-repl bundle)",
2990
+ run: function () {
2991
+ var repl = RS.web_repl();
2992
+ var js = bundle_compile(repl, [
2993
+ "async def aiter():",
2994
+ " yield 1",
2995
+ " yield 2",
2996
+ " yield 3",
2997
+ "it = aiter()",
2998
+ // The wrapper is sync — calling the async generator returns the
2999
+ // iterator immediately, NOT a Promise.
3000
+ "assrt.equal(jstype(it.next), 'function')",
3001
+ "assrt.equal(jstype(it.send), 'function')", // Python alias
3002
+ "assrt.equal(jstype(it.asend), 'function')", // async-gen alias
3003
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3004
+ // .next() returns a thenable Promise
3005
+ "p = it.next()",
3006
+ "assrt.equal(jstype(p.then), 'function')",
3007
+ ].join("\n"));
3008
+ run_js(js);
3009
+ },
3010
+ },
3011
+
3012
+ {
3013
+ name: "bundle_async_generator_await_inside",
3014
+ description: "async generators may use `await` between yields (web-repl bundle)",
3015
+ run: function () {
3016
+ var repl = RS.web_repl();
3017
+ var js = bundle_compile(repl, [
3018
+ "async def gen():",
3019
+ " a = await Promise.resolve(10)",
3020
+ " yield a",
3021
+ " b = await Promise.resolve(20)",
3022
+ " yield a + b",
3023
+ "it = gen()",
3024
+ "assrt.equal(jstype(it.next), 'function')",
3025
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3026
+ // .next() is thenable even when the body awaits before yielding
3027
+ "assrt.equal(jstype(it.next().then), 'function')",
3028
+ ].join("\n"));
3029
+ run_js(js);
3030
+ },
3031
+ },
3032
+
3033
+ {
3034
+ name: "bundle_async_for_compiles_and_resolves",
3035
+ description: "async for loop drives an async generator and resolves to expected values (web-repl bundle)",
3036
+ run: function () {
3037
+ var repl = RS.web_repl();
3038
+ var js = bundle_compile(repl, [
3039
+ "async def aiter():",
3040
+ " yield 'a'",
3041
+ " yield 'b'",
3042
+ " yield 'c'",
3043
+ "async def consume():",
3044
+ " out = []",
3045
+ " async for x in aiter():",
3046
+ " out.append(x)",
3047
+ " return out",
3048
+ "p = consume()",
3049
+ "assrt.equal(jstype(p.then), 'function')",
3050
+ // The resolved value is verified via a microtask callback that
3051
+ // throws on mismatch. Failing the assertion in a Promise chain
3052
+ // surfaces as an unhandled rejection — Node exits non-zero,
3053
+ // failing the test.
3054
+ "v\"p.then(function(v){ if (v.length !== 3 || v[0] !== 'a' || v[1] !== 'b' || v[2] !== 'c') throw new Error('async-for produced ' + JSON.stringify(v)); })\"",
3055
+ ].join("\n"));
3056
+ run_js(js);
3057
+ },
3058
+ },
3059
+
3060
+ {
3061
+ name: "bundle_async_generator_class_method",
3062
+ description: "async generator method on a class compiles and exposes async iterator (web-repl bundle)",
3063
+ run: function () {
3064
+ var repl = RS.web_repl();
3065
+ var js = bundle_compile(repl, [
3066
+ "class Counter:",
3067
+ " def __init__(self, limit):",
3068
+ " self.limit = limit",
3069
+ "",
3070
+ " async def values(self):",
3071
+ " i = 0",
3072
+ " while i < self.limit:",
3073
+ " yield i",
3074
+ " i += 1",
3075
+ "",
3076
+ "it = Counter(3).values()",
3077
+ "assrt.equal(jstype(it.next), 'function')",
3078
+ "assrt.equal(jstype(it.send), 'function')",
3079
+ "assrt.equal(jstype(it.asend), 'function')",
3080
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3081
+ "assrt.equal(jstype(it.next().then), 'function')",
3082
+ ].join("\n"));
3083
+ run_js(js);
3084
+ },
3085
+ },
3086
+
3087
+ {
3088
+ name: "bundle_urllib_parse_quote",
3089
+ description: "urllib.parse stdlib: quote, unquote, quote_plus, unquote_plus in the web-repl bundle",
3090
+ run: function () {
3091
+ var repl = RS.web_repl();
3092
+ var js = bundle_compile(repl, [
3093
+ "from urllib.parse import quote, unquote, quote_plus, unquote_plus",
3094
+ // quote: basic
3095
+ "assrt.equal(quote('hello world'), 'hello%20world')",
3096
+ "assrt.equal(quote('a/b/c'), 'a/b/c')", // '/' safe by default
3097
+ "assrt.equal(quote('a/b/c', safe=''), 'a%2Fb%2Fc')",
3098
+ "assrt.equal(quote('abc123-_.~'), 'abc123-_.~')", // RFC 3986 unreserved
3099
+ "assrt.equal(quote('a+b'), 'a%2Bb')",
3100
+ "assrt.equal(quote('!*'), '%21%2A')", // sub-delimiters encoded
3101
+ // unquote
3102
+ "assrt.equal(unquote('hello%20world'), 'hello world')",
3103
+ "assrt.equal(unquote('a%2Fb'), 'a/b')",
3104
+ "assrt.equal(unquote('a%2Bb'), 'a+b')", // '+' not decoded by unquote
3105
+ // quote_plus / unquote_plus
3106
+ "assrt.equal(quote_plus('hello world'), 'hello+world')",
3107
+ "assrt.equal(quote_plus('a+b'), 'a%2Bb')",
3108
+ "assrt.equal(unquote_plus('hello+world'), 'hello world')",
3109
+ "assrt.equal(unquote_plus('a%2Bb'), 'a+b')",
3110
+ // round-trips
3111
+ "s = 'a/b c+d=e&f'",
3112
+ "assrt.equal(unquote(quote(s, safe='')), s)",
3113
+ "assrt.equal(unquote_plus(quote_plus(s, safe='')), s)",
3114
+ ].join("\n"));
3115
+ run_js(js);
3116
+ },
3117
+ },
3118
+
3119
+ {
3120
+ name: "bundle_urllib_parse_urlencode",
3121
+ description: "urllib.parse stdlib: urlencode, parse_qs, parse_qsl in the web-repl bundle",
3122
+ run: function () {
3123
+ var repl = RS.web_repl();
3124
+ var js = bundle_compile(repl, [
3125
+ "from urllib.parse import urlencode, parse_qs, parse_qsl",
3126
+ // urlencode with list of pairs
3127
+ "assrt.equal(urlencode([['a', '1'], ['b', '2']]), 'a=1&b=2')",
3128
+ "assrt.equal(urlencode([['q', 'hello world']]), 'q=hello%20world')",
3129
+ "assrt.equal(urlencode([]), '')",
3130
+ // doseq
3131
+ "assrt.equal(urlencode([['a', ['x', 'y']]], doseq=True), 'a=x&a=y')",
3132
+ // parse_qsl
3133
+ "pairs = parse_qsl('a=1&b=2&a=3')",
3134
+ "assrt.equal(pairs.length, 3)",
3135
+ "assrt.equal(pairs[0][0], 'a')",
3136
+ "assrt.equal(pairs[0][1], '1')",
3137
+ "assrt.equal(pairs[2][0], 'a')",
3138
+ "assrt.equal(pairs[2][1], '3')",
3139
+ "assrt.equal(parse_qsl('a=hello+world')[0][1], 'hello world')",
3140
+ "assrt.equal(parse_qsl('q=a%20b')[0][1], 'a b')",
3141
+ // parse_qs
3142
+ "d = parse_qs('a=1&b=2&a=3')",
3143
+ "assrt.equal(d['a'].length, 2)",
3144
+ "assrt.equal(d['a'][0], '1')",
3145
+ "assrt.equal(d['a'][1], '3')",
3146
+ "assrt.equal(d['b'][0], '2')",
3147
+ "assrt.equal(parse_qs('q=hello+world')['q'][0], 'hello world')",
3148
+ ].join("\n"));
3149
+ run_js(js);
3150
+ },
3151
+ },
3152
+
3153
+ {
3154
+ name: "bundle_urllib_parse_urlparse",
3155
+ description: "urllib.parse stdlib: urlsplit, urlparse, urljoin, urlunsplit in the web-repl bundle",
3156
+ run: function () {
3157
+ var repl = RS.web_repl();
3158
+ var js = bundle_compile(repl, [
3159
+ "from urllib.parse import urlsplit, urlunsplit, urlparse, urlunparse, urljoin",
3160
+ // urlsplit
3161
+ "r = urlsplit('http://example.com/path?q=1#frag')",
3162
+ "assrt.equal(r.scheme, 'http')",
3163
+ "assrt.equal(r.netloc, 'example.com')",
3164
+ "assrt.equal(r.path, '/path')",
3165
+ "assrt.equal(r.query, 'q=1')",
3166
+ "assrt.equal(r.fragment, 'frag')",
3167
+ "assrt.equal(r.hostname, 'example.com')",
3168
+ "assrt.equal(r.port, None)",
3169
+ // authority with user:pass@host:port
3170
+ "r2 = urlsplit('https://user:pw@host:8080/p?x=1')",
3171
+ "assrt.equal(r2.hostname, 'host')",
3172
+ "assrt.equal(r2.port, 8080)",
3173
+ "assrt.equal(r2.username, 'user')",
3174
+ "assrt.equal(r2.password, 'pw')",
3175
+ // urlparse splits params
3176
+ "r3 = urlparse('http://example.com/path;params?q=1#frag')",
3177
+ "assrt.equal(r3.path, '/path')",
3178
+ "assrt.equal(r3.params, 'params')",
3179
+ // urlunsplit round-trip
3180
+ "assrt.equal(urlunsplit(('http', 'example.com', '/path', 'q=1', 'frag')), 'http://example.com/path?q=1#frag')",
3181
+ "assrt.equal(urlunsplit(('http', 'example.com', '/path', '', '')), 'http://example.com/path')",
3182
+ // urlunparse with params
3183
+ "assrt.equal(urlunparse(('http', 'example.com', '/path', 'par', 'q=1', 'frag')), 'http://example.com/path;par?q=1#frag')",
3184
+ // geturl round-trip
3185
+ "r4 = urlsplit('http://example.com/path?q=1#frag')",
3186
+ "assrt.equal(r4.geturl(), 'http://example.com/path?q=1#frag')",
3187
+ // urljoin
3188
+ "assrt.equal(urljoin('http://example.com/foo', 'bar'), 'http://example.com/bar')",
3189
+ "assrt.equal(urljoin('http://example.com/foo/', 'bar'), 'http://example.com/foo/bar')",
3190
+ "assrt.equal(urljoin('http://example.com/', '/other'), 'http://example.com/other')",
3191
+ "assrt.equal(urljoin('http://example.com/foo', 'http://other.com/'), 'http://other.com/')",
3192
+ ].join("\n"));
3193
+ run_js(js);
3194
+ },
3195
+ },
3196
+
3197
+ {
3198
+ name: "bundle_urllib_error",
3199
+ description: "urllib.error stdlib: URLError and HTTPError exception classes in the web-repl bundle",
3200
+ run: function () {
3201
+ var repl = RS.web_repl();
3202
+ var js = bundle_compile(repl, [
3203
+ "from urllib.error import URLError, HTTPError",
3204
+ // URLError
3205
+ "caught_url = False",
3206
+ "try:",
3207
+ " raise URLError('network failure')",
3208
+ "except URLError as e:",
3209
+ " caught_url = True",
3210
+ " assrt.ok('network failure' in str(e.reason))",
3211
+ "assrt.ok(caught_url)",
3212
+ // HTTPError
3213
+ "caught_http = False",
3214
+ "try:",
3215
+ " raise HTTPError('http://example.com', 404, 'Not Found', {}, None)",
3216
+ "except HTTPError as e:",
3217
+ " caught_http = True",
3218
+ " assrt.equal(e.code, 404)",
3219
+ " assrt.equal(e.msg, 'Not Found')",
3220
+ " assrt.equal(e.getcode(), 404)",
3221
+ " assrt.equal(e.geturl(), 'http://example.com')",
3222
+ "assrt.ok(caught_http)",
3223
+ // HTTPError is caught by URLError handler
3224
+ "caught_as_url = False",
3225
+ "try:",
3226
+ " raise HTTPError('http://x.com', 500, 'Server Error', {}, None)",
3227
+ "except URLError as e:",
3228
+ " caught_as_url = True",
3229
+ " assrt.equal(e.code, 500)",
3230
+ "assrt.ok(caught_as_url)",
3231
+ ].join("\n"));
3232
+ run_js(js);
3233
+ },
3234
+ },
3235
+
3236
+ {
3237
+ name: "bundle_bisect_basic",
3238
+ description: "bisect stdlib: bisect_left, bisect_right, bisect in the web-repl bundle",
3239
+ run: function () {
3240
+ var repl = RS.web_repl();
3241
+ var js = bundle_compile(repl, [
3242
+ "from bisect import bisect_left, bisect_right, bisect",
3243
+ "a = [1, 3, 5, 7, 9]",
3244
+ // bisect_left
3245
+ "assrt.equal(bisect_left(a, 0), 0)",
3246
+ "assrt.equal(bisect_left(a, 1), 0)",
3247
+ "assrt.equal(bisect_left(a, 5), 2)",
3248
+ "assrt.equal(bisect_left(a, 9), 4)",
3249
+ "assrt.equal(bisect_left(a, 10), 5)",
3250
+ // bisect_right
3251
+ "assrt.equal(bisect_right(a, 1), 1)",
3252
+ "assrt.equal(bisect_right(a, 5), 3)",
3253
+ "assrt.equal(bisect_right(a, 9), 5)",
3254
+ "assrt.equal(bisect_right(a, 10), 5)",
3255
+ // bisect alias == bisect_right
3256
+ "assrt.equal(bisect(a, 5), bisect_right(a, 5))",
3257
+ // empty list
3258
+ "assrt.equal(bisect_left([], 5), 0)",
3259
+ "assrt.equal(bisect_right([], 5), 0)",
3260
+ // all equal
3261
+ "eq = [3, 3, 3]",
3262
+ "assrt.equal(bisect_left(eq, 3), 0)",
3263
+ "assrt.equal(bisect_right(eq, 3), 3)",
3264
+ // lo/hi bounds
3265
+ "assrt.equal(bisect_left(a, 6, 1, 4), 3)",
3266
+ "assrt.equal(bisect_right(a, 3, 1, 4), 2)",
3267
+ ].join("\n"));
3268
+ run_js(js);
3269
+ },
3270
+ },
3271
+
3272
+ {
3273
+ name: "bundle_bisect_insort",
3274
+ description: "bisect stdlib: insort_left, insort_right, insort in the web-repl bundle",
3275
+ run: function () {
3276
+ var repl = RS.web_repl();
3277
+ var js = bundle_compile(repl, [
3278
+ "from bisect import insort_left, insort_right, insort",
3279
+ // insort_right basic
3280
+ "r = [1, 3, 5, 7]",
3281
+ "insort_right(r, 4)",
3282
+ "assrt.deepEqual(r, [1, 3, 4, 5, 7])",
3283
+ // insort_left: new value goes to the LEFT of equal elements
3284
+ "l = [1, 3, 3, 5]",
3285
+ "insort_left(l, 3)",
3286
+ "assrt.equal(l[1], 3)", // new 3 at index 1
3287
+ "assrt.equal(l.length, 5)",
3288
+ // insort alias == insort_right
3289
+ "s = [2, 4, 6]",
3290
+ "insort(s, 5)",
3291
+ "assrt.deepEqual(s, [2, 4, 5, 6])",
3292
+ // build sorted list from scratch
3293
+ "built = []",
3294
+ "for v in [5, 1, 3, 2, 4]:",
3295
+ " insort(built, v)",
3296
+ "assrt.deepEqual(built, [1, 2, 3, 4, 5])",
3297
+ ].join("\n"));
3298
+ run_js(js);
3299
+ },
3300
+ },
3301
+
3302
+ {
3303
+ name: "bundle_bisect_key",
3304
+ description: "bisect stdlib: key function parameter in the web-repl bundle",
3305
+ run: function () {
3306
+ var repl = RS.web_repl();
3307
+ var js = bundle_compile(repl, [
3308
+ "from bisect import bisect_left, bisect_right",
3309
+ // key extracts first element of each pair; x is the key value
3310
+ "pairs = [[1, 'a'], [3, 'b'], [5, 'c'], [7, 'd']]",
3311
+ "kfn = def(item): return item[0];",
3312
+ "assrt.equal(bisect_left(pairs, 3, 0, None, kfn), 1)",
3313
+ "assrt.equal(bisect_right(pairs, 3, 0, None, kfn), 2)",
3314
+ "assrt.equal(bisect_left(pairs, 4, 0, None, kfn), 2)",
3315
+ "assrt.equal(bisect_right(pairs, 4, 0, None, kfn), 2)",
3316
+ "assrt.equal(bisect_left(pairs, 0, 0, None, kfn), 0)",
3317
+ "assrt.equal(bisect_right(pairs, 8, 0, None, kfn), 4)",
3318
+ ].join("\n"));
3319
+ run_js(js);
3320
+ },
3321
+ },
3322
+
3323
+ {
3324
+ name: "bundle_bisect_errors",
3325
+ description: "bisect stdlib: ValueError for negative lo in the web-repl bundle",
3326
+ run: function () {
3327
+ var repl = RS.web_repl();
3328
+ var js = bundle_compile(repl, [
3329
+ "from bisect import bisect_left, bisect_right",
3330
+ "caught_left = False",
3331
+ "try:",
3332
+ " bisect_left([1, 2, 3], 2, -1)",
3333
+ "except ValueError:",
3334
+ " caught_left = True",
3335
+ "assrt.ok(caught_left)",
3336
+ "caught_right = False",
3337
+ "try:",
3338
+ " bisect_right([1, 2, 3], 2, -1)",
3339
+ "except ValueError:",
3340
+ " caught_right = True",
3341
+ "assrt.ok(caught_right)",
3342
+ // strings also work
3343
+ "words = ['bar', 'baz', 'foo', 'qux']",
3344
+ "assrt.equal(bisect_left(words, 'car'), 2)",
3345
+ "assrt.equal(bisect_right(words, 'car'), 2)",
3346
+ ].join("\n"));
3347
+ run_js(js);
3348
+ },
3349
+ },
3350
+
3351
+ // ── http stdlib ──────────────────────────────────────────────────────────
3352
+
3353
+ {
3354
+ name: "bundle_http_status",
3355
+ description: "http stdlib: HTTPStatus constants in the web-repl bundle",
3356
+ run: function () {
3357
+ var repl = RS.web_repl();
3358
+ var js = bundle_compile(repl, [
3359
+ "from http import HTTPStatus",
3360
+ "assrt.equal(HTTPStatus.OK, 200)",
3361
+ "assrt.equal(HTTPStatus.CREATED, 201)",
3362
+ "assrt.equal(HTTPStatus.NO_CONTENT, 204)",
3363
+ "assrt.equal(HTTPStatus.NOT_FOUND, 404)",
3364
+ "assrt.equal(HTTPStatus.INTERNAL_SERVER_ERROR, 500)",
3365
+ "assrt.equal(HTTPStatus.IM_A_TEAPOT, 418)",
3366
+ "assrt.equal(HTTPStatus.MOVED_PERMANENTLY, 301)",
3367
+ "assrt.equal(HTTPStatus.UNAUTHORIZED, 401)",
3368
+ ].join("\n"));
3369
+ run_js(js);
3370
+ },
3371
+ },
3372
+
3373
+ {
3374
+ name: "bundle_http_client_basics",
3375
+ description: "http.client stdlib: HTTPConnection, HTTPSConnection, HTTPResponse, exceptions in the web-repl bundle",
3376
+ run: function () {
3377
+ var repl = RS.web_repl();
3378
+ var js = bundle_compile(repl, [
3379
+ "from http.client import (HTTPConnection, HTTPSConnection, HTTPResponse,",
3380
+ " HTTPException, NotConnected, InvalidURL,",
3381
+ " RemoteDisconnected, HTTP_PORT, HTTPS_PORT)",
3382
+ "assrt.equal(HTTP_PORT, 80)",
3383
+ "assrt.equal(HTTPS_PORT, 443)",
3384
+ // URL building
3385
+ "conn = HTTPConnection('example.com')",
3386
+ "assrt.equal(conn._build_url('/path'), 'http://example.com/path')",
3387
+ "conn2 = HTTPConnection('example.com', 8080)",
3388
+ "assrt.equal(conn2._build_url('/api'), 'http://example.com:8080/api')",
3389
+ "sconn = HTTPSConnection('secure.example.com')",
3390
+ "assrt.equal(sconn._build_url('/data'), 'https://secure.example.com/data')",
3391
+ // request() stores state
3392
+ "conn3 = HTTPConnection('api.example.com')",
3393
+ "conn3.request('POST', '/items', 'a=1', {'Content-Type': 'text/plain'})",
3394
+ "assrt.equal(conn3._method, 'POST')",
3395
+ "assrt.equal(conn3._path, '/items')",
3396
+ "assrt.equal(conn3._body, 'a=1')",
3397
+ "assrt.equal(conn3._headers['content-type'], 'text/plain')",
3398
+ // HTTPResponse accessors
3399
+ "hdrs = {'content-type': 'application/json'}",
3400
+ "resp = HTTPResponse(200, 'OK', hdrs, '{\"n\":42}', 'https://x.com/')",
3401
+ "assrt.equal(resp.status, 200)",
3402
+ "assrt.equal(resp.reason, 'OK')",
3403
+ "assrt.equal(resp.getheader('content-type'), 'application/json')",
3404
+ "assrt.equal(resp.getheader('Content-Type'), 'application/json')",
3405
+ "assrt.equal(resp.getheader('missing', 'def'), 'def')",
3406
+ "assrt.ok(resp.read() is not None)",
3407
+ "assrt.ok(resp.json() is not None)",
3408
+ // exception hierarchy
3409
+ "caught = False",
3410
+ "try:",
3411
+ " raise NotConnected('nc')",
3412
+ "except HTTPException as e:",
3413
+ " caught = True",
3414
+ "assrt.ok(caught)",
3415
+ ].join("\n"));
3416
+ run_js(js);
3417
+ },
3418
+ },
3419
+
3420
+ {
3421
+ name: "bundle_http_cookies_basic",
3422
+ description: "http.cookies stdlib: SimpleCookie parsing and Morsel access in the web-repl bundle",
3423
+ run: function () {
3424
+ var repl = RS.web_repl();
3425
+ var js = bundle_compile(repl, [
3426
+ "from __python__ import overload_getitem",
3427
+ "from http.cookies import SimpleCookie, Morsel, CookieError",
3428
+ // basic parse
3429
+ "c = SimpleCookie()",
3430
+ "c.load('session=abc123; user=alice')",
3431
+ "assrt.ok('session' in c.keys())",
3432
+ "assrt.ok('user' in c.keys())",
3433
+ "assrt.equal(c['session'].value, 'abc123')",
3434
+ "assrt.equal(c['user'].value, 'alice')",
3435
+ // set a cookie
3436
+ "c2 = SimpleCookie()",
3437
+ "c2['token'] = 'xyz'",
3438
+ "assrt.ok('token' in c2.keys())",
3439
+ "assrt.equal(c2['token'].value, 'xyz')",
3440
+ // cookie attributes
3441
+ "c3 = SimpleCookie()",
3442
+ "c3['id'] = '1'",
3443
+ "c3['id']['path'] = '/'",
3444
+ "c3['id']['max-age'] = 3600",
3445
+ "assrt.equal(c3['id']['path'], '/')",
3446
+ "assrt.equal(c3['id']['max-age'], 3600)",
3447
+ // constructor with initial data
3448
+ "c4 = SimpleCookie('x=10; y=20')",
3449
+ "assrt.equal(c4['x'].value, '10')",
3450
+ "assrt.equal(c4['y'].value, '20')",
3451
+ ].join("\n"));
3452
+ run_js(js);
3453
+ },
3454
+ },
3455
+
3456
+ {
3457
+ name: "bundle_http_cookies_output",
3458
+ description: "http.cookies stdlib: SimpleCookie.output, Morsel.OutputString in the web-repl bundle",
3459
+ run: function () {
3460
+ var repl = RS.web_repl();
3461
+ var js = bundle_compile(repl, [
3462
+ "from __python__ import overload_getitem",
3463
+ "from http.cookies import SimpleCookie, Morsel, CookieError",
3464
+ // Morsel.OutputString
3465
+ "m = Morsel()",
3466
+ "m.set('token', 'abc', 'abc')",
3467
+ "m._attrs['path'] = '/'",
3468
+ "m._attrs['max-age'] = '3600'",
3469
+ "s = m.OutputString()",
3470
+ "assrt.ok('token=abc' in s)",
3471
+ "assrt.ok('Path=/' in s)",
3472
+ "assrt.ok('Max-Age=3600' in s)",
3473
+ // Morsel.output with header
3474
+ "out = m.output()",
3475
+ "assrt.ok('Set-Cookie: token=abc' in out)",
3476
+ // SimpleCookie.output
3477
+ "c = SimpleCookie()",
3478
+ "c['a'] = '1'",
3479
+ "c['b'] = '2'",
3480
+ "full = c.output()",
3481
+ "assrt.ok('Set-Cookie: a=1' in full)",
3482
+ "assrt.ok('Set-Cookie: b=2' in full)",
3483
+ // CookieError
3484
+ "caught = False",
3485
+ "try:",
3486
+ " raise CookieError('bad')",
3487
+ "except CookieError as e:",
3488
+ " caught = True",
3489
+ "assrt.ok(caught)",
3490
+ ].join("\n"));
3491
+ run_js(js);
3492
+ },
3493
+ },
3494
+
3495
+ // ── csv stdlib ────────────────────────────────────────────────────────
3496
+
3497
+ {
3498
+ name: "bundle_csv_reader_basic",
3499
+ description: "csv stdlib: reader parses CSV rows and writer produces CSV text in the web-repl bundle",
3500
+ run: function () {
3501
+ var repl = RS.web_repl();
3502
+ var js = bundle_compile(repl, [
3503
+ "import csv",
3504
+ "from io import StringIO",
3505
+ // reader — list input
3506
+ "rows = []",
3507
+ "for row in csv.reader(['a,b,c', '1,2,3']):",
3508
+ " rows.push(row)",
3509
+ "assrt.equal(rows.length, 2)",
3510
+ "assrt.deepEqual(rows[0], ['a', 'b', 'c'])",
3511
+ "assrt.deepEqual(rows[1], ['1', '2', '3'])",
3512
+ // reader — quoted field
3513
+ "rows2 = []",
3514
+ "for row in csv.reader(['\"hello, world\",foo']):",
3515
+ " rows2.push(row)",
3516
+ "assrt.equal(rows2[0][0], 'hello, world')",
3517
+ "assrt.equal(rows2[0][1], 'foo')",
3518
+ // writer
3519
+ "sio = StringIO()",
3520
+ "w = csv.writer(sio)",
3521
+ "w.writerow(['name', 'age'])",
3522
+ "w.writerow(['Alice', 30])",
3523
+ "out = sio.getvalue()",
3524
+ "assrt.ok('name,age' in out)",
3525
+ "assrt.ok('Alice,30' in out)",
3526
+ // round-trip
3527
+ "buf = StringIO()",
3528
+ "w2 = csv.writer(buf)",
3529
+ "w2.writerow(['x', 'needs, quoting', 'y'])",
3530
+ "buf.seek(0)",
3531
+ "rt = []",
3532
+ "for row in csv.reader(buf):",
3533
+ " rt.push(row)",
3534
+ "assrt.deepEqual(rt[0], ['x', 'needs, quoting', 'y'])",
3535
+ ].join("\n"));
3536
+ run_js(js);
3537
+ },
3538
+ },
3539
+
3540
+ {
3541
+ name: "bundle_csv_dictreader",
3542
+ description: "csv stdlib: DictReader reads rows as dicts with automatic fieldnames in the web-repl bundle",
3543
+ run: function () {
3544
+ var repl = RS.web_repl();
3545
+ var js = bundle_compile(repl, [
3546
+ "import csv",
3547
+ // DictReader — fieldnames from first row
3548
+ "rows = []",
3549
+ "for row in csv.DictReader(['name,age', 'Alice,30', 'Bob,25']):",
3550
+ " rows.push(row)",
3551
+ "assrt.equal(rows.length, 2)",
3552
+ "assrt.equal(rows[0]['name'], 'Alice')",
3553
+ "assrt.equal(rows[0]['age'], '30')",
3554
+ "assrt.equal(rows[1]['name'], 'Bob')",
3555
+ // DictReader — provided fieldnames
3556
+ "rows2 = []",
3557
+ "for row in csv.DictReader(['Alice,30', 'Bob,25'], fieldnames=['name','age']):",
3558
+ " rows2.push(row)",
3559
+ "assrt.equal(rows2.length, 2)",
3560
+ "assrt.equal(rows2[0]['name'], 'Alice')",
3561
+ // DictReader — restval for missing field
3562
+ "rows3 = []",
3563
+ "for row in csv.DictReader(['name,age,city', 'Alice,30'], restval='?'):",
3564
+ " rows3.push(row)",
3565
+ "assrt.equal(rows3[0]['city'], '?')",
3566
+ // DictReader — empty input yields no rows
3567
+ "empty_count = 0",
3568
+ "for row in csv.DictReader([]):",
3569
+ " empty_count += 1",
3570
+ "assrt.equal(empty_count, 0)",
3571
+ ].join("\n"));
3572
+ run_js(js);
3573
+ },
3574
+ },
3575
+
3576
+ {
3577
+ name: "bundle_csv_dictwriter",
3578
+ description: "csv stdlib: DictWriter writes header and rows from dicts in the web-repl bundle",
3579
+ run: function () {
3580
+ var repl = RS.web_repl();
3581
+ var js = bundle_compile(repl, [
3582
+ "import csv",
3583
+ "from io import StringIO",
3584
+ // DictWriter — writeheader + writerow
3585
+ "sio = StringIO()",
3586
+ "dw = csv.DictWriter(sio, ['name', 'score'])",
3587
+ "dw.writeheader()",
3588
+ "dw.writerow({'name': 'Eve', 'score': '99'})",
3589
+ "dw.writerow({'name': 'Frank', 'score': '88'})",
3590
+ "out = sio.getvalue()",
3591
+ "assrt.ok('name,score' in out)",
3592
+ "assrt.ok('Eve,99' in out)",
3593
+ "assrt.ok('Frank,88' in out)",
3594
+ // round-trip DictWriter → DictReader
3595
+ "buf = StringIO()",
3596
+ "dw2 = csv.DictWriter(buf, ['x', 'y'])",
3597
+ "dw2.writeheader()",
3598
+ "dw2.writerow({'x': 'hello', 'y': 'world'})",
3599
+ "buf.seek(0)",
3600
+ "rt = []",
3601
+ "for row in csv.DictReader(buf):",
3602
+ " rt.push(row)",
3603
+ "assrt.equal(rt.length, 1)",
3604
+ "assrt.equal(rt[0]['x'], 'hello')",
3605
+ "assrt.equal(rt[0]['y'], 'world')",
3606
+ ].join("\n"));
3607
+ run_js(js);
3608
+ },
3609
+ },
3610
+
3611
+ {
3612
+ name: "bundle_csv_dialects",
3613
+ description: "csv stdlib: dialect options, register_dialect, list_dialects, field_size_limit in the web-repl bundle",
3614
+ run: function () {
3615
+ var repl = RS.web_repl();
3616
+ var js = bundle_compile(repl, [
3617
+ "import csv",
3618
+ "from io import StringIO",
3619
+ // excel-tab dialect
3620
+ "rows = []",
3621
+ "for row in csv.reader(['a\\tb\\tc'], dialect='excel-tab'):",
3622
+ " rows.push(row)",
3623
+ "assrt.deepEqual(rows[0], ['a', 'b', 'c'])",
3624
+ // QUOTE_ALL
3625
+ "sio = StringIO()",
3626
+ "w = csv.writer(sio, quoting=csv.QUOTE_ALL)",
3627
+ "w.writerow(['x', 'y'])",
3628
+ "assrt.equal(sio.getvalue(), '\"x\",\"y\"\\r\\n')",
3629
+ // register_dialect / list_dialects / unregister_dialect
3630
+ "csv.register_dialect('pipes', delimiter='|')",
3631
+ "dialects = csv.list_dialects()",
3632
+ "assrt.ok(dialects.indexOf('pipes') >= 0)",
3633
+ "sio2 = StringIO()",
3634
+ "w2 = csv.writer(sio2, dialect='pipes')",
3635
+ "w2.writerow(['a', 'b'])",
3636
+ "assrt.equal(sio2.getvalue(), 'a|b\\r\\n')",
3637
+ "csv.unregister_dialect('pipes')",
3638
+ "assrt.ok(csv.list_dialects().indexOf('pipes') < 0)",
3639
+ // field_size_limit
3640
+ "old = csv.field_size_limit(65536)",
3641
+ "assrt.equal(old, 131072)",
3642
+ "assrt.equal(csv.field_size_limit(), 65536)",
3643
+ "csv.field_size_limit(131072)",
3644
+ ].join("\n"));
3645
+ run_js(js);
3646
+ },
3647
+ },
3648
+
3649
+ {
3650
+ name: "bundle_textwrap_wrap",
3651
+ description: "textwrap stdlib: wrap, fill, shorten in the web-repl bundle",
3652
+ run: function () {
3653
+ var repl = RS.web_repl();
3654
+ var js = bundle_compile(repl, [
3655
+ "from textwrap import wrap, fill, shorten",
3656
+ // basic wrap
3657
+ "r = wrap('one two three four five', 10)",
3658
+ "assrt.equal(r.length, 3)",
3659
+ "assrt.equal(r[0], 'one two')",
3660
+ "assrt.equal(r[1], 'three four')",
3661
+ "assrt.equal(r[2], 'five')",
3662
+ // short text fits on one line
3663
+ "assrt.deepEqual(wrap('hello', 20), ['hello'])",
3664
+ // empty string
3665
+ "assrt.deepEqual(wrap('', 10), [])",
3666
+ // fill joins with newlines
3667
+ "assrt.equal(fill('one two three', 8), 'one two\\nthree')",
3668
+ // fill with indents
3669
+ "assrt.equal(fill('one two three four', 12, initial_indent='> ', subsequent_indent=' '), '> one two\\n three four')",
3670
+ // shorten — fits
3671
+ "assrt.equal(shorten('hello world', 20), 'hello world')",
3672
+ // shorten — truncates; 'one two three' (13) + ' [...]' (6) = 19 ≤ 20
3673
+ "assrt.equal(shorten('one two three four five', 20), 'one two three [...]')",
3674
+ // shorten — custom placeholder
3675
+ "assrt.equal(shorten('hello world foo bar', 14, placeholder='...'), 'hello world...')",
3676
+ // shorten — normalises whitespace
3677
+ "assrt.equal(shorten('hello world', 20), 'hello world')",
3678
+ ].join("\n"));
3679
+ run_js(js);
3680
+ },
3681
+ },
3682
+
3683
+ {
3684
+ name: "bundle_textwrap_dedent",
3685
+ description: "textwrap stdlib: dedent in the web-repl bundle",
3686
+ run: function () {
3687
+ var repl = RS.web_repl();
3688
+ var js = bundle_compile(repl, [
3689
+ "from textwrap import dedent",
3690
+ // common indent removed
3691
+ "assrt.equal(dedent(' hello\\n world'), 'hello\\nworld')",
3692
+ // no common indent
3693
+ "assrt.equal(dedent('hello\\n world'), 'hello\\n world')",
3694
+ // empty lines ignored when computing margin
3695
+ "assrt.equal(dedent(' hello\\n\\n world'), 'hello\\n\\nworld')",
3696
+ // partial common indent: ' ' vs ' ' → margin ' '
3697
+ "assrt.equal(dedent(' foo\\n bar'), ' foo\\nbar')",
3698
+ // leading blank line then indented
3699
+ "assrt.equal(dedent('\\n foo\\n bar'), '\\nfoo\\nbar')",
3700
+ // tab-based indent
3701
+ "assrt.equal(dedent('\\thello\\n\\tworld'), 'hello\\nworld')",
3702
+ ].join("\n"));
3703
+ run_js(js);
3704
+ },
3705
+ },
3706
+
3707
+ {
3708
+ name: "bundle_textwrap_indent",
3709
+ description: "textwrap stdlib: indent with and without predicate in the web-repl bundle",
3710
+ run: function () {
3711
+ var repl = RS.web_repl();
3712
+ var js = bundle_compile(repl, [
3713
+ "from textwrap import indent",
3714
+ // basic
3715
+ "assrt.equal(indent('hello\\nworld', ' '), ' hello\\n world')",
3716
+ // empty lines not indented by default
3717
+ "assrt.equal(indent('hello\\n\\nworld', ' '), ' hello\\n\\n world')",
3718
+ // whitespace-only line not indented
3719
+ "assrt.equal(indent('hello\\n \\nworld', '> '), '> hello\\n \\n> world')",
3720
+ // custom predicate: indent all lines
3721
+ "_pred = def(line): return True;",
3722
+ "assrt.equal(indent('hello\\n\\nworld', '> ', _pred), '> hello\\n> \\n> world')",
3723
+ // predicate: only lines starting with '#'
3724
+ "_ph = def(line): return line.startsWith('#');",
3725
+ "assrt.equal(indent('# a\\ncode\\n# b', '!! ', _ph), '!! # a\\ncode\\n!! # b')",
3726
+ ].join("\n"));
3727
+ run_js(js);
3728
+ },
3729
+ },
3730
+
3731
+ {
3732
+ name: "bundle_textwrap_textwrapper",
3733
+ description: "textwrap stdlib: TextWrapper class with options in the web-repl bundle",
3734
+ run: function () {
3735
+ var repl = RS.web_repl();
3736
+ var js = bundle_compile(repl, [
3737
+ "from textwrap import TextWrapper",
3738
+ // basic wrap and fill
3739
+ "tw = TextWrapper(width=10)",
3740
+ "assrt.deepEqual(tw.wrap('one two three'), ['one two', 'three'])",
3741
+ "assrt.equal(tw.fill('one two three'), 'one two\\nthree')",
3742
+ // max_lines with placeholder
3743
+ // width=15, max_lines=2, placeholder=' ...' (4 chars)
3744
+ // line 1: 'alpha beta' (10) fits; line 2 truncated to 'gamma delta ...' (15)
3745
+ "tw2 = TextWrapper(width=15, max_lines=2, placeholder=' ...')",
3746
+ "r2 = tw2.wrap('alpha beta gamma delta epsilon')",
3747
+ "assrt.equal(r2.length, 2)",
3748
+ "assrt.equal(r2[0], 'alpha beta')",
3749
+ "assrt.equal(r2[1], 'gamma delta ...')",
3750
+ // break_long_words=False
3751
+ "tw3 = TextWrapper(width=5, break_long_words=False)",
3752
+ "r3 = tw3.wrap('superlongword short')",
3753
+ "assrt.equal(r3[0], 'superlongword')",
3754
+ "assrt.equal(r3[1], 'short')",
3755
+ // fix_sentence_endings
3756
+ "tw4 = TextWrapper(width=70, fix_sentence_endings=True)",
3757
+ "r4 = tw4.wrap('end of sentence. New sentence.')",
3758
+ "assrt.equal(r4[0], 'end of sentence. New sentence.')",
3759
+ ].join("\n"));
3760
+ run_js(js);
3761
+ },
3762
+ },
3763
+
3764
+ // ── logging ──────────────────────────────────────────────────────────────
3765
+
3766
+ {
3767
+ name: "bundle_logging_basic",
3768
+ description: "logging stdlib: StreamHandler with custom stream in the web-repl bundle",
3769
+ run: function () {
3770
+ var repl = RS.web_repl();
3771
+ var js = bundle_compile(repl, [
3772
+ "from logging import Logger, StreamHandler, Formatter, DEBUG, INFO, WARNING, ERROR",
3773
+ "class _Buf:",
3774
+ " def __init__(self):",
3775
+ " self.lines = []",
3776
+ " def write(self, s):",
3777
+ " self.lines.push(s)",
3778
+ "_buf = _Buf()",
3779
+ "_h = StreamHandler(_buf)",
3780
+ "_h.setFormatter(Formatter('%(levelname)s:%(name)s:%(message)s'))",
3781
+ "_h.setLevel(DEBUG)",
3782
+ "_log = Logger('myapp')",
3783
+ "_log.addHandler(_h)",
3784
+ "_log.setLevel(DEBUG)",
3785
+ "_log.propagate = False",
3786
+ "_log.debug('dbg')",
3787
+ "_log.info('hi')",
3788
+ "_log.warning('warn')",
3789
+ "_log.error('err')",
3790
+ "assrt.equal(_buf.lines.length, 4)",
3791
+ "assrt.equal(_buf.lines[0], 'DEBUG:myapp:dbg\\n')",
3792
+ "assrt.equal(_buf.lines[1], 'INFO:myapp:hi\\n')",
3793
+ "assrt.equal(_buf.lines[2], 'WARNING:myapp:warn\\n')",
3794
+ "assrt.equal(_buf.lines[3], 'ERROR:myapp:err\\n')",
3795
+ ].join("\n"));
3796
+ run_js(js);
3797
+ },
3798
+ },
3799
+
3800
+ {
3801
+ name: "bundle_logging_levels",
3802
+ description: "logging stdlib: level constants, getLevelName, addLevelName, %-format args",
3803
+ run: function () {
3804
+ var repl = RS.web_repl();
3805
+ var js = bundle_compile(repl, [
3806
+ "from logging import getLevelName, addLevelName, DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET",
3807
+ "assrt.equal(NOTSET, 0)",
3808
+ "assrt.equal(DEBUG, 10)",
3809
+ "assrt.equal(INFO, 20)",
3810
+ "assrt.equal(WARNING, 30)",
3811
+ "assrt.equal(ERROR, 40)",
3812
+ "assrt.equal(CRITICAL, 50)",
3813
+ "assrt.equal(getLevelName(DEBUG), 'DEBUG')",
3814
+ "assrt.equal(getLevelName(WARNING), 'WARNING')",
3815
+ "assrt.equal(getLevelName(42), 'Level 42')",
3816
+ "assrt.equal(getLevelName('ERROR'), ERROR)",
3817
+ "assrt.equal(getLevelName('WARN'), WARNING)",
3818
+ "addLevelName(15, 'VERBOSE')",
3819
+ "assrt.equal(getLevelName(15), 'VERBOSE')",
3820
+ "assrt.equal(getLevelName('VERBOSE'), 15)",
3821
+ // %-format args via Logger
3822
+ "from logging import Logger, StreamHandler, Formatter",
3823
+ "class _B:",
3824
+ " def __init__(self):",
3825
+ " self.out = []",
3826
+ " def write(self, s):",
3827
+ " self.out.push(s)",
3828
+ "_b = _B()",
3829
+ "_h2 = StreamHandler(_b)",
3830
+ "_h2.setFormatter(Formatter('%(message)s'))",
3831
+ "_l2 = Logger('fmt')",
3832
+ "_l2.addHandler(_h2)",
3833
+ "_l2.setLevel(DEBUG)",
3834
+ "_l2.propagate = False",
3835
+ "_l2.info('x=%d y=%s', 7, 'foo')",
3836
+ "assrt.equal(_b.out[0], 'x=7 y=foo\\n')",
3837
+ ].join("\n"));
3838
+ run_js(js);
3839
+ },
3840
+ },
3841
+
3842
+ {
3843
+ name: "bundle_logging_hierarchy",
3844
+ description: "logging stdlib: parent/child logger hierarchy and propagation",
3845
+ run: function () {
3846
+ var repl = RS.web_repl();
3847
+ var js = bundle_compile(repl, [
3848
+ "from logging import Logger, StreamHandler, Formatter, DEBUG, INFO",
3849
+ "class _B:",
3850
+ " def __init__(self):",
3851
+ " self.out = []",
3852
+ " def write(self, s):",
3853
+ " self.out.push(s)",
3854
+ "_b = _B()",
3855
+ "_h = StreamHandler(_b)",
3856
+ "_h.setFormatter(Formatter('%(name)s:%(message)s'))",
3857
+ "_parent = Logger('app')",
3858
+ "_parent.addHandler(_h)",
3859
+ "_parent.setLevel(DEBUG)",
3860
+ "_parent.propagate = False",
3861
+ "_child = Logger('app.sub')",
3862
+ "_child.parent = _parent",
3863
+ "_child.setLevel(DEBUG)",
3864
+ "_child.propagate = True",
3865
+ "_child.info('from child')",
3866
+ "assrt.equal(_b.out.length, 1)",
3867
+ "assrt.equal(_b.out[0], 'app.sub:from child\\n')",
3868
+ // child with propagate=False should NOT reach parent handler
3869
+ "_b2 = _B()",
3870
+ "_h2 = StreamHandler(_b2)",
3871
+ "_h2.setFormatter(Formatter('%(message)s'))",
3872
+ "_child2 = Logger('app.sub2')",
3873
+ "_child2.parent = _parent",
3874
+ "_child2.addHandler(_h2)",
3875
+ "_child2.setLevel(DEBUG)",
3876
+ "_child2.propagate = False",
3877
+ "_child2.info('isolated')",
3878
+ "assrt.equal(_b.out.length, 1)", // parent buffer unchanged
3879
+ "assrt.equal(_b2.out[0], 'isolated\\n')",
3880
+ ].join("\n"));
3881
+ run_js(js);
3882
+ },
3883
+ },
3884
+
3885
+ {
3886
+ name: "bundle_logging_basicconfig",
3887
+ description: "logging stdlib: basicConfig sets up root logger with custom stream",
3888
+ run: function () {
3889
+ var repl = RS.web_repl();
3890
+ var js = bundle_compile(repl, [
3891
+ "import logging",
3892
+ "class _B:",
3893
+ " def __init__(self):",
3894
+ " self.out = []",
3895
+ " def write(self, s):",
3896
+ " self.out.push(s)",
3897
+ "_b = _B()",
3898
+ "logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s', stream=_b)",
3899
+ "logging.debug('d')",
3900
+ "logging.info('i')",
3901
+ "logging.warning('w')",
3902
+ "assrt.equal(_b.out.length, 3)",
3903
+ "assrt.equal(_b.out[0], 'DEBUG:d\\n')",
3904
+ "assrt.equal(_b.out[1], 'INFO:i\\n')",
3905
+ "assrt.equal(_b.out[2], 'WARNING:w\\n')",
3906
+ // disable() suppresses messages at or below the given level
3907
+ "logging.disable(logging.ERROR)",
3908
+ "logging.warning('suppressed')",
3909
+ "logging.error('also suppressed')",
3910
+ "assrt.equal(_b.out.length, 3)",
3911
+ "logging.disable(logging.NOTSET)", // reset
3912
+ ].join("\n"));
3913
+ run_js(js);
3914
+ },
3915
+ },
3916
+
3917
+ {
3918
+ name: "bundle_type_enforcement_basic",
3919
+ description: "type_enforcement: max args, missing required, type annotations",
3920
+ run: function () {
3921
+ var repl = RS.web_repl();
3922
+ var js = bundle_compile(repl, [
3923
+ "from __python__ import type_enforcement",
3924
+ "def add(a: int, b: int):",
3925
+ " return a + b",
3926
+ // correct call
3927
+ "assrt.equal(add(2, 3), 5)",
3928
+ // too many positional args
3929
+ "caught = False",
3930
+ "try:",
3931
+ " add(1, 2, 3)",
3932
+ "except TypeError:",
3933
+ " caught = True",
3934
+ "assrt.ok(caught)",
3935
+ // missing required arg
3936
+ "caught2 = False",
3937
+ "try:",
3938
+ " add(1)",
3939
+ "except TypeError:",
3940
+ " caught2 = True",
3941
+ "assrt.ok(caught2)",
3942
+ // type mismatch
3943
+ "caught3 = False",
3944
+ "try:",
3945
+ " add('x', 2)",
3946
+ "except TypeError:",
3947
+ " caught3 = True",
3948
+ "assrt.ok(caught3)",
3949
+ ].join("\n"));
3950
+ run_js(js);
3951
+ },
3952
+ },
3953
+
3954
+ {
3955
+ name: "bundle_type_enforcement_posonly",
3956
+ description: "type_enforcement: positional-only args cannot be passed as kwargs",
3957
+ run: function () {
3958
+ var repl = RS.web_repl();
3959
+ var js = bundle_compile(repl, [
3960
+ "from __python__ import type_enforcement",
3961
+ "def sub(a, b, /):",
3962
+ " return a - b",
3963
+ // valid positional call
3964
+ "assrt.equal(sub(10, 3), 7)",
3965
+ // posonly passed as kwarg → TypeError
3966
+ "caught = False",
3967
+ "try:",
3968
+ " sub(a=10, b=3)",
3969
+ "except TypeError:",
3970
+ " caught = True",
3971
+ "assrt.ok(caught)",
3972
+ // mixed: posonly + default normal
3973
+ "def greet(name, /, greeting='Hello'):",
3974
+ " return greeting + ' ' + name",
3975
+ "assrt.equal(greet('Alice'), 'Hello Alice')",
3976
+ "assrt.equal(greet('Bob', greeting='Hi'), 'Hi Bob')",
3977
+ "caught2 = False",
3978
+ "try:",
3979
+ " greet(name='Carol')",
3980
+ "except TypeError:",
3981
+ " caught2 = True",
3982
+ "assrt.ok(caught2)",
3983
+ ].join("\n"));
3984
+ run_js(js);
3985
+ },
3986
+ },
3987
+
3988
+ {
3989
+ name: "bundle_type_enforcement_kwonly",
3990
+ description: "type_enforcement: keyword-only args must be supplied by name",
3991
+ run: function () {
3992
+ var repl = RS.web_repl();
3993
+ var js = bundle_compile(repl, [
3994
+ "from __python__ import type_enforcement",
3995
+ "def notify(msg, *, urgent):",
3996
+ " return msg + ('!' if urgent else '.')",
3997
+ // correct kwonly usage
3998
+ "assrt.equal(notify('hi', urgent=True), 'hi!')",
3999
+ "assrt.equal(notify('hi', urgent=False), 'hi.')",
4000
+ // missing required kwonly → TypeError
4001
+ "caught = False",
4002
+ "try:",
4003
+ " notify('hello')",
4004
+ "except TypeError:",
4005
+ " caught = True",
4006
+ "assrt.ok(caught)",
4007
+ // optional kwonly with default — no error when omitted
4008
+ "def fmt(val, *, prefix=''):",
4009
+ " return prefix + str(val)",
4010
+ "assrt.equal(fmt(42), '42')",
4011
+ "assrt.equal(fmt(42, prefix='>> '), '>> 42')",
4012
+ ].join("\n"));
4013
+ run_js(js);
4014
+ },
4015
+ },
4016
+
4017
+ {
4018
+ name: "bundle_type_enforcement_class",
4019
+ description: "type_enforcement: class method arg enforcement",
4020
+ run: function () {
4021
+ var repl = RS.web_repl();
4022
+ var js = bundle_compile(repl, [
4023
+ "from __python__ import type_enforcement",
4024
+ "class Vec:",
4025
+ " def __init__(self, x: int, y: int):",
4026
+ " self.x = x",
4027
+ " self.y = y",
4028
+ " def scale(self, factor: int, /, *, clamp=False):",
4029
+ " v = self.x * factor",
4030
+ " return min(v, 100) if clamp else v",
4031
+ "v = Vec(3, 4)",
4032
+ "assrt.equal(v.scale(2), 6)",
4033
+ "assrt.equal(v.scale(50, clamp=True), 100)",
4034
+ // type mismatch in __init__
4035
+ "caught = False",
4036
+ "try:",
4037
+ " Vec('a', 1)",
4038
+ "except TypeError:",
4039
+ " caught = True",
4040
+ "assrt.ok(caught)",
4041
+ // factor is posonly → cannot be kwarg
4042
+ "caught2 = False",
4043
+ "try:",
4044
+ " v.scale(factor=2)",
4045
+ "except TypeError:",
4046
+ " caught2 = True",
4047
+ "assrt.ok(caught2)",
4048
+ ].join("\n"));
4049
+ run_js(js);
4050
+ },
4051
+ },
4052
+
4053
+ {
4054
+ name: "bundle_heapq_push_pop",
4055
+ description: "heapq stdlib: heappush and heappop in the web-repl bundle",
4056
+ run: function () {
4057
+ var repl = RS.web_repl();
4058
+ var js = bundle_compile(repl, [
4059
+ "from heapq import heappush, heappop",
4060
+ // push in arbitrary order, pop should return sorted
4061
+ "h = []",
4062
+ "heappush(h, 3)",
4063
+ "heappush(h, 1)",
4064
+ "heappush(h, 4)",
4065
+ "heappush(h, 1)",
4066
+ "heappush(h, 5)",
4067
+ "assrt.equal(heappop(h), 1)",
4068
+ "assrt.equal(heappop(h), 1)",
4069
+ "assrt.equal(heappop(h), 3)",
4070
+ "assrt.equal(heappop(h), 4)",
4071
+ "assrt.equal(heappop(h), 5)",
4072
+ "assrt.equal(h.length, 0)",
4073
+ // heappop on empty raises IndexError
4074
+ "caught = False",
4075
+ "try:",
4076
+ " heappop([])",
4077
+ "except IndexError:",
4078
+ " caught = True",
4079
+ "assrt.ok(caught)",
4080
+ ].join("\n"));
4081
+ run_js(js);
4082
+ },
4083
+ },
4084
+
4085
+ {
4086
+ name: "bundle_heapq_heapify",
4087
+ description: "heapq stdlib: heapify in the web-repl bundle",
4088
+ run: function () {
4089
+ var repl = RS.web_repl();
4090
+ var js = bundle_compile(repl, [
4091
+ "from heapq import heapify, heappop",
4092
+ // heapify puts min at root
4093
+ "x = [5, 3, 8, 1, 2, 4]",
4094
+ "heapify(x)",
4095
+ "assrt.equal(x[0], 1)",
4096
+ // heapify + repeated heappop gives sorted order
4097
+ "data = [5, 3, 8, 1, 2, 4]",
4098
+ "heapify(data)",
4099
+ "result = []",
4100
+ "while data.length > 0:",
4101
+ " result.push(heappop(data))",
4102
+ "assrt.deepEqual(result, [1, 2, 3, 4, 5, 8])",
4103
+ // negative numbers
4104
+ "neg = [-3, -1, -4, -1, -5]",
4105
+ "heapify(neg)",
4106
+ "neg_out = []",
4107
+ "while neg.length > 0:",
4108
+ " neg_out.push(heappop(neg))",
4109
+ "assrt.deepEqual(neg_out, [-5, -4, -3, -1, -1])",
4110
+ ].join("\n"));
4111
+ run_js(js);
4112
+ },
4113
+ },
4114
+
4115
+ {
4116
+ name: "bundle_heapq_nsmallest_nlargest",
4117
+ description: "heapq stdlib: nsmallest and nlargest in the web-repl bundle",
4118
+ run: function () {
4119
+ var repl = RS.web_repl();
4120
+ var js = bundle_compile(repl, [
4121
+ "from heapq import nsmallest, nlargest",
4122
+ "data = [3, 1, 4, 1, 5, 9, 2, 6]",
4123
+ "assrt.deepEqual(nsmallest(3, data), [1, 1, 2])",
4124
+ "assrt.deepEqual(nlargest(3, data), [9, 6, 5])",
4125
+ "assrt.deepEqual(nsmallest(0, data), [])",
4126
+ "assrt.deepEqual(nlargest(0, data), [])",
4127
+ // n > len returns all sorted
4128
+ "assrt.deepEqual(nsmallest(100, [3, 1, 2]), [1, 2, 3])",
4129
+ "assrt.deepEqual(nlargest(100, [3, 1, 2]), [3, 2, 1])",
4130
+ // key function
4131
+ "pairs = [[3, 'c'], [1, 'a'], [4, 'd'], [1, 'b'], [5, 'e']]",
4132
+ "kfn = def(p): return p[0];",
4133
+ "sm = nsmallest(2, pairs, key=kfn)",
4134
+ "assrt.equal(sm[0][0], 1)",
4135
+ "assrt.equal(sm[1][0], 1)",
4136
+ "lg = nlargest(2, pairs, key=kfn)",
4137
+ "assrt.equal(lg[0][0], 5)",
4138
+ "assrt.equal(lg[1][0], 4)",
4139
+ ].join("\n"));
4140
+ run_js(js);
4141
+ },
4142
+ },
4143
+
4144
+ {
4145
+ name: "bundle_heapq_replace_pushpop",
4146
+ description: "heapq stdlib: heapreplace and heappushpop in the web-repl bundle",
4147
+ run: function () {
4148
+ var repl = RS.web_repl();
4149
+ var js = bundle_compile(repl, [
4150
+ "from heapq import heapify, heappop, heapreplace, heappushpop",
4151
+ // heapreplace: returns old min, inserts new item
4152
+ "r = [1, 3, 5, 7, 9]",
4153
+ "heapify(r)",
4154
+ "old = heapreplace(r, 4)",
4155
+ "assrt.equal(old, 1)",
4156
+ "assrt.equal(r[0], 3)",
4157
+ // heapreplace on empty raises IndexError
4158
+ "caught_rep = False",
4159
+ "try:",
4160
+ " heapreplace([], 1)",
4161
+ "except IndexError:",
4162
+ " caught_rep = True",
4163
+ "assrt.ok(caught_rep)",
4164
+ // heappushpop: item > root — returns root
4165
+ "pp = [1, 3, 5]",
4166
+ "heapify(pp)",
4167
+ "assrt.equal(heappushpop(pp, 2), 1)",
4168
+ "assrt.equal(pp[0], 2)",
4169
+ // heappushpop: item <= root — returns item unchanged
4170
+ "pp2 = [5, 7, 9]",
4171
+ "heapify(pp2)",
4172
+ "assrt.equal(heappushpop(pp2, 4), 4)",
4173
+ "assrt.equal(pp2[0], 5)",
4174
+ ].join("\n"));
4175
+ run_js(js);
4176
+ },
4177
+ },
4178
+
4179
+ // ── parenthesized with (Python 3.10+) ────────────────────────────────────
4180
+
4181
+ {
4182
+ name: "bundle_paren_with_single",
4183
+ description: "parenthesized with: single clause with alias compiles and runs in the web-repl bundle",
4184
+ run: function () {
4185
+ var repl = RS.web_repl();
4186
+ var js = bundle_compile(repl, [
4187
+ "class _CM:",
4188
+ " def __init__(self, val):",
4189
+ " self.val = val",
4190
+ " def __enter__(self):",
4191
+ " return self.val",
4192
+ " def __exit__(self):",
4193
+ " pass",
4194
+ "with (_CM(42) as x):",
4195
+ " assrt.equal(x, 42)",
4196
+ ].join("\n"));
4197
+ run_js(js);
4198
+ },
4199
+ },
4200
+
4201
+ {
4202
+ name: "bundle_paren_with_multi",
4203
+ description: "parenthesized with: multi-clause LIFO exit order compiles and runs in the web-repl bundle",
4204
+ run: function () {
4205
+ var repl = RS.web_repl();
4206
+ var js = bundle_compile(repl, [
4207
+ "log = []",
4208
+ "class _CM:",
4209
+ " def __init__(self, name):",
4210
+ " self.name = name",
4211
+ " def __enter__(self):",
4212
+ " log.push('enter:' + self.name)",
4213
+ " return self",
4214
+ " def __exit__(self):",
4215
+ " log.push('exit:' + self.name)",
4216
+ "with (_CM('a') as a, _CM('b') as b):",
4217
+ " log.push('body')",
4218
+ "assrt.equal(log[0], 'enter:a')",
4219
+ "assrt.equal(log[1], 'enter:b')",
4220
+ "assrt.equal(log[2], 'body')",
4221
+ "assrt.equal(log[3], 'exit:b')",
4222
+ "assrt.equal(log[4], 'exit:a')",
4223
+ ].join("\n"));
4224
+ run_js(js);
4225
+ },
4226
+ },
4227
+
4228
+ {
4229
+ name: "bundle_paren_with_multiline",
4230
+ description: "parenthesized with: multi-line trailing-comma form compiles and runs in the web-repl bundle",
4231
+ run: function () {
4232
+ var repl = RS.web_repl();
4233
+ var js = bundle_compile(repl, [
4234
+ "log = []",
4235
+ "class _CM:",
4236
+ " def __init__(self, name):",
4237
+ " self.name = name",
4238
+ " def __enter__(self):",
4239
+ " log.push('enter:' + self.name)",
4240
+ " return self",
4241
+ " def __exit__(self):",
4242
+ " log.push('exit:' + self.name)",
4243
+ "with (",
4244
+ " _CM('x') as x,",
4245
+ " _CM('y') as y,",
4246
+ "):",
4247
+ " log.push('body')",
4248
+ "assrt.equal(log[0], 'enter:x')",
4249
+ "assrt.equal(log[1], 'enter:y')",
4250
+ "assrt.equal(log[2], 'body')",
4251
+ "assrt.equal(log[3], 'exit:y')",
4252
+ "assrt.equal(log[4], 'exit:x')",
4253
+ ].join("\n"));
4254
+ run_js(js);
4255
+ },
4256
+ },
4257
+
4258
+ {
4259
+ name: "bundle_paren_with_trailing_comma",
4260
+ description: "parenthesized with: trailing comma accepted in the web-repl bundle",
4261
+ run: function () {
4262
+ var repl = RS.web_repl();
4263
+ var js = bundle_compile(repl, [
4264
+ "class _CM:",
4265
+ " def __init__(self, val):",
4266
+ " self.val = val",
4267
+ " def __enter__(self):",
4268
+ " return self.val",
4269
+ " def __exit__(self):",
4270
+ " pass",
4271
+ "with (_CM(7) as v,):",
4272
+ " assrt.equal(v, 7)",
4273
+ ].join("\n"));
4274
+ run_js(js);
4275
+ },
4276
+ },
4277
+
4278
+ ];
4279
+
4280
+ // ---------------------------------------------------------------------------
4281
+ // Runner
4282
+ // ---------------------------------------------------------------------------
4283
+
4284
+ function run_tests(filter) {
4285
+ var tests = filter
4286
+ ? TESTS.filter(function (t) { return t.name === filter; })
4287
+ : TESTS;
4288
+
4289
+ if (tests.length === 0) {
4290
+ console.error(colored("No test found: " + filter, "red"));
4291
+ process.exit(1);
4292
+ }
4293
+
4294
+ var failures = [];
4295
+ tests.forEach(function (test) {
4296
+ try {
4297
+ test.run();
4298
+ console.log(colored("PASS " + test.name, "green") + " – " + test.description);
4299
+ } catch (e) {
4300
+ failures.push(test.name);
4301
+ console.log(colored("FAIL " + test.name, "red") + "\n " + (e.stack || String(e)) + "\n");
4302
+ }
4303
+ });
4304
+
4305
+ var passed = tests.length - failures.length;
4306
+ console.log("");
4307
+ if (failures.length) {
4308
+ console.log(colored("Failed tests:", "red"));
4309
+ failures.forEach(function (name) {
4310
+ console.log(colored(" ✗ " + name, "red"));
4311
+ });
4312
+ console.log("");
2253
4313
  }
4314
+ var summary = "web-repl tests — " +
4315
+ colored("passed: " + passed, "green") + " " +
4316
+ (failures.length ? colored("failed: " + failures.length, "red") : colored("failed: 0", "green")) +
4317
+ " total: " + tests.length;
4318
+ console.log(summary);
2254
4319
  process.exit(failures.length ? 1 : 0);
2255
4320
  }
2256
4321