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.
- package/CHANGELOG.md +9 -0
- package/PYTHON_GAPS.md +7 -29
- package/README.md +16 -6
- package/TODO.md +2 -2
- package/language-service/index.js +82 -32
- package/package.json +1 -1
- package/release/baselib-plain-pretty.js +157 -31
- package/release/baselib-plain-ugly.js +5 -5
- package/release/compiler.js +724 -426
- package/release/signatures.json +29 -29
- package/src/ast.pyj +1 -0
- package/src/baselib-containers.pyj +18 -3
- package/src/baselib-errors.pyj +4 -3
- package/src/baselib-internal.pyj +47 -18
- package/src/baselib-str.pyj +3 -1
- package/src/monaco-language-service/completions.js +21 -14
- package/src/monaco-language-service/dts.js +593 -550
- package/src/monaco-language-service/package.json +3 -0
- package/src/output/classes.pyj +25 -2
- package/src/parse.pyj +14 -0
- package/test/chainmap.pyj +1 -1
- package/test/dataclasses.pyj +3 -4
- package/test/enum.pyj +1 -1
- package/test/pprint.pyj +15 -15
- package/test/python_features.pyj +1 -1
- package/test/str.pyj +4 -4
- package/test/unit/index.js +375 -0
- package/test/unit/language-service-dts.js +656 -543
- package/test/unit/language-service.js +72 -0
- package/test/unit/web-repl.js +33 -8
- package/tools/utils.js +15 -2
- package/web-repl/rapydscript.js +3 -3
package/src/output/classes.pyj
CHANGED
|
@@ -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__(),
|
|
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
|
|
package/test/dataclasses.pyj
CHANGED
|
@@ -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),
|
|
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(
|
|
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),
|
|
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),
|
|
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'), '
|
|
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}), '
|
|
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, '
|
|
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), '
|
|
63
|
-
ae(pformat(_d, sort_dicts=True), '
|
|
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), '
|
|
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}), '
|
|
100
|
-
ae(saferepr('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
|
-
'
|
|
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(), '
|
|
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'}), '
|
|
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'), '
|
|
221
|
-
ae(pformat('"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(), '
|
|
232
|
+
ae(_col3.getvalue(), "{'b': 2, 'a': 1}\n")
|
package/test/python_features.pyj
CHANGED
|
@@ -523,7 +523,7 @@ def _test_raise_from_none():
|
|
|
523
523
|
_test_raise_from_none()
|
|
524
524
|
|
|
525
525
|
# ── 23. __slots__ ────────────────────────────────────────────────────────────
|
|
526
|
-
# STATUS:
|
|
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']),
|
|
200
|
-
ae(f({1:[1, '2']}), '
|
|
201
|
-
ae(f({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'), '
|
|
203
|
+
ae(repr('a'), "'a'")
|
|
204
204
|
|
|
205
205
|
bytes = list(range(256))
|
|
206
206
|
assrt.deepEqual(bytes, list(encodings.base64decode(encodings.base64encode(bytes))))
|
package/test/unit/index.js
CHANGED
|
@@ -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 ───────────────────────────────────────────────────────────────────
|