rapydscript-ns 0.9.2 → 0.9.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/PYTHON_GAPS.md +352 -0
  3. package/README.md +176 -32
  4. package/TODO.md +1 -128
  5. package/bin/rapydscript +70 -70
  6. package/language-service/index.js +242 -11
  7. package/memory/project_string_impl.md +43 -0
  8. package/package.json +1 -1
  9. package/release/baselib-plain-pretty.js +248 -38
  10. package/release/baselib-plain-ugly.js +8 -8
  11. package/release/compiler.js +778 -277
  12. package/release/signatures.json +30 -30
  13. package/src/ast.pyj +10 -1
  14. package/src/baselib-builtins.pyj +56 -2
  15. package/src/baselib-containers.pyj +25 -1
  16. package/src/baselib-errors.pyj +7 -3
  17. package/src/baselib-internal.pyj +51 -6
  18. package/src/baselib-str.pyj +18 -5
  19. package/src/lib/asyncio.pyj +534 -0
  20. package/src/lib/base64.pyj +399 -0
  21. package/src/lib/bisect.pyj +73 -0
  22. package/src/lib/collections.pyj +228 -4
  23. package/src/lib/csv.pyj +494 -0
  24. package/src/lib/heapq.pyj +98 -0
  25. package/src/lib/html.pyj +382 -0
  26. package/src/lib/http/__init__.pyj +98 -0
  27. package/src/lib/http/client.pyj +304 -0
  28. package/src/lib/http/cookies.pyj +236 -0
  29. package/src/lib/logging.pyj +672 -0
  30. package/src/lib/pprint.pyj +455 -0
  31. package/src/lib/pythonize.pyj +20 -20
  32. package/src/lib/statistics.pyj +0 -0
  33. package/src/lib/string.pyj +357 -0
  34. package/src/lib/textwrap.pyj +329 -0
  35. package/src/lib/urllib/__init__.pyj +14 -0
  36. package/src/lib/urllib/error.pyj +66 -0
  37. package/src/lib/urllib/parse.pyj +475 -0
  38. package/src/lib/urllib/request.pyj +86 -0
  39. package/src/monaco-language-service/analyzer.js +5 -2
  40. package/src/monaco-language-service/completions.js +26 -0
  41. package/src/monaco-language-service/diagnostics.js +203 -4
  42. package/src/monaco-language-service/scope.js +1 -0
  43. package/src/output/codegen.pyj +4 -1
  44. package/src/output/functions.pyj +152 -6
  45. package/src/output/loops.pyj +17 -2
  46. package/src/output/modules.pyj +1 -1
  47. package/src/output/operators.pyj +15 -0
  48. package/src/output/stream.pyj +0 -1
  49. package/src/parse.pyj +108 -24
  50. package/src/tokenizer.pyj +19 -3
  51. package/test/async_generators.pyj +144 -0
  52. package/test/asyncio.pyj +307 -0
  53. package/test/base64.pyj +202 -0
  54. package/test/baselib.pyj +23 -0
  55. package/test/bisect.pyj +178 -0
  56. package/test/chainmap.pyj +185 -0
  57. package/test/csv.pyj +405 -0
  58. package/test/float_special.pyj +64 -0
  59. package/test/heapq.pyj +174 -0
  60. package/test/html.pyj +212 -0
  61. package/test/http.pyj +259 -0
  62. package/test/imports.pyj +79 -72
  63. package/test/logging.pyj +356 -0
  64. package/test/long.pyj +130 -0
  65. package/test/parenthesized_with.pyj +141 -0
  66. package/test/pprint.pyj +232 -0
  67. package/test/python_compat.pyj +3 -5
  68. package/test/python_modulo.pyj +76 -0
  69. package/test/python_modulo_off.pyj +21 -0
  70. package/test/statistics.pyj +224 -0
  71. package/test/str.pyj +14 -0
  72. package/test/string.pyj +245 -0
  73. package/test/textwrap.pyj +172 -0
  74. package/test/type_display.pyj +48 -0
  75. package/test/type_enforcement.pyj +164 -0
  76. package/test/unit/index.js +94 -6
  77. package/test/unit/language-service-completions.js +121 -0
  78. package/test/unit/language-service-scope.js +32 -0
  79. package/test/unit/language-service.js +190 -5
  80. package/test/unit/run-language-service.js +17 -3
  81. package/test/unit/web-repl.js +2401 -13
  82. package/test/urllib.pyj +193 -0
  83. package/tools/compile.js +1 -1
  84. package/tools/embedded_compiler.js +7 -7
  85. package/tools/export.js +4 -2
  86. package/web-repl/main.js +1 -1
  87. package/web-repl/rapydscript.js +7 -5
  88. 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
  });
@@ -271,6 +270,95 @@ var TESTS = [
271
270
  },
272
271
  },
273
272
 
273
+ {
274
+ name: "bundle_chainmap_basic",
275
+ description: "ChainMap construction and first-map-wins lookup work in the web-repl bundle",
276
+ run: function () {
277
+ var repl = RS.web_repl();
278
+ var js = bundle_compile(repl, [
279
+ "from collections import ChainMap",
280
+ "defaults = {'color': 'red', 'user': 'guest'}",
281
+ "overrides = {'user': 'admin'}",
282
+ "cm = ChainMap(overrides, defaults)",
283
+ "assrt.equal(cm['user'], 'admin')",
284
+ "assrt.equal(cm['color'], 'red')",
285
+ "assrt.equal(len(cm), 2)",
286
+ "assrt.ok('color' in cm)",
287
+ "assrt.ok('missing' not in cm)",
288
+ "assrt.equal(cm.get('missing', 'fallback'), 'fallback')",
289
+ ].join("\n"));
290
+ run_js(js);
291
+ },
292
+ },
293
+
294
+ {
295
+ name: "bundle_chainmap_writes",
296
+ description: "ChainMap writes, deletes and updates affect only the first map in the bundle",
297
+ run: function () {
298
+ var repl = RS.web_repl();
299
+ var js = bundle_compile(repl, [
300
+ "from collections import ChainMap",
301
+ "defaults = {'depth': 1}",
302
+ "cm = ChainMap({}, defaults)",
303
+ "cm['depth'] = 99",
304
+ "assrt.equal(cm['depth'], 99)",
305
+ "assrt.equal(defaults['depth'], 1)",
306
+ "del cm['depth']",
307
+ "assrt.equal(cm['depth'], 1)",
308
+ "cm.update({'a': 1}, b=2)",
309
+ "assrt.equal(cm['a'], 1)",
310
+ "assrt.equal(cm['b'], 2)",
311
+ "assrt.equal(cm.pop('a'), 1)",
312
+ "assrt.equal(cm.pop('depth', 'dflt'), 'dflt')",
313
+ "assrt.equal(cm.setdefault('new', 7), 7)",
314
+ "assrt.equal(cm['new'], 7)",
315
+ ].join("\n"));
316
+ run_js(js);
317
+ },
318
+ },
319
+
320
+ {
321
+ name: "bundle_chainmap_new_child",
322
+ description: "ChainMap new_child and parents work in the web-repl bundle",
323
+ run: function () {
324
+ var repl = RS.web_repl();
325
+ var js = bundle_compile(repl, [
326
+ "from collections import ChainMap",
327
+ "base = ChainMap({'x': 1})",
328
+ "child = base.new_child({'x': 2, 'y': 3})",
329
+ "assrt.equal(child['x'], 2)",
330
+ "assrt.equal(child['y'], 3)",
331
+ "assrt.equal(base['x'], 1)",
332
+ "assrt.equal(child.maps.length, 2)",
333
+ "parents = child.parents",
334
+ "assrt.equal(parents['x'], 1)",
335
+ "assrt.equal(parents.maps.length, 1)",
336
+ ].join("\n"));
337
+ run_js(js);
338
+ },
339
+ },
340
+
341
+ {
342
+ name: "bundle_chainmap_iteration",
343
+ description: "ChainMap keys/values/items, iteration order and copy work in the bundle",
344
+ run: function () {
345
+ var repl = RS.web_repl();
346
+ var js = bundle_compile(repl, [
347
+ "from collections import ChainMap",
348
+ "cm = ChainMap({'a': 1, 'b': 2}, {'c': 3})",
349
+ "assrt.deepEqual(list(cm), ['c', 'a', 'b'])",
350
+ "assrt.deepEqual(cm.keys(), ['c', 'a', 'b'])",
351
+ "assrt.deepEqual(cm.values(), [3, 1, 2])",
352
+ "dup = cm.copy()",
353
+ "dup['a'] = 100",
354
+ "assrt.equal(cm['a'], 1)",
355
+ "assrt.equal(dup['a'], 100)",
356
+ "assrt.equal(dup['c'], 3)",
357
+ ].join("\n"));
358
+ run_js(js);
359
+ },
360
+ },
361
+
274
362
  {
275
363
  name: "bundle_operator_overloading",
276
364
  description: "overload_operators flag works in the web-repl bundle",
@@ -1233,6 +1321,71 @@ var TESTS = [
1233
1321
  },
1234
1322
  },
1235
1323
 
1324
+ // ── f-string {x=} debugging format ───────────────────────────────────────
1325
+
1326
+ {
1327
+ name: "bundle_fstring_debug_simple",
1328
+ description: "f'{x=}' produces 'x=<value>' for simple variable",
1329
+ run: function () {
1330
+ var repl = RS.web_repl();
1331
+ var js = bundle_compile(repl, [
1332
+ "x = 42",
1333
+ "name = 'Alice'",
1334
+ "flag = True",
1335
+ "assrt.equal(f'{x=}', 'x=42')",
1336
+ "assrt.equal(f'{name=}', \"name=Alice\")",
1337
+ "assrt.equal(f'{flag=}', 'flag=true')",
1338
+ ].join("\n"));
1339
+ run_js(js);
1340
+ },
1341
+ },
1342
+
1343
+ {
1344
+ name: "bundle_fstring_debug_expr",
1345
+ description: "f'{expr=}' preserves the full expression text as prefix",
1346
+ run: function () {
1347
+ var repl = RS.web_repl();
1348
+ var js = bundle_compile(repl, [
1349
+ "x = 5",
1350
+ "nums = [10, 20, 30]",
1351
+ "assrt.equal(f'{x*2=}', 'x*2=10')",
1352
+ "assrt.equal(f'{nums[1]=}', 'nums[1]=20')",
1353
+ "assrt.equal(f'{x=} and {x*2=}', 'x=5 and x*2=10')",
1354
+ ].join("\n"));
1355
+ run_js(js);
1356
+ },
1357
+ },
1358
+
1359
+ {
1360
+ name: "bundle_fstring_debug_format_spec",
1361
+ description: "f'{x=:.2f}' combines debugging prefix with format spec",
1362
+ run: function () {
1363
+ var repl = RS.web_repl();
1364
+ var js = bundle_compile(repl, [
1365
+ "pi = 3.14159",
1366
+ "n = 1234567",
1367
+ "assrt.equal(f'{pi=:.2f}', 'pi=3.14')",
1368
+ "assrt.equal(f'{n=:,}', 'n=' + (1234567).toLocaleString())",
1369
+ ].join("\n"));
1370
+ run_js(js);
1371
+ },
1372
+ },
1373
+
1374
+ {
1375
+ name: "bundle_fstring_debug_repr",
1376
+ description: "f'{x=!r}' combines debugging prefix with !r repr conversion",
1377
+ run: function () {
1378
+ var repl = RS.web_repl();
1379
+ var js = bundle_compile(repl, [
1380
+ "msg = 'hello'",
1381
+ "nums = [1, 2, 3]",
1382
+ "assrt.equal(f'{msg=!r}', 'msg=\"hello\"')",
1383
+ "assrt.equal(f'{nums=!r}', 'nums=[1, 2, 3]')",
1384
+ ].join("\n"));
1385
+ run_js(js);
1386
+ },
1387
+ },
1388
+
1236
1389
  // ── object() builtin ──────────────────────────────────────────────────────
1237
1390
 
1238
1391
  {
@@ -2198,6 +2351,522 @@ var TESTS = [
2198
2351
  },
2199
2352
  },
2200
2353
 
2354
+ // ── float() special string values ────────────────────────────────────────
2355
+
2356
+ {
2357
+ name: "bundle_float_special_inf_nan",
2358
+ description: "float() accepts 'inf', '-inf', 'infinity', 'nan' (and variants) in the web-repl bundle",
2359
+ run: function () {
2360
+ var repl = RS.web_repl();
2361
+ var js = bundle_compile(repl, [
2362
+ // positive infinity
2363
+ "assrt.equal(float('inf'), Infinity)",
2364
+ "assrt.equal(float('+inf'), Infinity)",
2365
+ "assrt.equal(float('INF'), Infinity)",
2366
+ "assrt.equal(float('infinity'), Infinity)",
2367
+ "assrt.equal(float('+infinity'), Infinity)",
2368
+ "assrt.equal(float('Infinity'), Infinity)",
2369
+ "assrt.equal(float('INFINITY'), Infinity)",
2370
+ // negative infinity
2371
+ "assrt.equal(float('-inf'), -Infinity)",
2372
+ "assrt.equal(float('-infinity'), -Infinity)",
2373
+ "assrt.equal(float('-Infinity'), -Infinity)",
2374
+ // nan
2375
+ "assrt.ok(isNaN(float('nan')))",
2376
+ "assrt.ok(isNaN(float('NaN')))",
2377
+ "assrt.ok(isNaN(float('NAN')))",
2378
+ "assrt.ok(isNaN(float('+nan')))",
2379
+ "assrt.ok(isNaN(float('-nan')))",
2380
+ // whitespace stripped
2381
+ "assrt.equal(float(' inf '), Infinity)",
2382
+ "assrt.equal(float(' -inf '), -Infinity)",
2383
+ "assrt.ok(isNaN(float(' nan ')))",
2384
+ // numeric strings still work
2385
+ "assrt.equal(float('3.14'), 3.14)",
2386
+ "assrt.equal(float('-2.5'), -2.5)",
2387
+ // real Infinity passes through
2388
+ "assrt.equal(float(Infinity), Infinity)",
2389
+ "assrt.equal(float(-Infinity), -Infinity)",
2390
+ // ValueError still raised for bad strings
2391
+ "_err = False",
2392
+ "try:",
2393
+ " float('bad')",
2394
+ "except ValueError:",
2395
+ " _err = True",
2396
+ "assrt.ok(_err)",
2397
+ ].join("\n"));
2398
+ run_js(js);
2399
+ },
2400
+ },
2401
+
2402
+ // ── base64 stdlib ────────────────────────────────────────────────────────
2403
+
2404
+ {
2405
+ name: "bundle_base64_encode_decode",
2406
+ description: "base64 stdlib: b64encode/b64decode round-trips in the web-repl bundle",
2407
+ run: function () {
2408
+ var repl = RS.web_repl();
2409
+ var js = bundle_compile(repl, [
2410
+ "from base64 import b64encode, b64decode",
2411
+ // basic encode
2412
+ "enc = b64encode(bytes([77, 97, 110]))",
2413
+ "assrt.equal(enc.decode('ascii'), 'TWFu')",
2414
+ // empty
2415
+ "assrt.equal(len(b64encode(bytes([]))), 0)",
2416
+ // padding variants
2417
+ "assrt.equal(b64encode(bytes([0])).decode('ascii'), 'AA==')",
2418
+ "assrt.equal(b64encode(bytes([0, 0])).decode('ascii'), 'AAA=')",
2419
+ "assrt.equal(b64encode(bytes([0, 0, 0])).decode('ascii'), 'AAAA')",
2420
+ // decode round-trip
2421
+ "msg = bytes([104, 101, 108, 108, 111])", // hello
2422
+ "assrt.deepEqual(list(b64decode(b64encode(msg))), list(msg))",
2423
+ // decode accepts string input
2424
+ "d = b64decode('TWFu')",
2425
+ "assrt.equal(d[0], 77)",
2426
+ "assrt.equal(d[1], 97)",
2427
+ "assrt.equal(d[2], 110)",
2428
+ // decode strips whitespace
2429
+ "assrt.deepEqual(list(b64decode('aGVs bG8=')), list(msg))",
2430
+ // missing padding accepted
2431
+ "assrt.deepEqual(list(b64decode('aGVsbG8')), list(msg))",
2432
+ // isinstance check
2433
+ "assrt.ok(isinstance(b64encode(bytes([1,2,3])), bytes))",
2434
+ ].join("\n"));
2435
+ run_js(js);
2436
+ },
2437
+ },
2438
+
2439
+ {
2440
+ name: "bundle_base64_urlsafe",
2441
+ description: "base64 stdlib: URL-safe encoding and altchars in the web-repl bundle",
2442
+ run: function () {
2443
+ var repl = RS.web_repl();
2444
+ var js = bundle_compile(repl, [
2445
+ "from base64 import urlsafe_b64encode, urlsafe_b64decode, b64encode, b64decode",
2446
+ // URL-safe produces no + or /
2447
+ "enc = urlsafe_b64encode(bytes([251, 239, 190]))",
2448
+ "s = enc.decode('ascii')",
2449
+ "assrt.ok(s.indexOf('+') < 0, 'no + in URL-safe')",
2450
+ "assrt.ok(s.indexOf('/') < 0, 'no / in URL-safe')",
2451
+ // -_7- decodes to [251, 254, 254]
2452
+ "dec = urlsafe_b64decode('-_7-')",
2453
+ "assrt.equal(dec[0], 251)",
2454
+ "assrt.equal(dec[1], 254)",
2455
+ "assrt.equal(dec[2], 254)",
2456
+ // round-trip
2457
+ "data = bytes([0, 127, 128, 255])",
2458
+ "assrt.deepEqual(list(urlsafe_b64decode(urlsafe_b64encode(data))), list(data))",
2459
+ // altchars
2460
+ "enc_alt = b64encode(bytes([251, 254, 254]), altchars=bytes([45, 95]))",
2461
+ "assrt.equal(enc_alt.decode('ascii'), '-_7-')",
2462
+ "dec_alt = b64decode('-_7-', altchars=bytes([45, 95]))",
2463
+ "assrt.deepEqual(list(dec_alt), [251, 254, 254])",
2464
+ ].join("\n"));
2465
+ run_js(js);
2466
+ },
2467
+ },
2468
+
2469
+ {
2470
+ name: "bundle_base64_b32_b16",
2471
+ description: "base64 stdlib: b32encode/b32decode and b16encode/b16decode in the web-repl bundle",
2472
+ run: function () {
2473
+ var repl = RS.web_repl();
2474
+ var js = bundle_compile(repl, [
2475
+ "from base64 import b32encode, b32decode, b16encode, b16decode",
2476
+ // b32 round-trips
2477
+ "b32_msg = bytes([102, 111, 111])", // 'foo'
2478
+ "b32_enc = b32encode(b32_msg)",
2479
+ "assrt.equal(b32_enc.decode('ascii'), 'MZXW6===')",
2480
+ "assrt.deepEqual(list(b32decode(b32_enc)), list(b32_msg))",
2481
+ // casefold
2482
+ "assrt.deepEqual(list(b32decode('mzxw6===', casefold=True)), list(b32_msg))",
2483
+ // b16 (hex) encoding
2484
+ "b16_msg = bytes([0, 1, 254, 255])",
2485
+ "b16_enc = b16encode(b16_msg)",
2486
+ "assrt.equal(b16_enc.decode('ascii'), '0001FEFF')",
2487
+ "assrt.deepEqual(list(b16decode(b16_enc)), list(b16_msg))",
2488
+ // b16 casefold
2489
+ "assrt.deepEqual(list(b16decode('0001feff', casefold=True)), list(b16_msg))",
2490
+ // b16 Error on bad input
2491
+ "b16_err = False",
2492
+ "try:",
2493
+ " b16decode('ZZ')",
2494
+ "except ValueError:",
2495
+ " b16_err = True",
2496
+ "assrt.ok(b16_err, 'b16decode of non-hex raises ValueError')",
2497
+ ].join("\n"));
2498
+ run_js(js);
2499
+ },
2500
+ },
2501
+
2502
+ {
2503
+ name: "bundle_base64_encodebytes",
2504
+ description: "base64 stdlib: encodebytes/decodebytes and validate= flag in the web-repl bundle",
2505
+ run: function () {
2506
+ var repl = RS.web_repl();
2507
+ var js = bundle_compile(repl, [
2508
+ "from base64 import encodebytes, decodebytes, b64decode",
2509
+ // Note: catch ValueError (base64.Error subclasses ValueError).
2510
+ // Importing 'Error' from base64 in the web-repl context would
2511
+ // shadow the native JS Error constructor, causing issues.
2512
+ // encodebytes wraps at 76 chars per line
2513
+ "data = bytes(list(range(57)))",
2514
+ "enc = encodebytes(data)",
2515
+ "s = enc.decode('ascii')",
2516
+ "assrt.equal(s.charCodeAt(s.length - 1), 10, 'encodebytes ends with newline')",
2517
+ // decodebytes round-trip
2518
+ "assrt.deepEqual(list(decodebytes(enc)), list(data))",
2519
+ // validate=True raises on bad input (caught as ValueError)
2520
+ "b64_err = False",
2521
+ "try:",
2522
+ " b64decode('aGVs!G8=', validate=True)",
2523
+ "except ValueError:",
2524
+ " b64_err = True",
2525
+ "assrt.ok(b64_err, 'validate=True should raise ValueError on non-base64 char')",
2526
+ // validate=True passes on good input
2527
+ "good = b64decode('aGVsbG8=', validate=True)",
2528
+ "assrt.equal(good[0], 104)",
2529
+ ].join("\n"));
2530
+ run_js(js);
2531
+ },
2532
+ },
2533
+
2534
+ {
2535
+ name: "bundle_multiline_paren_import",
2536
+ description: "multi-line parenthesized import with trailing comma works in the web-repl bundle",
2537
+ run: function () {
2538
+ var repl = RS.web_repl();
2539
+ var js = bundle_compile(repl, [
2540
+ "from math import (",
2541
+ " floor,",
2542
+ " ceil,",
2543
+ " sqrt,",
2544
+ ")",
2545
+ "assrt.equal(floor(3.9), 3)",
2546
+ "assrt.equal(ceil(3.1), 4)",
2547
+ "assrt.equal(sqrt(9), 3)",
2548
+ ].join("\n"));
2549
+ run_js(js);
2550
+ },
2551
+ },
2552
+
2553
+ {
2554
+ name: "bundle_multiline_paren_import_alias",
2555
+ description: "multi-line parenthesized import with aliases and trailing comma works in the web-repl bundle",
2556
+ run: function () {
2557
+ var repl = RS.web_repl();
2558
+ var js = bundle_compile(repl, [
2559
+ "from math import (",
2560
+ " floor as fl,",
2561
+ " ceil as cl,",
2562
+ ")",
2563
+ "assrt.equal(fl(2.9), 2)",
2564
+ "assrt.equal(cl(2.1), 3)",
2565
+ ].join("\n"));
2566
+ run_js(js);
2567
+ },
2568
+ },
2569
+
2570
+ // ── string stdlib ────────────────────────────────────────────────────────
2571
+
2572
+ {
2573
+ name: "bundle_string_constants",
2574
+ description: "string stdlib: character constants in the web-repl bundle",
2575
+ run: function () {
2576
+ var repl = RS.web_repl();
2577
+ var js = bundle_compile(repl, [
2578
+ "from string import (",
2579
+ " ascii_lowercase, ascii_uppercase, ascii_letters,",
2580
+ " digits, hexdigits, octdigits,",
2581
+ " punctuation, whitespace, printable,",
2582
+ ")",
2583
+ // lengths
2584
+ "assrt.equal(len(ascii_lowercase), 26)",
2585
+ "assrt.equal(len(ascii_uppercase), 26)",
2586
+ "assrt.equal(len(ascii_letters), 52)",
2587
+ "assrt.equal(len(digits), 10)",
2588
+ "assrt.equal(len(hexdigits), 22)",
2589
+ "assrt.equal(len(octdigits), 8)",
2590
+ "assrt.equal(len(punctuation), 32)",
2591
+ "assrt.equal(len(whitespace), 6)",
2592
+ "assrt.equal(len(printable), len(digits) + len(ascii_letters) + len(punctuation) + len(whitespace))",
2593
+ // spot-checks
2594
+ "assrt.equal(ascii_letters, ascii_lowercase + ascii_uppercase)",
2595
+ "assrt.ok(digits.indexOf('5') >= 0)",
2596
+ "assrt.ok(hexdigits.indexOf('f') >= 0)",
2597
+ "assrt.ok(punctuation.indexOf('!') >= 0)",
2598
+ "assrt.ok(whitespace.indexOf(' ') >= 0)",
2599
+ "assrt.ok(printable.indexOf('A') >= 0)",
2600
+ ].join("\n"));
2601
+ run_js(js);
2602
+ },
2603
+ },
2604
+
2605
+ {
2606
+ name: "bundle_string_template",
2607
+ description: "string stdlib: Template class in the web-repl bundle",
2608
+ run: function () {
2609
+ var repl = RS.web_repl();
2610
+ var js = bundle_compile(repl, [
2611
+ "from string import Template",
2612
+ // basic substitution
2613
+ "t1 = Template('Hello $name!')",
2614
+ "assrt.equal(t1.substitute({'name': 'World'}), 'Hello World!')",
2615
+ // $$ → $
2616
+ "assrt.equal(Template('Price: $$5').substitute({}), 'Price: $5')",
2617
+ // brace form
2618
+ "assrt.equal(Template('${x}bar').substitute({'x': 'foo'}), 'foobar')",
2619
+ // multiple fields
2620
+ "assrt.equal(Template('$a and $b').substitute({'a': '1', 'b': '2'}), '1 and 2')",
2621
+ // numeric value
2622
+ "assrt.equal(Template('n=$n').substitute({'n': 42}), 'n=42')",
2623
+ // safe_substitute leaves missing intact
2624
+ "assrt.equal(Template('$x $y').safe_substitute({'x': 'hi'}), 'hi $y')",
2625
+ // safe_substitute with all keys
2626
+ "assrt.equal(Template('$a').safe_substitute({'a': 'z'}), 'z')",
2627
+ // substitute raises KeyError for missing key
2628
+ "_t_err = False",
2629
+ "try:",
2630
+ " Template('Hello $missing').substitute({})",
2631
+ "except KeyError:",
2632
+ " _t_err = True",
2633
+ "assrt.ok(_t_err)",
2634
+ // template attribute
2635
+ "assrt.equal(Template('hello $x').template, 'hello $x')",
2636
+ // class attribute
2637
+ "assrt.equal(Template.delimiter, '$')",
2638
+ ].join("\n"));
2639
+ run_js(js);
2640
+ },
2641
+ },
2642
+
2643
+ {
2644
+ name: "bundle_string_formatter",
2645
+ description: "string stdlib: Formatter class in the web-repl bundle",
2646
+ run: function () {
2647
+ var repl = RS.web_repl();
2648
+ var js = bundle_compile(repl, [
2649
+ "from string import Formatter",
2650
+ "f = Formatter()",
2651
+ // positional args
2652
+ "assrt.equal(f.format('Hello {}!', 'World'), 'Hello World!')",
2653
+ "assrt.equal(f.format('{0} {1}', 'a', 'b'), 'a b')",
2654
+ "assrt.equal(f.format('{1} {0}', 'a', 'b'), 'b a')",
2655
+ "assrt.equal(f.format('no fields'), 'no fields')",
2656
+ // format specs
2657
+ "assrt.equal(f.format('{:.2f}', 3.14159), '3.14')",
2658
+ "assrt.equal(f.format('{:d}', 42), '42')",
2659
+ "assrt.equal(f.format('{:x}', 255), 'ff')",
2660
+ // {{ }} escaping
2661
+ "assrt.equal(f.format('{{ }}'), '{ }')",
2662
+ // vformat with named kwargs
2663
+ "assrt.equal(f.vformat('{name}', [], {'name': 'Alice'}), 'Alice')",
2664
+ "assrt.equal(f.vformat('{0} {name}', ['hi'], {'name': 'Bob'}), 'hi Bob')",
2665
+ // convert_field
2666
+ "assrt.equal(f.convert_field(42, 's'), '42')",
2667
+ "assrt.equal(f.convert_field('x', None), 'x')",
2668
+ // format_field
2669
+ "assrt.equal(f.format_field(3.14159, '.2f'), '3.14')",
2670
+ "assrt.equal(f.format_field('hi', ''), 'hi')",
2671
+ // get_value
2672
+ "assrt.equal(f.get_value(0, ['a', 'b'], {}), 'a')",
2673
+ "assrt.equal(f.get_value('x', [], {'x': 99}), 99)",
2674
+ // parse
2675
+ "_p = f.parse('lit {0:.2f} end')",
2676
+ "assrt.equal(_p[0][0], 'lit ')",
2677
+ "assrt.equal(_p[0][1], '0')",
2678
+ "assrt.equal(_p[0][2], '.2f')",
2679
+ "assrt.equal(_p[1][0], ' end')",
2680
+ "assrt.equal(_p[1][1], None)",
2681
+ ].join("\n"));
2682
+ run_js(js);
2683
+ },
2684
+ },
2685
+
2686
+ {
2687
+ name: "bundle_html_escape_unescape",
2688
+ description: "html stdlib: escape and unescape functions in the web-repl bundle",
2689
+ run: function () {
2690
+ var repl = RS.web_repl();
2691
+ var js = bundle_compile(repl, [
2692
+ "from html import escape, unescape",
2693
+ // escape basics
2694
+ "assrt.equal(escape('<'), '&lt;')",
2695
+ "assrt.equal(escape('>'), '&gt;')",
2696
+ "assrt.equal(escape('&'), '&amp;')",
2697
+ "assrt.equal(escape('\"'), '&quot;')",
2698
+ "assrt.equal(escape(\"'\"), '&#x27;')",
2699
+ // quote=False
2700
+ "assrt.equal(escape('A & B', quote=False), 'A &amp; B')",
2701
+ "assrt.equal(escape('\"hi\"', quote=False), '\"hi\"')",
2702
+ // unescape named entities
2703
+ "assrt.equal(unescape('&amp;'), '&')",
2704
+ "assrt.equal(unescape('&lt;'), '<')",
2705
+ "assrt.equal(unescape('&gt;'), '>')",
2706
+ "assrt.equal(unescape('&quot;'), '\"')",
2707
+ "assrt.equal(unescape('&copy;'), '\\u00a9')",
2708
+ "assrt.equal(unescape('&euro;'), '\\u20ac')",
2709
+ // numeric references
2710
+ "assrt.equal(unescape('&#65;'), 'A')",
2711
+ "assrt.equal(unescape('&#x41;'), 'A')",
2712
+ // unknown entity left intact
2713
+ "assrt.equal(unescape('&nosuchentity;'), '&nosuchentity;')",
2714
+ // round-trip
2715
+ "s = '<Hello & \"World\">'",
2716
+ "assrt.equal(unescape(escape(s)), s)",
2717
+ ].join("\n"));
2718
+ run_js(js);
2719
+ },
2720
+ },
2721
+
2722
+ {
2723
+ name: "bundle_html_parser_basic",
2724
+ description: "html stdlib: HTMLParser start/end/data callbacks in the web-repl bundle",
2725
+ run: function () {
2726
+ var repl = RS.web_repl();
2727
+ var js = bundle_compile(repl, [
2728
+ "from html import HTMLParser",
2729
+ "class _P(HTMLParser):",
2730
+ " def __init__(self):",
2731
+ " HTMLParser.__init__(self)",
2732
+ " self.events = []",
2733
+ " def handle_starttag(self, tag, attrs):",
2734
+ " self.events.push(['start', tag])",
2735
+ " def handle_endtag(self, tag):",
2736
+ " self.events.push(['end', tag])",
2737
+ " def handle_data(self, data):",
2738
+ " self.events.push(['data', data])",
2739
+ "p = _P()",
2740
+ "p.feed('<p>Hello, World!</p>')",
2741
+ "assrt.equal(p.events[0][0], 'start')",
2742
+ "assrt.equal(p.events[0][1], 'p')",
2743
+ "assrt.equal(p.events[1][0], 'data')",
2744
+ "assrt.equal(p.events[1][1], 'Hello, World!')",
2745
+ "assrt.equal(p.events[2][0], 'end')",
2746
+ "assrt.equal(p.events[2][1], 'p')",
2747
+ // entity conversion in data
2748
+ "p2 = _P()",
2749
+ "p2.feed('<p>A &amp; B</p>')",
2750
+ "assrt.equal(p2.events[1][1], 'A & B')",
2751
+ // tag names lowercased
2752
+ "p3 = _P()",
2753
+ "p3.feed('<DIV></DIV>')",
2754
+ "assrt.equal(p3.events[0][1], 'div')",
2755
+ ].join("\n"));
2756
+ run_js(js);
2757
+ },
2758
+ },
2759
+
2760
+ {
2761
+ name: "bundle_html_parser_attrs",
2762
+ description: "html stdlib: HTMLParser attribute parsing in the web-repl bundle",
2763
+ run: function () {
2764
+ var repl = RS.web_repl();
2765
+ var js = bundle_compile(repl, [
2766
+ "from html import HTMLParser",
2767
+ "class _PA(HTMLParser):",
2768
+ " def __init__(self):",
2769
+ " HTMLParser.__init__(self)",
2770
+ " self.last_attrs = None",
2771
+ " def handle_starttag(self, tag, attrs):",
2772
+ " self.last_attrs = attrs",
2773
+ // multiple attrs
2774
+ "p = _PA()",
2775
+ "p.feed('<a href=\"http://example.com\" target=\"_blank\">')",
2776
+ "assrt.equal(p.last_attrs.length, 2)",
2777
+ "assrt.equal(p.last_attrs[0][0], 'href')",
2778
+ "assrt.equal(p.last_attrs[0][1], 'http://example.com')",
2779
+ "assrt.equal(p.last_attrs[1][0], 'target')",
2780
+ "assrt.equal(p.last_attrs[1][1], '_blank')",
2781
+ // valueless attribute
2782
+ "p2 = _PA()",
2783
+ "p2.feed('<input disabled>')",
2784
+ "assrt.equal(p2.last_attrs[0][0], 'disabled')",
2785
+ "assrt.equal(p2.last_attrs[0][1], None)",
2786
+ // self-closing
2787
+ "class _PSC(HTMLParser):",
2788
+ " def __init__(self):",
2789
+ " HTMLParser.__init__(self)",
2790
+ " self.events = []",
2791
+ " def handle_starttag(self, tag, attrs):",
2792
+ " self.events.push('start:' + tag)",
2793
+ " def handle_endtag(self, tag):",
2794
+ " self.events.push('end:' + tag)",
2795
+ "psc = _PSC()",
2796
+ "psc.feed('<br/>')",
2797
+ "assrt.equal(psc.events.length, 2)",
2798
+ "assrt.equal(psc.events[0], 'start:br')",
2799
+ "assrt.equal(psc.events[1], 'end:br')",
2800
+ ].join("\n"));
2801
+ run_js(js);
2802
+ },
2803
+ },
2804
+
2805
+ {
2806
+ name: "bundle_html_parser_special",
2807
+ description: "html stdlib: HTMLParser comments, DOCTYPE, get_starttag_text in the web-repl bundle",
2808
+ run: function () {
2809
+ var repl = RS.web_repl();
2810
+ var js = bundle_compile(repl, [
2811
+ "from html import HTMLParser",
2812
+ // comment
2813
+ "class _PC(HTMLParser):",
2814
+ " def __init__(self):",
2815
+ " HTMLParser.__init__(self)",
2816
+ " self.comments = []",
2817
+ " def handle_comment(self, data):",
2818
+ " self.comments.push(data)",
2819
+ "pc = _PC()",
2820
+ "pc.feed('<!-- hello -->')",
2821
+ "assrt.equal(pc.comments.length, 1)",
2822
+ "assrt.equal(pc.comments[0], ' hello ')",
2823
+ // doctype
2824
+ "class _PD(HTMLParser):",
2825
+ " def __init__(self):",
2826
+ " HTMLParser.__init__(self)",
2827
+ " self.decls = []",
2828
+ " def handle_decl(self, decl):",
2829
+ " self.decls.push(decl)",
2830
+ "pd = _PD()",
2831
+ "pd.feed('<!DOCTYPE html>')",
2832
+ "assrt.equal(pd.decls[0], 'DOCTYPE html')",
2833
+ // get_starttag_text
2834
+ "class _PR(HTMLParser):",
2835
+ " def __init__(self):",
2836
+ " HTMLParser.__init__(self)",
2837
+ " self.raw = None",
2838
+ " def handle_starttag(self, tag, attrs):",
2839
+ " self.raw = self.get_starttag_text()",
2840
+ "pr = _PR()",
2841
+ "pr.feed('<img src=\"pic.png\">')",
2842
+ "assrt.equal(pr.raw, '<img src=\"pic.png\">')",
2843
+ ].join("\n"));
2844
+ run_js(js);
2845
+ },
2846
+ },
2847
+
2848
+ {
2849
+ name: "bundle_python_modulo",
2850
+ description: "% operator gives Python-style modulo (sign of divisor) in the web-repl bundle",
2851
+ run: function () {
2852
+ var repl = RS.web_repl();
2853
+ var js = bundle_compile(repl, [
2854
+ "assrt.equal(-7 % 3, 2)",
2855
+ "assrt.equal(7 % -3, -2)",
2856
+ "assrt.equal(-7 % -3, -1)",
2857
+ "assrt.equal(7 % 3, 1)",
2858
+ "assrt.equal(0 % 5, 0)",
2859
+ "assrt.equal(-6 % 3, 0)",
2860
+ "assrt.equal(-7.5 % 2, 0.5)",
2861
+ "assrt.equal(7.5 % -2, -0.5)",
2862
+ "x = -7",
2863
+ "x %= 3",
2864
+ "assrt.equal(x, 2)",
2865
+ ].join("\n"));
2866
+ run_js(js);
2867
+ },
2868
+ },
2869
+
2201
2870
  {
2202
2871
  name: "repl_exists_persistence",
2203
2872
  description: "ρσ_exists accessible after baselib init — existential operator on non-SymbolRef in web-repl context",
@@ -2218,16 +2887,1727 @@ var TESTS = [
2218
2887
  },
2219
2888
  },
2220
2889
 
2221
- ];
2222
-
2223
- // ---------------------------------------------------------------------------
2224
- // Runner
2225
- // ---------------------------------------------------------------------------
2890
+ {
2891
+ name: "bundle_asyncio_exceptions_and_primitives",
2892
+ description: "asyncio stdlib: exception classes, Queue/Lock/Event/Semaphore synchronous state in the web-repl bundle",
2893
+ run: function () {
2894
+ var repl = RS.web_repl();
2895
+ var js = bundle_compile(repl, [
2896
+ "from asyncio import (",
2897
+ " CancelledError, TimeoutError, InvalidStateError, RuntimeError,",
2898
+ " QueueEmpty, QueueFull,",
2899
+ " Lock, Event, Semaphore, BoundedSemaphore, Queue",
2900
+ ")",
2901
+ // Exception classes
2902
+ "try:",
2903
+ " raise CancelledError('c')",
2904
+ "except CancelledError as e:",
2905
+ " assrt.equal(e.message, 'c')",
2906
+ "try:",
2907
+ " raise TimeoutError('t')",
2908
+ "except TimeoutError as e:",
2909
+ " assrt.equal(e.message, 't')",
2910
+ "try:",
2911
+ " raise QueueEmpty('empty')",
2912
+ "except QueueEmpty as e:",
2913
+ " assrt.equal(e.message, 'empty')",
2914
+ "try:",
2915
+ " raise QueueFull('full')",
2916
+ "except QueueFull as e:",
2917
+ " assrt.equal(e.message, 'full')",
2918
+ // Lock
2919
+ "lock = Lock()",
2920
+ "assrt.equal(lock.locked(), False)",
2921
+ "try:",
2922
+ " lock.release()",
2923
+ " assrt.ok(False)",
2924
+ "except RuntimeError:",
2925
+ " assrt.ok(True)",
2926
+ // Event
2927
+ "ev = Event()",
2928
+ "assrt.equal(ev.is_set(), False)",
2929
+ "ev.set()",
2930
+ "assrt.equal(ev.is_set(), True)",
2931
+ "ev.clear()",
2932
+ "assrt.equal(ev.is_set(), False)",
2933
+ // Semaphore
2934
+ "sem = Semaphore(2)",
2935
+ "assrt.equal(sem.locked(), False)",
2936
+ "sem.release()",
2937
+ "assrt.equal(sem.locked(), False)",
2938
+ // BoundedSemaphore
2939
+ "bsem = BoundedSemaphore(1)",
2940
+ "try:",
2941
+ " bsem.release()",
2942
+ " assrt.ok(False)",
2943
+ "except ValueError:",
2944
+ " assrt.ok(True)",
2945
+ // Queue synchronous ops
2946
+ "q = Queue()",
2947
+ "assrt.equal(q.empty(), True)",
2948
+ "q.put_nowait('a')",
2949
+ "q.put_nowait('b')",
2950
+ "assrt.equal(q.qsize(), 2)",
2951
+ "assrt.equal(q.get_nowait(), 'a')",
2952
+ "q.task_done()",
2953
+ "assrt.equal(q.get_nowait(), 'b')",
2954
+ "q.task_done()",
2955
+ "assrt.equal(q.empty(), True)",
2956
+ ].join("\n"));
2957
+ run_js(js);
2958
+ },
2959
+ },
2226
2960
 
2227
- function run_tests(filter) {
2228
- var tests = filter
2229
- ? TESTS.filter(function (t) { return t.name === filter; })
2230
- : TESTS;
2961
+ {
2962
+ name: "bundle_asyncio_coroutine_helpers",
2963
+ description: "asyncio stdlib: iscoroutine, iscoroutinefunction, sleep/gather/run return Promises in the web-repl bundle",
2964
+ run: function () {
2965
+ var repl = RS.web_repl();
2966
+ var js = bundle_compile(repl, [
2967
+ "from asyncio import iscoroutine, iscoroutinefunction, sleep, gather, create_task, run, shield",
2968
+ // sleep returns a thenable
2969
+ "p = sleep(0)",
2970
+ "assrt.ok(iscoroutine(p))",
2971
+ // gather returns a thenable
2972
+ "p2 = gather(Promise.resolve(1), Promise.resolve(2))",
2973
+ "assrt.ok(iscoroutine(p2))",
2974
+ // create_task / run pass through
2975
+ "p3 = Promise.resolve(99)",
2976
+ "assrt.ok(iscoroutine(create_task(p3)))",
2977
+ "assrt.ok(iscoroutine(run(p3)))",
2978
+ "assrt.ok(iscoroutine(shield(p3)))",
2979
+ // iscoroutine
2980
+ "assrt.ok(not iscoroutine(42))",
2981
+ "assrt.ok(not iscoroutine(None))",
2982
+ "assrt.ok(iscoroutine(sleep(0)))",
2983
+ // iscoroutinefunction
2984
+ "async def _af():",
2985
+ " return 1",
2986
+ "assrt.ok(iscoroutinefunction(_af))",
2987
+ "def _sf():",
2988
+ " return 1",
2989
+ "assrt.ok(not iscoroutinefunction(_sf))",
2990
+ // async functions return Promises
2991
+ "assrt.ok(iscoroutine(_af()))",
2992
+ ].join("\n"));
2993
+ run_js(js);
2994
+ },
2995
+ },
2996
+
2997
+ {
2998
+ name: "bundle_asyncio_queue_variants",
2999
+ description: "asyncio stdlib: LifoQueue and PriorityQueue ordering in the web-repl bundle",
3000
+ run: function () {
3001
+ var repl = RS.web_repl();
3002
+ var js = bundle_compile(repl, [
3003
+ "from asyncio import Queue, LifoQueue, PriorityQueue, QueueFull, QueueEmpty",
3004
+ // Bounded Queue — QueueFull
3005
+ "q2 = Queue(2)",
3006
+ "q2.put_nowait(1)",
3007
+ "q2.put_nowait(2)",
3008
+ "assrt.equal(q2.full(), True)",
3009
+ "try:",
3010
+ " q2.put_nowait(3)",
3011
+ " assrt.ok(False)",
3012
+ "except QueueFull:",
3013
+ " assrt.ok(True)",
3014
+ // LifoQueue ordering
3015
+ "lq = LifoQueue()",
3016
+ "lq.put_nowait(1)",
3017
+ "lq.put_nowait(2)",
3018
+ "lq.put_nowait(3)",
3019
+ "assrt.equal(lq.get_nowait(), 3)",
3020
+ "assrt.equal(lq.get_nowait(), 2)",
3021
+ "assrt.equal(lq.get_nowait(), 1)",
3022
+ // PriorityQueue ordering (lowest value first)
3023
+ "pq = PriorityQueue()",
3024
+ "pq.put_nowait(30)",
3025
+ "pq.put_nowait(10)",
3026
+ "pq.put_nowait(20)",
3027
+ "assrt.equal(pq.get_nowait(), 10)",
3028
+ "assrt.equal(pq.get_nowait(), 20)",
3029
+ "assrt.equal(pq.get_nowait(), 30)",
3030
+ // QueueEmpty
3031
+ "q3 = Queue()",
3032
+ "try:",
3033
+ " q3.get_nowait()",
3034
+ " assrt.ok(False)",
3035
+ "except QueueEmpty:",
3036
+ " assrt.ok(True)",
3037
+ ].join("\n"));
3038
+ run_js(js);
3039
+ },
3040
+ },
3041
+
3042
+ {
3043
+ name: "bundle_asyncio_async_await",
3044
+ description: "asyncio stdlib: async def / await compiles and returns Promise in the web-repl bundle",
3045
+ run: function () {
3046
+ var repl = RS.web_repl();
3047
+ var js = bundle_compile(repl, [
3048
+ "from asyncio import sleep, gather, iscoroutine, iscoroutinefunction, get_event_loop",
3049
+ // async functions compile and return Promises
3050
+ "async def _add(a, b):",
3051
+ " return a + b",
3052
+ "assrt.ok(iscoroutinefunction(_add))",
3053
+ "p = _add(2, 3)",
3054
+ "assrt.ok(iscoroutine(p))",
3055
+ // chained awaits produce a Promise
3056
+ "async def _chain():",
3057
+ " x = await Promise.resolve(10)",
3058
+ " y = await Promise.resolve(20)",
3059
+ " return x + y",
3060
+ "assrt.ok(iscoroutine(_chain()))",
3061
+ // gather of resolved Promises
3062
+ "async def _g():",
3063
+ " results = await gather(Promise.resolve(1), Promise.resolve(2))",
3064
+ " return results",
3065
+ "assrt.ok(iscoroutine(_g()))",
3066
+ // event loop stub
3067
+ "loop = get_event_loop()",
3068
+ "assrt.ok(loop is not None)",
3069
+ "assrt.ok(not loop.is_closed())",
3070
+ "assrt.ok(loop.is_running())",
3071
+ ].join("\n"));
3072
+ run_js(js);
3073
+ },
3074
+ },
3075
+
3076
+ {
3077
+ name: "bundle_async_generator_shape",
3078
+ description: "async def with yield returns an async iterator with .next/.send/.asend (web-repl bundle)",
3079
+ run: function () {
3080
+ var repl = RS.web_repl();
3081
+ var js = bundle_compile(repl, [
3082
+ "async def aiter():",
3083
+ " yield 1",
3084
+ " yield 2",
3085
+ " yield 3",
3086
+ "it = aiter()",
3087
+ // The wrapper is sync — calling the async generator returns the
3088
+ // iterator immediately, NOT a Promise.
3089
+ "assrt.equal(jstype(it.next), 'function')",
3090
+ "assrt.equal(jstype(it.send), 'function')", // Python alias
3091
+ "assrt.equal(jstype(it.asend), 'function')", // async-gen alias
3092
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3093
+ // .next() returns a thenable Promise
3094
+ "p = it.next()",
3095
+ "assrt.equal(jstype(p.then), 'function')",
3096
+ ].join("\n"));
3097
+ run_js(js);
3098
+ },
3099
+ },
3100
+
3101
+ {
3102
+ name: "bundle_async_generator_await_inside",
3103
+ description: "async generators may use `await` between yields (web-repl bundle)",
3104
+ run: function () {
3105
+ var repl = RS.web_repl();
3106
+ var js = bundle_compile(repl, [
3107
+ "async def gen():",
3108
+ " a = await Promise.resolve(10)",
3109
+ " yield a",
3110
+ " b = await Promise.resolve(20)",
3111
+ " yield a + b",
3112
+ "it = gen()",
3113
+ "assrt.equal(jstype(it.next), 'function')",
3114
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3115
+ // .next() is thenable even when the body awaits before yielding
3116
+ "assrt.equal(jstype(it.next().then), 'function')",
3117
+ ].join("\n"));
3118
+ run_js(js);
3119
+ },
3120
+ },
3121
+
3122
+ {
3123
+ name: "bundle_async_for_compiles_and_resolves",
3124
+ description: "async for loop drives an async generator and resolves to expected values (web-repl bundle)",
3125
+ run: function () {
3126
+ var repl = RS.web_repl();
3127
+ var js = bundle_compile(repl, [
3128
+ "async def aiter():",
3129
+ " yield 'a'",
3130
+ " yield 'b'",
3131
+ " yield 'c'",
3132
+ "async def consume():",
3133
+ " out = []",
3134
+ " async for x in aiter():",
3135
+ " out.append(x)",
3136
+ " return out",
3137
+ "p = consume()",
3138
+ "assrt.equal(jstype(p.then), 'function')",
3139
+ // The resolved value is verified via a microtask callback that
3140
+ // throws on mismatch. Failing the assertion in a Promise chain
3141
+ // surfaces as an unhandled rejection — Node exits non-zero,
3142
+ // failing the test.
3143
+ "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)); })\"",
3144
+ ].join("\n"));
3145
+ run_js(js);
3146
+ },
3147
+ },
3148
+
3149
+ {
3150
+ name: "bundle_async_generator_class_method",
3151
+ description: "async generator method on a class compiles and exposes async iterator (web-repl bundle)",
3152
+ run: function () {
3153
+ var repl = RS.web_repl();
3154
+ var js = bundle_compile(repl, [
3155
+ "class Counter:",
3156
+ " def __init__(self, limit):",
3157
+ " self.limit = limit",
3158
+ "",
3159
+ " async def values(self):",
3160
+ " i = 0",
3161
+ " while i < self.limit:",
3162
+ " yield i",
3163
+ " i += 1",
3164
+ "",
3165
+ "it = Counter(3).values()",
3166
+ "assrt.equal(jstype(it.next), 'function')",
3167
+ "assrt.equal(jstype(it.send), 'function')",
3168
+ "assrt.equal(jstype(it.asend), 'function')",
3169
+ "assrt.equal(jstype(it[v'Symbol.asyncIterator']), 'function')",
3170
+ "assrt.equal(jstype(it.next().then), 'function')",
3171
+ ].join("\n"));
3172
+ run_js(js);
3173
+ },
3174
+ },
3175
+
3176
+ {
3177
+ name: "bundle_urllib_parse_quote",
3178
+ description: "urllib.parse stdlib: quote, unquote, quote_plus, unquote_plus in the web-repl bundle",
3179
+ run: function () {
3180
+ var repl = RS.web_repl();
3181
+ var js = bundle_compile(repl, [
3182
+ "from urllib.parse import quote, unquote, quote_plus, unquote_plus",
3183
+ // quote: basic
3184
+ "assrt.equal(quote('hello world'), 'hello%20world')",
3185
+ "assrt.equal(quote('a/b/c'), 'a/b/c')", // '/' safe by default
3186
+ "assrt.equal(quote('a/b/c', safe=''), 'a%2Fb%2Fc')",
3187
+ "assrt.equal(quote('abc123-_.~'), 'abc123-_.~')", // RFC 3986 unreserved
3188
+ "assrt.equal(quote('a+b'), 'a%2Bb')",
3189
+ "assrt.equal(quote('!*'), '%21%2A')", // sub-delimiters encoded
3190
+ // unquote
3191
+ "assrt.equal(unquote('hello%20world'), 'hello world')",
3192
+ "assrt.equal(unquote('a%2Fb'), 'a/b')",
3193
+ "assrt.equal(unquote('a%2Bb'), 'a+b')", // '+' not decoded by unquote
3194
+ // quote_plus / unquote_plus
3195
+ "assrt.equal(quote_plus('hello world'), 'hello+world')",
3196
+ "assrt.equal(quote_plus('a+b'), 'a%2Bb')",
3197
+ "assrt.equal(unquote_plus('hello+world'), 'hello world')",
3198
+ "assrt.equal(unquote_plus('a%2Bb'), 'a+b')",
3199
+ // round-trips
3200
+ "s = 'a/b c+d=e&f'",
3201
+ "assrt.equal(unquote(quote(s, safe='')), s)",
3202
+ "assrt.equal(unquote_plus(quote_plus(s, safe='')), s)",
3203
+ ].join("\n"));
3204
+ run_js(js);
3205
+ },
3206
+ },
3207
+
3208
+ {
3209
+ name: "bundle_urllib_parse_urlencode",
3210
+ description: "urllib.parse stdlib: urlencode, parse_qs, parse_qsl in the web-repl bundle",
3211
+ run: function () {
3212
+ var repl = RS.web_repl();
3213
+ var js = bundle_compile(repl, [
3214
+ "from urllib.parse import urlencode, parse_qs, parse_qsl",
3215
+ // urlencode with list of pairs
3216
+ "assrt.equal(urlencode([['a', '1'], ['b', '2']]), 'a=1&b=2')",
3217
+ "assrt.equal(urlencode([['q', 'hello world']]), 'q=hello%20world')",
3218
+ "assrt.equal(urlencode([]), '')",
3219
+ // doseq
3220
+ "assrt.equal(urlencode([['a', ['x', 'y']]], doseq=True), 'a=x&a=y')",
3221
+ // parse_qsl
3222
+ "pairs = parse_qsl('a=1&b=2&a=3')",
3223
+ "assrt.equal(pairs.length, 3)",
3224
+ "assrt.equal(pairs[0][0], 'a')",
3225
+ "assrt.equal(pairs[0][1], '1')",
3226
+ "assrt.equal(pairs[2][0], 'a')",
3227
+ "assrt.equal(pairs[2][1], '3')",
3228
+ "assrt.equal(parse_qsl('a=hello+world')[0][1], 'hello world')",
3229
+ "assrt.equal(parse_qsl('q=a%20b')[0][1], 'a b')",
3230
+ // parse_qs
3231
+ "d = parse_qs('a=1&b=2&a=3')",
3232
+ "assrt.equal(d['a'].length, 2)",
3233
+ "assrt.equal(d['a'][0], '1')",
3234
+ "assrt.equal(d['a'][1], '3')",
3235
+ "assrt.equal(d['b'][0], '2')",
3236
+ "assrt.equal(parse_qs('q=hello+world')['q'][0], 'hello world')",
3237
+ ].join("\n"));
3238
+ run_js(js);
3239
+ },
3240
+ },
3241
+
3242
+ {
3243
+ name: "bundle_urllib_parse_urlparse",
3244
+ description: "urllib.parse stdlib: urlsplit, urlparse, urljoin, urlunsplit in the web-repl bundle",
3245
+ run: function () {
3246
+ var repl = RS.web_repl();
3247
+ var js = bundle_compile(repl, [
3248
+ "from urllib.parse import urlsplit, urlunsplit, urlparse, urlunparse, urljoin",
3249
+ // urlsplit
3250
+ "r = urlsplit('http://example.com/path?q=1#frag')",
3251
+ "assrt.equal(r.scheme, 'http')",
3252
+ "assrt.equal(r.netloc, 'example.com')",
3253
+ "assrt.equal(r.path, '/path')",
3254
+ "assrt.equal(r.query, 'q=1')",
3255
+ "assrt.equal(r.fragment, 'frag')",
3256
+ "assrt.equal(r.hostname, 'example.com')",
3257
+ "assrt.equal(r.port, None)",
3258
+ // authority with user:pass@host:port
3259
+ "r2 = urlsplit('https://user:pw@host:8080/p?x=1')",
3260
+ "assrt.equal(r2.hostname, 'host')",
3261
+ "assrt.equal(r2.port, 8080)",
3262
+ "assrt.equal(r2.username, 'user')",
3263
+ "assrt.equal(r2.password, 'pw')",
3264
+ // urlparse splits params
3265
+ "r3 = urlparse('http://example.com/path;params?q=1#frag')",
3266
+ "assrt.equal(r3.path, '/path')",
3267
+ "assrt.equal(r3.params, 'params')",
3268
+ // urlunsplit round-trip
3269
+ "assrt.equal(urlunsplit(('http', 'example.com', '/path', 'q=1', 'frag')), 'http://example.com/path?q=1#frag')",
3270
+ "assrt.equal(urlunsplit(('http', 'example.com', '/path', '', '')), 'http://example.com/path')",
3271
+ // urlunparse with params
3272
+ "assrt.equal(urlunparse(('http', 'example.com', '/path', 'par', 'q=1', 'frag')), 'http://example.com/path;par?q=1#frag')",
3273
+ // geturl round-trip
3274
+ "r4 = urlsplit('http://example.com/path?q=1#frag')",
3275
+ "assrt.equal(r4.geturl(), 'http://example.com/path?q=1#frag')",
3276
+ // urljoin
3277
+ "assrt.equal(urljoin('http://example.com/foo', 'bar'), 'http://example.com/bar')",
3278
+ "assrt.equal(urljoin('http://example.com/foo/', 'bar'), 'http://example.com/foo/bar')",
3279
+ "assrt.equal(urljoin('http://example.com/', '/other'), 'http://example.com/other')",
3280
+ "assrt.equal(urljoin('http://example.com/foo', 'http://other.com/'), 'http://other.com/')",
3281
+ ].join("\n"));
3282
+ run_js(js);
3283
+ },
3284
+ },
3285
+
3286
+ {
3287
+ name: "bundle_urllib_error",
3288
+ description: "urllib.error stdlib: URLError and HTTPError exception classes in the web-repl bundle",
3289
+ run: function () {
3290
+ var repl = RS.web_repl();
3291
+ var js = bundle_compile(repl, [
3292
+ "from urllib.error import URLError, HTTPError",
3293
+ // URLError
3294
+ "caught_url = False",
3295
+ "try:",
3296
+ " raise URLError('network failure')",
3297
+ "except URLError as e:",
3298
+ " caught_url = True",
3299
+ " assrt.ok('network failure' in str(e.reason))",
3300
+ "assrt.ok(caught_url)",
3301
+ // HTTPError
3302
+ "caught_http = False",
3303
+ "try:",
3304
+ " raise HTTPError('http://example.com', 404, 'Not Found', {}, None)",
3305
+ "except HTTPError as e:",
3306
+ " caught_http = True",
3307
+ " assrt.equal(e.code, 404)",
3308
+ " assrt.equal(e.msg, 'Not Found')",
3309
+ " assrt.equal(e.getcode(), 404)",
3310
+ " assrt.equal(e.geturl(), 'http://example.com')",
3311
+ "assrt.ok(caught_http)",
3312
+ // HTTPError is caught by URLError handler
3313
+ "caught_as_url = False",
3314
+ "try:",
3315
+ " raise HTTPError('http://x.com', 500, 'Server Error', {}, None)",
3316
+ "except URLError as e:",
3317
+ " caught_as_url = True",
3318
+ " assrt.equal(e.code, 500)",
3319
+ "assrt.ok(caught_as_url)",
3320
+ ].join("\n"));
3321
+ run_js(js);
3322
+ },
3323
+ },
3324
+
3325
+ {
3326
+ name: "bundle_bisect_basic",
3327
+ description: "bisect stdlib: bisect_left, bisect_right, bisect in the web-repl bundle",
3328
+ run: function () {
3329
+ var repl = RS.web_repl();
3330
+ var js = bundle_compile(repl, [
3331
+ "from bisect import bisect_left, bisect_right, bisect",
3332
+ "a = [1, 3, 5, 7, 9]",
3333
+ // bisect_left
3334
+ "assrt.equal(bisect_left(a, 0), 0)",
3335
+ "assrt.equal(bisect_left(a, 1), 0)",
3336
+ "assrt.equal(bisect_left(a, 5), 2)",
3337
+ "assrt.equal(bisect_left(a, 9), 4)",
3338
+ "assrt.equal(bisect_left(a, 10), 5)",
3339
+ // bisect_right
3340
+ "assrt.equal(bisect_right(a, 1), 1)",
3341
+ "assrt.equal(bisect_right(a, 5), 3)",
3342
+ "assrt.equal(bisect_right(a, 9), 5)",
3343
+ "assrt.equal(bisect_right(a, 10), 5)",
3344
+ // bisect alias == bisect_right
3345
+ "assrt.equal(bisect(a, 5), bisect_right(a, 5))",
3346
+ // empty list
3347
+ "assrt.equal(bisect_left([], 5), 0)",
3348
+ "assrt.equal(bisect_right([], 5), 0)",
3349
+ // all equal
3350
+ "eq = [3, 3, 3]",
3351
+ "assrt.equal(bisect_left(eq, 3), 0)",
3352
+ "assrt.equal(bisect_right(eq, 3), 3)",
3353
+ // lo/hi bounds
3354
+ "assrt.equal(bisect_left(a, 6, 1, 4), 3)",
3355
+ "assrt.equal(bisect_right(a, 3, 1, 4), 2)",
3356
+ ].join("\n"));
3357
+ run_js(js);
3358
+ },
3359
+ },
3360
+
3361
+ {
3362
+ name: "bundle_bisect_insort",
3363
+ description: "bisect stdlib: insort_left, insort_right, insort in the web-repl bundle",
3364
+ run: function () {
3365
+ var repl = RS.web_repl();
3366
+ var js = bundle_compile(repl, [
3367
+ "from bisect import insort_left, insort_right, insort",
3368
+ // insort_right basic
3369
+ "r = [1, 3, 5, 7]",
3370
+ "insort_right(r, 4)",
3371
+ "assrt.deepEqual(r, [1, 3, 4, 5, 7])",
3372
+ // insort_left: new value goes to the LEFT of equal elements
3373
+ "l = [1, 3, 3, 5]",
3374
+ "insort_left(l, 3)",
3375
+ "assrt.equal(l[1], 3)", // new 3 at index 1
3376
+ "assrt.equal(l.length, 5)",
3377
+ // insort alias == insort_right
3378
+ "s = [2, 4, 6]",
3379
+ "insort(s, 5)",
3380
+ "assrt.deepEqual(s, [2, 4, 5, 6])",
3381
+ // build sorted list from scratch
3382
+ "built = []",
3383
+ "for v in [5, 1, 3, 2, 4]:",
3384
+ " insort(built, v)",
3385
+ "assrt.deepEqual(built, [1, 2, 3, 4, 5])",
3386
+ ].join("\n"));
3387
+ run_js(js);
3388
+ },
3389
+ },
3390
+
3391
+ {
3392
+ name: "bundle_bisect_key",
3393
+ description: "bisect stdlib: key function parameter in the web-repl bundle",
3394
+ run: function () {
3395
+ var repl = RS.web_repl();
3396
+ var js = bundle_compile(repl, [
3397
+ "from bisect import bisect_left, bisect_right",
3398
+ // key extracts first element of each pair; x is the key value
3399
+ "pairs = [[1, 'a'], [3, 'b'], [5, 'c'], [7, 'd']]",
3400
+ "kfn = def(item): return item[0];",
3401
+ "assrt.equal(bisect_left(pairs, 3, 0, None, kfn), 1)",
3402
+ "assrt.equal(bisect_right(pairs, 3, 0, None, kfn), 2)",
3403
+ "assrt.equal(bisect_left(pairs, 4, 0, None, kfn), 2)",
3404
+ "assrt.equal(bisect_right(pairs, 4, 0, None, kfn), 2)",
3405
+ "assrt.equal(bisect_left(pairs, 0, 0, None, kfn), 0)",
3406
+ "assrt.equal(bisect_right(pairs, 8, 0, None, kfn), 4)",
3407
+ ].join("\n"));
3408
+ run_js(js);
3409
+ },
3410
+ },
3411
+
3412
+ {
3413
+ name: "bundle_bisect_errors",
3414
+ description: "bisect stdlib: ValueError for negative lo in the web-repl bundle",
3415
+ run: function () {
3416
+ var repl = RS.web_repl();
3417
+ var js = bundle_compile(repl, [
3418
+ "from bisect import bisect_left, bisect_right",
3419
+ "caught_left = False",
3420
+ "try:",
3421
+ " bisect_left([1, 2, 3], 2, -1)",
3422
+ "except ValueError:",
3423
+ " caught_left = True",
3424
+ "assrt.ok(caught_left)",
3425
+ "caught_right = False",
3426
+ "try:",
3427
+ " bisect_right([1, 2, 3], 2, -1)",
3428
+ "except ValueError:",
3429
+ " caught_right = True",
3430
+ "assrt.ok(caught_right)",
3431
+ // strings also work
3432
+ "words = ['bar', 'baz', 'foo', 'qux']",
3433
+ "assrt.equal(bisect_left(words, 'car'), 2)",
3434
+ "assrt.equal(bisect_right(words, 'car'), 2)",
3435
+ ].join("\n"));
3436
+ run_js(js);
3437
+ },
3438
+ },
3439
+
3440
+ // ── http stdlib ──────────────────────────────────────────────────────────
3441
+
3442
+ {
3443
+ name: "bundle_http_status",
3444
+ description: "http stdlib: HTTPStatus constants in the web-repl bundle",
3445
+ run: function () {
3446
+ var repl = RS.web_repl();
3447
+ var js = bundle_compile(repl, [
3448
+ "from http import HTTPStatus",
3449
+ "assrt.equal(HTTPStatus.OK, 200)",
3450
+ "assrt.equal(HTTPStatus.CREATED, 201)",
3451
+ "assrt.equal(HTTPStatus.NO_CONTENT, 204)",
3452
+ "assrt.equal(HTTPStatus.NOT_FOUND, 404)",
3453
+ "assrt.equal(HTTPStatus.INTERNAL_SERVER_ERROR, 500)",
3454
+ "assrt.equal(HTTPStatus.IM_A_TEAPOT, 418)",
3455
+ "assrt.equal(HTTPStatus.MOVED_PERMANENTLY, 301)",
3456
+ "assrt.equal(HTTPStatus.UNAUTHORIZED, 401)",
3457
+ ].join("\n"));
3458
+ run_js(js);
3459
+ },
3460
+ },
3461
+
3462
+ {
3463
+ name: "bundle_http_client_basics",
3464
+ description: "http.client stdlib: HTTPConnection, HTTPSConnection, HTTPResponse, exceptions in the web-repl bundle",
3465
+ run: function () {
3466
+ var repl = RS.web_repl();
3467
+ var js = bundle_compile(repl, [
3468
+ "from http.client import (HTTPConnection, HTTPSConnection, HTTPResponse,",
3469
+ " HTTPException, NotConnected, InvalidURL,",
3470
+ " RemoteDisconnected, HTTP_PORT, HTTPS_PORT)",
3471
+ "assrt.equal(HTTP_PORT, 80)",
3472
+ "assrt.equal(HTTPS_PORT, 443)",
3473
+ // URL building
3474
+ "conn = HTTPConnection('example.com')",
3475
+ "assrt.equal(conn._build_url('/path'), 'http://example.com/path')",
3476
+ "conn2 = HTTPConnection('example.com', 8080)",
3477
+ "assrt.equal(conn2._build_url('/api'), 'http://example.com:8080/api')",
3478
+ "sconn = HTTPSConnection('secure.example.com')",
3479
+ "assrt.equal(sconn._build_url('/data'), 'https://secure.example.com/data')",
3480
+ // request() stores state
3481
+ "conn3 = HTTPConnection('api.example.com')",
3482
+ "conn3.request('POST', '/items', 'a=1', {'Content-Type': 'text/plain'})",
3483
+ "assrt.equal(conn3._method, 'POST')",
3484
+ "assrt.equal(conn3._path, '/items')",
3485
+ "assrt.equal(conn3._body, 'a=1')",
3486
+ "assrt.equal(conn3._headers['content-type'], 'text/plain')",
3487
+ // HTTPResponse accessors
3488
+ "hdrs = {'content-type': 'application/json'}",
3489
+ "resp = HTTPResponse(200, 'OK', hdrs, '{\"n\":42}', 'https://x.com/')",
3490
+ "assrt.equal(resp.status, 200)",
3491
+ "assrt.equal(resp.reason, 'OK')",
3492
+ "assrt.equal(resp.getheader('content-type'), 'application/json')",
3493
+ "assrt.equal(resp.getheader('Content-Type'), 'application/json')",
3494
+ "assrt.equal(resp.getheader('missing', 'def'), 'def')",
3495
+ "assrt.ok(resp.read() is not None)",
3496
+ "assrt.ok(resp.json() is not None)",
3497
+ // exception hierarchy
3498
+ "caught = False",
3499
+ "try:",
3500
+ " raise NotConnected('nc')",
3501
+ "except HTTPException as e:",
3502
+ " caught = True",
3503
+ "assrt.ok(caught)",
3504
+ ].join("\n"));
3505
+ run_js(js);
3506
+ },
3507
+ },
3508
+
3509
+ {
3510
+ name: "bundle_http_cookies_basic",
3511
+ description: "http.cookies stdlib: SimpleCookie parsing and Morsel access in the web-repl bundle",
3512
+ run: function () {
3513
+ var repl = RS.web_repl();
3514
+ var js = bundle_compile(repl, [
3515
+ "from __python__ import overload_getitem",
3516
+ "from http.cookies import SimpleCookie, Morsel, CookieError",
3517
+ // basic parse
3518
+ "c = SimpleCookie()",
3519
+ "c.load('session=abc123; user=alice')",
3520
+ "assrt.ok('session' in c.keys())",
3521
+ "assrt.ok('user' in c.keys())",
3522
+ "assrt.equal(c['session'].value, 'abc123')",
3523
+ "assrt.equal(c['user'].value, 'alice')",
3524
+ // set a cookie
3525
+ "c2 = SimpleCookie()",
3526
+ "c2['token'] = 'xyz'",
3527
+ "assrt.ok('token' in c2.keys())",
3528
+ "assrt.equal(c2['token'].value, 'xyz')",
3529
+ // cookie attributes
3530
+ "c3 = SimpleCookie()",
3531
+ "c3['id'] = '1'",
3532
+ "c3['id']['path'] = '/'",
3533
+ "c3['id']['max-age'] = 3600",
3534
+ "assrt.equal(c3['id']['path'], '/')",
3535
+ "assrt.equal(c3['id']['max-age'], 3600)",
3536
+ // constructor with initial data
3537
+ "c4 = SimpleCookie('x=10; y=20')",
3538
+ "assrt.equal(c4['x'].value, '10')",
3539
+ "assrt.equal(c4['y'].value, '20')",
3540
+ ].join("\n"));
3541
+ run_js(js);
3542
+ },
3543
+ },
3544
+
3545
+ {
3546
+ name: "bundle_http_cookies_output",
3547
+ description: "http.cookies stdlib: SimpleCookie.output, Morsel.OutputString in the web-repl bundle",
3548
+ run: function () {
3549
+ var repl = RS.web_repl();
3550
+ var js = bundle_compile(repl, [
3551
+ "from __python__ import overload_getitem",
3552
+ "from http.cookies import SimpleCookie, Morsel, CookieError",
3553
+ // Morsel.OutputString
3554
+ "m = Morsel()",
3555
+ "m.set('token', 'abc', 'abc')",
3556
+ "m._attrs['path'] = '/'",
3557
+ "m._attrs['max-age'] = '3600'",
3558
+ "s = m.OutputString()",
3559
+ "assrt.ok('token=abc' in s)",
3560
+ "assrt.ok('Path=/' in s)",
3561
+ "assrt.ok('Max-Age=3600' in s)",
3562
+ // Morsel.output with header
3563
+ "out = m.output()",
3564
+ "assrt.ok('Set-Cookie: token=abc' in out)",
3565
+ // SimpleCookie.output
3566
+ "c = SimpleCookie()",
3567
+ "c['a'] = '1'",
3568
+ "c['b'] = '2'",
3569
+ "full = c.output()",
3570
+ "assrt.ok('Set-Cookie: a=1' in full)",
3571
+ "assrt.ok('Set-Cookie: b=2' in full)",
3572
+ // CookieError
3573
+ "caught = False",
3574
+ "try:",
3575
+ " raise CookieError('bad')",
3576
+ "except CookieError as e:",
3577
+ " caught = True",
3578
+ "assrt.ok(caught)",
3579
+ ].join("\n"));
3580
+ run_js(js);
3581
+ },
3582
+ },
3583
+
3584
+ // ── csv stdlib ────────────────────────────────────────────────────────
3585
+
3586
+ {
3587
+ name: "bundle_csv_reader_basic",
3588
+ description: "csv stdlib: reader parses CSV rows and writer produces CSV text in the web-repl bundle",
3589
+ run: function () {
3590
+ var repl = RS.web_repl();
3591
+ var js = bundle_compile(repl, [
3592
+ "import csv",
3593
+ "from io import StringIO",
3594
+ // reader — list input
3595
+ "rows = []",
3596
+ "for row in csv.reader(['a,b,c', '1,2,3']):",
3597
+ " rows.push(row)",
3598
+ "assrt.equal(rows.length, 2)",
3599
+ "assrt.deepEqual(rows[0], ['a', 'b', 'c'])",
3600
+ "assrt.deepEqual(rows[1], ['1', '2', '3'])",
3601
+ // reader — quoted field
3602
+ "rows2 = []",
3603
+ "for row in csv.reader(['\"hello, world\",foo']):",
3604
+ " rows2.push(row)",
3605
+ "assrt.equal(rows2[0][0], 'hello, world')",
3606
+ "assrt.equal(rows2[0][1], 'foo')",
3607
+ // writer
3608
+ "sio = StringIO()",
3609
+ "w = csv.writer(sio)",
3610
+ "w.writerow(['name', 'age'])",
3611
+ "w.writerow(['Alice', 30])",
3612
+ "out = sio.getvalue()",
3613
+ "assrt.ok('name,age' in out)",
3614
+ "assrt.ok('Alice,30' in out)",
3615
+ // round-trip
3616
+ "buf = StringIO()",
3617
+ "w2 = csv.writer(buf)",
3618
+ "w2.writerow(['x', 'needs, quoting', 'y'])",
3619
+ "buf.seek(0)",
3620
+ "rt = []",
3621
+ "for row in csv.reader(buf):",
3622
+ " rt.push(row)",
3623
+ "assrt.deepEqual(rt[0], ['x', 'needs, quoting', 'y'])",
3624
+ ].join("\n"));
3625
+ run_js(js);
3626
+ },
3627
+ },
3628
+
3629
+ {
3630
+ name: "bundle_csv_dictreader",
3631
+ description: "csv stdlib: DictReader reads rows as dicts with automatic fieldnames in the web-repl bundle",
3632
+ run: function () {
3633
+ var repl = RS.web_repl();
3634
+ var js = bundle_compile(repl, [
3635
+ "import csv",
3636
+ // DictReader — fieldnames from first row
3637
+ "rows = []",
3638
+ "for row in csv.DictReader(['name,age', 'Alice,30', 'Bob,25']):",
3639
+ " rows.push(row)",
3640
+ "assrt.equal(rows.length, 2)",
3641
+ "assrt.equal(rows[0]['name'], 'Alice')",
3642
+ "assrt.equal(rows[0]['age'], '30')",
3643
+ "assrt.equal(rows[1]['name'], 'Bob')",
3644
+ // DictReader — provided fieldnames
3645
+ "rows2 = []",
3646
+ "for row in csv.DictReader(['Alice,30', 'Bob,25'], fieldnames=['name','age']):",
3647
+ " rows2.push(row)",
3648
+ "assrt.equal(rows2.length, 2)",
3649
+ "assrt.equal(rows2[0]['name'], 'Alice')",
3650
+ // DictReader — restval for missing field
3651
+ "rows3 = []",
3652
+ "for row in csv.DictReader(['name,age,city', 'Alice,30'], restval='?'):",
3653
+ " rows3.push(row)",
3654
+ "assrt.equal(rows3[0]['city'], '?')",
3655
+ // DictReader — empty input yields no rows
3656
+ "empty_count = 0",
3657
+ "for row in csv.DictReader([]):",
3658
+ " empty_count += 1",
3659
+ "assrt.equal(empty_count, 0)",
3660
+ ].join("\n"));
3661
+ run_js(js);
3662
+ },
3663
+ },
3664
+
3665
+ {
3666
+ name: "bundle_csv_dictwriter",
3667
+ description: "csv stdlib: DictWriter writes header and rows from dicts in the web-repl bundle",
3668
+ run: function () {
3669
+ var repl = RS.web_repl();
3670
+ var js = bundle_compile(repl, [
3671
+ "import csv",
3672
+ "from io import StringIO",
3673
+ // DictWriter — writeheader + writerow
3674
+ "sio = StringIO()",
3675
+ "dw = csv.DictWriter(sio, ['name', 'score'])",
3676
+ "dw.writeheader()",
3677
+ "dw.writerow({'name': 'Eve', 'score': '99'})",
3678
+ "dw.writerow({'name': 'Frank', 'score': '88'})",
3679
+ "out = sio.getvalue()",
3680
+ "assrt.ok('name,score' in out)",
3681
+ "assrt.ok('Eve,99' in out)",
3682
+ "assrt.ok('Frank,88' in out)",
3683
+ // round-trip DictWriter → DictReader
3684
+ "buf = StringIO()",
3685
+ "dw2 = csv.DictWriter(buf, ['x', 'y'])",
3686
+ "dw2.writeheader()",
3687
+ "dw2.writerow({'x': 'hello', 'y': 'world'})",
3688
+ "buf.seek(0)",
3689
+ "rt = []",
3690
+ "for row in csv.DictReader(buf):",
3691
+ " rt.push(row)",
3692
+ "assrt.equal(rt.length, 1)",
3693
+ "assrt.equal(rt[0]['x'], 'hello')",
3694
+ "assrt.equal(rt[0]['y'], 'world')",
3695
+ ].join("\n"));
3696
+ run_js(js);
3697
+ },
3698
+ },
3699
+
3700
+ {
3701
+ name: "bundle_csv_dialects",
3702
+ description: "csv stdlib: dialect options, register_dialect, list_dialects, field_size_limit in the web-repl bundle",
3703
+ run: function () {
3704
+ var repl = RS.web_repl();
3705
+ var js = bundle_compile(repl, [
3706
+ "import csv",
3707
+ "from io import StringIO",
3708
+ // excel-tab dialect
3709
+ "rows = []",
3710
+ "for row in csv.reader(['a\\tb\\tc'], dialect='excel-tab'):",
3711
+ " rows.push(row)",
3712
+ "assrt.deepEqual(rows[0], ['a', 'b', 'c'])",
3713
+ // QUOTE_ALL
3714
+ "sio = StringIO()",
3715
+ "w = csv.writer(sio, quoting=csv.QUOTE_ALL)",
3716
+ "w.writerow(['x', 'y'])",
3717
+ "assrt.equal(sio.getvalue(), '\"x\",\"y\"\\r\\n')",
3718
+ // register_dialect / list_dialects / unregister_dialect
3719
+ "csv.register_dialect('pipes', delimiter='|')",
3720
+ "dialects = csv.list_dialects()",
3721
+ "assrt.ok(dialects.indexOf('pipes') >= 0)",
3722
+ "sio2 = StringIO()",
3723
+ "w2 = csv.writer(sio2, dialect='pipes')",
3724
+ "w2.writerow(['a', 'b'])",
3725
+ "assrt.equal(sio2.getvalue(), 'a|b\\r\\n')",
3726
+ "csv.unregister_dialect('pipes')",
3727
+ "assrt.ok(csv.list_dialects().indexOf('pipes') < 0)",
3728
+ // field_size_limit
3729
+ "old = csv.field_size_limit(65536)",
3730
+ "assrt.equal(old, 131072)",
3731
+ "assrt.equal(csv.field_size_limit(), 65536)",
3732
+ "csv.field_size_limit(131072)",
3733
+ ].join("\n"));
3734
+ run_js(js);
3735
+ },
3736
+ },
3737
+
3738
+ {
3739
+ name: "bundle_textwrap_wrap",
3740
+ description: "textwrap stdlib: wrap, fill, shorten in the web-repl bundle",
3741
+ run: function () {
3742
+ var repl = RS.web_repl();
3743
+ var js = bundle_compile(repl, [
3744
+ "from textwrap import wrap, fill, shorten",
3745
+ // basic wrap
3746
+ "r = wrap('one two three four five', 10)",
3747
+ "assrt.equal(r.length, 3)",
3748
+ "assrt.equal(r[0], 'one two')",
3749
+ "assrt.equal(r[1], 'three four')",
3750
+ "assrt.equal(r[2], 'five')",
3751
+ // short text fits on one line
3752
+ "assrt.deepEqual(wrap('hello', 20), ['hello'])",
3753
+ // empty string
3754
+ "assrt.deepEqual(wrap('', 10), [])",
3755
+ // fill joins with newlines
3756
+ "assrt.equal(fill('one two three', 8), 'one two\\nthree')",
3757
+ // fill with indents
3758
+ "assrt.equal(fill('one two three four', 12, initial_indent='> ', subsequent_indent=' '), '> one two\\n three four')",
3759
+ // shorten — fits
3760
+ "assrt.equal(shorten('hello world', 20), 'hello world')",
3761
+ // shorten — truncates; 'one two three' (13) + ' [...]' (6) = 19 ≤ 20
3762
+ "assrt.equal(shorten('one two three four five', 20), 'one two three [...]')",
3763
+ // shorten — custom placeholder
3764
+ "assrt.equal(shorten('hello world foo bar', 14, placeholder='...'), 'hello world...')",
3765
+ // shorten — normalises whitespace
3766
+ "assrt.equal(shorten('hello world', 20), 'hello world')",
3767
+ ].join("\n"));
3768
+ run_js(js);
3769
+ },
3770
+ },
3771
+
3772
+ {
3773
+ name: "bundle_textwrap_dedent",
3774
+ description: "textwrap stdlib: dedent in the web-repl bundle",
3775
+ run: function () {
3776
+ var repl = RS.web_repl();
3777
+ var js = bundle_compile(repl, [
3778
+ "from textwrap import dedent",
3779
+ // common indent removed
3780
+ "assrt.equal(dedent(' hello\\n world'), 'hello\\nworld')",
3781
+ // no common indent
3782
+ "assrt.equal(dedent('hello\\n world'), 'hello\\n world')",
3783
+ // empty lines ignored when computing margin
3784
+ "assrt.equal(dedent(' hello\\n\\n world'), 'hello\\n\\nworld')",
3785
+ // partial common indent: ' ' vs ' ' → margin ' '
3786
+ "assrt.equal(dedent(' foo\\n bar'), ' foo\\nbar')",
3787
+ // leading blank line then indented
3788
+ "assrt.equal(dedent('\\n foo\\n bar'), '\\nfoo\\nbar')",
3789
+ // tab-based indent
3790
+ "assrt.equal(dedent('\\thello\\n\\tworld'), 'hello\\nworld')",
3791
+ ].join("\n"));
3792
+ run_js(js);
3793
+ },
3794
+ },
3795
+
3796
+ {
3797
+ name: "bundle_textwrap_indent",
3798
+ description: "textwrap stdlib: indent with and without predicate in the web-repl bundle",
3799
+ run: function () {
3800
+ var repl = RS.web_repl();
3801
+ var js = bundle_compile(repl, [
3802
+ "from textwrap import indent",
3803
+ // basic
3804
+ "assrt.equal(indent('hello\\nworld', ' '), ' hello\\n world')",
3805
+ // empty lines not indented by default
3806
+ "assrt.equal(indent('hello\\n\\nworld', ' '), ' hello\\n\\n world')",
3807
+ // whitespace-only line not indented
3808
+ "assrt.equal(indent('hello\\n \\nworld', '> '), '> hello\\n \\n> world')",
3809
+ // custom predicate: indent all lines
3810
+ "_pred = def(line): return True;",
3811
+ "assrt.equal(indent('hello\\n\\nworld', '> ', _pred), '> hello\\n> \\n> world')",
3812
+ // predicate: only lines starting with '#'
3813
+ "_ph = def(line): return line.startsWith('#');",
3814
+ "assrt.equal(indent('# a\\ncode\\n# b', '!! ', _ph), '!! # a\\ncode\\n!! # b')",
3815
+ ].join("\n"));
3816
+ run_js(js);
3817
+ },
3818
+ },
3819
+
3820
+ {
3821
+ name: "bundle_textwrap_textwrapper",
3822
+ description: "textwrap stdlib: TextWrapper class with options in the web-repl bundle",
3823
+ run: function () {
3824
+ var repl = RS.web_repl();
3825
+ var js = bundle_compile(repl, [
3826
+ "from textwrap import TextWrapper",
3827
+ // basic wrap and fill
3828
+ "tw = TextWrapper(width=10)",
3829
+ "assrt.deepEqual(tw.wrap('one two three'), ['one two', 'three'])",
3830
+ "assrt.equal(tw.fill('one two three'), 'one two\\nthree')",
3831
+ // max_lines with placeholder
3832
+ // width=15, max_lines=2, placeholder=' ...' (4 chars)
3833
+ // line 1: 'alpha beta' (10) fits; line 2 truncated to 'gamma delta ...' (15)
3834
+ "tw2 = TextWrapper(width=15, max_lines=2, placeholder=' ...')",
3835
+ "r2 = tw2.wrap('alpha beta gamma delta epsilon')",
3836
+ "assrt.equal(r2.length, 2)",
3837
+ "assrt.equal(r2[0], 'alpha beta')",
3838
+ "assrt.equal(r2[1], 'gamma delta ...')",
3839
+ // break_long_words=False
3840
+ "tw3 = TextWrapper(width=5, break_long_words=False)",
3841
+ "r3 = tw3.wrap('superlongword short')",
3842
+ "assrt.equal(r3[0], 'superlongword')",
3843
+ "assrt.equal(r3[1], 'short')",
3844
+ // fix_sentence_endings
3845
+ "tw4 = TextWrapper(width=70, fix_sentence_endings=True)",
3846
+ "r4 = tw4.wrap('end of sentence. New sentence.')",
3847
+ "assrt.equal(r4[0], 'end of sentence. New sentence.')",
3848
+ ].join("\n"));
3849
+ run_js(js);
3850
+ },
3851
+ },
3852
+
3853
+ // ── logging ──────────────────────────────────────────────────────────────
3854
+
3855
+ {
3856
+ name: "bundle_logging_basic",
3857
+ description: "logging stdlib: StreamHandler with custom stream in the web-repl bundle",
3858
+ run: function () {
3859
+ var repl = RS.web_repl();
3860
+ var js = bundle_compile(repl, [
3861
+ "from logging import Logger, StreamHandler, Formatter, DEBUG, INFO, WARNING, ERROR",
3862
+ "class _Buf:",
3863
+ " def __init__(self):",
3864
+ " self.lines = []",
3865
+ " def write(self, s):",
3866
+ " self.lines.push(s)",
3867
+ "_buf = _Buf()",
3868
+ "_h = StreamHandler(_buf)",
3869
+ "_h.setFormatter(Formatter('%(levelname)s:%(name)s:%(message)s'))",
3870
+ "_h.setLevel(DEBUG)",
3871
+ "_log = Logger('myapp')",
3872
+ "_log.addHandler(_h)",
3873
+ "_log.setLevel(DEBUG)",
3874
+ "_log.propagate = False",
3875
+ "_log.debug('dbg')",
3876
+ "_log.info('hi')",
3877
+ "_log.warning('warn')",
3878
+ "_log.error('err')",
3879
+ "assrt.equal(_buf.lines.length, 4)",
3880
+ "assrt.equal(_buf.lines[0], 'DEBUG:myapp:dbg\\n')",
3881
+ "assrt.equal(_buf.lines[1], 'INFO:myapp:hi\\n')",
3882
+ "assrt.equal(_buf.lines[2], 'WARNING:myapp:warn\\n')",
3883
+ "assrt.equal(_buf.lines[3], 'ERROR:myapp:err\\n')",
3884
+ ].join("\n"));
3885
+ run_js(js);
3886
+ },
3887
+ },
3888
+
3889
+ {
3890
+ name: "bundle_logging_levels",
3891
+ description: "logging stdlib: level constants, getLevelName, addLevelName, %-format args",
3892
+ run: function () {
3893
+ var repl = RS.web_repl();
3894
+ var js = bundle_compile(repl, [
3895
+ "from logging import getLevelName, addLevelName, DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET",
3896
+ "assrt.equal(NOTSET, 0)",
3897
+ "assrt.equal(DEBUG, 10)",
3898
+ "assrt.equal(INFO, 20)",
3899
+ "assrt.equal(WARNING, 30)",
3900
+ "assrt.equal(ERROR, 40)",
3901
+ "assrt.equal(CRITICAL, 50)",
3902
+ "assrt.equal(getLevelName(DEBUG), 'DEBUG')",
3903
+ "assrt.equal(getLevelName(WARNING), 'WARNING')",
3904
+ "assrt.equal(getLevelName(42), 'Level 42')",
3905
+ "assrt.equal(getLevelName('ERROR'), ERROR)",
3906
+ "assrt.equal(getLevelName('WARN'), WARNING)",
3907
+ "addLevelName(15, 'VERBOSE')",
3908
+ "assrt.equal(getLevelName(15), 'VERBOSE')",
3909
+ "assrt.equal(getLevelName('VERBOSE'), 15)",
3910
+ // %-format args via Logger
3911
+ "from logging import Logger, StreamHandler, Formatter",
3912
+ "class _B:",
3913
+ " def __init__(self):",
3914
+ " self.out = []",
3915
+ " def write(self, s):",
3916
+ " self.out.push(s)",
3917
+ "_b = _B()",
3918
+ "_h2 = StreamHandler(_b)",
3919
+ "_h2.setFormatter(Formatter('%(message)s'))",
3920
+ "_l2 = Logger('fmt')",
3921
+ "_l2.addHandler(_h2)",
3922
+ "_l2.setLevel(DEBUG)",
3923
+ "_l2.propagate = False",
3924
+ "_l2.info('x=%d y=%s', 7, 'foo')",
3925
+ "assrt.equal(_b.out[0], 'x=7 y=foo\\n')",
3926
+ ].join("\n"));
3927
+ run_js(js);
3928
+ },
3929
+ },
3930
+
3931
+ {
3932
+ name: "bundle_logging_hierarchy",
3933
+ description: "logging stdlib: parent/child logger hierarchy and propagation",
3934
+ run: function () {
3935
+ var repl = RS.web_repl();
3936
+ var js = bundle_compile(repl, [
3937
+ "from logging import Logger, StreamHandler, Formatter, DEBUG, INFO",
3938
+ "class _B:",
3939
+ " def __init__(self):",
3940
+ " self.out = []",
3941
+ " def write(self, s):",
3942
+ " self.out.push(s)",
3943
+ "_b = _B()",
3944
+ "_h = StreamHandler(_b)",
3945
+ "_h.setFormatter(Formatter('%(name)s:%(message)s'))",
3946
+ "_parent = Logger('app')",
3947
+ "_parent.addHandler(_h)",
3948
+ "_parent.setLevel(DEBUG)",
3949
+ "_parent.propagate = False",
3950
+ "_child = Logger('app.sub')",
3951
+ "_child.parent = _parent",
3952
+ "_child.setLevel(DEBUG)",
3953
+ "_child.propagate = True",
3954
+ "_child.info('from child')",
3955
+ "assrt.equal(_b.out.length, 1)",
3956
+ "assrt.equal(_b.out[0], 'app.sub:from child\\n')",
3957
+ // child with propagate=False should NOT reach parent handler
3958
+ "_b2 = _B()",
3959
+ "_h2 = StreamHandler(_b2)",
3960
+ "_h2.setFormatter(Formatter('%(message)s'))",
3961
+ "_child2 = Logger('app.sub2')",
3962
+ "_child2.parent = _parent",
3963
+ "_child2.addHandler(_h2)",
3964
+ "_child2.setLevel(DEBUG)",
3965
+ "_child2.propagate = False",
3966
+ "_child2.info('isolated')",
3967
+ "assrt.equal(_b.out.length, 1)", // parent buffer unchanged
3968
+ "assrt.equal(_b2.out[0], 'isolated\\n')",
3969
+ ].join("\n"));
3970
+ run_js(js);
3971
+ },
3972
+ },
3973
+
3974
+ {
3975
+ name: "bundle_logging_basicconfig",
3976
+ description: "logging stdlib: basicConfig sets up root logger with custom stream",
3977
+ run: function () {
3978
+ var repl = RS.web_repl();
3979
+ var js = bundle_compile(repl, [
3980
+ "import logging",
3981
+ "class _B:",
3982
+ " def __init__(self):",
3983
+ " self.out = []",
3984
+ " def write(self, s):",
3985
+ " self.out.push(s)",
3986
+ "_b = _B()",
3987
+ "logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s', stream=_b)",
3988
+ "logging.debug('d')",
3989
+ "logging.info('i')",
3990
+ "logging.warning('w')",
3991
+ "assrt.equal(_b.out.length, 3)",
3992
+ "assrt.equal(_b.out[0], 'DEBUG:d\\n')",
3993
+ "assrt.equal(_b.out[1], 'INFO:i\\n')",
3994
+ "assrt.equal(_b.out[2], 'WARNING:w\\n')",
3995
+ // disable() suppresses messages at or below the given level
3996
+ "logging.disable(logging.ERROR)",
3997
+ "logging.warning('suppressed')",
3998
+ "logging.error('also suppressed')",
3999
+ "assrt.equal(_b.out.length, 3)",
4000
+ "logging.disable(logging.NOTSET)", // reset
4001
+ ].join("\n"));
4002
+ run_js(js);
4003
+ },
4004
+ },
4005
+
4006
+ // ── pprint stdlib ────────────────────────────────────────────────────────
4007
+
4008
+ {
4009
+ name: "bundle_pprint_basic",
4010
+ description: "pprint stdlib: pformat of atoms and short containers in the web-repl bundle",
4011
+ run: function () {
4012
+ var repl = RS.web_repl();
4013
+ var js = bundle_compile(repl, [
4014
+ "from pprint import pformat",
4015
+ // atoms -> repr()
4016
+ "assrt.equal(pformat(None), 'None')",
4017
+ "assrt.equal(pformat(True), 'True')",
4018
+ "assrt.equal(pformat(42), '42')",
4019
+ "assrt.equal(pformat('hi'), '\"hi\"')",
4020
+ // empty containers
4021
+ "assrt.equal(pformat([]), '[]')",
4022
+ "assrt.equal(pformat({}), '{}')",
4023
+ "assrt.equal(pformat(set()), 'set()')",
4024
+ // short containers fit on one line
4025
+ "assrt.equal(pformat([1, 2, 3]), '[1, 2, 3]')",
4026
+ "assrt.equal(pformat({'a': 1, 'b': 2}), '{\"a\": 1, \"b\": 2}')",
4027
+ "assrt.equal(pformat(frozenset([1])), 'frozenset({1})')",
4028
+ ].join("\n"));
4029
+ run_js(js);
4030
+ },
4031
+ },
4032
+
4033
+ {
4034
+ name: "bundle_pprint_wrap",
4035
+ description: "pprint stdlib: wide containers break across lines in the web-repl bundle",
4036
+ run: function () {
4037
+ var repl = RS.web_repl();
4038
+ var js = bundle_compile(repl, [
4039
+ "from pprint import pformat",
4040
+ // wide list breaks one element per line
4041
+ "assrt.equal(pformat([1, 2, 3], width=5), '[1,\\n 2,\\n 3]')",
4042
+ // wide dict breaks, keys sorted by default
4043
+ "assrt.equal(pformat({'name': 'Al', 'age': 3}, width=12), '{\"age\": 3,\\n \"name\": \"Al\"}')",
4044
+ // indent parameter widens inner indentation
4045
+ "assrt.equal(pformat([1, 2, 3], width=5, indent=4), '[ 1,\\n 2,\\n 3]')",
4046
+ // single-element containers never break
4047
+ "assrt.equal(pformat([42], width=1), '[42]')",
4048
+ ].join("\n"));
4049
+ run_js(js);
4050
+ },
4051
+ },
4052
+
4053
+ {
4054
+ name: "bundle_pprint_features",
4055
+ description: "pprint stdlib: depth, compact, sort_dicts, and pp() in the web-repl bundle",
4056
+ run: function () {
4057
+ var repl = RS.web_repl();
4058
+ var js = bundle_compile(repl, [
4059
+ "from pprint import pformat, pp",
4060
+ // depth limits expansion
4061
+ "assrt.equal(pformat([1, [2, [3]]], depth=1), '[1, [...]]')",
4062
+ "assrt.equal(pformat([1, [2, [3]]], depth=2), '[1, [2, [...]]]')",
4063
+ // sort_dicts=False keeps insertion order (ρσ_dict preserves it)
4064
+ "d = dict()",
4065
+ "d.set('c', 1)",
4066
+ "d.set('a', 2)",
4067
+ "assrt.equal(pformat(d, sort_dicts=False), '{\"c\": 1, \"a\": 2}')",
4068
+ "assrt.equal(pformat(d, sort_dicts=True), '{\"a\": 2, \"c\": 1}')",
4069
+ // compact packs multiple items per line
4070
+ "compact_out = pformat([1, 2, 3, 4, 5, 6, 7, 8], compact=True, width=15)",
4071
+ "assrt.equal(compact_out, '[1, 2, 3, 4, 5,\\n 6, 7, 8]')",
4072
+ // pp() writes to a custom stream and defaults sort_dicts=False
4073
+ "class _C:",
4074
+ " def __init__(self):",
4075
+ " self.parts = []",
4076
+ " def write(self, s):",
4077
+ " self.parts.push(s)",
4078
+ "col = _C()",
4079
+ "pp(d, stream=col)",
4080
+ "assrt.equal(col.parts.join(''), '{\"c\": 1, \"a\": 2}\\n')",
4081
+ ].join("\n"));
4082
+ run_js(js);
4083
+ },
4084
+ },
4085
+
4086
+ {
4087
+ name: "bundle_pprint_safe",
4088
+ description: "pprint stdlib: saferepr, isreadable, isrecursive, PrettyPrinter in the web-repl bundle",
4089
+ run: function () {
4090
+ var repl = RS.web_repl();
4091
+ var js = bundle_compile(repl, [
4092
+ "from pprint import saferepr, isreadable, isrecursive, PrettyPrinter",
4093
+ // saferepr of normal structures == single-line repr
4094
+ "assrt.equal(saferepr([1, 2, 3]), '[1, 2, 3]')",
4095
+ "assrt.equal(saferepr({'a': 1}), '{\"a\": 1}')",
4096
+ // isreadable / isrecursive on plain structures
4097
+ "assrt.ok(isreadable([1, 2, 3]))",
4098
+ "assrt.ok(not isrecursive([1, 2, 3]))",
4099
+ // self-referential list -> recursion detected and marked
4100
+ "cyclic = [1, 2]",
4101
+ "cyclic.push(cyclic)",
4102
+ "assrt.ok(isrecursive(cyclic))",
4103
+ "assrt.ok(not isreadable(cyclic))",
4104
+ "assrt.ok(saferepr(cyclic).indexOf('<Recursion on') >= 0)",
4105
+ // PrettyPrinter class: pformat + instance predicates
4106
+ "p = PrettyPrinter(indent=2, width=8)",
4107
+ "assrt.equal(p.pformat([10, 20, 30]), '[ 10,\\n 20,\\n 30]')",
4108
+ "assrt.ok(p.isrecursive(cyclic))",
4109
+ // invalid args raise ValueError
4110
+ "caught = False",
4111
+ "try:",
4112
+ " PrettyPrinter(depth=0)",
4113
+ "except ValueError:",
4114
+ " caught = True",
4115
+ "assrt.ok(caught)",
4116
+ ].join("\n"));
4117
+ run_js(js);
4118
+ },
4119
+ },
4120
+
4121
+ {
4122
+ name: "bundle_type_enforcement_basic",
4123
+ description: "type_enforcement: max args, missing required, type annotations",
4124
+ run: function () {
4125
+ var repl = RS.web_repl();
4126
+ var js = bundle_compile(repl, [
4127
+ "from __python__ import type_enforcement",
4128
+ "def add(a: int, b: int):",
4129
+ " return a + b",
4130
+ // correct call
4131
+ "assrt.equal(add(2, 3), 5)",
4132
+ // too many positional args
4133
+ "caught = False",
4134
+ "try:",
4135
+ " add(1, 2, 3)",
4136
+ "except TypeError:",
4137
+ " caught = True",
4138
+ "assrt.ok(caught)",
4139
+ // missing required arg
4140
+ "caught2 = False",
4141
+ "try:",
4142
+ " add(1)",
4143
+ "except TypeError:",
4144
+ " caught2 = True",
4145
+ "assrt.ok(caught2)",
4146
+ // type mismatch
4147
+ "caught3 = False",
4148
+ "try:",
4149
+ " add('x', 2)",
4150
+ "except TypeError:",
4151
+ " caught3 = True",
4152
+ "assrt.ok(caught3)",
4153
+ ].join("\n"));
4154
+ run_js(js);
4155
+ },
4156
+ },
4157
+
4158
+ {
4159
+ name: "bundle_type_enforcement_posonly",
4160
+ description: "type_enforcement: positional-only args cannot be passed as kwargs",
4161
+ run: function () {
4162
+ var repl = RS.web_repl();
4163
+ var js = bundle_compile(repl, [
4164
+ "from __python__ import type_enforcement",
4165
+ "def sub(a, b, /):",
4166
+ " return a - b",
4167
+ // valid positional call
4168
+ "assrt.equal(sub(10, 3), 7)",
4169
+ // posonly passed as kwarg → TypeError
4170
+ "caught = False",
4171
+ "try:",
4172
+ " sub(a=10, b=3)",
4173
+ "except TypeError:",
4174
+ " caught = True",
4175
+ "assrt.ok(caught)",
4176
+ // mixed: posonly + default normal
4177
+ "def greet(name, /, greeting='Hello'):",
4178
+ " return greeting + ' ' + name",
4179
+ "assrt.equal(greet('Alice'), 'Hello Alice')",
4180
+ "assrt.equal(greet('Bob', greeting='Hi'), 'Hi Bob')",
4181
+ "caught2 = False",
4182
+ "try:",
4183
+ " greet(name='Carol')",
4184
+ "except TypeError:",
4185
+ " caught2 = True",
4186
+ "assrt.ok(caught2)",
4187
+ ].join("\n"));
4188
+ run_js(js);
4189
+ },
4190
+ },
4191
+
4192
+ {
4193
+ name: "bundle_type_enforcement_kwonly",
4194
+ description: "type_enforcement: keyword-only args must be supplied by name",
4195
+ run: function () {
4196
+ var repl = RS.web_repl();
4197
+ var js = bundle_compile(repl, [
4198
+ "from __python__ import type_enforcement",
4199
+ "def notify(msg, *, urgent):",
4200
+ " return msg + ('!' if urgent else '.')",
4201
+ // correct kwonly usage
4202
+ "assrt.equal(notify('hi', urgent=True), 'hi!')",
4203
+ "assrt.equal(notify('hi', urgent=False), 'hi.')",
4204
+ // missing required kwonly → TypeError
4205
+ "caught = False",
4206
+ "try:",
4207
+ " notify('hello')",
4208
+ "except TypeError:",
4209
+ " caught = True",
4210
+ "assrt.ok(caught)",
4211
+ // optional kwonly with default — no error when omitted
4212
+ "def fmt(val, *, prefix=''):",
4213
+ " return prefix + str(val)",
4214
+ "assrt.equal(fmt(42), '42')",
4215
+ "assrt.equal(fmt(42, prefix='>> '), '>> 42')",
4216
+ ].join("\n"));
4217
+ run_js(js);
4218
+ },
4219
+ },
4220
+
4221
+ {
4222
+ name: "bundle_type_enforcement_class",
4223
+ description: "type_enforcement: class method arg enforcement",
4224
+ run: function () {
4225
+ var repl = RS.web_repl();
4226
+ var js = bundle_compile(repl, [
4227
+ "from __python__ import type_enforcement",
4228
+ "class Vec:",
4229
+ " def __init__(self, x: int, y: int):",
4230
+ " self.x = x",
4231
+ " self.y = y",
4232
+ " def scale(self, factor: int, /, *, clamp=False):",
4233
+ " v = self.x * factor",
4234
+ " return min(v, 100) if clamp else v",
4235
+ "v = Vec(3, 4)",
4236
+ "assrt.equal(v.scale(2), 6)",
4237
+ "assrt.equal(v.scale(50, clamp=True), 100)",
4238
+ // type mismatch in __init__
4239
+ "caught = False",
4240
+ "try:",
4241
+ " Vec('a', 1)",
4242
+ "except TypeError:",
4243
+ " caught = True",
4244
+ "assrt.ok(caught)",
4245
+ // factor is posonly → cannot be kwarg
4246
+ "caught2 = False",
4247
+ "try:",
4248
+ " v.scale(factor=2)",
4249
+ "except TypeError:",
4250
+ " caught2 = True",
4251
+ "assrt.ok(caught2)",
4252
+ ].join("\n"));
4253
+ run_js(js);
4254
+ },
4255
+ },
4256
+
4257
+ {
4258
+ name: "bundle_heapq_push_pop",
4259
+ description: "heapq stdlib: heappush and heappop in the web-repl bundle",
4260
+ run: function () {
4261
+ var repl = RS.web_repl();
4262
+ var js = bundle_compile(repl, [
4263
+ "from heapq import heappush, heappop",
4264
+ // push in arbitrary order, pop should return sorted
4265
+ "h = []",
4266
+ "heappush(h, 3)",
4267
+ "heappush(h, 1)",
4268
+ "heappush(h, 4)",
4269
+ "heappush(h, 1)",
4270
+ "heappush(h, 5)",
4271
+ "assrt.equal(heappop(h), 1)",
4272
+ "assrt.equal(heappop(h), 1)",
4273
+ "assrt.equal(heappop(h), 3)",
4274
+ "assrt.equal(heappop(h), 4)",
4275
+ "assrt.equal(heappop(h), 5)",
4276
+ "assrt.equal(h.length, 0)",
4277
+ // heappop on empty raises IndexError
4278
+ "caught = False",
4279
+ "try:",
4280
+ " heappop([])",
4281
+ "except IndexError:",
4282
+ " caught = True",
4283
+ "assrt.ok(caught)",
4284
+ ].join("\n"));
4285
+ run_js(js);
4286
+ },
4287
+ },
4288
+
4289
+ {
4290
+ name: "bundle_heapq_heapify",
4291
+ description: "heapq stdlib: heapify in the web-repl bundle",
4292
+ run: function () {
4293
+ var repl = RS.web_repl();
4294
+ var js = bundle_compile(repl, [
4295
+ "from heapq import heapify, heappop",
4296
+ // heapify puts min at root
4297
+ "x = [5, 3, 8, 1, 2, 4]",
4298
+ "heapify(x)",
4299
+ "assrt.equal(x[0], 1)",
4300
+ // heapify + repeated heappop gives sorted order
4301
+ "data = [5, 3, 8, 1, 2, 4]",
4302
+ "heapify(data)",
4303
+ "result = []",
4304
+ "while data.length > 0:",
4305
+ " result.push(heappop(data))",
4306
+ "assrt.deepEqual(result, [1, 2, 3, 4, 5, 8])",
4307
+ // negative numbers
4308
+ "neg = [-3, -1, -4, -1, -5]",
4309
+ "heapify(neg)",
4310
+ "neg_out = []",
4311
+ "while neg.length > 0:",
4312
+ " neg_out.push(heappop(neg))",
4313
+ "assrt.deepEqual(neg_out, [-5, -4, -3, -1, -1])",
4314
+ ].join("\n"));
4315
+ run_js(js);
4316
+ },
4317
+ },
4318
+
4319
+ {
4320
+ name: "bundle_heapq_nsmallest_nlargest",
4321
+ description: "heapq stdlib: nsmallest and nlargest in the web-repl bundle",
4322
+ run: function () {
4323
+ var repl = RS.web_repl();
4324
+ var js = bundle_compile(repl, [
4325
+ "from heapq import nsmallest, nlargest",
4326
+ "data = [3, 1, 4, 1, 5, 9, 2, 6]",
4327
+ "assrt.deepEqual(nsmallest(3, data), [1, 1, 2])",
4328
+ "assrt.deepEqual(nlargest(3, data), [9, 6, 5])",
4329
+ "assrt.deepEqual(nsmallest(0, data), [])",
4330
+ "assrt.deepEqual(nlargest(0, data), [])",
4331
+ // n > len returns all sorted
4332
+ "assrt.deepEqual(nsmallest(100, [3, 1, 2]), [1, 2, 3])",
4333
+ "assrt.deepEqual(nlargest(100, [3, 1, 2]), [3, 2, 1])",
4334
+ // key function
4335
+ "pairs = [[3, 'c'], [1, 'a'], [4, 'd'], [1, 'b'], [5, 'e']]",
4336
+ "kfn = def(p): return p[0];",
4337
+ "sm = nsmallest(2, pairs, key=kfn)",
4338
+ "assrt.equal(sm[0][0], 1)",
4339
+ "assrt.equal(sm[1][0], 1)",
4340
+ "lg = nlargest(2, pairs, key=kfn)",
4341
+ "assrt.equal(lg[0][0], 5)",
4342
+ "assrt.equal(lg[1][0], 4)",
4343
+ ].join("\n"));
4344
+ run_js(js);
4345
+ },
4346
+ },
4347
+
4348
+ {
4349
+ name: "bundle_heapq_replace_pushpop",
4350
+ description: "heapq stdlib: heapreplace and heappushpop in the web-repl bundle",
4351
+ run: function () {
4352
+ var repl = RS.web_repl();
4353
+ var js = bundle_compile(repl, [
4354
+ "from heapq import heapify, heappop, heapreplace, heappushpop",
4355
+ // heapreplace: returns old min, inserts new item
4356
+ "r = [1, 3, 5, 7, 9]",
4357
+ "heapify(r)",
4358
+ "old = heapreplace(r, 4)",
4359
+ "assrt.equal(old, 1)",
4360
+ "assrt.equal(r[0], 3)",
4361
+ // heapreplace on empty raises IndexError
4362
+ "caught_rep = False",
4363
+ "try:",
4364
+ " heapreplace([], 1)",
4365
+ "except IndexError:",
4366
+ " caught_rep = True",
4367
+ "assrt.ok(caught_rep)",
4368
+ // heappushpop: item > root — returns root
4369
+ "pp = [1, 3, 5]",
4370
+ "heapify(pp)",
4371
+ "assrt.equal(heappushpop(pp, 2), 1)",
4372
+ "assrt.equal(pp[0], 2)",
4373
+ // heappushpop: item <= root — returns item unchanged
4374
+ "pp2 = [5, 7, 9]",
4375
+ "heapify(pp2)",
4376
+ "assrt.equal(heappushpop(pp2, 4), 4)",
4377
+ "assrt.equal(pp2[0], 5)",
4378
+ ].join("\n"));
4379
+ run_js(js);
4380
+ },
4381
+ },
4382
+
4383
+ // ── parenthesized with (Python 3.10+) ────────────────────────────────────
4384
+
4385
+ {
4386
+ name: "bundle_paren_with_single",
4387
+ description: "parenthesized with: single clause with alias compiles and runs in the web-repl bundle",
4388
+ run: function () {
4389
+ var repl = RS.web_repl();
4390
+ var js = bundle_compile(repl, [
4391
+ "class _CM:",
4392
+ " def __init__(self, val):",
4393
+ " self.val = val",
4394
+ " def __enter__(self):",
4395
+ " return self.val",
4396
+ " def __exit__(self):",
4397
+ " pass",
4398
+ "with (_CM(42) as x):",
4399
+ " assrt.equal(x, 42)",
4400
+ ].join("\n"));
4401
+ run_js(js);
4402
+ },
4403
+ },
4404
+
4405
+ {
4406
+ name: "bundle_paren_with_multi",
4407
+ description: "parenthesized with: multi-clause LIFO exit order compiles and runs in the web-repl bundle",
4408
+ run: function () {
4409
+ var repl = RS.web_repl();
4410
+ var js = bundle_compile(repl, [
4411
+ "log = []",
4412
+ "class _CM:",
4413
+ " def __init__(self, name):",
4414
+ " self.name = name",
4415
+ " def __enter__(self):",
4416
+ " log.push('enter:' + self.name)",
4417
+ " return self",
4418
+ " def __exit__(self):",
4419
+ " log.push('exit:' + self.name)",
4420
+ "with (_CM('a') as a, _CM('b') as b):",
4421
+ " log.push('body')",
4422
+ "assrt.equal(log[0], 'enter:a')",
4423
+ "assrt.equal(log[1], 'enter:b')",
4424
+ "assrt.equal(log[2], 'body')",
4425
+ "assrt.equal(log[3], 'exit:b')",
4426
+ "assrt.equal(log[4], 'exit:a')",
4427
+ ].join("\n"));
4428
+ run_js(js);
4429
+ },
4430
+ },
4431
+
4432
+ {
4433
+ name: "bundle_paren_with_multiline",
4434
+ description: "parenthesized with: multi-line trailing-comma form compiles and runs in the web-repl bundle",
4435
+ run: function () {
4436
+ var repl = RS.web_repl();
4437
+ var js = bundle_compile(repl, [
4438
+ "log = []",
4439
+ "class _CM:",
4440
+ " def __init__(self, name):",
4441
+ " self.name = name",
4442
+ " def __enter__(self):",
4443
+ " log.push('enter:' + self.name)",
4444
+ " return self",
4445
+ " def __exit__(self):",
4446
+ " log.push('exit:' + self.name)",
4447
+ "with (",
4448
+ " _CM('x') as x,",
4449
+ " _CM('y') as y,",
4450
+ "):",
4451
+ " log.push('body')",
4452
+ "assrt.equal(log[0], 'enter:x')",
4453
+ "assrt.equal(log[1], 'enter:y')",
4454
+ "assrt.equal(log[2], 'body')",
4455
+ "assrt.equal(log[3], 'exit:y')",
4456
+ "assrt.equal(log[4], 'exit:x')",
4457
+ ].join("\n"));
4458
+ run_js(js);
4459
+ },
4460
+ },
4461
+
4462
+ {
4463
+ name: "bundle_paren_with_trailing_comma",
4464
+ description: "parenthesized with: trailing comma accepted in the web-repl bundle",
4465
+ run: function () {
4466
+ var repl = RS.web_repl();
4467
+ var js = bundle_compile(repl, [
4468
+ "class _CM:",
4469
+ " def __init__(self, val):",
4470
+ " self.val = val",
4471
+ " def __enter__(self):",
4472
+ " return self.val",
4473
+ " def __exit__(self):",
4474
+ " pass",
4475
+ "with (_CM(7) as v,):",
4476
+ " assrt.equal(v, 7)",
4477
+ ].join("\n"));
4478
+ run_js(js);
4479
+ },
4480
+ },
4481
+
4482
+ // ── statistics ───────────────────────────────────────────────────────────
4483
+
4484
+ {
4485
+ name: "bundle_statistics_averages",
4486
+ description: "statistics stdlib: averages and central location in the web-repl bundle",
4487
+ run: function () {
4488
+ var repl = RS.web_repl();
4489
+ var js = bundle_compile(repl, [
4490
+ "from statistics import mean, fmean, median, median_low, median_high",
4491
+ "from statistics import mode, multimode, harmonic_mean, geometric_mean",
4492
+ "assrt.equal(mean([1, 2, 3, 4]), 2.5)",
4493
+ "assrt.equal(mean([1, 2, 3]), 2)",
4494
+ "assrt.equal(fmean([1, 2, 3, 4]), 2.5)",
4495
+ "assrt.equal(median([1, 3, 5]), 3)",
4496
+ "assrt.equal(median([1, 3, 5, 7]), 4)",
4497
+ "assrt.equal(median([5, 1, 3]), 3)", // unsorted input
4498
+ "assrt.equal(median_low([1, 3, 5, 7]), 3)",
4499
+ "assrt.equal(median_high([1, 3, 5, 7]), 5)",
4500
+ "assrt.equal(mode([1, 1, 2, 3, 3, 3]), 3)",
4501
+ "assrt.equal(mode(['x', 'y', 'x']), 'x')",
4502
+ "assrt.deepEqual(multimode([1, 1, 2, 2, 3]), [1, 2])",
4503
+ "assrt.ok(Math.abs(harmonic_mean([40, 60]) - 48.0) < 1e-9)",
4504
+ "assrt.ok(Math.abs(geometric_mean([2, 8]) - 4.0) < 1e-9)",
4505
+ ].join("\n"));
4506
+ run_js(js);
4507
+ },
4508
+ },
4509
+
4510
+ {
4511
+ name: "bundle_statistics_spread",
4512
+ description: "statistics stdlib: variance, stdev and quantiles in the web-repl bundle",
4513
+ run: function () {
4514
+ var repl = RS.web_repl();
4515
+ var js = bundle_compile(repl, [
4516
+ "from statistics import variance, pvariance, stdev, pstdev, quantiles",
4517
+ "assrt.equal(variance([1, 2, 3, 4, 5]), 2.5)",
4518
+ "assrt.equal(pvariance([1, 2, 3, 4, 5]), 2.0)",
4519
+ "assrt.ok(Math.abs(stdev([1, 2, 3, 4, 5]) - Math.sqrt(2.5)) < 1e-9)",
4520
+ "assrt.ok(Math.abs(pstdev([1, 2, 3, 4, 5]) - Math.sqrt(2.0)) < 1e-9)",
4521
+ "assrt.deepEqual(quantiles([1, 2, 3, 4]), [1.25, 2.5, 3.75])",
4522
+ "q = quantiles([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], n=10)",
4523
+ "assrt.equal(q.length, 9)",
4524
+ ].join("\n"));
4525
+ run_js(js);
4526
+ },
4527
+ },
4528
+
4529
+ {
4530
+ name: "bundle_statistics_relations",
4531
+ description: "statistics stdlib: covariance, correlation, linear_regression in the bundle",
4532
+ run: function () {
4533
+ var repl = RS.web_repl();
4534
+ var js = bundle_compile(repl, [
4535
+ "from statistics import covariance, correlation, linear_regression",
4536
+ "x = [1, 2, 3, 4, 5, 6, 7, 8, 9]",
4537
+ "y = [1, 2, 3, 1, 2, 3, 1, 2, 3]",
4538
+ "assrt.ok(Math.abs(covariance(x, y) - 0.75) < 1e-9)",
4539
+ "assrt.ok(Math.abs(correlation(x, x) - 1.0) < 1e-9)",
4540
+ "lr = linear_regression([1, 2, 3, 4, 5], [2, 4, 6, 8, 10])",
4541
+ "assrt.ok(Math.abs(lr.slope - 2.0) < 1e-9)",
4542
+ "assrt.ok(Math.abs(lr.intercept - 0.0) < 1e-9)",
4543
+ ].join("\n"));
4544
+ run_js(js);
4545
+ },
4546
+ },
4547
+
4548
+ {
4549
+ name: "bundle_statistics_normaldist",
4550
+ description: "statistics stdlib: NormalDist in the web-repl bundle",
4551
+ run: function () {
4552
+ var repl = RS.web_repl();
4553
+ var js = bundle_compile(repl, [
4554
+ "from statistics import NormalDist",
4555
+ "nd = NormalDist(100, 15)",
4556
+ "assrt.equal(nd.mean, 100)",
4557
+ "assrt.equal(nd.stdev, 15)",
4558
+ "assrt.equal(nd.variance, 225)",
4559
+ "assrt.ok(Math.abs(nd.cdf(100) - 0.5) < 1e-6)",
4560
+ "assrt.equal(nd.inv_cdf(0.5), 100)",
4561
+ "assrt.equal(nd.zscore(115), 1.0)",
4562
+ "fs = NormalDist.from_samples([1, 2, 3, 4, 5])",
4563
+ "assrt.ok(Math.abs(fs.mean - 3.0) < 1e-9)",
4564
+ // operator overloading dispatches to __add__
4565
+ "combined = nd + NormalDist(50, 20)",
4566
+ "assrt.equal(combined.mean, 150)",
4567
+ "assrt.equal(combined.stdev, 25)",
4568
+ ].join("\n"));
4569
+ run_js(js);
4570
+ },
4571
+ },
4572
+
4573
+ // ── list sort: comparator detection + __lt__ dispatch ────────────────────
4574
+
4575
+ {
4576
+ name: "bundle_sort_comparator_and_lt",
4577
+ description: "list sort: positional comparator and __lt__ dispatch in the web-repl bundle",
4578
+ run: function () {
4579
+ var repl = RS.web_repl();
4580
+ var js = bundle_compile(repl, [
4581
+ // a positional two-argument function is treated as a comparator
4582
+ "assrt.deepEqual(sorted([3, 1, 2, 10], def(x, y): return x - y;), [1, 2, 3, 10])",
4583
+ "m = [5, 2, 8, 1]",
4584
+ "m.sort(def(x, y): return y - x;)",
4585
+ "assrt.deepEqual(m, [8, 5, 2, 1])",
4586
+ // a one-argument function is still a key function
4587
+ "assrt.deepEqual(sorted([3, 1, 2], key=def(x): return -x;), [3, 2, 1])",
4588
+ // custom objects are ordered through their __lt__ method
4589
+ "class Ord:",
4590
+ " def __init__(self, v):",
4591
+ " self.v = v",
4592
+ " def __lt__(self, other):",
4593
+ " return self.v < other.v",
4594
+ "s = sorted([Ord(3), Ord(1), Ord(2)])",
4595
+ "assrt.deepEqual([s[0].v, s[1].v, s[2].v], [1, 2, 3])",
4596
+ ].join("\n"));
4597
+ run_js(js);
4598
+ },
4599
+ },
4600
+
4601
+ ];
4602
+
4603
+ // ---------------------------------------------------------------------------
4604
+ // Runner
4605
+ // ---------------------------------------------------------------------------
4606
+
4607
+ function run_tests(filter) {
4608
+ var tests = filter
4609
+ ? TESTS.filter(function (t) { return t.name === filter; })
4610
+ : TESTS;
2231
4611
 
2232
4612
  if (tests.length === 0) {
2233
4613
  console.error(colored("No test found: " + filter, "red"));
@@ -2245,12 +4625,20 @@ function run_tests(filter) {
2245
4625
  }
2246
4626
  });
2247
4627
 
4628
+ var passed = tests.length - failures.length;
2248
4629
  console.log("");
2249
4630
  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"));
4631
+ console.log(colored("Failed tests:", "red"));
4632
+ failures.forEach(function (name) {
4633
+ console.log(colored(" " + name, "red"));
4634
+ });
4635
+ console.log("");
2253
4636
  }
4637
+ var summary = "web-repl tests — " +
4638
+ colored("passed: " + passed, "green") + " " +
4639
+ (failures.length ? colored("failed: " + failures.length, "red") : colored("failed: 0", "green")) +
4640
+ " total: " + tests.length;
4641
+ console.log(summary);
2254
4642
  process.exit(failures.length ? 1 : 0);
2255
4643
  }
2256
4644