rapydscript-ns 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.agignore +1 -1
  2. package/.github/workflows/ci.yml +38 -38
  3. package/=template.pyj +5 -5
  4. package/CHANGELOG.md +19 -0
  5. package/HACKING.md +103 -103
  6. package/LICENSE +24 -24
  7. package/PYTHON_GAPS.md +420 -0
  8. package/README.md +153 -29
  9. package/TODO.md +16 -118
  10. package/add-toc-to-readme +2 -2
  11. package/bin/export +75 -75
  12. package/bin/rapydscript +70 -70
  13. package/bin/web-repl-export +102 -102
  14. package/build +2 -2
  15. package/language-service/index.js +237 -8
  16. package/memory/project_string_impl.md +43 -0
  17. package/package.json +1 -1
  18. package/publish.py +37 -37
  19. package/release/baselib-plain-pretty.js +248 -38
  20. package/release/baselib-plain-ugly.js +8 -8
  21. package/release/compiler.js +778 -277
  22. package/release/signatures.json +30 -30
  23. package/session.vim +4 -4
  24. package/setup.cfg +2 -2
  25. package/src/ast.pyj +4 -1
  26. package/src/baselib-builtins.pyj +56 -2
  27. package/src/baselib-containers.pyj +2 -0
  28. package/src/baselib-errors.pyj +7 -3
  29. package/src/baselib-internal.pyj +51 -6
  30. package/src/baselib-str.pyj +5 -3
  31. package/src/compiler.pyj +36 -36
  32. package/src/errors.pyj +30 -30
  33. package/src/lib/aes.pyj +646 -646
  34. package/src/lib/asyncio.pyj +534 -0
  35. package/src/lib/base64.pyj +399 -0
  36. package/src/lib/bisect.pyj +73 -0
  37. package/src/lib/collections.pyj +1 -1
  38. package/src/lib/copy.pyj +120 -120
  39. package/src/lib/csv.pyj +494 -0
  40. package/src/lib/elementmaker.pyj +83 -83
  41. package/src/lib/encodings.pyj +126 -126
  42. package/src/lib/gettext.pyj +569 -569
  43. package/src/lib/heapq.pyj +98 -0
  44. package/src/lib/html.pyj +382 -0
  45. package/src/lib/http/__init__.pyj +98 -0
  46. package/src/lib/http/client.pyj +304 -0
  47. package/src/lib/http/cookies.pyj +236 -0
  48. package/src/lib/itertools.pyj +580 -580
  49. package/src/lib/logging.pyj +672 -0
  50. package/src/lib/math.pyj +193 -193
  51. package/src/lib/operator.pyj +11 -11
  52. package/src/lib/pythonize.pyj +20 -20
  53. package/src/lib/random.pyj +118 -118
  54. package/src/lib/react.pyj +74 -74
  55. package/src/lib/string.pyj +357 -0
  56. package/src/lib/textwrap.pyj +329 -0
  57. package/src/lib/traceback.pyj +63 -63
  58. package/src/lib/urllib/__init__.pyj +14 -0
  59. package/src/lib/urllib/error.pyj +66 -0
  60. package/src/lib/urllib/parse.pyj +475 -0
  61. package/src/lib/urllib/request.pyj +86 -0
  62. package/src/lib/uuid.pyj +77 -77
  63. package/src/monaco-language-service/analyzer.js +5 -2
  64. package/src/monaco-language-service/completions.js +26 -0
  65. package/src/monaco-language-service/diagnostics.js +202 -3
  66. package/src/monaco-language-service/dts.js +550 -550
  67. package/src/monaco-language-service/scope.js +1 -0
  68. package/src/output/comments.pyj +45 -45
  69. package/src/output/exceptions.pyj +201 -201
  70. package/src/output/functions.pyj +152 -6
  71. package/src/output/jsx.pyj +164 -164
  72. package/src/output/loops.pyj +17 -2
  73. package/src/output/modules.pyj +1 -1
  74. package/src/output/operators.pyj +15 -0
  75. package/src/output/stream.pyj +0 -1
  76. package/src/output/treeshake.pyj +182 -182
  77. package/src/output/utils.pyj +72 -72
  78. package/src/parse.pyj +80 -17
  79. package/src/string_interpolation.pyj +72 -72
  80. package/src/tokenizer.pyj +1 -1
  81. package/src/unicode_aliases.pyj +576 -576
  82. package/src/utils.pyj +192 -192
  83. package/test/_import_one.pyj +37 -37
  84. package/test/_import_two/__init__.pyj +11 -11
  85. package/test/_import_two/level2/deep.pyj +4 -4
  86. package/test/_import_two/other.pyj +6 -6
  87. package/test/_import_two/sub.pyj +13 -13
  88. package/test/aes_vectors.pyj +421 -421
  89. package/test/annotations.pyj +80 -80
  90. package/test/async_generators.pyj +144 -0
  91. package/test/asyncio.pyj +307 -0
  92. package/test/base64.pyj +202 -0
  93. package/test/bisect.pyj +178 -0
  94. package/test/csv.pyj +405 -0
  95. package/test/decorators.pyj +77 -77
  96. package/test/docstrings.pyj +39 -39
  97. package/test/elementmaker_test.pyj +45 -45
  98. package/test/float_special.pyj +64 -0
  99. package/test/functions.pyj +151 -151
  100. package/test/generators.pyj +41 -41
  101. package/test/generic.pyj +370 -370
  102. package/test/heapq.pyj +174 -0
  103. package/test/html.pyj +212 -0
  104. package/test/http.pyj +259 -0
  105. package/test/imports.pyj +79 -72
  106. package/test/internationalization.pyj +73 -73
  107. package/test/lint.pyj +164 -164
  108. package/test/logging.pyj +356 -0
  109. package/test/long.pyj +130 -0
  110. package/test/loops.pyj +85 -85
  111. package/test/numpy.pyj +734 -734
  112. package/test/parenthesized_with.pyj +141 -0
  113. package/test/python_compat.pyj +3 -5
  114. package/test/python_modulo.pyj +76 -0
  115. package/test/python_modulo_off.pyj +21 -0
  116. package/test/repl.pyj +121 -121
  117. package/test/scoped_flags.pyj +76 -76
  118. package/test/str.pyj +14 -0
  119. package/test/string.pyj +245 -0
  120. package/test/textwrap.pyj +172 -0
  121. package/test/type_display.pyj +48 -0
  122. package/test/type_enforcement.pyj +164 -0
  123. package/test/unit/index.js +14 -6
  124. package/test/unit/language-service-completions.js +119 -0
  125. package/test/unit/language-service-dts.js +543 -543
  126. package/test/unit/language-service-hover.js +455 -455
  127. package/test/unit/language-service-scope.js +32 -0
  128. package/test/unit/language-service.js +127 -3
  129. package/test/unit/run-language-service.js +17 -3
  130. package/test/unit/web-repl.js +2094 -29
  131. package/test/urllib.pyj +193 -0
  132. package/tools/compile.js +1 -1
  133. package/tools/compiler.d.ts +367 -367
  134. package/tools/completer.js +131 -131
  135. package/tools/embedded_compiler.js +7 -7
  136. package/tools/gettext.js +185 -185
  137. package/tools/ini.js +65 -65
  138. package/tools/msgfmt.js +187 -187
  139. package/tools/repl.js +223 -223
  140. package/tools/test.js +118 -118
  141. package/tools/utils.js +128 -128
  142. package/tools/web_repl.js +95 -95
  143. package/try +41 -41
  144. package/web-repl/env.js +196 -196
  145. package/web-repl/index.html +163 -163
  146. package/web-repl/main.js +1 -1
  147. package/web-repl/prism.css +139 -139
  148. package/web-repl/prism.js +113 -113
  149. package/web-repl/rapydscript.js +224 -224
  150. package/web-repl/sha1.js +25 -25
  151. package/test/omit_function_metadata.pyj +0 -20
@@ -0,0 +1,202 @@
1
+ # globals: assrt
2
+ # vim:fileencoding=utf-8
3
+ #
4
+ # base64.pyj
5
+ # Tests for the base64 standard library module.
6
+
7
+ from base64 import b64encode, b64decode, standard_b64encode, standard_b64decode
8
+ from base64 import urlsafe_b64encode, urlsafe_b64decode
9
+ from base64 import b32encode, b32decode
10
+ from base64 import b16encode, b16decode
11
+ from base64 import encodebytes, decodebytes
12
+ from base64 import Error
13
+
14
+ ae = assrt.equal
15
+ ade = assrt.deepEqual
16
+ ok = assrt.ok
17
+ throws = assrt.throws
18
+
19
+ # Helper: convert bytes to ASCII string for easy comparison
20
+ def _str(b):
21
+ return b.decode('ascii')
22
+
23
+
24
+ # ── 1. b64encode — basic round-trips ─────────────────────────────────────────
25
+
26
+ ae(_str(b64encode(bytes([]))), '')
27
+ ae(_str(b64encode(bytes([0]))), 'AA==')
28
+ ae(_str(b64encode(bytes([0, 0]))), 'AAA=')
29
+ ae(_str(b64encode(bytes([0, 0, 0]))), 'AAAA')
30
+ ae(_str(b64encode(bytes([77, 97, 110]))), 'TWFu') # 'Man'
31
+
32
+ # Known Python vectors
33
+ ae(_str(b64encode(bytes([104, 101, 108, 108, 111]))), 'aGVsbG8=') # 'hello'
34
+ ae(_str(b64encode(bytes([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]))), 'aGVsbG8gd29ybGQ=') # 'hello world'
35
+
36
+
37
+ # ── 2. b64decode ──────────────────────────────────────────────────────────────
38
+
39
+ _dec1 = b64decode('TWFu')
40
+ ade(list(_dec1), [77, 97, 110])
41
+
42
+ _dec2 = b64decode('aGVsbG8=')
43
+ ade(list(_dec2), [104, 101, 108, 108, 111])
44
+
45
+ # Empty string
46
+ ade(list(b64decode('')), [])
47
+
48
+ # Missing padding — Python strips whitespace and pads internally
49
+ _dec3 = b64decode('aGVsbG8')
50
+ ade(list(_dec3), [104, 101, 108, 108, 111])
51
+
52
+ # With embedded whitespace (stripped by default)
53
+ _dec4 = b64decode('aGVs bG8=')
54
+ ade(list(_dec4), [104, 101, 108, 108, 111])
55
+
56
+ # Accepts bytes input
57
+ _dec5 = b64decode(bytes([84, 87, 70, 117])) # b'TWFu'
58
+ ade(list(_dec5), [77, 97, 110])
59
+
60
+
61
+ # ── 3. b64encode / b64decode with altchars ────────────────────────────────────
62
+
63
+ # altchars b'-_': replaces + and /
64
+ _enc_alt = b64encode(bytes([251, 239, 190]), altchars=bytes([45, 95])) # b'-_'
65
+ _enc_std = b64encode(bytes([251, 239, 190]))
66
+ ok(_str(_enc_alt).indexOf('-') >= 0 or _str(_enc_alt).indexOf('_') >= 0
67
+ or (_str(_enc_alt) == _str(_enc_std)),
68
+ 'altchars encoding should differ or be identical if no + or / present')
69
+
70
+ # Round-trip with altchars
71
+ _data_alt = bytes([200, 100, 50, 25])
72
+ _enc2 = b64encode(_data_alt, altchars=bytes([45, 95]))
73
+ _dec6 = b64decode(_enc2, altchars=bytes([45, 95]))
74
+ ade(list(_dec6), list(_data_alt))
75
+
76
+
77
+ # ── 4. standard_b64encode / standard_b64decode ────────────────────────────────
78
+
79
+ _s_enc = standard_b64encode(bytes([104, 101, 108, 108, 111]))
80
+ ae(_str(_s_enc), 'aGVsbG8=')
81
+
82
+ _s_dec = standard_b64decode('aGVsbG8=')
83
+ ade(list(_s_dec), [104, 101, 108, 108, 111])
84
+
85
+
86
+ # ── 5. urlsafe_b64encode / urlsafe_b64decode ──────────────────────────────────
87
+
88
+ # bytes([251, 239, 190]) encodes to '----' in URL-safe (all 6-bit groups are 62)
89
+ _us_enc = urlsafe_b64encode(bytes([251, 239, 190]))
90
+ ok(_str(_us_enc).indexOf('+') < 0, 'URL-safe should not contain +')
91
+ ok(_str(_us_enc).indexOf('/') < 0, 'URL-safe should not contain /')
92
+ ae(_str(_us_enc), '----')
93
+
94
+ # Round-trip with data that produces + and / in standard encoding
95
+ _us_data = bytes([251, 254, 254]) # encodes to '-_7-' in URL-safe
96
+ _us_enc2 = urlsafe_b64encode(_us_data)
97
+ ae(_str(_us_enc2), '-_7-')
98
+
99
+ # URL-safe decode
100
+ _us_dec = urlsafe_b64decode('-_7-')
101
+ ade(list(_us_dec), [251, 254, 254])
102
+
103
+ # Round-trip with general data
104
+ _us_rt_data = bytes([0, 1, 2, 127, 128, 255])
105
+ _us_rt = urlsafe_b64decode(urlsafe_b64encode(_us_rt_data))
106
+ ade(list(_us_rt), list(_us_rt_data))
107
+
108
+
109
+ # ── 6. b32encode / b32decode ─────────────────────────────────────────────────
110
+
111
+ ae(_str(b32encode(bytes([]))), '')
112
+ ae(_str(b32encode(bytes([102]))), 'MY======') # b'f' → 'MY======'
113
+ ae(_str(b32encode(bytes([102, 111]))), 'MZXQ====') # b'fo' → 'MZXQ===='
114
+ ae(_str(b32encode(bytes([102, 111, 111]))), 'MZXW6===') # b'foo'
115
+ ae(_str(b32encode(bytes([102, 111, 111, 98]))), 'MZXW6YQ=') # b'foob'
116
+ ae(_str(b32encode(bytes([102, 111, 111, 98, 97]))), 'MZXW6YTB') # b'fooba'
117
+
118
+ # Round-trip several lengths
119
+ for _rt_data in [bytes([]), bytes([42]), bytes([1, 2, 3]), bytes([255, 0, 128, 64])]:
120
+ ade(list(b32decode(b32encode(_rt_data))), list(_rt_data))
121
+
122
+ # casefold
123
+ _b32_lc = b32decode('mzxw6ytb', casefold=True)
124
+ ade(list(_b32_lc), list(bytes([102, 111, 111, 98, 97])))
125
+
126
+
127
+ # ── 7. b16encode / b16decode ──────────────────────────────────────────────────
128
+
129
+ ae(_str(b16encode(bytes([]))), '')
130
+ ae(_str(b16encode(bytes([0]))), '00')
131
+ ae(_str(b16encode(bytes([255]))), 'FF')
132
+ ae(_str(b16encode(bytes([0, 1, 254, 255]))), '0001FEFF')
133
+ ae(_str(b16encode(bytes([171]))), 'AB')
134
+
135
+ # Round-trip
136
+ ade(list(b16decode(b16encode(bytes([10, 20, 30, 40, 250])))), [10, 20, 30, 40, 250])
137
+
138
+ # casefold
139
+ ade(list(b16decode('0001feff', casefold=True)), [0, 1, 254, 255])
140
+
141
+ # Error on invalid hex
142
+ _b16_err = False
143
+ try:
144
+ b16decode('ZZ')
145
+ except Error:
146
+ _b16_err = True
147
+ ok(_b16_err, 'b16decode of non-hex should raise Error')
148
+
149
+ # Error on odd-length string
150
+ _b16_odd = False
151
+ try:
152
+ b16decode('A')
153
+ except Error:
154
+ _b16_odd = True
155
+ ok(_b16_odd, 'b16decode of odd-length string should raise Error')
156
+
157
+
158
+ # ── 8. encodebytes / decodebytes ──────────────────────────────────────────────
159
+
160
+ # encodebytes wraps at 76 chars per line
161
+ _long_data = bytes(list(range(57))) # 57 bytes → exactly 76 base64 chars (one line)
162
+ _enc_lines = encodebytes(_long_data)
163
+ _enc_lines_str = _str(_enc_lines)
164
+ # Each line ends with \n
165
+ ok(_enc_lines_str[-1:] == '\n', 'encodebytes output should end with newline')
166
+
167
+ # Decode should recover original
168
+ _dec_lines = decodebytes(_enc_lines)
169
+ ade(list(_dec_lines), list(_long_data))
170
+
171
+ # More than 57 bytes → multiple lines
172
+ _long2 = bytes(list(range(100)))
173
+ _enc2b = encodebytes(_long2)
174
+ ok(_str(_enc2b).split('\n').length > 2, 'long encodebytes should have multiple lines')
175
+ ade(list(decodebytes(_enc2b)), list(_long2))
176
+
177
+ # Empty bytes
178
+ ade(list(decodebytes(encodebytes(bytes([])))), [])
179
+
180
+
181
+ # ── 9. b64decode validate=True ────────────────────────────────────────────────
182
+
183
+ # Valid input should not raise
184
+ _valid = b64decode('aGVsbG8=', validate=True)
185
+ ade(list(_valid), [104, 101, 108, 108, 111])
186
+
187
+ # Invalid character raises Error when validate=True
188
+ _bad_raised = False
189
+ try:
190
+ b64decode('aGVs!G8=', validate=True)
191
+ except Error:
192
+ _bad_raised = True
193
+ ok(_bad_raised, 'validate=True should raise Error on non-base64 character')
194
+
195
+
196
+ # ── 10. b64encode returns bytes ───────────────────────────────────────────────
197
+
198
+ _enc_type = b64encode(bytes([1, 2, 3]))
199
+ ok(isinstance(_enc_type, bytes), 'b64encode should return bytes')
200
+
201
+ _dec_type = b64decode('AQID')
202
+ ok(isinstance(_dec_type, bytes), 'b64decode should return bytes')
@@ -0,0 +1,178 @@
1
+ # globals: assrt
2
+ # vim:fileencoding=utf-8
3
+ #
4
+ # bisect.pyj
5
+ # Tests for the bisect standard library module.
6
+
7
+ from bisect import bisect_left, bisect_right, bisect, insort_left, insort_right, insort
8
+
9
+ ae = assrt.equal
10
+ ade = assrt.deepEqual
11
+ ok = assrt.ok
12
+
13
+ # ── 1. bisect_left — basic ────────────────────────────────────────────────────
14
+
15
+ _a = [1, 3, 5, 7, 9]
16
+
17
+ ae(bisect_left(_a, 0), 0) # before all
18
+ ae(bisect_left(_a, 1), 0) # at first element (left of existing)
19
+ ae(bisect_left(_a, 2), 1) # between 1 and 3
20
+ ae(bisect_left(_a, 3), 1) # at existing element (left)
21
+ ae(bisect_left(_a, 5), 2)
22
+ ae(bisect_left(_a, 6), 3) # between 5 and 7
23
+ ae(bisect_left(_a, 9), 4) # at last element (left)
24
+ ae(bisect_left(_a, 10), 5) # after all
25
+
26
+ # ── 2. bisect_right — basic ───────────────────────────────────────────────────
27
+
28
+ ae(bisect_right(_a, 0), 0) # before all
29
+ ae(bisect_right(_a, 1), 1) # at first element (right of existing)
30
+ ae(bisect_right(_a, 2), 1) # between 1 and 3
31
+ ae(bisect_right(_a, 3), 2) # at existing element (right)
32
+ ae(bisect_right(_a, 5), 3)
33
+ ae(bisect_right(_a, 6), 3) # between 5 and 7
34
+ ae(bisect_right(_a, 9), 5) # at last element (right)
35
+ ae(bisect_right(_a, 10), 5) # after all
36
+
37
+ # ── 3. bisect alias ───────────────────────────────────────────────────────────
38
+
39
+ ae(bisect(_a, 3), bisect_right(_a, 3))
40
+ ae(bisect(_a, 5), bisect_right(_a, 5))
41
+
42
+ # ── 4. Empty list ─────────────────────────────────────────────────────────────
43
+
44
+ ae(bisect_left([], 5), 0)
45
+ ae(bisect_right([], 5), 0)
46
+
47
+ # ── 5. All elements equal ─────────────────────────────────────────────────────
48
+
49
+ _eq = [3, 3, 3, 3, 3]
50
+ ae(bisect_left(_eq, 3), 0)
51
+ ae(bisect_right(_eq, 3), 5)
52
+ ae(bisect_left(_eq, 2), 0)
53
+ ae(bisect_right(_eq, 2), 0)
54
+ ae(bisect_left(_eq, 4), 5)
55
+ ae(bisect_right(_eq, 4), 5)
56
+
57
+ # ── 6. Single-element list ────────────────────────────────────────────────────
58
+
59
+ _one = [5]
60
+ ae(bisect_left(_one, 5), 0)
61
+ ae(bisect_right(_one, 5), 1)
62
+ ae(bisect_left(_one, 4), 0)
63
+ ae(bisect_right(_one, 6), 1)
64
+
65
+ # ── 7. lo and hi bounds ───────────────────────────────────────────────────────
66
+
67
+ _b = [1, 3, 5, 7, 9]
68
+ # search only in [3, 5, 7] (indices 1..3)
69
+ ae(bisect_left(_b, 3, 1, 4), 1)
70
+ ae(bisect_right(_b, 3, 1, 4), 2)
71
+ ae(bisect_left(_b, 6, 1, 4), 3)
72
+
73
+ # lo == hi: insertion point is lo regardless of x
74
+ ae(bisect_left(_b, 5, 2, 2), 2)
75
+ ae(bisect_right(_b, 5, 2, 2), 2)
76
+
77
+ # ── 8. ValueError for negative lo ────────────────────────────────────────────
78
+
79
+ _neg_lo_raised = False
80
+ try:
81
+ bisect_left([1, 2], 1, -1)
82
+ except ValueError:
83
+ _neg_lo_raised = True
84
+ ok(_neg_lo_raised, 'bisect_left with negative lo should raise ValueError')
85
+
86
+ _neg_lo_raised2 = False
87
+ try:
88
+ bisect_right([1, 2], 1, -1)
89
+ except ValueError:
90
+ _neg_lo_raised2 = True
91
+ ok(_neg_lo_raised2, 'bisect_right with negative lo should raise ValueError')
92
+
93
+ # ── 9. insort_left — basic ────────────────────────────────────────────────────
94
+
95
+ _il = [1, 3, 5, 7]
96
+ insort_left(_il, 4)
97
+ ade(_il, [1, 3, 4, 5, 7])
98
+
99
+ insort_left(_il, 3) # insert at left of equal elements
100
+ ae(_il[1], 3)
101
+ ae(_il[2], 3)
102
+ ae(_il.length, 6)
103
+
104
+ # ── 10. insort_right — basic ──────────────────────────────────────────────────
105
+
106
+ _ir = [1, 3, 5, 7]
107
+ insort_right(_ir, 4)
108
+ ade(_ir, [1, 3, 4, 5, 7])
109
+
110
+ insort_right(_ir, 3) # insert at right of equal elements — new 3 goes at index 2
111
+ ade(_ir, [1, 3, 3, 4, 5, 7])
112
+ ae(_ir.length, 6)
113
+
114
+ # ── 11. insort alias ──────────────────────────────────────────────────────────
115
+
116
+ _ins = [2, 4, 6]
117
+ insort(_ins, 5)
118
+ ade(_ins, [2, 4, 5, 6])
119
+
120
+ # ── 12. insort builds a sorted list from scratch ──────────────────────────────
121
+
122
+ _scratch = []
123
+ for _v in [5, 1, 3, 2, 4]:
124
+ insort(_scratch, _v)
125
+ ade(_scratch, [1, 2, 3, 4, 5])
126
+
127
+ # ── 13. Strings (lexicographic) ───────────────────────────────────────────────
128
+
129
+ _s = ['bar', 'baz', 'foo', 'qux']
130
+ ae(bisect_left(_s, 'baz'), 1)
131
+ ae(bisect_right(_s, 'baz'), 2)
132
+ ae(bisect_left(_s, 'car'), 2) # 'car' > 'baz' and < 'foo'
133
+ ae(bisect_right(_s, 'car'), 2)
134
+
135
+ # ── 14. key function — bisect_left / bisect_right ────────────────────────────
136
+
137
+ # list of pairs sorted by first element; search by first element
138
+ _pairs = [[1, 'a'], [3, 'b'], [5, 'c'], [7, 'd']]
139
+ _kfn = def(item): return item[0];
140
+
141
+ ae(bisect_left(_pairs, 3, 0, None, _kfn), 1)
142
+ ae(bisect_right(_pairs, 3, 0, None, _kfn), 2)
143
+ ae(bisect_left(_pairs, 4, 0, None, _kfn), 2)
144
+ ae(bisect_right(_pairs, 4, 0, None, _kfn), 2)
145
+ ae(bisect_left(_pairs, 0, 0, None, _kfn), 0)
146
+ ae(bisect_right(_pairs, 8, 0, None, _kfn), 4)
147
+
148
+ # ── 15. insort with lo/hi bounds ─────────────────────────────────────────────
149
+ # Only the sub-slice [10, 20, 30] (indices 3..5) is searched;
150
+ # 15 is inserted at index 4, between 10 and 20.
151
+
152
+ _bounded = [1, 2, 3, 10, 20, 30]
153
+ insort_right(_bounded, 15, 3, 6)
154
+ ade(_bounded, [1, 2, 3, 10, 15, 20, 30])
155
+
156
+ _bounded2 = [1, 2, 3, 10, 20, 30]
157
+ insort_left(_bounded2, 20, 3, 6) # left of existing 20
158
+ ae(_bounded2[4], 20)
159
+ ae(_bounded2[5], 20)
160
+ ae(_bounded2.length, 7)
161
+
162
+ # ── 16. Duplicate-value stability ────────────────────────────────────────────
163
+
164
+ _dup = [1, 2, 2, 2, 3]
165
+ ae(bisect_left(_dup, 2), 1)
166
+ ae(bisect_right(_dup, 2), 4)
167
+
168
+ _dl = [1, 2, 2, 3]
169
+ insort_left(_dl, 2)
170
+ # new 2 inserted at index 1 (leftmost)
171
+ ae(_dl[1], 2)
172
+ ae(_dl.length, 5)
173
+
174
+ _dr = [1, 2, 2, 3]
175
+ insort_right(_dr, 2)
176
+ # new 2 inserted at index 3 (rightmost)
177
+ ae(_dr[3], 2)
178
+ ae(_dr.length, 5)