concurrent-c-node 0.23.0__tar.gz → 0.23.2__tar.gz

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 (19) hide show
  1. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/PKG-INFO +64 -42
  2. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/README.md +63 -41
  3. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/__init__.py +8 -6
  4. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/benchmarks/vs_alts.py +75 -2
  5. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/concurrent_c_node.egg-info/PKG-INFO +64 -42
  6. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/pyproject.toml +1 -1
  7. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/benchmarks/__init__.py +0 -0
  8. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/benchmarks/multi_domain.py +0 -0
  9. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/broker.cjs +0 -0
  10. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/examples/__init__.py +0 -0
  11. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/examples/bench_wire.py +0 -0
  12. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/examples/use_node.py +0 -0
  13. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/magics.py +0 -0
  14. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/cc_node/stdio_line.cjs +0 -0
  15. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/concurrent_c_node.egg-info/SOURCES.txt +0 -0
  16. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
  17. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/concurrent_c_node.egg-info/requires.txt +0 -0
  18. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/concurrent_c_node.egg-info/top_level.txt +0 -0
  19. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/sreekotay/concurrent-c
@@ -18,6 +18,13 @@ Requires-Dist: ipython>=7; extra == "jupyter"
18
18
  Call Node (and npm packages) from Python.
19
19
  Native types, exceptions, callbacks, and async all cross the boundary.
20
20
 
21
+ ## Why use this
22
+
23
+ Call real Node from Python — `require`, native addons, npm — not a JS
24
+ engine in-process. One child, shm for bulk, `from cc_node import require`
25
+ in Jupyter/Colab with no `%load_ext`. pythonia is the packaged peer; this
26
+ is faster on the wire and honest about bulk and callbacks.
27
+
21
28
  Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
22
29
  strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
23
30
  with your host C compiler. (This bridge itself is pure Python stdlib —
@@ -28,15 +35,19 @@ bridge):
28
35
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
29
36
 
30
37
  ```python
31
- import cc_node
32
-
33
- js = cc_node.create() # always a child `node` process
34
- _ = js.require('lodash') # cwd node_modules
38
+ from cc_node import require # one session child (lazy)
39
+ _ = require('lodash') # cwd node_modules
35
40
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
41
+ ```
36
42
 
43
+ A private child (`create()`) is still there when you want N Nodes or
44
+ an explicit lifetime — not required for the first call.
45
+
46
+ ```python
47
+ import cc_node
48
+ js = cc_node.create() # a private Node, not the session
37
49
  semver = js.require('semver')
38
50
  semver.satisfies('1.2.3', '^1.0.0') # True
39
-
40
51
  js.destroy() # or: with cc_node.create() as js:
41
52
  ```
42
53
 
@@ -44,12 +55,13 @@ The other direction (Python from Node):
44
55
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
45
56
  (in-process by default; vs pymport / ncp / pythonia in that README).
46
57
 
47
- Every `create()` here is a separate Node — real addons, crash isolation,
48
- measurable wire. N domains = N processes.
58
+ Every `create()` is a separate Node — real addons, crash isolation,
59
+ measurable wire. N domains = N processes. `require()` / `get()` share
60
+ one session for the process (the Jupyter kernel).
49
61
 
50
62
  | | this package | CC hosted (`cc_js_new(false, …)`) |
51
63
  |---|---|---|
52
- | API | `cc_node.create()` | `.ccs` program |
64
+ | API | `require()` / `create()` | `.ccs` program |
53
65
  | Where | child `node` | libnode in-process |
54
66
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
55
67
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -60,11 +72,12 @@ measurable wire. N domains = N processes.
60
72
 
61
73
  - Always a child `node`. A call blocks until JS answers; thenables wait
62
74
  in the child. No `{ async: true }`.
75
+ - `from cc_node import require` is the session. `create()` is a private
76
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
63
77
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
78
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
- child per cell. `%load_ext` still works (idempotent).
67
- - `cc_node.require('path')` is `get().require('path')`.
79
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
80
+ session as `require()`.
68
81
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
69
82
  cwd `require` once.
70
83
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -74,35 +87,36 @@ measurable wire. N domains = N processes.
74
87
 
75
88
  ```
76
89
  pip install concurrent-c-node # needs node on PATH
77
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
90
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
78
91
  python -m cc_node.examples.use_node
79
92
  python -m cc_node.examples.bench_wire
80
93
  python -m cc_node.benchmarks.multi_domain
81
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
94
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
82
95
  ```
83
96
 
84
97
  ## Jupyter / Colab
85
98
 
86
- Colab and the usual Jupyter kernel are **Python** this package. Same
87
- calling convention as a script: a cell blocks until Node answers;
88
- thenables wait in the child. Magics and `get()` share **one** session
89
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
- lands in the cell (inherited stdio, line-buffered).
99
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
100
+ `destroy()` for the happy path. One session child for the kernel;
101
+ `console.log` lands in the cell.
91
102
 
92
103
  ```python
93
104
  %pip install concurrent-c-node
94
105
  # if `node` is missing (typical Colab):
95
106
  !apt-get install -y nodejs
96
107
 
97
- import cc_node # magics register; no %load_ext
98
- path = cc_node.require('path')
99
- path.join('a', 'b') # 'a/b'
108
+ from cc_node import require
109
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
100
110
  ```
101
111
 
112
+ `import cc_node` also registers `%%js` (IPython already running; the
113
+ `[jupyter]` extra is only if you want magics without IPython already
114
+ installed).
115
+
102
116
  ```python
103
117
  %%js
104
118
  console.log('hi') # shows in the cell
105
- require('path').join('a', 'b') # last expression comes back as Python
119
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
106
120
  ```
107
121
 
108
122
  ```python
@@ -114,9 +128,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
114
128
 
115
129
  | | |
116
130
  |---|---|
117
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
- | `%load_ext cc_node` | same, idempotent |
119
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
131
+ | `from cc_node import require` | session `require`; spawns on first call |
132
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
133
+ | `%load_ext cc_node` | same, idempotent; not required |
134
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
120
135
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
121
136
  | `-t chunks` / `--to` | store the result in the notebook namespace |
122
137
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -161,25 +176,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
161
176
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
162
177
  0700 dir, 0600 files, removed with the bridge.
163
178
 
164
- ### Vs pythonmonkey / mini-racer / DIY node
179
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
165
180
 
166
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
167
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
181
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
182
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
183
+ packaged peer that is: `require()` on import, one child. Bulk is a
184
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
185
+ lies). Snapshot:
168
186
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
169
187
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
170
188
 
171
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
172
- |---|---|---|---|---|---|
173
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
174
- | callback | **36µs** | — | — | 1µs | — |
175
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
176
- | `require('fs')` | yes | yes | yes | no | no |
177
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
189
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
190
+ |---|---|---|---|---|---|---|
191
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
192
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
193
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
194
+ | `require('fs')` | yes | yes | yes | yes | no | no |
195
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
178
196
 
179
197
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
180
198
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
181
- package. Isolated `node -e` per call is ~1000× a persistent child.
182
- Optional engines SKIP if not importable not package deps.
199
+ package ~2× pythonia on identity, shm for bulk, Python callables are
200
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
201
+ ~1000× a persistent child. Optional engines SKIP if not importable —
202
+ not package deps.
183
203
 
184
204
  The other direction (Python from Node):
185
205
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -233,9 +253,11 @@ total(array.array('d', range(1_000_000)))
233
253
  ## Common issues
234
254
 
235
255
  **`Cannot find module`.** `require` / `import` resolve from the Python
236
- process cwd (`node_modules` next to your program), not from this wheel’s
237
- site-packages. `npm install lodash` in the project directory is the fix;
238
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
256
+ process cwd (`node_modules` next to your notebook or program), not from
257
+ this wheel’s site-packages, and this package does **not** `npm install`
258
+ on a miss (pythonia does). `npm install lodash` in that directory is
259
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
260
+ `PATH`.
239
261
 
240
262
  ### Empty `{}` stays a handle
241
263
 
@@ -293,7 +315,7 @@ Both bridges (npm OIDC + PyPI OIDC):
293
315
 
294
316
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
295
317
  `benchmarks.vs_alts`.
296
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
318
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
297
319
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
298
320
  Own hot path in C/CC → native module (40–90ns) —
299
321
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -3,6 +3,13 @@
3
3
  Call Node (and npm packages) from Python.
4
4
  Native types, exceptions, callbacks, and async all cross the boundary.
5
5
 
6
+ ## Why use this
7
+
8
+ Call real Node from Python — `require`, native addons, npm — not a JS
9
+ engine in-process. One child, shm for bulk, `from cc_node import require`
10
+ in Jupyter/Colab with no `%load_ext`. pythonia is the packaged peer; this
11
+ is faster on the wire and honest about bulk and callbacks.
12
+
6
13
  Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
7
14
  strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
8
15
  with your host C compiler. (This bridge itself is pure Python stdlib —
@@ -13,15 +20,19 @@ bridge):
13
20
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
14
21
 
15
22
  ```python
16
- import cc_node
17
-
18
- js = cc_node.create() # always a child `node` process
19
- _ = js.require('lodash') # cwd node_modules
23
+ from cc_node import require # one session child (lazy)
24
+ _ = require('lodash') # cwd node_modules
20
25
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
26
+ ```
21
27
 
28
+ A private child (`create()`) is still there when you want N Nodes or
29
+ an explicit lifetime — not required for the first call.
30
+
31
+ ```python
32
+ import cc_node
33
+ js = cc_node.create() # a private Node, not the session
22
34
  semver = js.require('semver')
23
35
  semver.satisfies('1.2.3', '^1.0.0') # True
24
-
25
36
  js.destroy() # or: with cc_node.create() as js:
26
37
  ```
27
38
 
@@ -29,12 +40,13 @@ The other direction (Python from Node):
29
40
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
30
41
  (in-process by default; vs pymport / ncp / pythonia in that README).
31
42
 
32
- Every `create()` here is a separate Node — real addons, crash isolation,
33
- measurable wire. N domains = N processes.
43
+ Every `create()` is a separate Node — real addons, crash isolation,
44
+ measurable wire. N domains = N processes. `require()` / `get()` share
45
+ one session for the process (the Jupyter kernel).
34
46
 
35
47
  | | this package | CC hosted (`cc_js_new(false, …)`) |
36
48
  |---|---|---|
37
- | API | `cc_node.create()` | `.ccs` program |
49
+ | API | `require()` / `create()` | `.ccs` program |
38
50
  | Where | child `node` | libnode in-process |
39
51
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
40
52
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -45,11 +57,12 @@ measurable wire. N domains = N processes.
45
57
 
46
58
  - Always a child `node`. A call blocks until JS answers; thenables wait
47
59
  in the child. No `{ async: true }`.
60
+ - `from cc_node import require` is the session. `create()` is a private
61
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
48
62
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
49
63
  else is a `JsHandle` until `str()` / attrs / a call.
50
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
51
- child per cell. `%load_ext` still works (idempotent).
52
- - `cc_node.require('path')` is `get().require('path')`.
64
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
65
+ session as `require()`.
53
66
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
54
67
  cwd `require` once.
55
68
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -59,35 +72,36 @@ measurable wire. N domains = N processes.
59
72
 
60
73
  ```
61
74
  pip install concurrent-c-node # needs node on PATH
62
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
75
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
63
76
  python -m cc_node.examples.use_node
64
77
  python -m cc_node.examples.bench_wire
65
78
  python -m cc_node.benchmarks.multi_domain
66
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
79
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
67
80
  ```
68
81
 
69
82
  ## Jupyter / Colab
70
83
 
71
- Colab and the usual Jupyter kernel are **Python** this package. Same
72
- calling convention as a script: a cell blocks until Node answers;
73
- thenables wait in the child. Magics and `get()` share **one** session
74
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
75
- lands in the cell (inherited stdio, line-buffered).
84
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
85
+ `destroy()` for the happy path. One session child for the kernel;
86
+ `console.log` lands in the cell.
76
87
 
77
88
  ```python
78
89
  %pip install concurrent-c-node
79
90
  # if `node` is missing (typical Colab):
80
91
  !apt-get install -y nodejs
81
92
 
82
- import cc_node # magics register; no %load_ext
83
- path = cc_node.require('path')
84
- path.join('a', 'b') # 'a/b'
93
+ from cc_node import require
94
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
85
95
  ```
86
96
 
97
+ `import cc_node` also registers `%%js` (IPython already running; the
98
+ `[jupyter]` extra is only if you want magics without IPython already
99
+ installed).
100
+
87
101
  ```python
88
102
  %%js
89
103
  console.log('hi') # shows in the cell
90
- require('path').join('a', 'b') # last expression comes back as Python
104
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
91
105
  ```
92
106
 
93
107
  ```python
@@ -99,9 +113,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
99
113
 
100
114
  | | |
101
115
  |---|---|
102
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
103
- | `%load_ext cc_node` | same, idempotent |
104
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
116
+ | `from cc_node import require` | session `require`; spawns on first call |
117
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
118
+ | `%load_ext cc_node` | same, idempotent; not required |
119
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
105
120
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
106
121
  | `-t chunks` / `--to` | store the result in the notebook namespace |
107
122
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -146,25 +161,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
146
161
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
147
162
  0700 dir, 0600 files, removed with the bridge.
148
163
 
149
- ### Vs pythonmonkey / mini-racer / DIY node
164
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
150
165
 
151
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
152
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
166
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
167
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
168
+ packaged peer that is: `require()` on import, one child. Bulk is a
169
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
170
+ lies). Snapshot:
153
171
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
154
172
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
155
173
 
156
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
157
- |---|---|---|---|---|---|
158
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
159
- | callback | **36µs** | — | — | 1µs | — |
160
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
161
- | `require('fs')` | yes | yes | yes | no | no |
162
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
174
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
175
+ |---|---|---|---|---|---|---|
176
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
177
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
178
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
179
+ | `require('fs')` | yes | yes | yes | yes | no | no |
180
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
163
181
 
164
182
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
165
183
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
166
- package. Isolated `node -e` per call is ~1000× a persistent child.
167
- Optional engines SKIP if not importable not package deps.
184
+ package ~2× pythonia on identity, shm for bulk, Python callables are
185
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
186
+ ~1000× a persistent child. Optional engines SKIP if not importable —
187
+ not package deps.
168
188
 
169
189
  The other direction (Python from Node):
170
190
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -218,9 +238,11 @@ total(array.array('d', range(1_000_000)))
218
238
  ## Common issues
219
239
 
220
240
  **`Cannot find module`.** `require` / `import` resolve from the Python
221
- process cwd (`node_modules` next to your program), not from this wheel’s
222
- site-packages. `npm install lodash` in the project directory is the fix;
223
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
241
+ process cwd (`node_modules` next to your notebook or program), not from
242
+ this wheel’s site-packages, and this package does **not** `npm install`
243
+ on a miss (pythonia does). `npm install lodash` in that directory is
244
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
245
+ `PATH`.
224
246
 
225
247
  ### Empty `{}` stays a handle
226
248
 
@@ -278,7 +300,7 @@ Both bridges (npm OIDC + PyPI OIDC):
278
300
 
279
301
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
280
302
  `benchmarks.vs_alts`.
281
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
303
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
282
304
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
283
305
  Own hot path in C/CC → native module (40–90ns) —
284
306
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -1,9 +1,11 @@
1
1
  """cc-node: JavaScript (and every npm package) from Python.
2
2
 
3
- import cc_node
4
- js = cc_node.create() # an Isolation Domain: one node child
5
- _ = js.require('lodash') # resolved from YOUR cwd's node_modules
3
+ from cc_node import require
4
+ _ = require('lodash') # session child; cwd node_modules
6
5
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
6
+
7
+ import cc_node
8
+ js = cc_node.create() # a private child, not the session
7
9
  js.destroy()
8
10
 
9
11
  The mirror of the cc-python bridge, same rules pointed the other way:
@@ -19,8 +21,8 @@ Handles never cross domains; every door after destroy() answers
19
21
  articulately; destroy is idempotent and `with cc_node.create() as js:`
20
22
  scopes it.
21
23
 
22
- Notebooks (Jupyter / Colab): `import cc_node` then `%%js` or
23
- `cc_node.get()` — one session domain, same calling convention.
24
+ Notebooks (Jupyter / Colab): `from cc_node import require` or
25
+ `import cc_node` then `%%js` — one session, same calling convention.
24
26
  """
25
27
  import array
26
28
  import atexit
@@ -41,7 +43,7 @@ __all__ = [
41
43
  "JsError", "JsHandle",
42
44
  "load_ipython_extension", "unload_ipython_extension", "__version__",
43
45
  ]
44
- __version__ = "0.23.0"
46
+ __version__ = "0.23.2"
45
47
 
46
48
  _NO_NODE = (
47
49
  "cc-node: no node executable (install Node, or set CC_NODE_BIN). "
@@ -6,8 +6,9 @@ Real Node (this package)
6
6
  `require('fs')`, native addons, the npm in cwd, crash isolation.
7
7
  Honest alternatives: drive a `node` child yourself. Spawn-per-call
8
8
  (`node -e`) is what people write first. A persistent JSON-stdio loop
9
- is the DIY that actually competes. PyExecJS was that, abandoned.
10
- `pythonodejs` claims an embed; treat as optional if importable.
9
+ is the DIY that actually competes. pythonia (PyPI `javascript`,
10
+ JSPyBridge) is the packaged peer with the same DX — `require()` on
11
+ import, one child. PyExecJS was eval-a-string; abandoned.
11
12
 
12
13
  A JS engine inside CPython (not Node)
13
14
  pythonmonkey — SpiderMonkey + CommonJS `require` of *pure JS*.
@@ -218,6 +219,77 @@ def bench_cc_node():
218
219
  js.destroy()
219
220
 
220
221
 
222
+ def bench_pythonia():
223
+ """JSPyBridge: PyPI `javascript`, npm twin `pythonia`. Real Node child.
224
+ A Python callable the JS side sees is a Promise (await in the child)."""
225
+ try:
226
+ t0 = time.perf_counter()
227
+ from javascript import eval_js, require
228
+ eval_js("return 1")
229
+ _result("pythonia.spawn_ms", round((time.perf_counter() - t0) * 1000))
230
+ except ImportError:
231
+ _result("pythonia.rtt_us", "SKIP not installed")
232
+ _cap("pythonia.require_path", False, "not installed")
233
+ _cap("pythonia.require_fs", False, "not installed")
234
+ return
235
+
236
+ f = eval_js("return (x) => x")
237
+ f(1)
238
+ dt = _time_loop(500, f)
239
+ _result("pythonia.rtt_us", _us(dt, 500))
240
+
241
+ try:
242
+ g = eval_js(
243
+ "return async (cb) => { const v = await cb(21); return v * 2 }")
244
+ if g(lambda x: x + 1) != 44:
245
+ raise RuntimeError("callback mismatch")
246
+ dt = _time_loop(200, lambda _i: g(lambda x: x + 1))
247
+ _result("pythonia.callback_roundtrip_us", _us(dt, 200))
248
+ _cap("pythonia.python_callback", True,
249
+ "JS sees a Promise; await in the child")
250
+ except Exception as e:
251
+ _result("pythonia.callback_roundtrip_us",
252
+ "SKIP %s" % type(e).__name__)
253
+ _cap("pythonia.python_callback", False, str(e)[:80])
254
+
255
+ _result("pythonia.bulk_8mb_shm_ms", "SKIP JSON IPC, no shm")
256
+ try:
257
+ biglist = _bulk_list()
258
+ sm = eval_js("return " + SUM_JS)
259
+ got = sm(biglist)
260
+ expect = _bulk_sum_expect(biglist)
261
+ if abs(float(got) - expect) > 1e-3:
262
+ raise RuntimeError("pythonia bulk sum mismatch")
263
+ t0 = time.perf_counter()
264
+ for _ in range(3):
265
+ sm(biglist)
266
+ _result("pythonia.bulk_8mb_json_list_ms",
267
+ round((time.perf_counter() - t0) / 3 * 1000))
268
+ except Exception as e:
269
+ _result("pythonia.bulk_8mb_json_list_ms",
270
+ "SKIP %s" % type(e).__name__)
271
+
272
+ _cap("pythonia.require_path",
273
+ require("path").join("a", "b") == "a/b")
274
+ fd, tmp = tempfile.mkstemp()
275
+ try:
276
+ os.write(fd, b"hi")
277
+ os.close(fd)
278
+ got = require("fs").readFileSync(tmp, "utf8")
279
+ _cap("pythonia.require_fs", got == "hi")
280
+ finally:
281
+ try:
282
+ os.unlink(tmp)
283
+ except OSError:
284
+ pass
285
+ try:
286
+ then = eval_js(
287
+ "return async (x) => { await Promise.resolve(); return x * 2 }")
288
+ _cap("pythonia.thenable", then(21) == 42)
289
+ except Exception as e:
290
+ _cap("pythonia.thenable", False, str(e)[:80])
291
+
292
+
221
293
  def bench_diy_stdio():
222
294
  t0 = time.perf_counter()
223
295
  diy = DiyStdio()
@@ -408,6 +480,7 @@ def main():
408
480
  print("SKIP need node on PATH")
409
481
  return 1
410
482
  bench_cc_node()
483
+ bench_pythonia()
411
484
  bench_diy_stdio()
412
485
  bench_spawn_each()
413
486
  bench_pythonmonkey()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/sreekotay/concurrent-c
@@ -18,6 +18,13 @@ Requires-Dist: ipython>=7; extra == "jupyter"
18
18
  Call Node (and npm packages) from Python.
19
19
  Native types, exceptions, callbacks, and async all cross the boundary.
20
20
 
21
+ ## Why use this
22
+
23
+ Call real Node from Python — `require`, native addons, npm — not a JS
24
+ engine in-process. One child, shm for bulk, `from cc_node import require`
25
+ in Jupyter/Colab with no `%load_ext`. pythonia is the packaged peer; this
26
+ is faster on the wire and honest about bulk and callbacks.
27
+
21
28
  Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
22
29
  strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
23
30
  with your host C compiler. (This bridge itself is pure Python stdlib —
@@ -28,15 +35,19 @@ bridge):
28
35
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
29
36
 
30
37
  ```python
31
- import cc_node
32
-
33
- js = cc_node.create() # always a child `node` process
34
- _ = js.require('lodash') # cwd node_modules
38
+ from cc_node import require # one session child (lazy)
39
+ _ = require('lodash') # cwd node_modules
35
40
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
41
+ ```
36
42
 
43
+ A private child (`create()`) is still there when you want N Nodes or
44
+ an explicit lifetime — not required for the first call.
45
+
46
+ ```python
47
+ import cc_node
48
+ js = cc_node.create() # a private Node, not the session
37
49
  semver = js.require('semver')
38
50
  semver.satisfies('1.2.3', '^1.0.0') # True
39
-
40
51
  js.destroy() # or: with cc_node.create() as js:
41
52
  ```
42
53
 
@@ -44,12 +55,13 @@ The other direction (Python from Node):
44
55
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
45
56
  (in-process by default; vs pymport / ncp / pythonia in that README).
46
57
 
47
- Every `create()` here is a separate Node — real addons, crash isolation,
48
- measurable wire. N domains = N processes.
58
+ Every `create()` is a separate Node — real addons, crash isolation,
59
+ measurable wire. N domains = N processes. `require()` / `get()` share
60
+ one session for the process (the Jupyter kernel).
49
61
 
50
62
  | | this package | CC hosted (`cc_js_new(false, …)`) |
51
63
  |---|---|---|
52
- | API | `cc_node.create()` | `.ccs` program |
64
+ | API | `require()` / `create()` | `.ccs` program |
53
65
  | Where | child `node` | libnode in-process |
54
66
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
55
67
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -60,11 +72,12 @@ measurable wire. N domains = N processes.
60
72
 
61
73
  - Always a child `node`. A call blocks until JS answers; thenables wait
62
74
  in the child. No `{ async: true }`.
75
+ - `from cc_node import require` is the session. `create()` is a private
76
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
63
77
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
78
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
- child per cell. `%load_ext` still works (idempotent).
67
- - `cc_node.require('path')` is `get().require('path')`.
79
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
80
+ session as `require()`.
68
81
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
69
82
  cwd `require` once.
70
83
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -74,35 +87,36 @@ measurable wire. N domains = N processes.
74
87
 
75
88
  ```
76
89
  pip install concurrent-c-node # needs node on PATH
77
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
90
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
78
91
  python -m cc_node.examples.use_node
79
92
  python -m cc_node.examples.bench_wire
80
93
  python -m cc_node.benchmarks.multi_domain
81
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
94
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
82
95
  ```
83
96
 
84
97
  ## Jupyter / Colab
85
98
 
86
- Colab and the usual Jupyter kernel are **Python** this package. Same
87
- calling convention as a script: a cell blocks until Node answers;
88
- thenables wait in the child. Magics and `get()` share **one** session
89
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
- lands in the cell (inherited stdio, line-buffered).
99
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
100
+ `destroy()` for the happy path. One session child for the kernel;
101
+ `console.log` lands in the cell.
91
102
 
92
103
  ```python
93
104
  %pip install concurrent-c-node
94
105
  # if `node` is missing (typical Colab):
95
106
  !apt-get install -y nodejs
96
107
 
97
- import cc_node # magics register; no %load_ext
98
- path = cc_node.require('path')
99
- path.join('a', 'b') # 'a/b'
108
+ from cc_node import require
109
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
100
110
  ```
101
111
 
112
+ `import cc_node` also registers `%%js` (IPython already running; the
113
+ `[jupyter]` extra is only if you want magics without IPython already
114
+ installed).
115
+
102
116
  ```python
103
117
  %%js
104
118
  console.log('hi') # shows in the cell
105
- require('path').join('a', 'b') # last expression comes back as Python
119
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
106
120
  ```
107
121
 
108
122
  ```python
@@ -114,9 +128,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
114
128
 
115
129
  | | |
116
130
  |---|---|
117
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
- | `%load_ext cc_node` | same, idempotent |
119
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
131
+ | `from cc_node import require` | session `require`; spawns on first call |
132
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
133
+ | `%load_ext cc_node` | same, idempotent; not required |
134
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
120
135
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
121
136
  | `-t chunks` / `--to` | store the result in the notebook namespace |
122
137
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -161,25 +176,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
161
176
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
162
177
  0700 dir, 0600 files, removed with the bridge.
163
178
 
164
- ### Vs pythonmonkey / mini-racer / DIY node
179
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
165
180
 
166
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
167
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
181
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
182
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
183
+ packaged peer that is: `require()` on import, one child. Bulk is a
184
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
185
+ lies). Snapshot:
168
186
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
169
187
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
170
188
 
171
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
172
- |---|---|---|---|---|---|
173
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
174
- | callback | **36µs** | — | — | 1µs | — |
175
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
176
- | `require('fs')` | yes | yes | yes | no | no |
177
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
189
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
190
+ |---|---|---|---|---|---|---|
191
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
192
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
193
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
194
+ | `require('fs')` | yes | yes | yes | yes | no | no |
195
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
178
196
 
179
197
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
180
198
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
181
- package. Isolated `node -e` per call is ~1000× a persistent child.
182
- Optional engines SKIP if not importable not package deps.
199
+ package ~2× pythonia on identity, shm for bulk, Python callables are
200
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
201
+ ~1000× a persistent child. Optional engines SKIP if not importable —
202
+ not package deps.
183
203
 
184
204
  The other direction (Python from Node):
185
205
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -233,9 +253,11 @@ total(array.array('d', range(1_000_000)))
233
253
  ## Common issues
234
254
 
235
255
  **`Cannot find module`.** `require` / `import` resolve from the Python
236
- process cwd (`node_modules` next to your program), not from this wheel’s
237
- site-packages. `npm install lodash` in the project directory is the fix;
238
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
256
+ process cwd (`node_modules` next to your notebook or program), not from
257
+ this wheel’s site-packages, and this package does **not** `npm install`
258
+ on a miss (pythonia does). `npm install lodash` in that directory is
259
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
260
+ `PATH`.
239
261
 
240
262
  ### Empty `{}` stays a handle
241
263
 
@@ -293,7 +315,7 @@ Both bridges (npm OIDC + PyPI OIDC):
293
315
 
294
316
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
295
317
  `benchmarks.vs_alts`.
296
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
318
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
297
319
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
298
320
  Own hot path in C/CC → native module (40–90ns) —
299
321
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.23.0"
7
+ version = "0.23.2"
8
8
  description = "JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"