de-shell 0.2.1__tar.gz → 0.4.0__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.
- {de_shell-0.2.1 → de_shell-0.4.0}/PKG-INFO +24 -8
- {de_shell-0.2.1 → de_shell-0.4.0}/README.md +21 -5
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/__init__.py +1 -1
- de_shell-0.4.0/de_shell/js/main/framing.test.ts +125 -0
- de_shell-0.4.0/de_shell/js/main/framing.ts +34 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/index.ts +4 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/pythonEnv.test.ts +16 -4
- de_shell-0.4.0/de_shell/js/main/relay.test.ts +815 -0
- de_shell-0.4.0/de_shell/js/main/relay.ts +258 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/FigureFrame.tsx +15 -2
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/figureBridge.test.ts +29 -1
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/figureBridge.ts +30 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/index.ts +1 -1
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/testing/harness.cjs +35 -20
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/testing/harness.test.cjs +30 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/figure.py +80 -1
- de_shell-0.4.0/de_shell/remote_client.py +199 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell.egg-info/PKG-INFO +24 -8
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell.egg-info/SOURCES.txt +6 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell.egg-info/requires.txt +1 -1
- {de_shell-0.2.1 → de_shell-0.4.0}/pyproject.toml +70 -2
- {de_shell-0.2.1 → de_shell-0.4.0}/tests/test_boundary.py +1 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/tests/test_figure.py +82 -1
- de_shell-0.4.0/tests/test_remote_client.py +408 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/LICENSE +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/context.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/figure_registry.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/lifecycle.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/registry.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/actions/wizard.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/app.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/compute.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/debug_flags.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/ipc.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/__main__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/backendProcess.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/backendProcess.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/config.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/dialogs.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/envProgress.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/errorReport.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/problemLog.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/pythonEnv.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/sentryEnvelope.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/sentryEnvelope.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/stdoutDemux.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/stdoutDemux.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/updater.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/updaterErrors.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/updaterErrors.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/main/window.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/package.json +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/preload/index.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/figureBridge.react.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/protocol.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/shellState.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/shellState.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/sizeReporter.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/js/renderer/sizeReporter.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/log_stream.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/colormaps.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/selectors/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/selectors/utils.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/plotting/stream.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/process_guard.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/session.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/testing/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell/timing.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell.egg-info/dependency_links.txt +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/de_shell.egg-info/top_level.txt +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/setup.cfg +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/tests/test_actions.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/tests/test_session_base.py +0 -0
- {de_shell-0.2.1 → de_shell-0.4.0}/tests/test_stream.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: de-shell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The Electron + Python desktop-app shell Direct Electron's apps (SpyDE, Ground Crew, Autopilot) are assembled from
|
|
5
5
|
Author-email: Carter Francis <cartsfrancis@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/directelectron/de-shell
|
|
8
8
|
Project-URL: Repository, https://github.com/directelectron/de-shell
|
|
9
9
|
Project-URL: Issues, https://github.com/directelectron/de-shell/issues
|
|
10
|
-
Project-URL: Changelog, https://github.com/directelectron/de-shell/blob/main/CHANGELOG.
|
|
10
|
+
Project-URL: Changelog, https://github.com/directelectron/de-shell/blob/main/CHANGELOG.rst
|
|
11
11
|
Keywords: electron,desktop,anyplotlib,electron-microscopy,direct-electron
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.10
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: numpy>=1.20.0
|
|
29
|
-
Requires-Dist: anyplotlib>=0.
|
|
29
|
+
Requires-Dist: anyplotlib>=0.10.0
|
|
30
30
|
Requires-Dist: pyyaml
|
|
31
31
|
Provides-Extra: tests
|
|
32
32
|
Requires-Dist: pytest>=3.6; extra == "tests"
|
|
@@ -122,9 +122,10 @@ peer dependencies — react, electron, electron-updater, @playwright/test —
|
|
|
122
122
|
are the app's to declare; every app already does. The e2e specs take the
|
|
123
123
|
harness from `shell/testing/harness.cjs`.
|
|
124
124
|
|
|
125
|
-
Autopilot
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
All three apps are wired this way: Autopilot, SpyDE
|
|
126
|
+
([directelectron/spyde#152](https://github.com/directelectron/spyde/pull/152))
|
|
127
|
+
and Ground Crew ([directelectron/de_ground_crew#183](https://github.com/directelectron/de_ground_crew/pull/183)),
|
|
128
|
+
pinned to the `0.2` line.
|
|
128
129
|
|
|
129
130
|
## Developing it
|
|
130
131
|
|
|
@@ -149,8 +150,20 @@ tests, and builds the wheel and checks what it carries.
|
|
|
149
150
|
|
|
150
151
|
The version is written once, in `de_shell/__init__.py`. To release:
|
|
151
152
|
|
|
152
|
-
1.
|
|
153
|
-
|
|
153
|
+
1. Run **Prepare Release** from the Actions tab and pick the bump (`minor`,
|
|
154
|
+
`bugfix`, `major`, `pre-release`, or `finalize` to drop a `bN` suffix). It
|
|
155
|
+
bumps `__version__`, assembles `CHANGELOG.rst` from the news fragments in
|
|
156
|
+
`upcoming_changes/`, runs the pre-flight checks, and opens a release PR that
|
|
157
|
+
names the one tag that will pass.
|
|
158
|
+
2. Review and merge that PR, then tag the merge commit and push the tag — the
|
|
159
|
+
PR body has the exact commands.
|
|
160
|
+
|
|
161
|
+
The tag has to match `__version__` exactly; going through the workflow makes
|
|
162
|
+
them agree by construction, because the PR *is* the bump. To assemble the
|
|
163
|
+
changelog by hand instead, `uv tool run towncrier build --version X.Y.Z` does
|
|
164
|
+
the same thing — but stage `upcoming_changes/` with `git add -A`, because
|
|
165
|
+
towncrier deletes the fragments it consumed and a plain `git add CHANGELOG.rst`
|
|
166
|
+
leaves the deletions behind for the next release to re-publish.
|
|
154
167
|
|
|
155
168
|
`.github/workflows/publish.yml` builds the distributions, refuses a tag that
|
|
156
169
|
does not match `__version__`, and uploads to PyPI through trusted publishing
|
|
@@ -190,6 +203,9 @@ SpyDE commit the app copies were taken from:
|
|
|
190
203
|
* **The protocol is the contract.** `PLOTAPP:` JSON lines and `PLOTBIN:`
|
|
191
204
|
binary frames over the sidecar's stdio. Both halves of it live in this one
|
|
192
205
|
package on purpose; keep it that way.
|
|
206
|
+
* **Every pull request carries its own changelog entry**, as a news fragment
|
|
207
|
+
under [`upcoming_changes/`](upcoming_changes/README.rst) — one file per PR,
|
|
208
|
+
so two of them never conflict over the same lines of `CHANGELOG.rst`.
|
|
193
209
|
* **LF line endings**, enforced by `.gitattributes`.
|
|
194
210
|
|
|
195
211
|
## License
|
|
@@ -87,9 +87,10 @@ peer dependencies — react, electron, electron-updater, @playwright/test —
|
|
|
87
87
|
are the app's to declare; every app already does. The e2e specs take the
|
|
88
88
|
harness from `shell/testing/harness.cjs`.
|
|
89
89
|
|
|
90
|
-
Autopilot
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
All three apps are wired this way: Autopilot, SpyDE
|
|
91
|
+
([directelectron/spyde#152](https://github.com/directelectron/spyde/pull/152))
|
|
92
|
+
and Ground Crew ([directelectron/de_ground_crew#183](https://github.com/directelectron/de_ground_crew/pull/183)),
|
|
93
|
+
pinned to the `0.2` line.
|
|
93
94
|
|
|
94
95
|
## Developing it
|
|
95
96
|
|
|
@@ -114,8 +115,20 @@ tests, and builds the wheel and checks what it carries.
|
|
|
114
115
|
|
|
115
116
|
The version is written once, in `de_shell/__init__.py`. To release:
|
|
116
117
|
|
|
117
|
-
1.
|
|
118
|
-
|
|
118
|
+
1. Run **Prepare Release** from the Actions tab and pick the bump (`minor`,
|
|
119
|
+
`bugfix`, `major`, `pre-release`, or `finalize` to drop a `bN` suffix). It
|
|
120
|
+
bumps `__version__`, assembles `CHANGELOG.rst` from the news fragments in
|
|
121
|
+
`upcoming_changes/`, runs the pre-flight checks, and opens a release PR that
|
|
122
|
+
names the one tag that will pass.
|
|
123
|
+
2. Review and merge that PR, then tag the merge commit and push the tag — the
|
|
124
|
+
PR body has the exact commands.
|
|
125
|
+
|
|
126
|
+
The tag has to match `__version__` exactly; going through the workflow makes
|
|
127
|
+
them agree by construction, because the PR *is* the bump. To assemble the
|
|
128
|
+
changelog by hand instead, `uv tool run towncrier build --version X.Y.Z` does
|
|
129
|
+
the same thing — but stage `upcoming_changes/` with `git add -A`, because
|
|
130
|
+
towncrier deletes the fragments it consumed and a plain `git add CHANGELOG.rst`
|
|
131
|
+
leaves the deletions behind for the next release to re-publish.
|
|
119
132
|
|
|
120
133
|
`.github/workflows/publish.yml` builds the distributions, refuses a tag that
|
|
121
134
|
does not match `__version__`, and uploads to PyPI through trusted publishing
|
|
@@ -155,6 +168,9 @@ SpyDE commit the app copies were taken from:
|
|
|
155
168
|
* **The protocol is the contract.** `PLOTAPP:` JSON lines and `PLOTBIN:`
|
|
156
169
|
binary frames over the sidecar's stdio. Both halves of it live in this one
|
|
157
170
|
package on purpose; keep it that way.
|
|
171
|
+
* **Every pull request carries its own changelog entry**, as a news fragment
|
|
172
|
+
under [`upcoming_changes/`](upcoming_changes/README.rst) — one file per PR,
|
|
173
|
+
so two of them never conflict over the same lines of `CHANGELOG.rst`.
|
|
158
174
|
* **LF line endings**, enforced by `.gitattributes`.
|
|
159
175
|
|
|
160
176
|
## License
|
|
@@ -20,6 +20,6 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
#: The package version — the one place it is written. pyproject.toml reads it
|
|
22
22
|
#: (dynamic version) and the release workflow refuses a tag that disagrees.
|
|
23
|
-
__version__ = "0.
|
|
23
|
+
__version__ = "0.4.0"
|
|
24
24
|
|
|
25
25
|
__all__ = ["ipc", "log_stream", "process_guard", "debug_flags", "compute"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* framing.test.ts — the encoders against the existing demuxer: whatever
|
|
3
|
+
* framing.ts writes, stdoutDemux.ts must read back unchanged, whole or split
|
|
4
|
+
* one byte at a time.
|
|
5
|
+
*
|
|
6
|
+
* Run: `node --test de_shell/js/main/framing.test.ts`, or via `npm run test:unit`.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { encodeBinary, encodeMessage } from './framing.ts'
|
|
11
|
+
import { createStdoutDemux } from './stdoutDemux.ts'
|
|
12
|
+
|
|
13
|
+
type Event =
|
|
14
|
+
| { kind: 'message'; msg: Record<string, unknown> }
|
|
15
|
+
| { kind: 'stream'; text: string }
|
|
16
|
+
| { kind: 'binary'; header: Record<string, unknown>; payload: string }
|
|
17
|
+
|
|
18
|
+
/** Feed `stream` to a fresh demuxer in `step`-byte slices; return the event trace. */
|
|
19
|
+
function decode(stream: Buffer, step = stream.length || 1): Event[] {
|
|
20
|
+
const events: Event[] = []
|
|
21
|
+
const demux = createStdoutDemux({
|
|
22
|
+
onMessage: (msg) => events.push({ kind: 'message', msg }),
|
|
23
|
+
onStream: (text) => events.push({ kind: 'stream', text }),
|
|
24
|
+
onBinary: (header, payload) =>
|
|
25
|
+
events.push({ kind: 'binary', header, payload: payload.toString('hex') }),
|
|
26
|
+
})
|
|
27
|
+
for (let pos = 0; pos < stream.length; pos += step) {
|
|
28
|
+
demux.push(stream.subarray(pos, pos + step))
|
|
29
|
+
}
|
|
30
|
+
return events
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A whole binary frame as it goes on the wire: prefix, header, then the caller's payload. */
|
|
34
|
+
function frame(header: Record<string, unknown>, payload: Buffer): Buffer {
|
|
35
|
+
return Buffer.concat([...encodeBinary(header, payload), payload])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function patternPayload(n: number): Buffer {
|
|
39
|
+
const p = Buffer.allocUnsafe(n)
|
|
40
|
+
for (let i = 0; i < n; i++) p[i] = i & 0xff // includes 0x0a bytes
|
|
41
|
+
return p
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('a message is one PLOTAPP line and decodes to the same object', () => {
|
|
45
|
+
const msg = { type: 'state_update', key: 'clim', value: [0, 255], nested: { ok: true, none: null } }
|
|
46
|
+
const bytes = encodeMessage(msg)
|
|
47
|
+
assert.equal(bytes.subarray(0, 8).toString('ascii'), 'PLOTAPP:')
|
|
48
|
+
assert.equal(bytes.indexOf(0x0a), bytes.length - 1, 'exactly one newline, at the end')
|
|
49
|
+
assert.deepEqual(decode(bytes), [{ kind: 'message', msg }])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('a newline inside a string stays escaped, so the message is still one line', () => {
|
|
53
|
+
const msg = { type: 'status', text: 'line one\nline two\r\n' }
|
|
54
|
+
const bytes = encodeMessage(msg)
|
|
55
|
+
assert.equal(bytes.indexOf(0x0a), bytes.length - 1)
|
|
56
|
+
assert.deepEqual(decode(bytes), [{ kind: 'message', msg }])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('a frame with an empty payload decodes with an empty payload', () => {
|
|
60
|
+
assert.deepEqual(decode(frame({ fig_id: 'f3', key: 'spec' }, Buffer.alloc(0))), [
|
|
61
|
+
{ kind: 'binary', header: { fig_id: 'f3', key: 'spec' }, payload: '' },
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a payload holding newlines and PLOTBIN:/PLOTAPP: lookalikes decodes whole, at any chunking', () => {
|
|
66
|
+
const nasty = Buffer.concat([
|
|
67
|
+
Buffer.from('\nPLOTBIN:9:9\nPLOTAPP:{}\n', 'ascii'),
|
|
68
|
+
patternPayload(3000),
|
|
69
|
+
])
|
|
70
|
+
const stream = Buffer.concat([
|
|
71
|
+
frame({ fig_id: 'f1', key: 'image' }, nasty),
|
|
72
|
+
encodeMessage({ type: 'done' }),
|
|
73
|
+
])
|
|
74
|
+
const expected: Event[] = [
|
|
75
|
+
{ kind: 'binary', header: { fig_id: 'f1', key: 'image' }, payload: nasty.toString('hex') },
|
|
76
|
+
{ kind: 'message', msg: { type: 'done' } },
|
|
77
|
+
]
|
|
78
|
+
for (const step of [stream.length, 65536, 7, 1]) {
|
|
79
|
+
assert.deepEqual(decode(stream, step), expected, `diverged at ${step}-byte chunks`)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a non-ASCII header: hlen counts UTF-8 bytes, not characters', () => {
|
|
84
|
+
const header = { fig_id: 'f', key: 'k', label: 'εxx Å' }
|
|
85
|
+
const [prefix, head] = encodeBinary(header, Buffer.from([1, 2, 3]))
|
|
86
|
+
const json = JSON.stringify(header)
|
|
87
|
+
assert.notEqual(Buffer.byteLength(json, 'utf8'), json.length, 'the fixture must be multi-byte')
|
|
88
|
+
assert.equal(prefix.toString('ascii'), `PLOTBIN:${Buffer.byteLength(json, 'utf8')}:3\n`)
|
|
89
|
+
assert.equal(head.toString('utf8'), json)
|
|
90
|
+
assert.deepEqual(decode(frame(header, Buffer.from([1, 2, 3])), 1), [
|
|
91
|
+
{ kind: 'binary', header, payload: '010203' },
|
|
92
|
+
])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('encodeBinary returns the prefix and header only; the payload is never copied', () => {
|
|
96
|
+
const payload = patternPayload(64)
|
|
97
|
+
const parts = encodeBinary({ fig_id: 'f', key: 'k' }, payload)
|
|
98
|
+
assert.equal(parts.length, 2)
|
|
99
|
+
assert.ok(!parts.includes(payload))
|
|
100
|
+
assert.match(parts[0].toString('ascii'), /^PLOTBIN:\d+:64\n$/)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('the layout is the documented wire format, byte for byte', () => {
|
|
104
|
+
// PLOTBIN:<hlen>:<plen>\n<header json><payload>, as anyplotlib's
|
|
105
|
+
// _binary_frame.encode_frame writes it and stdoutDemux.ts parses it.
|
|
106
|
+
const header = { fig_id: 'f', key: 'k', dims: [2, 2] }
|
|
107
|
+
const payload = Buffer.from([0, 10, 255, 10])
|
|
108
|
+
const json = JSON.stringify(header)
|
|
109
|
+
assert.deepEqual(frame(header, payload), Buffer.concat([
|
|
110
|
+
Buffer.from(`PLOTBIN:${Buffer.byteLength(json)}:4\n${json}`, 'utf8'),
|
|
111
|
+
payload,
|
|
112
|
+
]))
|
|
113
|
+
assert.deepEqual(encodeMessage({ type: 'done' }), Buffer.from('PLOTAPP:{"type":"done"}\n', 'utf8'))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('a non-finite number is refused rather than framed', () => {
|
|
117
|
+
for (const bad of [NaN, Infinity, -Infinity]) {
|
|
118
|
+
assert.throws(() => encodeMessage({ type: 'fit', value: bad }), RangeError)
|
|
119
|
+
assert.throws(
|
|
120
|
+
() => encodeBinary({ fig_id: 'f', key: 'k', clim: [0, bad] }, Buffer.alloc(0)),
|
|
121
|
+
RangeError,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
assert.throws(() => encodeMessage({ a: { b: [1, NaN] } }), /non-finite/)
|
|
125
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* framing.ts — encoders for the backend wire format; the inverse of
|
|
3
|
+
* stdoutDemux.ts.
|
|
4
|
+
*
|
|
5
|
+
* PLOTAPP:<json>\n a message
|
|
6
|
+
* PLOTBIN:<hlen>:<plen>\n<header json><payload> a binary frame
|
|
7
|
+
*
|
|
8
|
+
* The layout ipc.emit and anyplotlib's _binary_frame.encode_frame write on the
|
|
9
|
+
* Python side. hlen and plen are byte counts. The JSON text is JSON.stringify's
|
|
10
|
+
* own (compact, raw UTF-8); every decoder of the format accepts it. Imports
|
|
11
|
+
* nothing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** JSON.stringify replacer: a non-finite number is not JSON, so refuse it rather than emit null or NaN. */
|
|
15
|
+
function refuseNonFinite(key: string, value: unknown): unknown {
|
|
16
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
17
|
+
throw new RangeError(`cannot frame the non-finite number ${value}${key ? ` at key "${key}"` : ''}`)
|
|
18
|
+
}
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `PLOTAPP:<json>\n`. JSON escapes every newline inside a string, so the message is one line. */
|
|
23
|
+
export function encodeMessage(obj: Record<string, unknown>): Buffer {
|
|
24
|
+
return Buffer.from(`PLOTAPP:${JSON.stringify(obj, refuseNonFinite)}\n`, 'utf8')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The prefix line and the header bytes of a PLOTBIN frame. The payload is the
|
|
29
|
+
* caller's Buffer, written as its own chunk after these two, never copied.
|
|
30
|
+
*/
|
|
31
|
+
export function encodeBinary(header: Record<string, unknown>, payload: Buffer): [Buffer, Buffer] {
|
|
32
|
+
const head = Buffer.from(JSON.stringify(header, refuseNonFinite), 'utf8')
|
|
33
|
+
return [Buffer.from(`PLOTBIN:${head.length}:${payload.length}\n`, 'ascii'), head]
|
|
34
|
+
}
|
|
@@ -22,6 +22,10 @@ export {
|
|
|
22
22
|
export { recordProblem, recordedProblems } from './problemLog'
|
|
23
23
|
export type { Problem } from './problemLog'
|
|
24
24
|
|
|
25
|
+
export { createRelay } from './relay'
|
|
26
|
+
export type { Relay, RelayConnection, RelayCloseReason, RelayOptions } from './relay'
|
|
27
|
+
export { encodeMessage, encodeBinary } from './framing'
|
|
28
|
+
|
|
25
29
|
export {
|
|
26
30
|
initErrorReporting, reportingConfigured, collectDiagnostics, submitReport,
|
|
27
31
|
} from './errorReport'
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* Run: `node --test src/pythonEnv.test.ts` (from packages/shell-main/), or via
|
|
11
11
|
* the `test:unit` npm script.
|
|
12
12
|
*/
|
|
13
|
-
import { test } from 'node:test'
|
|
13
|
+
import { test, after } from 'node:test'
|
|
14
14
|
import assert from 'node:assert/strict'
|
|
15
|
-
import { mkdtempSync, writeFileSync, mkdirSync } from 'fs'
|
|
15
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs'
|
|
16
16
|
import { join } from 'path'
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { configureShell } from './config.ts'
|
|
@@ -24,17 +24,29 @@ configureShell({
|
|
|
24
24
|
pythonModule: 'testapp',
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
+
/** Every temp directory these tests make, removed when the file is done. */
|
|
28
|
+
const tempDirs: string[] = []
|
|
29
|
+
after(() => {
|
|
30
|
+
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function tempDir(prefix: string): string {
|
|
34
|
+
const dir = mkdtempSync(join(tmpdir(), prefix))
|
|
35
|
+
tempDirs.push(dir)
|
|
36
|
+
return dir
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
/** A directory containing a uv stub under both spellings, so these tests do not
|
|
28
40
|
* fork on the host platform. */
|
|
29
41
|
function dirWithUv(): string {
|
|
30
|
-
const dir =
|
|
42
|
+
const dir = tempDir('uv-stub-')
|
|
31
43
|
writeFileSync(join(dir, 'uv'), '')
|
|
32
44
|
writeFileSync(join(dir, 'uv.exe'), '')
|
|
33
45
|
return dir
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
function emptyDir(): string {
|
|
37
|
-
return
|
|
49
|
+
return tempDir('uv-none-')
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/** An env whose PATH and every fallback root point somewhere we control. */
|