rapydscript-ns 0.9.4 → 0.9.5

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -125,8 +125,8 @@ def print_class(output):
125
125
  output.indent()
126
126
  output.spaced('if', '(this.ρσ_object_id', '===', 'undefined)', 'Object.defineProperty(this,', '"ρσ_object_id",', '{"value":++ρσ_object_counter})')
127
127
  output.end_statement()
128
- if self.has_attr_dunders:
129
- # Wrap in a Proxy so __getattr__/__setattr__/__delattr__/__getattribute__ are triggered.
128
+ if self.has_attr_dunders or self.slots:
129
+ # Wrap in a Proxy so __getattr__/__setattr__/__delattr__/__getattribute__/__slots__ are triggered.
130
130
  output.indent()
131
131
  output.print('var ρσ_proxy = ρσ_JS_Proxy ? new ρσ_JS_Proxy(this, ρσ_attr_proxy_handler) : this')
132
132
  output.end_statement()
@@ -330,6 +330,29 @@ def print_class(output):
330
330
  output.print('.prototype, "__class__", {get: function() { return this.constructor; }, configurable: true})')
331
331
  output.end_statement()
332
332
 
333
+ # __slots__ enforcement: emit allowed attribute set on prototype
334
+ if self.slots:
335
+ output.indent()
336
+ self.name.print(output)
337
+ # Merge parent slots (if any) with this class's slots + bound methods
338
+ slot_obj = '{'
339
+ for i in range(self.slots.length):
340
+ if i > 0:
341
+ slot_obj += ', '
342
+ slot_obj += JSON.stringify(self.slots[i]) + ': true'
343
+ for i in range(self.bound.length):
344
+ if self.slots.length or i > 0:
345
+ slot_obj += ', '
346
+ slot_obj += JSON.stringify(self.bound[i]) + ': true'
347
+ slot_obj += '}'
348
+ if self.parent:
349
+ output.print('.prototype.__ρσ_slots__ = Object.assign({}, ')
350
+ self.parent.print(output)
351
+ output.print('.prototype.__ρσ_slots__, ' + slot_obj + ')')
352
+ else:
353
+ output.print('.prototype.__ρσ_slots__ = Object.assign({}, ' + slot_obj + ')')
354
+ output.end_statement()
355
+
333
356
  # Other statements in the class context (including nested class definitions).
334
357
  # Emitted BEFORE __init_subclass__ so that the hook can see class variables.
335
358
  # This matches Python's behaviour: the class body executes first, then the
package/src/parse.pyj CHANGED
@@ -1622,6 +1622,20 @@ def create_parser_ctx(S, import_dirs, module_id, baselib_items, imported_module_
1622
1622
  if parent_details and parent_details.has_attr_dunders:
1623
1623
  definition.has_attr_dunders = True
1624
1624
  class_details.has_attr_dunders = True
1625
+ # Detect __slots__ = [...] in class body
1626
+ for stmt in definition.body:
1627
+ assign = stmt
1628
+ if is_node_type(stmt, AST_SimpleStatement):
1629
+ assign = stmt.body
1630
+ if is_node_type(assign, AST_Assign) and is_node_type(assign.left, AST_SymbolRef) and assign.left.name is '__slots__':
1631
+ if is_node_type(assign.right, AST_Array):
1632
+ slot_names = []
1633
+ for elem in assign.right.elements:
1634
+ if is_node_type(elem, AST_String):
1635
+ slot_names.push(elem.value)
1636
+ definition.slots = slot_names
1637
+ class_details.slots = slot_names
1638
+ break
1625
1639
  # find the class variables
1626
1640
  class_var_names = {}
1627
1641
  # Ensure that if a class variable refers to another class variable in
package/test/chainmap.pyj CHANGED
@@ -170,7 +170,7 @@ ok(not (ChainMap({'a': 1}) == {'a': 1, 'b': 2}))
170
170
  # ── 11. repr ──────────────────────────────────────────────────────────────────
171
171
 
172
172
  # RapydScript's repr() quotes strings with double quotes (as Counter/OrderedDict do)
173
- ae(ChainMap({'a': 1}, {'b': 2}).__repr__(), 'ChainMap({"a": 1}, {"b": 2})')
173
+ ae(ChainMap({'a': 1}, {'b': 2}).__repr__(), "ChainMap({'a': 1}, {'b': 2})")
174
174
 
175
175
  # ── 12. dynamic — changes to underlying maps are visible ──────────────────────
176
176
 
@@ -76,7 +76,7 @@ class _Hidden:
76
76
  secret: str = field('s3cr3t', repr=False)
77
77
 
78
78
  h = _Hidden("visible")
79
- ae(repr(h), '_Hidden(public="visible")')
79
+ ae(repr(h), "_Hidden(public='visible')")
80
80
  ae(h.secret, 's3cr3t')
81
81
 
82
82
  # ── 6. field() init=False ─────────────────────────────────────────────────────
@@ -209,11 +209,10 @@ ae(ch2.y, 0)
209
209
  ae(ch2.z, 'default')
210
210
 
211
211
  # repr shows all fields including inherited
212
- # (RapydScript repr() uses double quotes for strings, like JavaScript)
213
212
  r = repr(ch2)
214
213
  ok('x=10' in r)
215
214
  ok('y=0' in r)
216
- ok('z="default"' in r)
215
+ ok("z='default'" in r)
217
216
 
218
217
  # ── 15. MISSING sentinel ──────────────────────────────────────────────────────
219
218
 
@@ -240,7 +239,7 @@ class _Explicit:
240
239
 
241
240
  ex = _Explicit()
242
241
  ae(ex.name, 'x')
243
- ae(repr(ex), '_Explicit(name="x")')
242
+ ae(repr(ex), "_Explicit(name='x')")
244
243
 
245
244
  # ── 18. Nested list field with asdict ────────────────────────────────────────
246
245
 
package/test/enum.pyj CHANGED
@@ -94,7 +94,7 @@ ae(_Direction.NORTH.name, 'NORTH')
94
94
  ae(_Direction.NORTH.value, 'north')
95
95
  ae(_Direction.SOUTH.value, 'south')
96
96
  # RapydScript repr() wraps strings in double quotes (JS convention)
97
- ae(repr(_Direction.NORTH), '<_Direction.NORTH: "north">')
97
+ ae(repr(_Direction.NORTH), "<_Direction.NORTH: 'north'>")
98
98
  ae(str(_Direction.EAST), '_Direction.EAST')
99
99
  ae(len(list(_Direction)), 4)
100
100
 
package/test/pprint.pyj CHANGED
@@ -25,8 +25,8 @@ ae(pformat(True), 'True')
25
25
  ae(pformat(False), 'False')
26
26
  ae(pformat(42), '42')
27
27
  ae(pformat(3.14), '3.14')
28
- ae(pformat('hello'), '"hello"')
29
- ae(pformat(''), '""')
28
+ ae(pformat('hello'), "'hello'")
29
+ ae(pformat(''), "''")
30
30
 
31
31
  # ── 2. Empty containers ─────────────────────────────────────────────────────
32
32
 
@@ -38,7 +38,7 @@ ae(pformat(frozenset()), 'frozenset()')
38
38
  # ── 3. Short containers fit on a single line ────────────────────────────────
39
39
 
40
40
  ae(pformat([1, 2, 3]), '[1, 2, 3]')
41
- ae(pformat({'a': 1, 'b': 2}), '{"a": 1, "b": 2}')
41
+ ae(pformat({'a': 1, 'b': 2}), "{'a': 1, 'b': 2}")
42
42
  ae(pformat({1, 2, 3}), '{1, 2, 3}')
43
43
  ae(pformat(frozenset([1, 2, 3])), 'frozenset({1, 2, 3})')
44
44
 
@@ -50,7 +50,7 @@ ae(_wide, '[1,\n 2,\n 3,\n 4,\n 5]')
50
50
  # ── 5. Wide dict breaks across lines, keys sorted by default ────────────────
51
51
 
52
52
  _wd = pformat({'name': 'Alice', 'age': 30}, width=15)
53
- ae(_wd, '{"age": 30,\n "name": "Alice"}')
53
+ ae(_wd, "{'age': 30,\n 'name': 'Alice'}")
54
54
 
55
55
  # ── 6. sort_dicts=False preserves insertion order ───────────────────────────
56
56
 
@@ -59,8 +59,8 @@ _d = dict()
59
59
  _d.set('c', 1)
60
60
  _d.set('a', 2)
61
61
  _d.set('b', 3)
62
- ae(pformat(_d, sort_dicts=False), '{"c": 1, "a": 2, "b": 3}')
63
- ae(pformat(_d, sort_dicts=True), '{"a": 2, "b": 3, "c": 1}')
62
+ ae(pformat(_d, sort_dicts=False), "{'c': 1, 'a': 2, 'b': 3}")
63
+ ae(pformat(_d, sort_dicts=True), "{'a': 2, 'b': 3, 'c': 1}")
64
64
 
65
65
  # ── 7. pp() defaults sort_dicts=False, has same parameters as pprint ────────
66
66
 
@@ -71,7 +71,7 @@ pp({'c': 1, 'a': 2})
71
71
 
72
72
  ae(pformat([1, [2, [3, [4]]]], depth=1), '[1, [...]]')
73
73
  ae(pformat([1, [2, [3, [4]]]], depth=2), '[1, [2, [...]]]')
74
- ae(pformat({'a': {'b': {'c': 1}}}, depth=1), '{"a": {...}}')
74
+ ae(pformat({'a': {'b': {'c': 1}}}, depth=1), "{'a': {...}}")
75
75
 
76
76
  # ── 9. Nested containers — multi-line indentation ───────────────────────────
77
77
 
@@ -96,8 +96,8 @@ ade(_c.split('\n'), ['[1, 2, 3, 4, 5,', ' 6, 7, 8]'])
96
96
  # ── 12. saferepr — single-line repr; recursive marker for cycles ────────────
97
97
 
98
98
  ae(saferepr([1, 2, 3]), '[1, 2, 3]')
99
- ae(saferepr({'a': 1}), '{"a": 1}')
100
- ae(saferepr('text'), '"text"')
99
+ ae(saferepr({'a': 1}), "{'a': 1}")
100
+ ae(saferepr('text'), "'text'")
101
101
 
102
102
  # Self-referential list -> recursive marker, not infinite recursion
103
103
  _self_list = [1, 2]
@@ -175,7 +175,7 @@ _data = {
175
175
 
176
176
  # Width permits inline lists.
177
177
  ae(pformat(_data, width=120),
178
- '{"count": 2, "users": [{"age": 30, "name": "Alice"}, {"age": 25, "name": "Bob"}]}')
178
+ "{'count': 2, 'users': [{'age': 30, 'name': 'Alice'}, {'age': 25, 'name': 'Bob'}]}")
179
179
 
180
180
  # Narrow width forces multi-line expansion.
181
181
  _narrow = pformat(_data, width=30)
@@ -202,12 +202,12 @@ ae(_col.getvalue(), '[1, 2, 3]\n')
202
202
 
203
203
  _col2 = _Collector()
204
204
  pprint({'a': 1}, stream=_col2)
205
- ae(_col2.getvalue(), '{"a": 1}\n')
205
+ ae(_col2.getvalue(), "{'a': 1}\n")
206
206
 
207
207
  # ── 21. Empty / single-element containers do not break ──────────────────────
208
208
 
209
209
  ae(pformat([42]), '[42]')
210
- ae(pformat({'k': 'v'}), '{"k": "v"}')
210
+ ae(pformat({'k': 'v'}), "{'k': 'v'}")
211
211
  ae(pformat([], width=1), '[]')
212
212
  ae(pformat({}, width=1), '{}')
213
213
 
@@ -217,8 +217,8 @@ ae(pformat([True, False, None, 0]), '[True, False, None, 0]')
217
217
 
218
218
  # ── 23. Strings with special characters get repr-escaped ────────────────────
219
219
 
220
- ae(pformat('a\nb'), '"a\\nb"')
221
- ae(pformat('"quoted"'), '"\\"quoted\\""')
220
+ ae(pformat('a\nb'), "'a\\nb'")
221
+ ae(pformat('"quoted"'), '\'"quoted"\'')
222
222
 
223
223
  # ── 24. underscore_numbers parameter is accepted (currently ignored) ────────
224
224
 
@@ -229,4 +229,4 @@ ae(pformat(1000000, underscore_numbers=True), '1000000')
229
229
  _col3 = _Collector()
230
230
  pp({'b': 2, 'a': 1}, stream=_col3, width=80)
231
231
  # pp defaults sort_dicts=False, so 'b' comes before 'a' (insertion order)
232
- ae(_col3.getvalue(), '{"b": 2, "a": 1}\n')
232
+ ae(_col3.getvalue(), "{'b': 2, 'a': 1}\n")
@@ -523,7 +523,7 @@ def _test_raise_from_none():
523
523
  _test_raise_from_none()
524
524
 
525
525
  # ── 23. __slots__ ────────────────────────────────────────────────────────────
526
- # STATUS: NOT ENFORCED — __slots__ is accepted but does not restrict attrs.
526
+ # STATUS: WORKS — __slots__ enforced via Proxy; raises AttributeError for undeclared attrs.
527
527
 
528
528
  # ── 24. Nested classes ───────────────────────────────────────────────────────
529
529
  # STATUS: ✓ WORKS — added in commit 44c9802; tested fully in test/classes.pyj
package/test/str.pyj CHANGED
@@ -196,11 +196,11 @@ for f in (str, repr):
196
196
  ae(f(False), 'False')
197
197
  ae(f(None), 'None')
198
198
  ae(f(1), '1')
199
- ae(f([1,'2']), '[1, "2"]')
200
- ae(f({1:[1, '2']}), '{"1":[1, "2"]}')
201
- ae(f({1:'a', 2:'b'}), '{"1":"a", "2":"b"}')
199
+ ae(f([1,'2']), "[1, '2']")
200
+ ae(f({1:[1, '2']}), "{'1': [1, '2']}")
201
+ ae(f({1:'a', 2:'b'}), "{'1': 'a', '2': 'b'}")
202
202
  ae(str('a'), 'a')
203
- ae(repr('a'), '"a"')
203
+ ae(repr('a'), "'a'")
204
204
 
205
205
  bytes = list(range(256))
206
206
  assrt.deepEqual(bytes, list(encodings.base64decode(encodings.base64encode(bytes))))
@@ -2996,6 +2996,96 @@ assrt.equal(fib(15), 610)
2996
2996
  js_checks: ["ρσ_op_add(", "ρσ_op_sub(", "ρσ_op_or(", "ρσ_op_and("],
2997
2997
  },
2998
2998
 
2999
+ {
3000
+ name: "operator_dict_or",
3001
+ description: "| on plain objects creates a new merged dict; numeric | still works",
3002
+ src: [
3003
+ "# globals: assrt",
3004
+ "from __python__ import overload_operators",
3005
+ "a = {'x': 1, 'y': 2}",
3006
+ "b = {'z': 3}",
3007
+ "c = a | b",
3008
+ "assrt.deepEqual(c, {'x': 1, 'y': 2, 'z': 3})",
3009
+ "assrt.notStrictEqual(c, a)",
3010
+ "assrt.equal(5 | 3, 7)",
3011
+ ].join("\n"),
3012
+ },
3013
+
3014
+ {
3015
+ name: "operator_dict_ior",
3016
+ description: "|= on plain objects merges in-place",
3017
+ src: [
3018
+ "# globals: assrt",
3019
+ "from __python__ import overload_operators",
3020
+ "d = {'a': 1}",
3021
+ "ref = d",
3022
+ "d |= {'b': 2}",
3023
+ "assrt.deepEqual(d, {'a': 1, 'b': 2})",
3024
+ "assrt.strictEqual(d, ref)",
3025
+ ].join("\n"),
3026
+ },
3027
+
3028
+ {
3029
+ name: "operator_dict_or_override",
3030
+ description: "| on plain objects: right-hand values override left on key collision",
3031
+ src: [
3032
+ "# globals: assrt",
3033
+ "from __python__ import overload_operators",
3034
+ "a = {'x': 1, 'y': 2}",
3035
+ "b = {'y': 99, 'z': 3}",
3036
+ "c = a | b",
3037
+ "assrt.equal(c['y'], 99)",
3038
+ "assrt.equal(c['x'], 1)",
3039
+ "assrt.equal(c['z'], 3)",
3040
+ ].join("\n"),
3041
+ },
3042
+
3043
+ {
3044
+ name: "operator_type_error_bitwise",
3045
+ description: "bitwise operators raise TypeError for invalid types",
3046
+ src: [
3047
+ "# globals: assrt",
3048
+ "from __python__ import overload_operators",
3049
+ "def check(fn, msg):",
3050
+ " try:",
3051
+ " fn()",
3052
+ " assrt.ok(False, 'expected TypeError for ' + msg)",
3053
+ " except TypeError:",
3054
+ " assrt.ok(True)",
3055
+ 'check(def(): "str" & 5;, "str & int")',
3056
+ 'check(def(): None | 5;, "None | int")',
3057
+ 'check(def(): "a" ^ 1;, "str ^ int")',
3058
+ 'check(def(): "a" << 1;, "str << int")',
3059
+ 'check(def(): "a" >> 1;, "str >> int")',
3060
+ "assrt.equal(7 & 3, 3)",
3061
+ "assrt.equal(1 | 2, 3)",
3062
+ "assrt.equal(5 ^ 3, 6)",
3063
+ "assrt.equal(1 << 3, 8)",
3064
+ "assrt.equal(8 >> 2, 2)",
3065
+ ].join("\n"),
3066
+ },
3067
+
3068
+ {
3069
+ name: "operator_type_error_unary",
3070
+ description: "unary operators raise TypeError for invalid types",
3071
+ src: [
3072
+ "# globals: assrt",
3073
+ "from __python__ import overload_operators",
3074
+ "def check(fn, msg):",
3075
+ " try:",
3076
+ " fn()",
3077
+ " assrt.ok(False, 'expected TypeError for ' + msg)",
3078
+ " except TypeError:",
3079
+ " assrt.ok(True)",
3080
+ 'check(def(): -"hello";, "neg str")',
3081
+ 'check(def(): +None;, "pos None")',
3082
+ 'check(def(): ~"world";, "invert str")',
3083
+ "assrt.equal(-5, -5)",
3084
+ "assrt.equal(+3, 3)",
3085
+ "assrt.equal(~0, -1)",
3086
+ ].join("\n"),
3087
+ },
3088
+
2999
3089
  // ── nested comprehensions ──────────────────────────────────────────────
3000
3090
 
3001
3091
  {
@@ -5373,6 +5463,291 @@ assrt.equal(fib(15), 610)
5373
5463
  ].join("\n"),
5374
5464
  },
5375
5465
 
5466
+ // ── Exception.args ───────────────────────────────────────────────────────
5467
+ {
5468
+ name: "exception_args_single",
5469
+ description: "Exception with single arg populates .args and .message",
5470
+ src: [
5471
+ "# globals: assrt",
5472
+ "e = Exception('hello')",
5473
+ "assrt.deepEqual(e.args, ['hello'])",
5474
+ "assrt.equal(e.message, 'hello')",
5475
+ ].join("\n"),
5476
+ },
5477
+ {
5478
+ name: "exception_args_multiple",
5479
+ description: "Exception with multiple args populates .args tuple",
5480
+ src: [
5481
+ "# globals: assrt",
5482
+ "e = Exception('err', 42, 'extra')",
5483
+ "assrt.deepEqual(e.args, ['err', 42, 'extra'])",
5484
+ "assrt.equal(e.message, 'err')",
5485
+ "assrt.equal(e.args[1], 42)",
5486
+ "assrt.equal(e.args[2], 'extra')",
5487
+ ].join("\n"),
5488
+ },
5489
+ {
5490
+ name: "exception_args_empty",
5491
+ description: "Exception with no args has empty .args and empty .message",
5492
+ src: [
5493
+ "# globals: assrt",
5494
+ "e = Exception()",
5495
+ "assrt.deepEqual(e.args, [])",
5496
+ "assrt.equal(e.message, '')",
5497
+ ].join("\n"),
5498
+ },
5499
+ {
5500
+ name: "exception_args_subclass",
5501
+ description: "Exception subclass inherits .args behavior",
5502
+ src: [
5503
+ "# globals: assrt",
5504
+ "e = ValueError('bad', 'value')",
5505
+ "assrt.deepEqual(e.args, ['bad', 'value'])",
5506
+ "assrt.equal(e.message, 'bad')",
5507
+ "assrt.ok(isinstance(e, ValueError))",
5508
+ "assrt.ok(isinstance(e, Exception))",
5509
+ ].join("\n"),
5510
+ },
5511
+ {
5512
+ name: "exception_args_catch",
5513
+ description: "Caught exception preserves .args",
5514
+ src: [
5515
+ "# globals: assrt",
5516
+ "try:",
5517
+ " raise ValueError('oops', 123)",
5518
+ "except ValueError as e:",
5519
+ " assrt.deepEqual(e.args, ['oops', 123])",
5520
+ " assrt.equal(e.message, 'oops')",
5521
+ " assrt.equal(e.args[1], 123)",
5522
+ ].join("\n"),
5523
+ },
5524
+ {
5525
+ name: "exception_args_len",
5526
+ description: "len() works on exception .args",
5527
+ src: [
5528
+ "# globals: assrt",
5529
+ "e = Exception('a', 'b', 'c')",
5530
+ "assrt.equal(len(e.args), 3)",
5531
+ "e2 = Exception()",
5532
+ "assrt.equal(len(e2.args), 0)",
5533
+ ].join("\n"),
5534
+ },
5535
+ {
5536
+ name: "exception_args_exception_group",
5537
+ description: "ExceptionGroup .args contains message and exceptions list",
5538
+ src: [
5539
+ "# globals: assrt",
5540
+ "eg = ExceptionGroup('grp', [ValueError('v')])",
5541
+ "assrt.equal(eg.args[0], 'grp')",
5542
+ "assrt.equal(len(eg.args), 2)",
5543
+ "assrt.equal(eg.message, 'grp')",
5544
+ "assrt.ok(isinstance(eg.args[1][0], ValueError))",
5545
+ ].join("\n"),
5546
+ },
5547
+ {
5548
+ name: "exception_args_custom_class",
5549
+ description: "Custom exception class with __init__ can use .args",
5550
+ src: [
5551
+ "# globals: assrt",
5552
+ "class MyError(Exception):",
5553
+ " def __init__(self, code, detail):",
5554
+ " Exception.__init__(self, code, detail)",
5555
+ " self.code = code",
5556
+ " self.detail = detail",
5557
+ "e = MyError(404, 'not found')",
5558
+ "assrt.deepEqual(e.args, [404, 'not found'])",
5559
+ "assrt.equal(e.code, 404)",
5560
+ "assrt.equal(e.detail, 'not found')",
5561
+ "assrt.equal(e.message, 404)",
5562
+ ].join("\n"),
5563
+ },
5564
+
5565
+ // ── __slots__ ─────────────────────────────────────────────────────────────
5566
+ {
5567
+ name: "slots_basic",
5568
+ description: "__slots__ allows declared attributes",
5569
+ src: [
5570
+ "# globals: assrt",
5571
+ "class Point:",
5572
+ " __slots__ = ['x', 'y']",
5573
+ " def __init__(self, x, y):",
5574
+ " self.x = x",
5575
+ " self.y = y",
5576
+ "p = Point(1, 2)",
5577
+ "assrt.equal(p.x, 1)",
5578
+ "assrt.equal(p.y, 2)",
5579
+ "p.x = 10",
5580
+ "assrt.equal(p.x, 10)",
5581
+ ].join("\n"),
5582
+ },
5583
+ {
5584
+ name: "slots_raises_attributeerror",
5585
+ description: "__slots__ raises AttributeError for undeclared attrs",
5586
+ src: [
5587
+ "# globals: assrt",
5588
+ "class Point:",
5589
+ " __slots__ = ['x', 'y']",
5590
+ " def __init__(self, x, y):",
5591
+ " self.x = x",
5592
+ " self.y = y",
5593
+ "p = Point(1, 2)",
5594
+ "try:",
5595
+ " p.z = 3",
5596
+ " assrt.ok(False, 'should have raised')",
5597
+ "except AttributeError as e:",
5598
+ " assrt.ok(str(e).indexOf('z') != -1)",
5599
+ ].join("\n"),
5600
+ },
5601
+ {
5602
+ name: "slots_inherited",
5603
+ description: "Subclass with __slots__ merges parent slots",
5604
+ src: [
5605
+ "# globals: assrt",
5606
+ "class Base:",
5607
+ " __slots__ = ['x']",
5608
+ " def __init__(self):",
5609
+ " self.x = 1",
5610
+ "class Child(Base):",
5611
+ " __slots__ = ['y']",
5612
+ " def __init__(self):",
5613
+ " Base.__init__(self)",
5614
+ " self.y = 2",
5615
+ "c = Child()",
5616
+ "assrt.equal(c.x, 1)",
5617
+ "assrt.equal(c.y, 2)",
5618
+ "try:",
5619
+ " c.z = 3",
5620
+ " assrt.ok(False, 'should have raised')",
5621
+ "except AttributeError:",
5622
+ " assrt.ok(True)",
5623
+ ].join("\n"),
5624
+ },
5625
+ {
5626
+ name: "slots_subclass_no_slots",
5627
+ description: "Subclass without __slots__ is unrestricted",
5628
+ src: [
5629
+ "# globals: assrt",
5630
+ "class Base:",
5631
+ " __slots__ = ['x']",
5632
+ " def __init__(self):",
5633
+ " self.x = 1",
5634
+ "class Child(Base):",
5635
+ " def __init__(self):",
5636
+ " Base.__init__(self)",
5637
+ " self.y = 2",
5638
+ "c = Child()",
5639
+ "assrt.equal(c.x, 1)",
5640
+ "assrt.equal(c.y, 2)",
5641
+ "c.z = 3",
5642
+ "assrt.equal(c.z, 3)",
5643
+ ].join("\n"),
5644
+ },
5645
+ {
5646
+ name: "slots_empty",
5647
+ description: "__slots__ = [] allows no instance attributes",
5648
+ src: [
5649
+ "# globals: assrt",
5650
+ "class Empty:",
5651
+ " __slots__ = []",
5652
+ " pass",
5653
+ "e = Empty()",
5654
+ "try:",
5655
+ " e.x = 1",
5656
+ " assrt.ok(False, 'should have raised')",
5657
+ "except AttributeError:",
5658
+ " assrt.ok(True)",
5659
+ ].join("\n"),
5660
+ },
5661
+ {
5662
+ name: "slots_with_attr_dunders",
5663
+ description: "__setattr__ takes priority over __slots__",
5664
+ src: [
5665
+ "# globals: assrt",
5666
+ "class Tracked:",
5667
+ " __slots__ = ['x']",
5668
+ " def __init__(self):",
5669
+ " object.__setattr__(self, 'log', [])",
5670
+ " self.x = 1",
5671
+ " def __setattr__(self, name, value):",
5672
+ " self.log.append(name)",
5673
+ " object.__setattr__(self, name, value)",
5674
+ "t = Tracked()",
5675
+ "assrt.equal(t.x, 1)",
5676
+ "assrt.ok(t.log.length > 0)",
5677
+ ].join("\n"),
5678
+ },
5679
+
5680
+ // ── Container pretty-printing ─────────────────────────────────────────────
5681
+
5682
+ {
5683
+ name: "repr_list_pretty",
5684
+ description: "repr/str of lists uses repr on elements (quotes strings, shows None/True)",
5685
+ src: [
5686
+ "# globals: assrt",
5687
+ "assrt.equal(repr([1, 'hello', None, True]), \"[1, 'hello', None, True]\")",
5688
+ "assrt.equal(str([1, 'hello', None, True]), \"[1, 'hello', None, True]\")",
5689
+ "assrt.equal(repr([]), '[]')",
5690
+ "assrt.equal(str([]), '[]')",
5691
+ ].join("\n"),
5692
+ },
5693
+
5694
+ {
5695
+ name: "repr_dict_pretty",
5696
+ description: "repr/str of dicts uses single-quoted keys and values",
5697
+ src: [
5698
+ "# globals: assrt",
5699
+ "d = dict()",
5700
+ "d.set('key', 'val')",
5701
+ "assrt.equal(repr(d), \"{'key': 'val'}\")",
5702
+ "assrt.equal(str(d), \"{'key': 'val'}\")",
5703
+ ].join("\n"),
5704
+ },
5705
+
5706
+ {
5707
+ name: "repr_set_pretty",
5708
+ description: "repr/str of sets uses repr on elements",
5709
+ src: [
5710
+ "# globals: assrt",
5711
+ "s = {1, 'two'}",
5712
+ "r = repr(s)",
5713
+ "assrt.ok(r.indexOf(\"'two'\") >= 0, 'set repr should quote strings')",
5714
+ "assrt.ok(r.indexOf('1') >= 0, 'set repr should include numbers')",
5715
+ "assrt.ok(r[0] is '{' and r[r.length-1] is '}', 'set repr uses braces')",
5716
+ ].join("\n"),
5717
+ },
5718
+
5719
+ {
5720
+ name: "repr_nested_containers",
5721
+ description: "repr handles nested containers correctly",
5722
+ src: [
5723
+ "# globals: assrt",
5724
+ "assrt.equal(repr([[1], [2, 3]]), '[[1], [2, 3]]')",
5725
+ "assrt.equal(repr([None, True, False]), '[None, True, False]')",
5726
+ ].join("\n"),
5727
+ },
5728
+
5729
+ {
5730
+ name: "repr_string_single_quotes",
5731
+ description: "repr of strings uses Python-style single quotes",
5732
+ src: [
5733
+ "# globals: assrt",
5734
+ "assrt.equal(repr('hello'), \"'hello'\")",
5735
+ "assrt.equal(repr(''), \"''\")",
5736
+ ].join("\n"),
5737
+ },
5738
+
5739
+ {
5740
+ name: "repr_frozenset_pretty",
5741
+ description: "repr/str of frozenset uses repr on elements",
5742
+ src: [
5743
+ "# globals: assrt",
5744
+ "fs = frozenset([1, 'x'])",
5745
+ "r = repr(fs)",
5746
+ "assrt.ok(r.indexOf('frozenset(') is 0, 'starts with frozenset(')",
5747
+ "assrt.ok(r.indexOf(\"'x'\") >= 0, 'frozenset repr should quote strings')",
5748
+ ].join("\n"),
5749
+ },
5750
+
5376
5751
  ];
5377
5752
 
5378
5753
  // ── Runner ───────────────────────────────────────────────────────────────────