de-shell 0.2.1__tar.gz → 0.3.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.3.0}/PKG-INFO +24 -8
- {de_shell-0.2.1 → de_shell-0.3.0}/README.md +21 -5
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/__init__.py +1 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/pythonEnv.test.ts +16 -4
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/FigureFrame.tsx +15 -2
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/figureBridge.test.ts +29 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/figureBridge.ts +30 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/index.ts +1 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/testing/harness.cjs +35 -20
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/testing/harness.test.cjs +30 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/figure.py +80 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell.egg-info/PKG-INFO +24 -8
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell.egg-info/requires.txt +1 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/pyproject.toml +70 -2
- {de_shell-0.2.1 → de_shell-0.3.0}/tests/test_figure.py +82 -1
- {de_shell-0.2.1 → de_shell-0.3.0}/LICENSE +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/context.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/figure_registry.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/lifecycle.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/registry.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/actions/wizard.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/app.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/compute.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/debug_flags.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/ipc.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/__main__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/backendProcess.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/backendProcess.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/config.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/dialogs.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/envProgress.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/errorReport.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/index.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/problemLog.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/pythonEnv.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/sentryEnvelope.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/sentryEnvelope.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/stdoutDemux.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/stdoutDemux.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/updater.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/updaterErrors.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/updaterErrors.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/main/window.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/package.json +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/preload/index.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/figureBridge.react.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/protocol.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/shellState.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/shellState.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/sizeReporter.test.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/js/renderer/sizeReporter.ts +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/log_stream.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/colormaps.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/selectors/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/selectors/utils.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/plotting/stream.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/process_guard.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/session.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/testing/__init__.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell/timing.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell.egg-info/SOURCES.txt +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell.egg-info/dependency_links.txt +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/de_shell.egg-info/top_level.txt +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/setup.cfg +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/tests/test_actions.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/tests/test_boundary.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.0}/tests/test_session_base.py +0 -0
- {de_shell-0.2.1 → de_shell-0.3.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.3.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.3.0"
|
|
24
24
|
|
|
25
25
|
__all__ = ["ipc", "log_stream", "process_guard", "debug_flags", "compute"]
|
|
@@ -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. */
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* the host page needs `script-src … blob:` for anyplotlib's ESM boot.
|
|
25
25
|
*/
|
|
26
26
|
import React, { useEffect, useRef } from 'react'
|
|
27
|
-
import { attachFigure, type FigureBridge } from './figureBridge'
|
|
27
|
+
import { attachFigure, readoutListener, type FigureBridge, type ReadoutInfo } from './figureBridge'
|
|
28
28
|
import { createSizeReporter } from './sizeReporter'
|
|
29
29
|
|
|
30
30
|
export interface FigureFrameProps {
|
|
@@ -37,13 +37,16 @@ export interface FigureFrameProps {
|
|
|
37
37
|
title?: string
|
|
38
38
|
/** Called with the frame's pixel size whenever it changes. */
|
|
39
39
|
onResize?: (width: number, height: number) => void
|
|
40
|
+
/** The hover readout, on the shown grid; null when the cursor leaves. Pair it
|
|
41
|
+
* with `FigureView.set_readout_visible(False)` to replace the on-image pill. */
|
|
42
|
+
onReadout?: (info: ReadoutInfo | null) => void
|
|
40
43
|
className?: string
|
|
41
44
|
style?: React.CSSProperties
|
|
42
45
|
'data-testid'?: string
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export function FigureFrame({
|
|
46
|
-
bridge, figId, html, fileUrl, title, onResize, className, style,
|
|
49
|
+
bridge, figId, html, fileUrl, title, onResize, onReadout, className, style,
|
|
47
50
|
'data-testid': testId,
|
|
48
51
|
}: FigureFrameProps) {
|
|
49
52
|
const ref = useRef<HTMLIFrameElement | null>(null)
|
|
@@ -71,6 +74,16 @@ export function FigureFrame({
|
|
|
71
74
|
return () => ro.disconnect()
|
|
72
75
|
}, [figId])
|
|
73
76
|
|
|
77
|
+
// In a ref for the same reason as onResize: an inline prop must not re-run this.
|
|
78
|
+
const onReadoutRef = useRef(onReadout)
|
|
79
|
+
onReadoutRef.current = onReadout
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const listen = readoutListener(() => ref.current?.contentWindow,
|
|
82
|
+
(info) => onReadoutRef.current?.(info))
|
|
83
|
+
window.addEventListener('message', listen)
|
|
84
|
+
return () => window.removeEventListener('message', listen)
|
|
85
|
+
}, [figId])
|
|
86
|
+
|
|
74
87
|
// REGISTRATION IS OWNED BY AN EFFECT, not by the ref callback alone, and it
|
|
75
88
|
// re-registers on every run — see `attachFigure`, which holds the reason and
|
|
76
89
|
// is where the StrictMode double-invoke is pinned by a test.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { test, describe } from 'node:test'
|
|
11
11
|
import assert from 'node:assert/strict'
|
|
12
12
|
|
|
13
|
-
import { attachFigure, createFigureBridge } from './figureBridge.ts'
|
|
13
|
+
import { attachFigure, createFigureBridge, readoutListener } from './figureBridge.ts'
|
|
14
14
|
|
|
15
15
|
/** A stand-in iframe that records what was posted into it. */
|
|
16
16
|
function fakeIframe(testid = 'frame') {
|
|
@@ -245,3 +245,31 @@ describe('attachFigure — the mount lifecycle a figure has to survive', () => {
|
|
|
245
245
|
assert.equal(bridge.iframes.current.get('f1'), frame.el)
|
|
246
246
|
})
|
|
247
247
|
})
|
|
248
|
+
|
|
249
|
+
describe('readoutListener — one frame\'s relayed readout', () => {
|
|
250
|
+
const mine = {} as Window
|
|
251
|
+
const other = {} as Window
|
|
252
|
+
const info = { col: 3, row: 4, value: 7.5, exact: true, text: 'x:3 y:4 v:7.5' }
|
|
253
|
+
const msg = (data: unknown, source: unknown) =>
|
|
254
|
+
({ data, source }) as unknown as MessageEvent
|
|
255
|
+
|
|
256
|
+
test('the frame\'s readout reaches the host, and null clears it', () => {
|
|
257
|
+
const heard: unknown[] = []
|
|
258
|
+
const listen = readoutListener(() => mine, (i) => { heard.push(i) })
|
|
259
|
+
listen(msg({ type: 'apl_readout', info }, mine))
|
|
260
|
+
listen(msg({ type: 'apl_readout', info: null }, mine))
|
|
261
|
+
assert.deepEqual(heard, [info, null])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('another frame\'s readout, another message, or no frame is ignored', () => {
|
|
265
|
+
// A figure mounted twice: each mount hears only its own iframe.
|
|
266
|
+
const heard: unknown[] = []
|
|
267
|
+
readoutListener(() => mine, (i) => { heard.push(i) })(
|
|
268
|
+
msg({ type: 'apl_readout', info }, other))
|
|
269
|
+
readoutListener(() => mine, (i) => { heard.push(i) })(
|
|
270
|
+
msg({ type: 'awi_event', figId: 'f1', data: '{}' }, mine))
|
|
271
|
+
readoutListener(() => null, (i) => { heard.push(i) })(
|
|
272
|
+
msg({ type: 'apl_readout', info }, mine))
|
|
273
|
+
assert.deepEqual(heard, [])
|
|
274
|
+
})
|
|
275
|
+
})
|
|
@@ -90,6 +90,36 @@ export function attachFigure(bridge: FigureBridge, figId: string,
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** The hover readout anyplotlib computes on the SHOWN grid: `col`/`row` index the
|
|
94
|
+
* array the figure holds, `value` is its value there. */
|
|
95
|
+
export interface ReadoutInfo {
|
|
96
|
+
panel_id: string
|
|
97
|
+
img_x: number
|
|
98
|
+
img_y: number
|
|
99
|
+
col: number
|
|
100
|
+
row: number
|
|
101
|
+
xdata: number
|
|
102
|
+
ydata: number
|
|
103
|
+
units: string
|
|
104
|
+
value: number | null
|
|
105
|
+
exact: boolean
|
|
106
|
+
rgba: number[] | null
|
|
107
|
+
text: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A window `message` listener passing on the readouts ONE frame relays
|
|
111
|
+
* (`fill_iframe_html`'s relay); `null` means the cursor left the image. */
|
|
112
|
+
export function readoutListener(
|
|
113
|
+
frame: () => Window | null | undefined,
|
|
114
|
+
onReadout: (info: ReadoutInfo | null) => void,
|
|
115
|
+
): (e: MessageEvent) => void {
|
|
116
|
+
return (e) => {
|
|
117
|
+
const src = frame()
|
|
118
|
+
if (e.data?.type !== 'apl_readout' || !src || e.source !== src) return
|
|
119
|
+
onReadout(e.data.info ?? null)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
|
|
94
124
|
export function createFigureBridge(
|
|
95
125
|
log: (label: string, detail: Record<string, unknown>) => void = () => {},
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export {
|
|
12
12
|
createFigureBridge, useFigureBridge, useFigureEventForwarding,
|
|
13
13
|
} from './figureBridge.react'
|
|
14
|
-
export type { FigureBridge, BinaryFrame, RefLike } from './figureBridge'
|
|
14
|
+
export type { FigureBridge, BinaryFrame, RefLike, ReadoutInfo } from './figureBridge'
|
|
15
15
|
|
|
16
16
|
export { FigureFrame } from './FigureFrame'
|
|
17
17
|
export type { FigureFrameProps } from './FigureFrame'
|
|
@@ -30,7 +30,7 @@ function appRequire(name) {
|
|
|
30
30
|
}
|
|
31
31
|
const { _electron: electron } = appRequire('@playwright/test')
|
|
32
32
|
const { spawnSync } = require('child_process')
|
|
33
|
-
const { mkdtempSync } = require('fs')
|
|
33
|
+
const { mkdtempSync, rmSync } = require('fs')
|
|
34
34
|
const { join } = require('path')
|
|
35
35
|
const { tmpdir } = require('os')
|
|
36
36
|
|
|
@@ -51,6 +51,7 @@ async function launchApp(opts) {
|
|
|
51
51
|
env = {}, timeout = 60_000,
|
|
52
52
|
} = opts
|
|
53
53
|
if (!appDir || !appId) throw new Error('launchApp needs { appDir, appId }')
|
|
54
|
+
const profileDir = mkdtempSync(join(tmpdir(), `${appId}-e2e-profile-`))
|
|
54
55
|
|
|
55
56
|
const app = await electron.launch({
|
|
56
57
|
// Resolve Electron from the APP's tree, not Playwright's. Without an
|
|
@@ -63,7 +64,7 @@ async function launchApp(opts) {
|
|
|
63
64
|
executablePath: require(require.resolve('electron/index.js', { paths: [appDir] })),
|
|
64
65
|
args: [
|
|
65
66
|
join(appDir, 'out', 'main', 'index.js'),
|
|
66
|
-
`--user-data-dir=${
|
|
67
|
+
`--user-data-dir=${profileDir}`,
|
|
67
68
|
],
|
|
68
69
|
env: { ...process.env, ...env },
|
|
69
70
|
})
|
|
@@ -80,9 +81,9 @@ async function launchApp(opts) {
|
|
|
80
81
|
for (const type of readyMessages) await backend.waitForMessage(type, timeout)
|
|
81
82
|
|
|
82
83
|
return {
|
|
83
|
-
app, page, backend, jsErrors,
|
|
84
|
+
app, page, backend, jsErrors, profileDir,
|
|
84
85
|
assertNoJsErrors: () => assertNoJsErrors(jsErrors),
|
|
85
|
-
close: (opts) => closeApp(app, opts),
|
|
86
|
+
close: (opts) => closeApp(app, { profileDir, ...opts }),
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -109,23 +110,37 @@ async function firstWindowWithLog(app, logBuffer, timeout = 60_000) {
|
|
|
109
110
|
* runner's timeout, leaving an Electron tree alive that (for a DE app) still
|
|
110
111
|
* owns the server's single connection — every later launch then hangs on a
|
|
111
112
|
* connection that cannot be made. Returns 'closed' | 'killed' | 'noop' so a
|
|
112
|
-
* caller can log what teardown actually did.
|
|
113
|
+
* caller can log what teardown actually did. `opts.profileDir` (the launch's
|
|
114
|
+
* temp profile) is removed once the process is down, on every path.
|
|
113
115
|
*/
|
|
114
116
|
async function closeApp(app, opts = {}) {
|
|
115
|
-
const { timeout = 15_000, killTree = hardKillTree } = opts
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
const { timeout = 15_000, killTree = hardKillTree, profileDir = null } = opts
|
|
118
|
+
try {
|
|
119
|
+
if (!app) return 'noop'
|
|
120
|
+
const pid = app.process()?.pid
|
|
121
|
+
const closed = await new Promise((resolve) => {
|
|
122
|
+
const timer = setTimeout(() => resolve(false), timeout)
|
|
123
|
+
timer.unref?.()
|
|
124
|
+
Promise.resolve()
|
|
125
|
+
.then(() => app.close())
|
|
126
|
+
.then(() => { clearTimeout(timer); resolve(true) },
|
|
127
|
+
() => { clearTimeout(timer); resolve(false) })
|
|
128
|
+
})
|
|
129
|
+
if (closed) return 'closed'
|
|
130
|
+
if (pid) killTree(pid)
|
|
131
|
+
return pid ? 'killed' : 'noop'
|
|
132
|
+
} finally {
|
|
133
|
+
if (profileDir) removeProfileDir(profileDir)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Remove a launch's temp profile. Retries cover a handle Chromium is still
|
|
138
|
+
* letting go of on Windows; a directory that still will not go is left for the
|
|
139
|
+
* OS rather than failing the spec's teardown. */
|
|
140
|
+
function removeProfileDir(dir) {
|
|
141
|
+
try {
|
|
142
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
143
|
+
} catch { /* left behind; not the spec's failure */ }
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
/** Force-kill `pid` AND everything under it (the Python sidecar and its
|
|
@@ -240,5 +255,5 @@ async function countColorPixels(page, kind) {
|
|
|
240
255
|
|
|
241
256
|
module.exports = {
|
|
242
257
|
launchApp, createBackend, countColorPixels, assertNoJsErrors, errorLines,
|
|
243
|
-
firstWindowWithLog, closeApp, hardKillTree,
|
|
258
|
+
firstWindowWithLog, closeApp, hardKillTree, removeProfileDir,
|
|
244
259
|
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
const { test } = require('node:test')
|
|
17
17
|
const assert = require('node:assert/strict')
|
|
18
|
+
const { mkdtempSync, existsSync, writeFileSync } = require('fs')
|
|
19
|
+
const { join } = require('path')
|
|
20
|
+
const { tmpdir } = require('os')
|
|
18
21
|
const { firstWindowWithLog, closeApp } = require('./harness.cjs')
|
|
19
22
|
|
|
20
23
|
test('a missing first window reports the backend log, not a bare timeout', async () => {
|
|
@@ -71,3 +74,30 @@ test('closeApp tolerates a missing app', async () => {
|
|
|
71
74
|
assert.equal(await closeApp(null), 'noop')
|
|
72
75
|
assert.equal(await closeApp(undefined), 'noop')
|
|
73
76
|
})
|
|
77
|
+
|
|
78
|
+
/** A stand-in for the launch's --user-data-dir, with something inside it. */
|
|
79
|
+
function fakeProfileDir() {
|
|
80
|
+
const dir = mkdtempSync(join(tmpdir(), 'harness-test-profile-'))
|
|
81
|
+
writeFileSync(join(dir, 'Preferences'), '{}')
|
|
82
|
+
return dir
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('a clean close removes the profile dir', async () => {
|
|
86
|
+
const profileDir = fakeProfileDir()
|
|
87
|
+
const app = { process: () => ({ pid: 4242 }), close: async () => {} }
|
|
88
|
+
assert.equal(await closeApp(app, { timeout: 1000, killTree: () => {}, profileDir }), 'closed')
|
|
89
|
+
assert.equal(existsSync(profileDir), false, 'profile dir left behind')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('a hard-killed close removes the profile dir too', async () => {
|
|
93
|
+
const profileDir = fakeProfileDir()
|
|
94
|
+
const app = { process: () => ({ pid: 4242 }), close: () => new Promise(() => {}) }
|
|
95
|
+
assert.equal(await closeApp(app, { timeout: 100, killTree: () => {}, profileDir }), 'killed')
|
|
96
|
+
assert.equal(existsSync(profileDir), false, 'profile dir left behind')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('closeApp with no app still removes the profile dir', async () => {
|
|
100
|
+
const profileDir = fakeProfileDir()
|
|
101
|
+
assert.equal(await closeApp(null, { profileDir }), 'noop')
|
|
102
|
+
assert.equal(existsSync(profileDir), false, 'profile dir left behind')
|
|
103
|
+
})
|
|
@@ -32,6 +32,7 @@ from __future__ import annotations
|
|
|
32
32
|
|
|
33
33
|
import logging
|
|
34
34
|
import warnings
|
|
35
|
+
from collections.abc import Sequence
|
|
35
36
|
|
|
36
37
|
import numpy as np
|
|
37
38
|
|
|
@@ -140,6 +141,15 @@ PIN_SCROLL = (
|
|
|
140
141
|
"},{capture:true,passive:true})</script>"
|
|
141
142
|
)
|
|
142
143
|
|
|
144
|
+
#: Relays anyplotlib's hover readout (`apl:readout`, which fires whether or not
|
|
145
|
+
#: the pill is shown) to the host page, for `FigureFrame`'s `onReadout`.
|
|
146
|
+
#: `info` is null when the cursor leaves the image.
|
|
147
|
+
RELAY_READOUT = (
|
|
148
|
+
"<script>document.addEventListener('apl:readout',function(e){"
|
|
149
|
+
"if(parent!==window)parent.postMessage({type:'apl_readout',info:e.detail},'*')"
|
|
150
|
+
"})</script>"
|
|
151
|
+
)
|
|
152
|
+
|
|
143
153
|
|
|
144
154
|
def fill_iframe_html(html: str, *, background: str = FIGURE_BACKGROUND,
|
|
145
155
|
extra_head: str = "") -> str:
|
|
@@ -161,7 +171,8 @@ def fill_iframe_html(html: str, *, background: str = FIGURE_BACKGROUND,
|
|
|
161
171
|
f"#widget-root{{background:{background} !important;"
|
|
162
172
|
"width:100% !important;height:100% !important;display:block !important}"
|
|
163
173
|
"</style>")
|
|
164
|
-
return html.replace("<body>",
|
|
174
|
+
return html.replace("<body>",
|
|
175
|
+
style + PIN_SCROLL + RELAY_READOUT + extra_head + "<body>", 1)
|
|
165
176
|
|
|
166
177
|
|
|
167
178
|
class FigureView:
|
|
@@ -454,6 +465,19 @@ class FigureView:
|
|
|
454
465
|
log.debug("set_clim(%s, %s) failed: %s", vmin, vmax, e)
|
|
455
466
|
return False
|
|
456
467
|
|
|
468
|
+
def set_readout_visible(self, visible: bool) -> bool:
|
|
469
|
+
"""Show or hide the on-image hover pill. Hidden, the readout still reaches
|
|
470
|
+
the host through `FigureFrame`'s `onReadout`, so an app can print it in
|
|
471
|
+
its own units."""
|
|
472
|
+
if not self.is_open:
|
|
473
|
+
return False
|
|
474
|
+
try:
|
|
475
|
+
self._plot2d.set_readout_visible(bool(visible))
|
|
476
|
+
return True
|
|
477
|
+
except Exception as e:
|
|
478
|
+
log.debug("set_readout_visible(%s) failed: %s", visible, e)
|
|
479
|
+
return False
|
|
480
|
+
|
|
457
481
|
def on_event(self, *event_types: str):
|
|
458
482
|
"""Register a handler for anyplotlib pointer/key events on this figure.
|
|
459
483
|
|
|
@@ -574,6 +598,61 @@ class FigureView:
|
|
|
574
598
|
log.warning("could not observe line widget: %s", e)
|
|
575
599
|
return w
|
|
576
600
|
|
|
601
|
+
def add_rectangle_widget(self, *, x: float, y: float, w: float, h: float,
|
|
602
|
+
color: str = "#00e5ff", on_change=None,
|
|
603
|
+
max_extent=None, show_handles: bool = True,
|
|
604
|
+
linewidth: float = 2):
|
|
605
|
+
"""A draggable rectangle overlay. Returns the widget, or None.
|
|
606
|
+
|
|
607
|
+
`x, y` is the top-left corner. `max_extent` (a scalar, or
|
|
608
|
+
`(max_w, max_h)`) stops growth at the cap while a corner is dragged.
|
|
609
|
+
`on_change(x, y, w, h)` fires when a drag settles.
|
|
610
|
+
"""
|
|
611
|
+
if not self.is_open:
|
|
612
|
+
return None
|
|
613
|
+
try:
|
|
614
|
+
rect = self._plot2d.add_rectangle_widget(
|
|
615
|
+
x=float(x), y=float(y), w=float(w), h=float(h), color=color,
|
|
616
|
+
linewidth=float(linewidth), show_handles=bool(show_handles),
|
|
617
|
+
max_extent=max_extent)
|
|
618
|
+
except Exception as e:
|
|
619
|
+
log.warning("could not add rectangle widget: %s", e)
|
|
620
|
+
return None
|
|
621
|
+
|
|
622
|
+
def _settled(_event, _w=rect):
|
|
623
|
+
try:
|
|
624
|
+
if on_change is not None:
|
|
625
|
+
on_change(float(_w.x), float(_w.y),
|
|
626
|
+
float(_w.w), float(_w.h))
|
|
627
|
+
except Exception as e:
|
|
628
|
+
log.debug("rectangle widget callback failed: %s", e)
|
|
629
|
+
|
|
630
|
+
if on_change is not None:
|
|
631
|
+
try:
|
|
632
|
+
rect.add_event_handler(_settled, "pointer_up")
|
|
633
|
+
except Exception as e:
|
|
634
|
+
log.warning("could not observe rectangle widget: %s", e)
|
|
635
|
+
return rect
|
|
636
|
+
|
|
637
|
+
def add_texts(self, offsets, texts, *, name: str | None = None,
|
|
638
|
+
color: str | Sequence[str] = "#ffffff", fontsize: int = 12,
|
|
639
|
+
fontweight: str = "normal", outline_color: str | None = None):
|
|
640
|
+
"""Text labels at image-pixel positions (top-left anchors). Returns the
|
|
641
|
+
group, or None. The same `name` again replaces it in place;
|
|
642
|
+
`remove_widget` drops it. `color` is one colour or one per text.
|
|
643
|
+
`outline_color` draws a legibility halo.
|
|
644
|
+
"""
|
|
645
|
+
if not self.is_open:
|
|
646
|
+
return None
|
|
647
|
+
try:
|
|
648
|
+
return self._plot2d.add_texts(
|
|
649
|
+
offsets, [str(t) for t in texts], name=name, color=color,
|
|
650
|
+
fontsize=fontsize, fontweight=fontweight,
|
|
651
|
+
outline_color=outline_color)
|
|
652
|
+
except Exception as e:
|
|
653
|
+
log.warning("could not add texts: %s", e)
|
|
654
|
+
return None
|
|
655
|
+
|
|
577
656
|
@staticmethod
|
|
578
657
|
def set_widget_geometry(widget, **geometry) -> bool:
|
|
579
658
|
"""Move an overlay from Python. Returns whether it landed.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: de-shell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -42,7 +42,10 @@ dependencies = [
|
|
|
42
42
|
# the frame's shape, given a tile source on its zeros placeholder, then
|
|
43
43
|
# painted. On 0.7.3 a state push landing between the open and the first
|
|
44
44
|
# frame could leave the pane showing the placeholder.
|
|
45
|
-
|
|
45
|
+
#
|
|
46
|
+
# 0.10.0 for the hover readout's `apl:readout` event and `set_readout_visible`
|
|
47
|
+
# (0.9.0), and `add_texts`' `outline_color` halo (0.10.0).
|
|
48
|
+
"anyplotlib>=0.10.0",
|
|
46
49
|
"pyyaml",
|
|
47
50
|
]
|
|
48
51
|
|
|
@@ -56,7 +59,7 @@ tests = [
|
|
|
56
59
|
Homepage = "https://github.com/directelectron/de-shell"
|
|
57
60
|
Repository = "https://github.com/directelectron/de-shell"
|
|
58
61
|
Issues = "https://github.com/directelectron/de-shell/issues"
|
|
59
|
-
Changelog = "https://github.com/directelectron/de-shell/blob/main/CHANGELOG.
|
|
62
|
+
Changelog = "https://github.com/directelectron/de-shell/blob/main/CHANGELOG.rst"
|
|
60
63
|
|
|
61
64
|
# One version, in one place: de_shell.__version__. The release workflow refuses
|
|
62
65
|
# a tag that does not match it.
|
|
@@ -87,3 +90,68 @@ de_shell = [
|
|
|
87
90
|
[tool.pytest.ini_options]
|
|
88
91
|
testpaths = ["tests"]
|
|
89
92
|
timeout = 120
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Changelog management (towncrier)
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# towncrier assembles CHANGELOG.rst from one fragment file per PR, written while
|
|
98
|
+
# the change is fresh. The alternative — deriving notes from commit subjects at
|
|
99
|
+
# release time — reads like a commit log, because that is what it is: it cannot
|
|
100
|
+
# say what a change means to an app upgrading, only what the author was doing.
|
|
101
|
+
# It also stops two pull requests conflicting over the same few changelog lines.
|
|
102
|
+
#
|
|
103
|
+
# `package` is set: this repo's version of record IS de_shell.__version__ (the
|
|
104
|
+
# publish workflow refuses a tag that disagrees with it), so towncrier can read
|
|
105
|
+
# the version itself and `--version` is only needed to build notes for a bump
|
|
106
|
+
# that has not been written yet. See upcoming_changes/README.rst.
|
|
107
|
+
[tool.towncrier]
|
|
108
|
+
package = "de_shell"
|
|
109
|
+
package_dir = "."
|
|
110
|
+
directory = "upcoming_changes"
|
|
111
|
+
filename = "CHANGELOG.rst"
|
|
112
|
+
start_string = ".. towncrier release notes start\n"
|
|
113
|
+
issue_format = "`#{issue} <https://github.com/directelectron/de-shell/pull/{issue}>`_"
|
|
114
|
+
title_format = "{version} ({project_date})"
|
|
115
|
+
underlines = ["=", "-", "~"]
|
|
116
|
+
|
|
117
|
+
# Behaviour changes lead the notes: they are the entries an app upgrading has
|
|
118
|
+
# to ACT on, and they are easy to miss filed under "Bug Fixes".
|
|
119
|
+
[[tool.towncrier.type]]
|
|
120
|
+
directory = "api_change"
|
|
121
|
+
name = "API and Behaviour Changes"
|
|
122
|
+
showcontent = true
|
|
123
|
+
|
|
124
|
+
[[tool.towncrier.type]]
|
|
125
|
+
directory = "new_feature"
|
|
126
|
+
name = "New Features"
|
|
127
|
+
showcontent = true
|
|
128
|
+
|
|
129
|
+
[[tool.towncrier.type]]
|
|
130
|
+
directory = "bugfix"
|
|
131
|
+
name = "Bug Fixes"
|
|
132
|
+
showcontent = true
|
|
133
|
+
|
|
134
|
+
[[tool.towncrier.type]]
|
|
135
|
+
directory = "performance"
|
|
136
|
+
name = "Performance"
|
|
137
|
+
showcontent = true
|
|
138
|
+
|
|
139
|
+
[[tool.towncrier.type]]
|
|
140
|
+
directory = "deprecation"
|
|
141
|
+
name = "Deprecations"
|
|
142
|
+
showcontent = true
|
|
143
|
+
|
|
144
|
+
[[tool.towncrier.type]]
|
|
145
|
+
directory = "removal"
|
|
146
|
+
name = "Removals"
|
|
147
|
+
showcontent = true
|
|
148
|
+
|
|
149
|
+
[[tool.towncrier.type]]
|
|
150
|
+
directory = "doc"
|
|
151
|
+
name = "Documentation"
|
|
152
|
+
showcontent = true
|
|
153
|
+
|
|
154
|
+
[[tool.towncrier.type]]
|
|
155
|
+
directory = "maintenance"
|
|
156
|
+
name = "Maintenance"
|
|
157
|
+
showcontent = true
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import pytest
|
|
12
12
|
|
|
13
|
-
from de_shell.plotting.figure import FigureView, robust_levels
|
|
13
|
+
from de_shell.plotting.figure import FigureView, fill_iframe_html, robust_levels
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestRobustLevels:
|
|
@@ -156,3 +156,84 @@ class TestTileQuantisationBand:
|
|
|
156
156
|
def test_small_frame_stays_untiled_and_unaffected(self):
|
|
157
157
|
v, _ = self._open_and_show((64, 64))
|
|
158
158
|
assert not v._plot2d._state.get("tile_enabled")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestOverlays:
|
|
162
|
+
"""Widgets and labels on a real, open figure."""
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _open():
|
|
166
|
+
v = FigureView(0, title="t")
|
|
167
|
+
v.open((64, 64))
|
|
168
|
+
return v
|
|
169
|
+
|
|
170
|
+
def test_a_rectangle_carries_its_geometry_style_and_cap(self):
|
|
171
|
+
w = self._open().add_rectangle_widget(
|
|
172
|
+
x=1, y=2, w=3, h=4, color="#123456", max_extent=10,
|
|
173
|
+
show_handles=False, linewidth=1)
|
|
174
|
+
assert (w.x, w.y, w.w, w.h) == (1.0, 2.0, 3.0, 4.0)
|
|
175
|
+
assert w.color == "#123456" and w.linewidth == 1.0
|
|
176
|
+
assert w.show_handles is False and (w.max_w, w.max_h) == (10.0, 10.0)
|
|
177
|
+
|
|
178
|
+
def test_a_rectangle_reports_where_a_drag_settles(self):
|
|
179
|
+
heard = []
|
|
180
|
+
w = self._open().add_rectangle_widget(
|
|
181
|
+
x=1, y=2, w=3, h=4, on_change=lambda *g: heard.append(g))
|
|
182
|
+
w._update_from_js({"x": 5.0, "y": 6.0}, "pointer_move")
|
|
183
|
+
assert heard == [] # once per drag, not per frame
|
|
184
|
+
w._update_from_js({"w": 7.0, "h": 8.0}, "pointer_up")
|
|
185
|
+
assert heard == [(5.0, 6.0, 7.0, 8.0)]
|
|
186
|
+
|
|
187
|
+
def test_a_rectangle_moved_from_python_lands_without_a_callback(self):
|
|
188
|
+
heard = []
|
|
189
|
+
w = self._open().add_rectangle_widget(
|
|
190
|
+
x=1, y=2, w=3, h=4, on_change=lambda *g: heard.append(g))
|
|
191
|
+
assert FigureView.set_widget_geometry(w, x=9, y=9, w=5, h=5)
|
|
192
|
+
assert (w.x, w.y, w.w, w.h) == (9.0, 9.0, 5.0, 5.0) and heard == []
|
|
193
|
+
|
|
194
|
+
def test_a_rectangle_on_a_closed_figure_is_none(self):
|
|
195
|
+
assert FigureView(0).add_rectangle_widget(x=0, y=0, w=1, h=1) is None
|
|
196
|
+
|
|
197
|
+
def test_a_refused_rectangle_is_none_and_a_warning(self, monkeypatch, caplog):
|
|
198
|
+
v = self._open()
|
|
199
|
+
|
|
200
|
+
def boom(**_kw):
|
|
201
|
+
raise RuntimeError("boom")
|
|
202
|
+
|
|
203
|
+
monkeypatch.setattr(v._plot2d, "add_rectangle_widget", boom)
|
|
204
|
+
assert v.add_rectangle_widget(x=0, y=0, w=1, h=1) is None
|
|
205
|
+
assert "rectangle" in caplog.text
|
|
206
|
+
|
|
207
|
+
def test_texts_are_drawn_and_a_name_replaces_them_in_place(self):
|
|
208
|
+
v = self._open()
|
|
209
|
+
v.add_texts([[4, 5]], ["edge (auto)"], name="labels",
|
|
210
|
+
outline_color="#000000")
|
|
211
|
+
g = v.add_texts([[6, 7]], ["noise"], name="labels")
|
|
212
|
+
texts = v._plot2d.markers["texts"]
|
|
213
|
+
assert len(texts) == 1 and g._data["texts"] == ["noise"]
|
|
214
|
+
v.remove_widget(g)
|
|
215
|
+
assert "labels" not in texts
|
|
216
|
+
|
|
217
|
+
def test_texts_accept_one_colour_per_text(self):
|
|
218
|
+
v = self._open()
|
|
219
|
+
g = v.add_texts([[4, 5], [6, 7]], ["a", "b"],
|
|
220
|
+
color=["#ff0000", "#00ff00"])
|
|
221
|
+
assert g._data["color"] == ["#ff0000", "#00ff00"]
|
|
222
|
+
|
|
223
|
+
def test_texts_on_a_closed_figure_are_none(self):
|
|
224
|
+
assert FigureView(0).add_texts([[0, 0]], ["x"]) is None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestReadout:
|
|
228
|
+
def test_the_pill_can_be_hidden_for_the_host_to_draw_its_own(self):
|
|
229
|
+
v = FigureView(0)
|
|
230
|
+
v.open((64, 64))
|
|
231
|
+
assert v.set_readout_visible(False) is True
|
|
232
|
+
assert v._plot2d._state["readout_visible"] is False
|
|
233
|
+
|
|
234
|
+
def test_hiding_the_pill_on_a_closed_figure_is_a_no_op(self):
|
|
235
|
+
assert FigureView(0).set_readout_visible(False) is False
|
|
236
|
+
|
|
237
|
+
def test_every_figure_frame_relays_its_readout_to_the_host(self):
|
|
238
|
+
head = fill_iframe_html("<head></head><body></body>").split("<body>")[0]
|
|
239
|
+
assert "apl:readout" in head and "apl_readout" in head
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|