rapydscript-ns 0.8.4 → 0.9.1

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 (141) hide show
  1. package/.agignore +1 -1
  2. package/.github/workflows/ci.yml +38 -38
  3. package/=template.pyj +5 -5
  4. package/CHANGELOG.md +26 -0
  5. package/HACKING.md +103 -103
  6. package/LICENSE +24 -24
  7. package/README.md +716 -169
  8. package/TODO.md +7 -2
  9. package/add-toc-to-readme +2 -2
  10. package/bin/export +75 -75
  11. package/bin/rapydscript +70 -70
  12. package/bin/web-repl-export +102 -102
  13. package/build +2 -2
  14. package/language-service/index.js +36 -27
  15. package/package.json +1 -1
  16. package/publish.py +37 -37
  17. package/release/baselib-plain-pretty.js +2358 -168
  18. package/release/baselib-plain-ugly.js +73 -3
  19. package/release/compiler.js +6283 -3093
  20. package/release/signatures.json +31 -30
  21. package/session.vim +4 -4
  22. package/setup.cfg +2 -2
  23. package/src/ast.pyj +1 -0
  24. package/src/baselib-builtins.pyj +340 -2
  25. package/src/baselib-bytes.pyj +664 -0
  26. package/src/baselib-errors.pyj +1 -1
  27. package/src/baselib-internal.pyj +267 -60
  28. package/src/baselib-itertools.pyj +110 -97
  29. package/src/baselib-str.pyj +22 -4
  30. package/src/compiler.pyj +36 -36
  31. package/src/errors.pyj +30 -30
  32. package/src/lib/abc.pyj +317 -0
  33. package/src/lib/aes.pyj +646 -646
  34. package/src/lib/contextlib.pyj +379 -0
  35. package/src/lib/copy.pyj +120 -120
  36. package/src/lib/dataclasses.pyj +532 -0
  37. package/src/lib/datetime.pyj +712 -0
  38. package/src/lib/elementmaker.pyj +83 -83
  39. package/src/lib/encodings.pyj +126 -126
  40. package/src/lib/enum.pyj +125 -0
  41. package/src/lib/gettext.pyj +569 -569
  42. package/src/lib/io.pyj +500 -0
  43. package/src/lib/itertools.pyj +580 -580
  44. package/src/lib/json.pyj +227 -0
  45. package/src/lib/math.pyj +193 -193
  46. package/src/lib/operator.pyj +11 -11
  47. package/src/lib/pythonize.pyj +20 -20
  48. package/src/lib/random.pyj +118 -118
  49. package/src/lib/re.pyj +504 -470
  50. package/src/lib/react.pyj +74 -74
  51. package/src/lib/traceback.pyj +63 -63
  52. package/src/lib/typing.pyj +577 -0
  53. package/src/lib/uuid.pyj +77 -77
  54. package/src/monaco-language-service/builtins.js +14 -4
  55. package/src/monaco-language-service/diagnostics.js +19 -20
  56. package/src/monaco-language-service/dts.js +550 -550
  57. package/src/output/classes.pyj +62 -26
  58. package/src/output/comments.pyj +45 -45
  59. package/src/output/exceptions.pyj +201 -201
  60. package/src/output/functions.pyj +78 -5
  61. package/src/output/jsx.pyj +164 -164
  62. package/src/output/loops.pyj +5 -2
  63. package/src/output/operators.pyj +100 -34
  64. package/src/output/treeshake.pyj +182 -182
  65. package/src/output/utils.pyj +72 -72
  66. package/src/parse.pyj +80 -16
  67. package/src/string_interpolation.pyj +72 -72
  68. package/src/tokenizer.pyj +10 -5
  69. package/src/unicode_aliases.pyj +576 -576
  70. package/src/utils.pyj +192 -192
  71. package/test/_import_one.pyj +37 -37
  72. package/test/_import_two/__init__.pyj +11 -11
  73. package/test/_import_two/level2/deep.pyj +4 -4
  74. package/test/_import_two/other.pyj +6 -6
  75. package/test/_import_two/sub.pyj +13 -13
  76. package/test/abc.pyj +291 -0
  77. package/test/aes_vectors.pyj +421 -421
  78. package/test/annotations.pyj +80 -80
  79. package/test/arithmetic_nostrict.pyj +88 -0
  80. package/test/arithmetic_types.pyj +169 -0
  81. package/test/baselib.pyj +91 -0
  82. package/test/bytes.pyj +467 -0
  83. package/test/classes.pyj +1 -0
  84. package/test/comparison_ops.pyj +173 -0
  85. package/test/contextlib.pyj +362 -0
  86. package/test/dataclasses.pyj +253 -0
  87. package/test/datetime.pyj +500 -0
  88. package/test/debugger_stmt.pyj +41 -0
  89. package/test/decorators.pyj +77 -77
  90. package/test/docstrings.pyj +39 -39
  91. package/test/elementmaker_test.pyj +45 -45
  92. package/test/enum.pyj +134 -0
  93. package/test/eval_exec.pyj +56 -0
  94. package/test/format.pyj +148 -0
  95. package/test/functions.pyj +151 -151
  96. package/test/generators.pyj +41 -41
  97. package/test/generic.pyj +370 -370
  98. package/test/imports.pyj +72 -72
  99. package/test/internationalization.pyj +73 -73
  100. package/test/io.pyj +316 -0
  101. package/test/json.pyj +196 -0
  102. package/test/lint.pyj +164 -164
  103. package/test/loops.pyj +85 -85
  104. package/test/numpy.pyj +734 -734
  105. package/test/object.pyj +64 -0
  106. package/test/omit_function_metadata.pyj +20 -20
  107. package/test/python_compat.pyj +17 -15
  108. package/test/python_features.pyj +70 -15
  109. package/test/regexp.pyj +83 -55
  110. package/test/repl.pyj +121 -121
  111. package/test/scoped_flags.pyj +76 -76
  112. package/test/tuples.pyj +96 -0
  113. package/test/typing.pyj +469 -0
  114. package/test/unit/index.js +116 -7
  115. package/test/unit/language-service-dts.js +543 -543
  116. package/test/unit/language-service-hover.js +455 -455
  117. package/test/unit/language-service.js +84 -0
  118. package/test/unit/web-repl.js +1337 -1
  119. package/test/vars_locals_globals.pyj +94 -0
  120. package/tools/cli.js +558 -547
  121. package/tools/compile.js +224 -219
  122. package/tools/completer.js +131 -131
  123. package/tools/embedded_compiler.js +262 -251
  124. package/tools/gettext.js +185 -185
  125. package/tools/ini.js +65 -65
  126. package/tools/lint.js +16 -19
  127. package/tools/msgfmt.js +187 -187
  128. package/tools/repl.js +223 -223
  129. package/tools/test.js +118 -118
  130. package/tools/utils.js +128 -128
  131. package/tools/web_repl.js +95 -95
  132. package/try +41 -41
  133. package/web-repl/env.js +196 -196
  134. package/web-repl/index.html +163 -163
  135. package/web-repl/main.js +252 -252
  136. package/web-repl/prism.css +139 -139
  137. package/web-repl/prism.js +113 -113
  138. package/web-repl/rapydscript.js +224 -224
  139. package/web-repl/sha1.js +25 -25
  140. package/PYTHON_DIFFERENCES_REPORT.md +0 -291
  141. package/PYTHON_FEATURE_COVERAGE.md +0 -200
@@ -1,39 +1,39 @@
1
- # vim:fileencoding=utf-8
2
- # License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
3
- # globals: ρσ_module_doc__
4
-
5
- import _import_one
6
-
7
- def f():
8
- " A basic docstring "
9
- pass
10
-
11
- assrt.equal(f.__doc__, 'A basic docstring')
12
- assrt.equal(_import_one.__doc__, 'Module level ds1\n\nModule level ds2\nline2\n\nModule level ds 3')
13
-
14
- def g():
15
- '''
16
- A more complex docstring:
17
- xxx
18
- yyyy
19
-
20
- the end
21
- '''
22
- pass
23
-
24
- assrt.equal(g.__doc__, 'A more complex docstring:\n xxx\n yyyy\n\nthe end')
25
-
26
- class D:
27
- ' Docstring for a class '
28
-
29
- def method(self):
30
- 'ds for a method'
31
- pass
32
-
33
- assrt.equal(D().__doc__, 'Docstring for a class')
34
- assrt.equal(D().method.__doc__, 'ds for a method')
35
-
36
- x = def():
37
- 'xxx'
38
-
39
- assrt.equal(x.__doc__, 'xxx')
1
+ # vim:fileencoding=utf-8
2
+ # License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
3
+ # globals: ρσ_module_doc__
4
+
5
+ import _import_one
6
+
7
+ def f():
8
+ " A basic docstring "
9
+ pass
10
+
11
+ assrt.equal(f.__doc__, 'A basic docstring')
12
+ assrt.equal(_import_one.__doc__, 'Module level ds1\n\nModule level ds2\nline2\n\nModule level ds 3')
13
+
14
+ def g():
15
+ '''
16
+ A more complex docstring:
17
+ xxx
18
+ yyyy
19
+
20
+ the end
21
+ '''
22
+ pass
23
+
24
+ assrt.equal(g.__doc__, 'A more complex docstring:\n xxx\n yyyy\n\nthe end')
25
+
26
+ class D:
27
+ ' Docstring for a class '
28
+
29
+ def method(self):
30
+ 'ds for a method'
31
+ pass
32
+
33
+ assrt.equal(D().__doc__, 'Docstring for a class')
34
+ assrt.equal(D().method.__doc__, 'ds for a method')
35
+
36
+ x = def():
37
+ 'xxx'
38
+
39
+ assrt.equal(x.__doc__, 'xxx')
@@ -1,45 +1,45 @@
1
- # vim:fileencoding=utf-8
2
- # License: BSD Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
3
- # globals: assrt
4
-
5
- from elementmaker import E
6
-
7
- eq = assrt.equal
8
-
9
- def dummy_elem_eq(a, b):
10
- eq(jstype(a), jstype(b))
11
- if (jstype(a) == 'string'):
12
- eq(a, b)
13
- return
14
- eq(a.attributes.length, b.attributes.length)
15
- eq(a.children.length, b.children.length)
16
- eq(a.name, b.name)
17
- for attr in a.attributes:
18
- eq(a[attr], b[attr])
19
- for i, child in enumerate(a.children):
20
- dummy_elem_eq(child, b.children[i])
21
-
22
- q = E.div('text', id='1', class_='c', data_x='x')
23
- dummy_elem_eq(q, {'name':'div', 'children':['text'], 'attributes':{'id':'1', 'class':'c', 'data-x': 'x'}})
24
-
25
- q = E.div(
26
- E.span('a'),
27
- E.span('b'),
28
- E.a(),
29
- id='1',
30
- boolean_attr=True,
31
- )
32
- dummy_elem_eq(q, {'name':'div', 'children':[
33
- {
34
- 'name':'span',
35
- 'children':['a'], 'attributes': {}
36
- },
37
- {
38
- 'name':'span',
39
- 'children':['b'], 'attributes': {}
40
- },
41
- {
42
- 'name':'a',
43
- 'children':[], 'attributes': {}
44
- },
45
- ], 'attributes':{'id':'1', 'boolean-attr':'boolean-attr'}})
1
+ # vim:fileencoding=utf-8
2
+ # License: BSD Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
3
+ # globals: assrt
4
+
5
+ from elementmaker import E
6
+
7
+ eq = assrt.equal
8
+
9
+ def dummy_elem_eq(a, b):
10
+ eq(jstype(a), jstype(b))
11
+ if (jstype(a) == 'string'):
12
+ eq(a, b)
13
+ return
14
+ eq(a.attributes.length, b.attributes.length)
15
+ eq(a.children.length, b.children.length)
16
+ eq(a.name, b.name)
17
+ for attr in a.attributes:
18
+ eq(a[attr], b[attr])
19
+ for i, child in enumerate(a.children):
20
+ dummy_elem_eq(child, b.children[i])
21
+
22
+ q = E.div('text', id='1', class_='c', data_x='x')
23
+ dummy_elem_eq(q, {'name':'div', 'children':['text'], 'attributes':{'id':'1', 'class':'c', 'data-x': 'x'}})
24
+
25
+ q = E.div(
26
+ E.span('a'),
27
+ E.span('b'),
28
+ E.a(),
29
+ id='1',
30
+ boolean_attr=True,
31
+ )
32
+ dummy_elem_eq(q, {'name':'div', 'children':[
33
+ {
34
+ 'name':'span',
35
+ 'children':['a'], 'attributes': {}
36
+ },
37
+ {
38
+ 'name':'span',
39
+ 'children':['b'], 'attributes': {}
40
+ },
41
+ {
42
+ 'name':'a',
43
+ 'children':[], 'attributes': {}
44
+ },
45
+ ], 'attributes':{'id':'1', 'boolean-attr':'boolean-attr'}})
package/test/enum.pyj ADDED
@@ -0,0 +1,134 @@
1
+ # globals: assrt
2
+ # vim:fileencoding=utf-8
3
+ #
4
+ # enum.pyj
5
+ # Tests for the enum standard library (from enum import Enum).
6
+
7
+ ae = assrt.equal
8
+ ade = assrt.deepEqual
9
+ ok = assrt.ok
10
+
11
+ from enum import Enum
12
+
13
+ # ── 1. Basic member access: .name and .value ─────────────────────────────────
14
+ # STATUS: ✓ WORKS
15
+
16
+ class _Color(Enum):
17
+ RED = 1
18
+ GREEN = 2
19
+ BLUE = 3
20
+
21
+ ae(_Color.RED.name, 'RED')
22
+ ae(_Color.RED.value, 1)
23
+ ae(_Color.GREEN.name, 'GREEN')
24
+ ae(_Color.GREEN.value, 2)
25
+ ae(_Color.BLUE.name, 'BLUE')
26
+ ae(_Color.BLUE.value, 3)
27
+
28
+ # ── 2. Member singletons (identity) ──────────────────────────────────────────
29
+ # STATUS: ✓ WORKS
30
+
31
+ ok(_Color.RED is _Color.RED)
32
+ ok(_Color.RED is not _Color.GREEN)
33
+ ok(_Color.GREEN is not _Color.BLUE)
34
+
35
+ # ── 3. repr and str ──────────────────────────────────────────────────────────
36
+ # STATUS: ✓ WORKS
37
+ # Note: repr() for integer values uses the numeric representation.
38
+ # The class name in repr/str reflects the actual JS constructor name.
39
+
40
+ ae(repr(_Color.RED), '<_Color.RED: 1>')
41
+ ae(repr(_Color.GREEN), '<_Color.GREEN: 2>')
42
+ ae(str(_Color.RED), '_Color.RED')
43
+ ae(str(_Color.BLUE), '_Color.BLUE')
44
+
45
+ # ── 4. isinstance checks ─────────────────────────────────────────────────────
46
+ # STATUS: ✓ WORKS
47
+
48
+ ok(isinstance(_Color.RED, _Color))
49
+ ok(isinstance(_Color.RED, Enum))
50
+ ok(isinstance(_Color.GREEN, _Color))
51
+ ok(not isinstance(1, _Color))
52
+ ok(not isinstance(1, Enum))
53
+
54
+ # ── 5. Iteration via list() ───────────────────────────────────────────────────
55
+ # STATUS: ✓ WORKS
56
+
57
+ members = list(_Color)
58
+ ae(len(members), 3)
59
+ ae(members[0].name, 'RED')
60
+ ae(members[1].name, 'GREEN')
61
+ ae(members[2].name, 'BLUE')
62
+ ae(members[0].value, 1)
63
+ ae(members[1].value, 2)
64
+ ae(members[2].value, 3)
65
+
66
+ # ── 6. for loop iteration ─────────────────────────────────────────────────────
67
+ # STATUS: ✓ WORKS
68
+
69
+ _found_names = []
70
+ for _m in _Color:
71
+ _found_names.push(_m.name)
72
+ ade(_found_names, ['RED', 'GREEN', 'BLUE'])
73
+
74
+ _found_values = []
75
+ for _m in _Color:
76
+ _found_values.push(_m.value)
77
+ ade(_found_values, [1, 2, 3])
78
+
79
+ # ── 7. _member_names_ registry ───────────────────────────────────────────────
80
+ # STATUS: ✓ WORKS
81
+
82
+ ade(_Color._member_names_, ['RED', 'GREEN', 'BLUE'])
83
+
84
+ # ── 8. String-valued enum ─────────────────────────────────────────────────────
85
+ # STATUS: ✓ WORKS
86
+
87
+ class _Direction(Enum):
88
+ NORTH = 'north'
89
+ SOUTH = 'south'
90
+ EAST = 'east'
91
+ WEST = 'west'
92
+
93
+ ae(_Direction.NORTH.name, 'NORTH')
94
+ ae(_Direction.NORTH.value, 'north')
95
+ ae(_Direction.SOUTH.value, 'south')
96
+ # RapydScript repr() wraps strings in double quotes (JS convention)
97
+ ae(repr(_Direction.NORTH), '<_Direction.NORTH: "north">')
98
+ ae(str(_Direction.EAST), '_Direction.EAST')
99
+ ae(len(list(_Direction)), 4)
100
+
101
+ # ── 9. Enum subclass with extra methods ───────────────────────────────────────
102
+ # STATUS: ✓ WORKS
103
+
104
+ class _Status(Enum):
105
+ ACTIVE = 1
106
+ INACTIVE = 0
107
+
108
+ def is_active(self):
109
+ return this is _Status.ACTIVE
110
+
111
+ ok(_Status.ACTIVE.is_active())
112
+ ok(not _Status.INACTIVE.is_active())
113
+
114
+ # ── 10. Multiple Enum subclasses are independent ──────────────────────────────
115
+ # STATUS: ✓ WORKS
116
+
117
+ ae(len(list(_Color)), 3)
118
+ ae(len(list(_Direction)), 4)
119
+ ae(len(list(_Status)), 2)
120
+ ok(_Color._member_names_ is not _Direction._member_names_)
121
+ ok(_Color.RED is not _Status.ACTIVE) # different classes, different members
122
+
123
+ # ── 11. Members are not confused with plain class instances ───────────────────
124
+ # STATUS: ✓ WORKS
125
+
126
+ ok(isinstance(_Color.RED, _Color))
127
+ ok(_Color.RED is _Color.RED) # same object every time
128
+
129
+ # ── 12. Values from _members_by_value_ lookup ────────────────────────────────
130
+ # STATUS: ✓ WORKS
131
+
132
+ ae(_Color._members_by_value_[1].name, 'RED')
133
+ ae(_Color._members_by_value_[2].name, 'GREEN')
134
+ ae(_Color._members_by_value_[3].name, 'BLUE')
@@ -0,0 +1,56 @@
1
+ # vim:fileencoding=utf-8
2
+ # globals: assrt
3
+
4
+ ae = assrt.equal
5
+ ade = assrt.deepEqual
6
+ ok = assrt.ok
7
+
8
+ # ── eval — basic expression evaluation ────────────────────────────────────────
9
+
10
+ ae(eval("1 + 2"), 3)
11
+ ae(eval("10 * 5"), 50)
12
+ ae(eval("100 - 37"), 63)
13
+ ae(eval('"hello" + " world"'), "hello world")
14
+ ae(eval("Math.pow(2, 8)"), 256)
15
+ ae(eval("True"), True)
16
+ ae(eval("None"), None)
17
+
18
+ # ── eval — with explicit globals dict ─────────────────────────────────────────
19
+
20
+ ae(eval("x + y", {"x": 10, "y": 5}), 15)
21
+ ae(eval("a * b", {"a": 3, "b": 4}), 12)
22
+ ae(eval("n * n", {"n": 7}), 49)
23
+
24
+ # ── eval — locals override globals ────────────────────────────────────────────
25
+
26
+ ae(eval("x", {"x": 1}, {"x": 99}), 99)
27
+ ae(eval("x + y", {"x": 10, "y": 20}, {"y": 1}), 11)
28
+
29
+ # ── exec — always returns None ────────────────────────────────────────────────
30
+
31
+ ae(exec("1 + 2"), None)
32
+ ae(exec("_ignored = 42"), None)
33
+
34
+ # ── exec — side effects via mutable objects in globals ────────────────────────
35
+
36
+ log = []
37
+ exec("log.push('hello')", {"log": log})
38
+ ae(log[0], "hello")
39
+
40
+ exec("log.push(1 + 2)", {"log": log})
41
+ ae(log[1], 3)
42
+
43
+ # exec with a function in globals
44
+ out = []
45
+ def _adder(a, b): out.push(a + b);
46
+ exec("fn(10, 7)", {"fn": _adder, "out": out})
47
+ ae(out[0], 17)
48
+
49
+ # exec returns None even when globals/locals provided
50
+ ae(exec("1 + 1", {"x": 5}), None)
51
+
52
+ # ── exec — JS loop, mutation visible in caller ────────────────────────────────
53
+
54
+ nums = []
55
+ exec("for _i in range(4):\n nums.append(_i * _i)", {"nums": nums})
56
+ ade(nums, [0, 1, 4, 9])
@@ -0,0 +1,148 @@
1
+ # vim:fileencoding=utf-8
2
+ # License: BSD
3
+ # Tests for __format__ dunder dispatch in format(), str.format(), and f-strings
4
+ # globals: assrt
5
+
6
+ ae = assrt.equal
7
+
8
+ # ── Basic custom __format__ ───────────────────────────────────────────────────
9
+
10
+ class Currency:
11
+ def __init__(self, amount):
12
+ self.amount = amount
13
+ def __str__(self):
14
+ return str(self.amount)
15
+ def __format__(self, spec):
16
+ if spec == 'usd':
17
+ return '$' + str(self.amount)
18
+ if spec == 'eur':
19
+ return '€' + str(self.amount)
20
+ return format(self.amount, spec)
21
+
22
+ c = Currency(42)
23
+
24
+ # format() builtin dispatches to __format__
25
+ ae(format(c, 'usd'), '$42')
26
+ ae(format(c, 'eur'), '€42')
27
+ ae(format(c, '.2f'), '42.00')
28
+
29
+ # str.format() dispatches to __format__
30
+ ae(str.format('{:usd}', c), '$42')
31
+ ae(str.format('{:eur}', c), '€42')
32
+ ae(str.format('{:.2f}', c), '42.00')
33
+
34
+ # f-strings dispatch to __format__
35
+ ae(f'{c:usd}', '$42')
36
+ ae(f'{c:eur}', '€42')
37
+ ae(f'{c:.2f}', '42.00')
38
+
39
+ # ── format() with empty spec calls __format__('') ────────────────────────────
40
+
41
+ class Tagged:
42
+ def __init__(self, v):
43
+ self.v = v
44
+ def __str__(self):
45
+ return 'str:' + str(self.v)
46
+ def __format__(self, spec):
47
+ if not spec:
48
+ return 'fmt:' + str(self.v)
49
+ return 'fmt:' + format(self.v, spec)
50
+
51
+ t = Tagged(7)
52
+ ae(format(t), 'fmt:7')
53
+ ae(format(t, ''), 'fmt:7')
54
+ ae(str.format('{}', t), 'fmt:7')
55
+ ae(f'{t}', 'fmt:7')
56
+
57
+ # ── Default __format__ delegates to __str__ when spec is empty ────────────────
58
+
59
+ class Simple:
60
+ def __str__(self):
61
+ return 'simple!'
62
+
63
+ s = Simple()
64
+ ae(format(s), 'simple!')
65
+ ae(format(s, ''), 'simple!')
66
+ ae(str.format('{}', s), 'simple!')
67
+ ae(f'{s}', 'simple!')
68
+
69
+ # ── Default __format__ raises TypeError for non-empty spec ───────────────────
70
+
71
+ class Plain:
72
+ def __str__(self):
73
+ return 'plain'
74
+
75
+ p = Plain()
76
+ raised = False
77
+ try:
78
+ format(p, 'd')
79
+ except TypeError:
80
+ raised = True
81
+ ae(raised, True)
82
+
83
+ raised = False
84
+ try:
85
+ str.format('{:d}', p)
86
+ except TypeError:
87
+ raised = True
88
+ ae(raised, True)
89
+
90
+ # ── Inheritance: subclass inherits parent __format__ ─────────────────────────
91
+
92
+ class Base:
93
+ def __format__(self, spec):
94
+ return 'base:' + spec
95
+
96
+ class Child(Base):
97
+ pass
98
+
99
+ child = Child()
100
+ ae(format(child, 'x'), 'base:x')
101
+ ae(str.format('{:x}', child), 'base:x')
102
+ ae(f'{child:x}', 'base:x')
103
+
104
+ # ── Subclass can override __format__ ─────────────────────────────────────────
105
+
106
+ class Override(Base):
107
+ def __format__(self, spec):
108
+ return 'override:' + spec
109
+
110
+ o = Override()
111
+ ae(format(o, 'y'), 'override:y')
112
+ ae(str.format('{:y}', o), 'override:y')
113
+
114
+ # ── !r and !s transformers bypass __format__ ─────────────────────────────────
115
+
116
+ class WithRepr:
117
+ def __repr__(self):
118
+ return 'MyRepr'
119
+ def __str__(self):
120
+ return 'MyStr'
121
+ def __format__(self, spec):
122
+ return 'MyFmt'
123
+
124
+ wr = WithRepr()
125
+ # With no transformer, __format__ is called
126
+ ae(str.format('{}', wr), 'MyFmt')
127
+ # !s applies str(), result is a string (no __format__)
128
+ ae(str.format('{!s}', wr), 'MyStr')
129
+ # !r applies repr()
130
+ ae(str.format('{!r}', wr), 'MyRepr')
131
+
132
+ # ── Multiple fields with __format__ ──────────────────────────────────────────
133
+
134
+ class Qty:
135
+ def __init__(self, n):
136
+ self.n = n
137
+ def __str__(self):
138
+ return str(self.n)
139
+ def __format__(self, spec):
140
+ if spec == 'w':
141
+ words = ['zero', 'one', 'two', 'three', 'four', 'five']
142
+ return words[self.n] if self.n < len(words) else str(self.n)
143
+ return format(self.n, spec)
144
+
145
+ a, b = Qty(1), Qty(2)
146
+ ae(str.format('{:w} and {:w}', a, b), 'one and two')
147
+ ae(f'{a:w} + {b:w}', 'one + two')
148
+ ae(str.format('{0:d} + {1:d}', a, b), '1 + 2')