rapydscript-ns 0.9.1 → 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.
- package/CHANGELOG.md +22 -1
- package/PYTHON_GAPS.md +420 -0
- package/README.md +154 -30
- package/TODO.md +22 -7
- package/language-service/index.js +241 -12
- package/language-service/language-service.d.ts +1 -1
- package/memory/project_string_impl.md +43 -0
- package/package.json +6 -2
- package/release/baselib-plain-pretty.js +248 -38
- package/release/baselib-plain-ugly.js +8 -8
- package/release/compiler.js +821 -305
- package/release/signatures.json +15 -15
- package/src/ast.pyj +4 -1
- package/src/baselib-builtins.pyj +56 -2
- package/src/baselib-containers.pyj +2 -0
- package/src/baselib-errors.pyj +7 -3
- package/src/baselib-internal.pyj +51 -6
- package/src/baselib-str.pyj +5 -3
- package/src/lib/asyncio.pyj +534 -0
- package/src/lib/base64.pyj +399 -0
- package/src/lib/bisect.pyj +73 -0
- package/src/lib/collections.pyj +1 -1
- package/src/lib/csv.pyj +494 -0
- package/src/lib/heapq.pyj +98 -0
- package/src/lib/html.pyj +382 -0
- package/src/lib/http/__init__.pyj +98 -0
- package/src/lib/http/client.pyj +304 -0
- package/src/lib/http/cookies.pyj +236 -0
- package/src/lib/logging.pyj +672 -0
- package/src/lib/pythonize.pyj +1 -1
- package/src/lib/string.pyj +357 -0
- package/src/lib/textwrap.pyj +329 -0
- package/src/lib/urllib/__init__.pyj +14 -0
- package/src/lib/urllib/error.pyj +66 -0
- package/src/lib/urllib/parse.pyj +475 -0
- package/src/lib/urllib/request.pyj +86 -0
- package/src/monaco-language-service/analyzer.js +5 -2
- package/src/monaco-language-service/completions.js +26 -0
- package/src/monaco-language-service/diagnostics.js +204 -5
- package/src/monaco-language-service/index.js +2 -2
- package/src/monaco-language-service/scope.js +1 -0
- package/src/output/functions.pyj +152 -6
- package/src/output/loops.pyj +26 -2
- package/src/output/modules.pyj +1 -1
- package/src/output/operators.pyj +15 -0
- package/src/output/stream.pyj +0 -1
- package/src/parse.pyj +80 -17
- package/src/tokenizer.pyj +1 -1
- package/test/async_generators.pyj +144 -0
- package/test/asyncio.pyj +307 -0
- package/test/base64.pyj +202 -0
- package/test/bisect.pyj +178 -0
- package/test/csv.pyj +405 -0
- package/test/float_special.pyj +64 -0
- package/test/heapq.pyj +174 -0
- package/test/html.pyj +212 -0
- package/test/http.pyj +259 -0
- package/test/imports.pyj +7 -0
- package/test/logging.pyj +356 -0
- package/test/long.pyj +130 -0
- package/test/parenthesized_with.pyj +141 -0
- package/test/python_compat.pyj +3 -5
- package/test/python_modulo.pyj +76 -0
- package/test/python_modulo_off.pyj +21 -0
- package/test/str.pyj +14 -0
- package/test/string.pyj +245 -0
- package/test/textwrap.pyj +172 -0
- package/test/type_display.pyj +48 -0
- package/test/type_enforcement.pyj +164 -0
- package/test/unit/index.js +80 -6
- package/test/unit/language-service-completions.js +119 -0
- package/test/unit/language-service-scope.js +32 -0
- package/test/unit/language-service.js +128 -4
- package/test/unit/run-language-service.js +17 -3
- package/test/unit/web-repl.js +2094 -29
- package/test/urllib.pyj +193 -0
- package/tools/compile.js +1 -1
- package/tools/compiler.d.ts +367 -0
- package/tools/embedded_compiler.js +7 -7
- package/web-repl/main.js +1 -1
- package/web-repl/rapydscript.js +3 -3
- package/test/omit_function_metadata.pyj +0 -20
package/README.md
CHANGED
|
@@ -592,30 +592,64 @@ RapydScript supports Python's ``/`` and ``*`` parameter separators:
|
|
|
592
592
|
greet("Bob", greeting="Hi") # Hi, Bob!
|
|
593
593
|
greet("Carol", punctuation=".") # Hello, Carol.
|
|
594
594
|
greet("Dave", greeting="Hey", punctuation="?") # Hey, Dave?
|
|
595
|
-
|
|
596
|
-
# name is positional-only: greet(name="Alice") would silently ignore the kwarg
|
|
597
|
-
# punctuation is keyword-only: must be passed as punctuation="."
|
|
598
595
|
```
|
|
599
596
|
|
|
600
597
|
The two separators can be combined, and each section can have its own default
|
|
601
598
|
values. All combinations supported by Python 3.8+ are accepted.
|
|
602
599
|
|
|
603
|
-
RapydScript is lenient
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
This is consistent with RapydScript's general approach of favouring
|
|
607
|
-
interoperability over strict enforcement.
|
|
600
|
+
By default RapydScript is lenient about argument conventions (to favour
|
|
601
|
+
JavaScript interoperability). Enable strict Python-style enforcement with the
|
|
602
|
+
``type_enforcement`` flag described below.
|
|
608
603
|
|
|
609
604
|
The Monaco language service correctly shows ``/`` and ``*`` separators in
|
|
610
605
|
signature help and hover tooltips.
|
|
611
606
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
607
|
+
### Argument type enforcement
|
|
608
|
+
|
|
609
|
+
``from __python__ import type_enforcement`` activates Python-compatible runtime
|
|
610
|
+
argument checking for every function defined in that scope:
|
|
611
|
+
|
|
612
|
+
| Check | Behaviour |
|
|
613
|
+
|---|---|
|
|
614
|
+
| Too many positional arguments | ``TypeError`` |
|
|
615
|
+
| Missing required positional argument | ``TypeError`` |
|
|
616
|
+
| Positional-only arg passed as a keyword argument | ``TypeError`` |
|
|
617
|
+
| Missing required keyword-only argument | ``TypeError`` |
|
|
618
|
+
| Argument value does not match its type annotation | ``TypeError`` |
|
|
619
|
+
|
|
620
|
+
```py
|
|
621
|
+
from __python__ import type_enforcement
|
|
622
|
+
|
|
623
|
+
def greet(name, /, greeting="Hello", *, punctuation="!"):
|
|
624
|
+
return greeting + ", " + name + punctuation
|
|
625
|
+
|
|
626
|
+
greet("Alice") # Hello, Alice!
|
|
627
|
+
greet("Bob", greeting="Hi") # Hi, Bob!
|
|
628
|
+
greet("Carol", punctuation=".") # Hello, Carol.
|
|
629
|
+
|
|
630
|
+
greet(name="Dave") # TypeError: positional-only arg 'name' passed as kwarg
|
|
631
|
+
greet("Eve") # TypeError: missing required keyword-only argument 'punctuation'
|
|
632
|
+
# (only when punctuation has no default)
|
|
633
|
+
|
|
634
|
+
def add(a: int, b: int) -> int:
|
|
635
|
+
return a + b
|
|
636
|
+
|
|
637
|
+
add(1, 2) # 3
|
|
638
|
+
add("x", 2) # TypeError: argument 'a' must be int
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The flag applies to the function definitions that follow it in scope — it does
|
|
642
|
+
not retroactively affect imported functions or functions defined before the
|
|
643
|
+
import. It can be combined with any other ``from __python__ import`` flag.
|
|
644
|
+
|
|
645
|
+
The Monaco language service also reports violations **statically** for
|
|
646
|
+
calls to locally-defined enforced functions, underlining the problem at the
|
|
647
|
+
call site with an error marker.
|
|
648
|
+
|
|
649
|
+
One difference compared to un-enforced RapydScript: without
|
|
650
|
+
``type_enforcement``, missing arguments become ``undefined`` (JavaScript
|
|
651
|
+
default) instead of raising ``TypeError``, and positional/keyword-only
|
|
652
|
+
conventions are not checked.
|
|
619
653
|
|
|
620
654
|
When creating callbacks to pass to other JavaScript libraries, it is often the
|
|
621
655
|
case that the external library expects a function that receives an *options
|
|
@@ -1343,6 +1377,49 @@ ba.clear() # remove all bytes
|
|
|
1343
1377
|
ba += bytearray([7, 8]) # in-place concatenation
|
|
1344
1378
|
```
|
|
1345
1379
|
|
|
1380
|
+
### `long` — arbitrary-precision integers
|
|
1381
|
+
|
|
1382
|
+
RapydScript provides a `long` builtin backed by JavaScript's native `BigInt`,
|
|
1383
|
+
giving you arbitrary-precision integers beyond the safe range of JS `Number`.
|
|
1384
|
+
|
|
1385
|
+
```python
|
|
1386
|
+
a = long(10)
|
|
1387
|
+
b = long(3)
|
|
1388
|
+
|
|
1389
|
+
a + b # long(13)
|
|
1390
|
+
a - b # long(7)
|
|
1391
|
+
a * b # long(30)
|
|
1392
|
+
a // b # long(3) — floor division (Python semantics, not JS truncation)
|
|
1393
|
+
a % b # long(1) — Python-style modulo (result has same sign as divisor)
|
|
1394
|
+
a ** b # long(1000)
|
|
1395
|
+
long(-7) // long(2) # long(-4) — floors toward −∞ (JS BigInt would give −3)
|
|
1396
|
+
long(-7) % long(3) # long(2) — result matches sign of divisor
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
#### Precision beyond JS Number
|
|
1400
|
+
|
|
1401
|
+
The main motivation for `long` is numbers outside the safe integer range of
|
|
1402
|
+
JavaScript `Number` (`2^53 − 1 = 9007199254740991`):
|
|
1403
|
+
|
|
1404
|
+
```python
|
|
1405
|
+
n = long('9007199254740993') # 2^53 + 1 — cannot be represented as a JS Number
|
|
1406
|
+
str(n) # '9007199254740993' (exact)
|
|
1407
|
+
str(n + long(1)) # '9007199254740994' (still exact)
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
#### Operator overloading
|
|
1411
|
+
|
|
1412
|
+
`//`, `%`, and `**` require `overload_operators` (on by default) to emit the
|
|
1413
|
+
BigInt-aware helpers rather than the raw JS operators. If you have explicitly
|
|
1414
|
+
disabled operator overloading with `from __python__ import no_overload_operators`,
|
|
1415
|
+
`//` will call `Math.floor(a / b)` which throws a `TypeError` at runtime for
|
|
1416
|
+
`long` values.
|
|
1417
|
+
|
|
1418
|
+
#### Browser / environment support
|
|
1419
|
+
|
|
1420
|
+
`BigInt` is supported in all modern browsers (Chrome 67+, Firefox 68+, Safari
|
|
1421
|
+
14+, Node.js 10.3+). Code using `long` will not run in Internet Explorer.
|
|
1422
|
+
|
|
1346
1423
|
### `issubclass`
|
|
1347
1424
|
|
|
1348
1425
|
`issubclass(cls, classinfo)` checks whether a class is a subclass of another
|
|
@@ -3046,6 +3123,34 @@ By default, generators are down-converted to ES 5 switch statements. Pass
|
|
|
3046
3123
|
compiler options) to emit native ES 6 generator functions instead, which are
|
|
3047
3124
|
smaller and faster.
|
|
3048
3125
|
|
|
3126
|
+
### Async generators (`async def` with `yield`)
|
|
3127
|
+
|
|
3128
|
+
Async generators — `async def` functions that contain `yield` — are also
|
|
3129
|
+
supported, with the same syntax as Python:
|
|
3130
|
+
|
|
3131
|
+
```py
|
|
3132
|
+
async def stream(urls):
|
|
3133
|
+
for url in urls:
|
|
3134
|
+
body = await fetch(url)
|
|
3135
|
+
yield body
|
|
3136
|
+
|
|
3137
|
+
async def main():
|
|
3138
|
+
async for body in stream(['/a', '/b']):
|
|
3139
|
+
print(body)
|
|
3140
|
+
```
|
|
3141
|
+
|
|
3142
|
+
Calling an async-generator function returns an async iterator immediately
|
|
3143
|
+
(not a `Promise`). Each `.next()` call returns a `Promise` that resolves to a
|
|
3144
|
+
`{value, done}` record, matching the JS async-iterator protocol. Manual
|
|
3145
|
+
iteration via `await it.next()` and `await it.asend()` works (`.asend` is
|
|
3146
|
+
aliased to `.next`, mirroring Python's async-generator API). Whole iterators
|
|
3147
|
+
are best consumed with `async for x in iter:`, which compiles to JS
|
|
3148
|
+
`for await ... of`.
|
|
3149
|
+
|
|
3150
|
+
`async for` is an ES2018 feature, so it is always emitted in modern form
|
|
3151
|
+
regardless of `--js-version`. The runtime must support `Symbol.asyncIterator`
|
|
3152
|
+
(Node 10+, Chrome 63+, Firefox 57+, Safari 11.1+).
|
|
3153
|
+
|
|
3049
3154
|
Modules
|
|
3050
3155
|
-------
|
|
3051
3156
|
|
|
@@ -3059,6 +3164,16 @@ You can import things from modules, just like you would in python:
|
|
|
3059
3164
|
from mypackage.mymodule import something, something_else
|
|
3060
3165
|
```
|
|
3061
3166
|
|
|
3167
|
+
Multi-line parenthesized imports (Python 3.10+ style) are fully supported, including trailing commas:
|
|
3168
|
+
|
|
3169
|
+
```py
|
|
3170
|
+
from mypackage.mymodule import (
|
|
3171
|
+
something,
|
|
3172
|
+
something_else as alias,
|
|
3173
|
+
another_thing,
|
|
3174
|
+
)
|
|
3175
|
+
```
|
|
3176
|
+
|
|
3062
3177
|
When you import modules, the RapydScript compiler automatically generates a
|
|
3063
3178
|
single large JavaScript file containing all the imported packages/modules and
|
|
3064
3179
|
their dependencies, recursively. This makes it very easy to integrate the
|
|
@@ -3349,6 +3464,7 @@ One of Python's main strengths is the number of libraries available to the devel
|
|
|
3349
3464
|
# groupby, islice, pairwise, starmap, takewhile, zip_longest,
|
|
3350
3465
|
# product, permutations, combinations, combinations_with_replacement
|
|
3351
3466
|
io # StringIO (in-memory text stream), BytesIO (in-memory binary stream)
|
|
3467
|
+
base64 # b64encode/decode, urlsafe_b64encode/decode, b32encode/decode, b16encode/decode, encodebytes/decodebytes
|
|
3352
3468
|
|
|
3353
3469
|
For the most part, the logic implemented in these libraries functions identically to the Python versions. I'd be happy to include more libraries, if other members of the community want them. However, unlike most other Python-to-JavaScript compilers, RapydScript doesn't need them to be complete since there are already tons of available JavaScript libraries that it can use natively.
|
|
3354
3470
|
|
|
@@ -3618,6 +3734,7 @@ with no flags enabled, pass ``--legacy-rapydscript`` on the command line.
|
|
|
3618
3734
|
| `overload_getitem` | `obj[key]` dispatches to `__getitem__` / `__setitem__` / `__delitem__` on objects that define them. On by default. |
|
|
3619
3735
|
| `overload_operators` | Arithmetic and bitwise operators (`+`, `-`, `*`, `/`, `//`, `%`, `**`, `&`, `\|`, `^`, `<<`, `>>`) dispatch to dunder methods (`__add__`, `__sub__`, etc.) and their reflected variants. Unary `-`/`+`/`~` dispatch to `__neg__`/`__pos__`/`__invert__`. On by default. |
|
|
3620
3736
|
| `strict_arithmetic` | When `overload_operators` is active, incompatible operand types (e.g. `int + str`) raise `TypeError` instead of silently coercing as JavaScript would. On by default; disable with `from __python__ import no_strict_arithmetic` to revert to JavaScript coercion behaviour. Internal RapydScript library code is unaffected. |
|
|
3737
|
+
| `python_modulo` | The `%` operator follows Python semantics: the result takes the sign of the divisor (`-7 % 3 == 2`, `7 % -3 == -2`). On by default. Custom `__mod__` / `__rmod__` are dispatched even without `overload_operators`. Disable with `from __python__ import no_python_modulo` to revert to raw JS remainder. Has no effect when `overload_operators` is active (the operator dispatch path always uses Python semantics). |
|
|
3621
3738
|
| `truthiness` | Boolean tests and `bool()` dispatch to `__bool__` and treat empty containers as falsy, matching Python semantics. On by default. |
|
|
3622
3739
|
| `bound_methods` | Method references (`obj.method`) are automatically bound to their object, so they can be passed as callbacks without losing `self`. On by default. |
|
|
3623
3740
|
| `hash_literals` | `{k: v}` creates a Python `dict` (alias for `dict_literals`; kept for backward compatibility). On by default. |
|
|
@@ -3688,7 +3805,7 @@ when the editor is torn down.
|
|
|
3688
3805
|
| `stdlibFiles` | `{name: source}` | `{}` | Like `virtualFiles` but treated as stdlib — always available and never produce bad-import warnings. |
|
|
3689
3806
|
| `dtsFiles` | `[{name, content}]` | `[]` | TypeScript `.d.ts` files loaded at startup. |
|
|
3690
3807
|
| `loadDts` | `(name) => Promise<string>` | — | Async callback for lazy-loading `.d.ts` content on demand. |
|
|
3691
|
-
| `extraBuiltins` | `
|
|
3808
|
+
| `extraBuiltins` | `string[]` | `[]` | Extra global names that suppress undefined-symbol warnings. |
|
|
3692
3809
|
| `pythonFlags` | string | — | Comma-separated Python flags to enable globally (e.g. `"dict_literals,overload_getitem"`). See [Python Flags](#python-flags) above. |
|
|
3693
3810
|
|
|
3694
3811
|
### Runtime API
|
|
@@ -3838,7 +3955,6 @@ original `.py` file with working breakpoints and correct error stack frames.
|
|
|
3838
3955
|
| `python_flags` | string | — | Comma-separated Python flags to enable for this compilation (e.g. `"dict_literals,overload_operators"`). See [Python Flags](#python-flags) above. Flags set here override any inherited from a previous `compile()` call on a streaming compiler. |
|
|
3839
3956
|
| `virtual_files` | `{name: source}` | — | Map of module-name → RapydScript source for modules importable via `import`. Only used when the underlying streaming compiler was created with a virtual-file context (as `web_repl()` does). |
|
|
3840
3957
|
| `discard_asserts` | bool | `false` | Strip all `assert` statements from the output. |
|
|
3841
|
-
| `omit_function_metadata` | bool | `false` | Omit per-function metadata (e.g. argument names) from the output for smaller bundles. |
|
|
3842
3958
|
| `write_name` | bool | `false` | Emit a `var __name__ = "…"` assignment at the top of the output. |
|
|
3843
3959
|
| `tree_shake` | bool | `false` | Remove unused imported names from the output (requires stdlib imports). |
|
|
3844
3960
|
| `filename` | string | `'<input>'` | Source filename embedded in the source map and used in error messages. |
|
|
@@ -3886,7 +4002,7 @@ Python Feature Coverage
|
|
|
3886
4002
|
| `try / else` | `else` block runs only when no exception was raised |
|
|
3887
4003
|
| `for / else` | `else` block runs when loop completes without `break`; nested break isolation works |
|
|
3888
4004
|
| `while / else` | `else` block runs when loop condition becomes `False` without a `break`; nested `break` isolation correct |
|
|
3889
|
-
| `with A() as a, B() as b:`
|
|
4005
|
+
| `with A() as a, B() as b:` / `with (A() as a, B() as b):` | Multiple context managers in one statement (flat or parenthesized 3.10+ form); exits in LIFO order (Python-correct); multi-line and trailing comma supported |
|
|
3890
4006
|
| `callable(fn)` | Works for plain functions and objects with `__call__` |
|
|
3891
4007
|
| `round(x, ndigits=0)` | Full Python semantics including negative `ndigits` |
|
|
3892
4008
|
| `enumerate(iterable, start=0)` | `start` parameter supported |
|
|
@@ -3904,10 +4020,13 @@ Python Feature Coverage
|
|
|
3904
4020
|
| `list + list` concatenation | `[1,2] + [3,4]` returns `[1, 2, 3, 4]`; `+=` extends in-place. No flag required. |
|
|
3905
4021
|
| `match / case` | Structural pattern matching (Python 3.10) fully supported |
|
|
3906
4022
|
| Variable type annotations `x: int = 1` | Parsed and ignored (no runtime enforcement); annotated assignments work normally |
|
|
4023
|
+
| Argument type enforcement (`from __python__ import type_enforcement`) | Activates runtime `TypeError` for wrong argument count, positional-only kwargs, missing required keyword-only args, and type-annotated arg mismatches; Monaco language service reports violations statically at call sites |
|
|
3907
4024
|
| Ellipsis literal `...` as expression | Parsed as a valid expression; evaluates to JS `undefined` at runtime |
|
|
3908
4025
|
| Generator `.throw()` | Works via JS generator protocol |
|
|
3909
4026
|
| Generator `.send()` | Works via `g.next(value)` |
|
|
3910
4027
|
| `yield from` | Works; return value of sub-generator is not accessible |
|
|
4028
|
+
| `async def` with `yield` (async generators) | Combining `async def` with `yield` produces an async generator. Calling it returns an async iterator immediately; `.next()` / `.asend()` return `Promise` objects, matching Python's `__anext__` / `asend` API (`.send` and `.asend` are aliased to `.next`). `await` may appear before, between, or after `yield`. Compiled to a sync wrapper around `async function*`. |
|
|
4029
|
+
| `async for x in iter:` | Drives async iterators (`Symbol.asyncIterator`) and async generators. Compiles to native `for await ... of` (ES2018) regardless of `--js-version`. Works with sync iterables that yield Promises as well. |
|
|
3911
4030
|
| `+=`, `-=`, `*=`, `/=`, `//=`, `**=`, `%=`, `&=`, `\|=`, `^=`, `<<=`, `>>=` | All augmented assignments work |
|
|
3912
4031
|
| `raise X from Y` exception chaining | Sets `__cause__` on the thrown exception; `from None` also supported |
|
|
3913
4032
|
| Starred assignment `a, *b, c = ...` | Works |
|
|
@@ -3922,7 +4041,7 @@ Python Feature Coverage
|
|
|
3922
4041
|
| Classes, inheritance, decorators, `__dunder__` methods | Fully supported |
|
|
3923
4042
|
| Nested class definitions | Accessible as `Outer.Inner` and via instance (`self.Inner`); arbitrary nesting depth; nested class may inherit from outer-scope classes |
|
|
3924
4043
|
| List / dict / set comprehensions, generator expressions | Fully supported |
|
|
3925
|
-
| f-strings, `str.format()`, `format()` builtin, all common `str.*` methods | Fully supported |
|
|
4044
|
+
| f-strings, `str.format()`, `format()` builtin, all common `str.*` methods | Fully supported; includes `f'{x=}'` debug format (prints `x=<value>`), format-spec (`f'{x=:.2f}'`), and conversion (`f'{x=!r}'`) |
|
|
3926
4045
|
| `abs()`, `divmod()`, `any()`, `all()`, `sum()`, `min()`, `max()` | All work |
|
|
3927
4046
|
| `sorted()`, `reversed()`, `zip()`, `map()`, `filter()` | All work |
|
|
3928
4047
|
| `zip(strict=True)` | Raises `ValueError` when iterables have different lengths; equal-length iterables work normally |
|
|
@@ -3951,6 +4070,7 @@ Python Feature Coverage
|
|
|
3951
4070
|
| `dict \| dict` and `dict \|= dict` (Python 3.9+) | Dict merge via `\|` creates a new merged dict (right-side values win); `\|=` updates in-place. Active via `overload_operators` + `dict_literals` (both on by default). |
|
|
3952
4071
|
| `__format__` dunder | `format()`, `str.format()`, and f-strings all dispatch to `__format__`; default `__format__` auto-generated for classes (returns `__str__()` for empty spec, raises `TypeError` for non-empty spec); `!r`/`!s`/`!a` transformers bypass `__format__` correctly |
|
|
3953
4072
|
| `slice(start, stop[, step])` | Full Python `slice` class: 1-, 2-, and 3-argument forms; `.start`, `.stop`, `.step` attributes; `.indices(length)` → `(start, stop, step)`; `str()` / `repr()`; `isinstance(s, slice)`; equality `==`; use as subscript `lst[s]` (read, write, `del`) all work. |
|
|
4073
|
+
| Multi-line parenthesized `from … import (…)` with optional trailing comma | Each imported name (with optional `as` alias) may appear on its own line; a trailing comma before `)` is accepted — identical to Python syntax |
|
|
3954
4074
|
| `__import__(name[, globals, locals, fromlist, level])` | Runtime lookup in the compiled module registry (`ρσ_modules`). Without `fromlist` (or empty `fromlist`) returns the top-level package, matching Python's semantics. `ImportError` / `ModuleNotFoundError` raised for unknown modules. **Constraint**: the module must have been statically imported elsewhere in the source so it is present in `ρσ_modules`. |
|
|
3955
4075
|
| `ImportError`, `ModuleNotFoundError` | Both defined as runtime exception classes; `ModuleNotFoundError` is a subclass of `ImportError` (same as Python 3.6+). |
|
|
3956
4076
|
| `bytes(source[, encoding[, errors]])` and `bytearray(source[, encoding[, errors]])` | Full Python semantics: construction from integer (n zero bytes), list/iterable of ints (0–255), string + encoding (`utf-8`, `latin-1`, `ascii`), `Uint8Array`, or another `bytes`/`bytearray`. Key methods: `hex([sep[, bytes_per_sep]])`, `decode(encoding)`, `fromhex(s)` (static), `count`, `find`, `rfind`, `index`, `rindex`, `startswith`, `endswith`, `join`, `split`, `replace`, `strip`, `lstrip`, `rstrip`, `upper`, `lower`, `copy`. `bytearray` adds: `append`, `extend`, `insert`, `pop`, `remove`, `reverse`, `clear`, `__setitem__` (single and slice). Slicing returns a new `bytes`/`bytearray`. `+` concatenates; `*` repeats; `==` compares element-wise; `in` tests integer or subsequence membership; `isinstance(x, bytes)` / `isinstance(x, bytearray)` work; `bytearray` is a subclass of `bytes`. `repr()` returns `b'...'` notation. `Uint8Array` values may be passed anywhere a `bytes`-like object is accepted. |
|
|
@@ -3958,6 +4078,7 @@ Python Feature Coverage
|
|
|
3958
4078
|
| `float.is_integer()` | Returns `True` if the float has no fractional part (i.e. is a whole number), `False` otherwise. `float('inf').is_integer()` and `float('nan').is_integer()` both return `False`, matching Python semantics. Added to `Number.prototype` in the baselib so it works on any numeric literal or variable. |
|
|
3959
4079
|
| `int.bit_length()` | Returns the number of bits needed to represent the integer in binary, excluding the sign and leading zeros. `(0).bit_length()` → `0`; `(255).bit_length()` → `8`; `(256).bit_length()` → `9`; sign is ignored (`(-5).bit_length()` → `3`). Added to `Number.prototype` in the baselib. |
|
|
3960
4080
|
| Arithmetic type coercion — `TypeError` on incompatible operands | `1 + '1'` raises `TypeError: unsupported operand type(s) for +: 'int' and 'str'`; all arithmetic operators (`+`, `-`, `*`, `/`, `//`, `%`, `**`) enforce compatible types in their `ρσ_op_*` helpers. `bool` is treated as numeric (like Python's `int` subclass). Activated by `overload_operators` (on by default). String `+` string and numeric `+` numeric are allowed; mixed types raise `TypeError` with a Python-style message. |
|
|
4081
|
+
| `long(val[, base])` — arbitrary-precision integers | Backed by JS `BigInt`. `long(42)`, `long('ff', 16)`, `long('1010', 2)`, `long(True)`. Arithmetic: `+`, `-`, `*`, `//`, `%`, `**` — `//` and `%` follow Python floor-division semantics (floor toward −∞), not JS BigInt truncation. `/` raises `TypeError` (use `//`). Bitwise: `&`, `\|`, `^`, `<<`, `>>`. Comparisons: `<`, `<=`, `>`, `>=`, `==`, `!=`. `isinstance(x, long)` works. Mixing `long` with `int` or `float` raises `TypeError`. Requires `BigInt` (Chrome 67+, Firefox 68+, Safari 14+, Node 10.3+). |
|
|
3961
4082
|
| `complex(real=0, imag=0)` and complex literals `3+4j` | Full complex number type via `ρσ_complex` class. `complex(real, imag)`, `complex(string)` (parses `'3+4j'`), and `j`/`J` imaginary literal suffix (e.g. `4j`, `3.5J`). Attributes: `.real`, `.imag`. Methods: `conjugate()`, `__abs__()`, `__bool__()`, `__repr__()`, `__str__()`. Arithmetic: `+`, `-`, `*`, `/`, `**` via dunder methods (or operator overloading with `overload_operators`). `abs(z)` dispatches `__abs__`. `isinstance(z, complex)` works. String representation matches Python: `(3+4j)`, `4j`, `(3-0j)`. |
|
|
3962
4083
|
| `eval(expr[, globals[, locals]])` | String literals are compiled as **RapydScript source** at compile time (the compiler parses and transpiles the string, just like Python's `eval` takes Python source). `eval(expr)` maps to native JS direct `eval` for scope access. `eval(expr, globals)` / `eval(expr, globals, locals)` use `Function` constructor with explicit bindings; `locals` override `globals`. Runtime `ρσ_` helpers referenced in the compiled string are automatically injected into the Function scope. Only string *literals* are transformed at compile time; dynamic strings are passed through unchanged. |
|
|
3963
4084
|
| `exec(code[, globals[, locals]])` | String literals are compiled as **RapydScript source** at compile time. Executes the compiled code string; always returns `None`. Without `globals`/`locals` uses native `eval` (scope access). With `globals`/`locals` uses `Function` constructor — mutable objects (lists, dicts) passed in `globals` are accessible by reference, so side-effects are visible after the call. `ρσ_dict` instances (created when `dict_literals` flag is active) are correctly unwrapped via their `jsmap` backing store. |
|
|
@@ -4041,7 +4162,7 @@ Modules with a `src/lib/` implementation available are marked ✅. All others ar
|
|
|
4041
4162
|
| `math` | ✅ | Full implementation in `src/lib/math.pyj` |
|
|
4042
4163
|
| `random` | ✅ | RC4-seeded PRNG in `src/lib/random.pyj` |
|
|
4043
4164
|
| `re` | ✅ | Regex wrapper in `src/lib/re.pyj`; uses the JS engine — full PCRE-level support on modern runtimes: positive/negative lookbehind (ES2018+, including variable-width), unicode via automatic `u` flag (ES2015+), `re.fullmatch()`, `re.S`/`re.NOFLAG` aliases. `MatchObject.start()`/`.end()` return exact positions on runtimes with the ES2022 `d` flag (Node 18+); heuristic fallback on older runtimes. Conditional groups `(?(id)yes\|no)` are not supported (JS limitation) and raise `re.error`. |
|
|
4044
|
-
| `encodings` | ✅ |
|
|
4165
|
+
| `encodings` | ✅ | UTF-8 encode/decode helpers and low-level base64 utilities; for the standard Python API use the `base64` module instead |
|
|
4045
4166
|
| `collections` | ✅ | `defaultdict`, `Counter`, `OrderedDict`, `deque` |
|
|
4046
4167
|
| `functools` | ✅ | `reduce`, `partial`, `wraps`, `lru_cache` |
|
|
4047
4168
|
| `itertools` | ✅ | Common iteration tools |
|
|
@@ -4055,19 +4176,22 @@ Modules with a `src/lib/` implementation available are marked ✅. All others ar
|
|
|
4055
4176
|
| `datetime` | ✅ | `date`, `time`, `datetime`, `timedelta`, `MINYEAR`, `MAXYEAR` in `src/lib/datetime.pyj`; construction, `isoformat()`, `fromisoformat()`, `today()`/`now()`, `combine()`, arithmetic (via `__add__`/`__sub__`; operators need `overload_operators`), comparisons, `replace()`, `strftime()` (%Y %m %d %H %M %S %f %A %a %B %b %j %p %I %%); no tzinfo/timezone support |
|
|
4056
4177
|
| `json` | ✅ | `dumps()`, `loads()`, `dump()`, `load()`, `JSONDecodeError` in `src/lib/json.pyj`; `indent`, `sort_keys`, `separators`, `dflt` (Python's `default`) callback, `object_hook`, `object_pairs_hook`, `parse_float`, `parse_int` supported; backed by the JS `JSON` global; note: `default` is a JS reserved word — use `dflt=` instead |
|
|
4057
4178
|
| `io` | ✅ | `StringIO`, `BytesIO`, `UnsupportedOperation` in `src/lib/io.pyj`; `read([size])`, `readline([size])`, `readlines([hint])`, `write()`, `writelines()`, `seek(pos[, whence])`, `tell()`, `truncate([pos])`, `getvalue()`, `close()`, `closed`; context manager support; `readable()`, `writable()`, `seekable()` return True; `newline` parameter accepted for API compatibility |
|
|
4058
|
-
| `
|
|
4179
|
+
| `base64` | ✅ | `b64encode(s[, altchars])`, `b64decode(s[, altchars][, validate])`, `standard_b64encode`, `standard_b64decode`, `urlsafe_b64encode`, `urlsafe_b64decode`, `b32encode`, `b32decode([casefold][, map01])`, `b16encode`, `b16decode([casefold])`, `encodebytes`, `decodebytes`, `Error` in `src/lib/base64.pyj`; pure-JS implementation, works in browser and Node; all encode functions return `bytes` |
|
|
4180
|
+
| `string` | ✅ | Character constants (`ascii_letters`, `ascii_lowercase`, `ascii_uppercase`, `digits`, `hexdigits`, `octdigits`, `punctuation`, `whitespace`, `printable`), `Template` ($-substitution with `substitute(mapping)` and `safe_substitute(mapping)`), `Formatter` (`format(*args)`, `vformat(fmt, args, kwargs)`, `format_field`, `convert_field`, `get_value`, `get_field`, `parse`) in `src/lib/string.pyj`; `Formatter.format()` accepts positional args — for named-field substitution use `vformat(fmt, [], {'key': val})`; `Template.substitute()` and `safe_substitute()` accept a dict (or plain object) |
|
|
4181
|
+
| `html` | ✅ | `escape(s[, quote=True])`, `unescape(s)`, `HTMLParser` (event-driven parser; subclass and override `handle_starttag`, `handle_endtag`, `handle_data`, `handle_comment`, `handle_decl`, etc.) in `src/lib/html.pyj`; full HTML4 + common HTML5 named entity table; `convert_charrefs=True` by default (entities in text and attributes auto-decoded); `html.parser` sub-module not available as a separate import — use `from html import HTMLParser` |
|
|
4182
|
+
| `asyncio` | ✅ | `sleep()`, `gather()`, `create_task()`, `ensure_future()`, `run()`, `shield()`, `wait_for()`, `wait()`, `iscoroutine()`, `iscoroutinefunction()`, `current_task()`, `all_tasks()`, `get_event_loop()`, `get_running_loop()`, `new_event_loop()` in `src/lib/asyncio.pyj`; synchronization primitives: `Lock`, `Event`, `Semaphore`, `BoundedSemaphore`, `Queue`, `LifoQueue`, `PriorityQueue`; exceptions: `CancelledError`, `TimeoutError`, `InvalidStateError`, `QueueEmpty`, `QueueFull`; `async def` / `await` syntax compiles to native JS async functions; `async def` with `yield` (async generators) and `async for` are supported (see "Async generators" section); `asyncio.run(coro)` returns the Promise (cannot block in JS); `async with` is not supported — use `.acquire()`/`.release()` directly |
|
|
4183
|
+
| `urllib` | ✅ | `urllib.parse`: `quote()`, `unquote()`, `quote_plus()`, `unquote_plus()`, `urlencode()`, `urlsplit()` → `SplitResult`, `urlparse()` → `ParseResult`, `urlunsplit()`, `urlunparse()`, `urljoin()`, `parse_qs()`, `parse_qsl()` in `src/lib/urllib/parse.pyj`; `urllib.error`: `URLError`, `HTTPError` in `src/lib/urllib/error.pyj`; `urllib.request`: `urlopen(url[, data[, timeout]])` returns a Promise wrapping the Fetch API in `src/lib/urllib/request.pyj` — use with `await` in an async function; `SplitResult`/`ParseResult` expose `.scheme`, `.netloc`, `.path`, `.query`, `.fragment` plus `.hostname`, `.port`, `.username`, `.password` and `.geturl()`; backed by the JS `URL` constructor and `encodeURIComponent`/`decodeURIComponent`; RFC 3986 unreserved characters (`A-Z a-z 0-9 - _ . ~`) never encoded |
|
|
4184
|
+
| `bisect` | ✅ | `bisect_left(a, x[, lo[, hi[, key]]])`, `bisect_right(a, x[, lo[, hi[, key]]])`, `bisect` (alias for `bisect_right`), `insort_left(a, x[, lo[, hi[, key]]])`, `insort_right(a, x[, lo[, hi[, key]]])`, `insort` (alias for `insort_right`) in `src/lib/bisect.pyj`; pure binary-search implementation matching Python semantics; optional `lo`/`hi` bounds for sub-slice search; optional `key` function (Python 3.10+ API) applied to array elements only — `x` must be directly comparable to `key(a[i])` results |
|
|
4185
|
+
| `http` | ✅ | `http`: `HTTPStatus` class with integer constants for all standard status codes (`OK=200`, `NOT_FOUND=404`, `IM_A_TEAPOT=418`, etc.) in `src/lib/http/__init__.pyj`; `http.client`: `HTTPConnection(host[, port[, timeout]])`, `HTTPSConnection(host[, port[, timeout]])`, `HTTPResponse` (`.status`, `.reason`, `.headers`, `.url`, `getheader(name[, default])`, `getheaders()`, `read()` → Promise, `json()` → Promise), `HTTPException`, `NotConnected`, `InvalidURL`, `RemoteDisconnected`, `HTTP_PORT=80`, `HTTPS_PORT=443` in `src/lib/http/client.pyj`; `http.cookies`: `SimpleCookie` (dict-like cookie container with `load(rawdata)`, `output([header, sep])`, `items()`, `keys()`, `values()`), `Morsel` (`.key`, `.value`, `.coded_value`, `OutputString()`, `output([header])`), `CookieError` in `src/lib/http/cookies.pyj`; `getresponse()` wraps the Fetch API and returns a Promise — use with `await`; non-2xx responses are returned normally (check `resp.status` yourself, unlike `urllib.request`); `http.server` not available |
|
|
4186
|
+
| `csv` | ✅ | `reader(csvfile[, dialect][, **fmtparams])`, `writer(csvfile[, dialect][, **fmtparams])`, `DictReader(f[, fieldnames[, restkey[, restval[, dialect, ...]]]])`, `DictWriter(f, fieldnames[, restval[, extrasaction[, dialect, ...]]])` in `src/lib/csv.pyj`; `Dialect`, `excel`, `excel_tab`, `unix_dialect` classes; `register_dialect`, `unregister_dialect`, `get_dialect`, `list_dialects`, `field_size_limit`; `QUOTE_MINIMAL`, `QUOTE_ALL`, `QUOTE_NONNUMERIC`, `QUOTE_NONE` constants; `Error` exception; backed by pure-JS parser with full support for quoted fields (including multi-line), custom delimiters, `skipinitialspace`, double-quote escaping, escape characters; accepts list-of-strings, file-like objects (with `.read()`), or any iterable; note: `csv.Error` shadows the native JS Error if imported via `from csv import Error` in web-repl mode — catch via `Exception` instead |
|
|
4187
|
+
| `textwrap` | ✅ | `wrap(text[, width=70[, ...]])`, `fill(text[, width=70[, ...]])`, `shorten(text, width[, ...])`, `dedent(text)`, `indent(text, prefix[, predicate])`, `TextWrapper` class in `src/lib/textwrap.pyj`; `TextWrapper` supports all standard options: `width`, `initial_indent`, `subsequent_indent`, `expand_tabs`, `tabsize`, `replace_whitespace`, `fix_sentence_endings`, `break_long_words`, `break_on_hyphens`, `drop_whitespace`, `max_lines`, `placeholder`; module-level `wrap`/`fill`/`shorten` accept the same options as explicit keyword parameters; `shorten()` normalises internal whitespace before truncating; `break_on_hyphens=True` (default) splits on em-dashes (2+ hyphens) between word characters, matching Python 3 behaviour |
|
|
4188
|
+
| `logging` | ✅ | `getLogger(name)`, `basicConfig(**kwargs)`, `debug/info/warning/error/critical/exception/log()` module-level shortcuts, `disable(level)`, `addLevelName(level, name)`, `getLevelName(level)`, `captureWarnings(capture)`, `makeLogRecord(dict)`, `setLogRecordFactory(factory)`, `getLogRecordFactory()` in `src/lib/logging.pyj`; `Logger` class with `setLevel`, `isEnabledFor`, `getEffectiveLevel`, `addHandler`, `removeHandler`, `hasHandlers`, `addFilter`, `removeFilter`, `debug/info/warning/error/critical/exception/log` methods; `Handler` base class; `StreamHandler(stream=None)` (writes to `console.debug/info/warn/error` when no stream given, or to any object with `.write(s)`); `NullHandler`; `Formatter(fmt, datefmt, style)` with full `%(attr)s/d/f` record formatting and `formatTime(record, datefmt)` (supports `%Y %m %d %H %M %S %f` strftime codes); `Filter(name)` and `Filterer` mixin; `LogRecord` with `getMessage()` supporting `%s/%d/%f/%e/%g/%x/%o/%%` %-formatting; level constants `NOTSET=0`, `DEBUG=10`, `INFO=20`, `WARNING=30`, `ERROR=40`, `CRITICAL=50`; full logger hierarchy (dotted names, `propagate`, `parent`); `lastResort` handler; note: `FileHandler` raises `NotImplementedError` (no file I/O in JS); `exception()` logs at ERROR without automatic exc_info capture |
|
|
4189
|
+
| `heapq` | ✅ | `heappush(heap, item)`, `heappop(heap)`, `heapify(x)`, `heapreplace(heap, item)`, `heappushpop(heap, item)`, `nsmallest(n, iterable[, key])`, `nlargest(n, iterable[, key])` in `src/lib/heapq.pyj`; min-heap operations on plain lists; heap invariant maintained by sift-up/sift-down (ported from CPython); `nsmallest`/`nlargest` support an optional `key` function; `heappop` and `heapreplace` raise `IndexError` on an empty heap; original iterable is not mutated by `nsmallest`/`nlargest` |
|
|
4059
4190
|
| `inspect` | ❌ | `signature`, `getmembers`, `isfunction` etc. not available |
|
|
4060
|
-
| `asyncio` | ❌ | Event loop, `gather`, `sleep`, `Queue`, `Task` wrappers not available; use `async`/`await` |
|
|
4061
4191
|
| `struct` | ❌ | Binary packing/unpacking not available |
|
|
4062
4192
|
| `hashlib` | ❌ | MD5, SHA-256 etc. not available; use Web Crypto API via verbatim JS |
|
|
4063
4193
|
| `hmac` | ❌ | Keyed hashing not available |
|
|
4064
|
-
| `base64` | ❌ (partial) | Partial coverage via `encodings` module; no full `base64` module |
|
|
4065
|
-
| `urllib` | ❌ | URL parsing/encoding (`urllib.parse`) not available; use JS `URL` API |
|
|
4066
|
-
| `html` | ❌ | `escape`, `unescape` not available; use JS DOM APIs |
|
|
4067
|
-
| `csv` | ❌ | CSV parsing not available |
|
|
4068
|
-
| `textwrap` | ❌ | `wrap`, `fill`, `dedent`, `indent` not available |
|
|
4069
4194
|
| `pprint` | ❌ | Pretty-printing not available |
|
|
4070
|
-
| `logging` | ❌ | Logging framework not available; use `console.*` directly |
|
|
4071
4195
|
| `unittest` | ❌ | Not available; RapydScript uses a custom test runner (`node bin/rapydscript test`) |
|
|
4072
4196
|
|
|
4073
4197
|
---
|
|
@@ -4080,7 +4204,7 @@ Features that exist in RapydScript but behave differently from standard Python:
|
|
|
4080
4204
|
|---|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
4081
4205
|
| `is` / `is not` | Object identity | Strict equality `===` / `!==`, which is not object/pointer comparison for JS primitives |
|
|
4082
4206
|
| `//` floor division on floats | `math.floor(a/b)` always | uses `Math.floor` - same result for well-behaved floats |
|
|
4083
|
-
| `%` on negative numbers |
|
|
4207
|
+
| `%` on negative numbers | Result takes the sign of the divisor (`-7 % 3 == 2`) | Same as Python by default (via `python_modulo`, on by default). Disable with `from __python__ import no_python_modulo` to revert to raw JS remainder semantics. |
|
|
4084
4208
|
| `global` / `nonlocal` scoping | Full cross-scope declaration | `global` works for module-level; if a variable exists in both an intermediate outer scope **and** the module-level scope, the outer scope takes precedence (differs from Python where `global` always forces module-level) |
|
|
4085
4209
|
| `Exception.message` | Not standard; use `.args[0]` | `.message` is the standard attribute (JS `Error` style) |
|
|
4086
4210
|
| Function call argument count | Too few args → `TypeError`; too many → `TypeError` | Too few args → extra params are `undefined`; too many → extras silently discarded. No `TypeError` is raised in either case. |
|
|
@@ -4093,7 +4217,7 @@ Features that exist in RapydScript but behave differently from standard Python:
|
|
|
4093
4217
|
| Generators — output format | Native Python generator objects | Down-compiled to ES5 state-machine switch statements by default; pass `--js-version 6` for native ES6 generators (smaller and faster) |
|
|
4094
4218
|
| `dict` key ordering | Insertion order guaranteed (3.7+) | Depends on JS engine (V8 preserves insertion order in practice) |
|
|
4095
4219
|
| Reserved keywords | Python keywords only | All JavaScript reserved words (`default`, `switch`, `delete`, `void`, `typeof`, etc.) are also reserved in RapydScript, since it compiles to JS |
|
|
4096
|
-
| `parenthesized with (A() as a, B() as b):` | Multiple context managers in parenthesized form (3.10+) |
|
|
4220
|
+
| `parenthesized with (A() as a, B() as b):` | Multiple context managers in parenthesized form (3.10+) | Fully supported; multi-line and trailing comma accepted; semantics identical to the flat form |
|
|
4097
4221
|
|
|
4098
4222
|
---
|
|
4099
4223
|
|
package/TODO.md
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
|
|
2
2
|
### libraries
|
|
3
3
|
|
|
4
|
+
- The 7n literal isn't supported by the parser — use BigInt()
|
|
4
5
|
|
|
5
6
|
- remove repl_mode and make repl make a new context for each "run" press
|
|
6
|
-
- `.replace(str, str)` replaces only the first occurrence.
|
|
7
|
-
- eval is js
|
|
8
7
|
|
|
9
|
-
-
|
|
8
|
+
- vscode plugin based on language service?
|
|
10
9
|
|
|
11
|
-
- omit_function_metadata breaks imports - it needs to be changed to only affect imported modules, maybe?
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
I would like you to add support for [python async def functions with yield (async generators)] to rapydscript. It should have the same syntax as the Python implementation, and be transpiled into equivalent javascript. Ensure with unit tests that it transpiles and the output JS runs correctly, and that the language service correctly handles it in parsed code. Make sure it works in the web-repl. Update the README if it has any outdated info about this, and the PYTHON_FEATURE_COVERAGE report. Add a simple example to the bottom of the TODO document using this feature (make no other changes to that file). Remove the suggestion from PYTHON_GAPS if it is there.
|
|
12
|
+
|
|
13
|
+
### Async generator example
|
|
14
|
+
|
|
15
|
+
```py
|
|
16
|
+
# `async def` + `yield` makes an async generator. The function returns an
|
|
17
|
+
# async iterator immediately; values are pulled with `await it.next()` or
|
|
18
|
+
# consumed with `async for`.
|
|
19
|
+
|
|
20
|
+
from asyncio import sleep
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
async def countdown(n):
|
|
23
|
+
while n > 0:
|
|
24
|
+
await sleep(0) # cooperative yield to the event loop
|
|
25
|
+
yield n
|
|
26
|
+
n -= 1
|
|
16
27
|
|
|
28
|
+
async def main():
|
|
29
|
+
async for x in countdown(3):
|
|
30
|
+
print(x) # prints 3, then 2, then 1
|
|
17
31
|
|
|
18
|
-
|
|
32
|
+
main()
|
|
33
|
+
```
|
|
19
34
|
|