de-shell 0.2.0__py3-none-any.whl
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/__init__.py +25 -0
- de_shell/actions/__init__.py +0 -0
- de_shell/actions/context.py +62 -0
- de_shell/actions/figure_registry.py +53 -0
- de_shell/actions/lifecycle.py +295 -0
- de_shell/actions/registry.py +141 -0
- de_shell/actions/wizard.py +115 -0
- de_shell/app.py +170 -0
- de_shell/compute.py +103 -0
- de_shell/debug_flags.py +69 -0
- de_shell/ipc.py +236 -0
- de_shell/js/__init__.py +38 -0
- de_shell/js/__main__.py +4 -0
- de_shell/js/main/backendProcess.test.ts +70 -0
- de_shell/js/main/backendProcess.ts +330 -0
- de_shell/js/main/config.ts +53 -0
- de_shell/js/main/dialogs.ts +62 -0
- de_shell/js/main/envProgress.ts +126 -0
- de_shell/js/main/errorReport.ts +261 -0
- de_shell/js/main/index.ts +57 -0
- de_shell/js/main/problemLog.ts +53 -0
- de_shell/js/main/pythonEnv.test.ts +125 -0
- de_shell/js/main/pythonEnv.ts +442 -0
- de_shell/js/main/sentryEnvelope.test.ts +94 -0
- de_shell/js/main/sentryEnvelope.ts +100 -0
- de_shell/js/main/updater.ts +322 -0
- de_shell/js/main/updaterErrors.test.ts +111 -0
- de_shell/js/main/updaterErrors.ts +65 -0
- de_shell/js/main/window.ts +141 -0
- de_shell/js/package.json +5 -0
- de_shell/js/preload/index.ts +130 -0
- de_shell/js/renderer/FigureFrame.tsx +88 -0
- de_shell/js/renderer/figureBridge.react.ts +58 -0
- de_shell/js/renderer/figureBridge.test.ts +184 -0
- de_shell/js/renderer/figureBridge.ts +169 -0
- de_shell/js/renderer/index.ts +34 -0
- de_shell/js/renderer/protocol.ts +164 -0
- de_shell/js/renderer/shellState.test.ts +193 -0
- de_shell/js/renderer/shellState.ts +310 -0
- de_shell/js/testing/harness.cjs +244 -0
- de_shell/js/testing/harness.test.cjs +73 -0
- de_shell/log_stream.py +185 -0
- de_shell/plotting/__init__.py +0 -0
- de_shell/plotting/colormaps.py +27 -0
- de_shell/plotting/figure.py +601 -0
- de_shell/plotting/selectors/__init__.py +0 -0
- de_shell/plotting/selectors/utils.py +29 -0
- de_shell/plotting/stream.py +172 -0
- de_shell/process_guard.py +190 -0
- de_shell/session.py +211 -0
- de_shell/testing/__init__.py +0 -0
- de_shell/timing.py +28 -0
- de_shell-0.2.0.dist-info/METADATA +196 -0
- de_shell-0.2.0.dist-info/RECORD +57 -0
- de_shell-0.2.0.dist-info/WHEEL +5 -0
- de_shell-0.2.0.dist-info/licenses/LICENSE +21 -0
- de_shell-0.2.0.dist-info/top_level.txt +1 -0
de_shell/log_stream.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
log_stream.py — forward Python ``logging`` records to the Electron app-log panel.
|
|
3
|
+
|
|
4
|
+
A single :class:`IPCLogHandler` is attached to the root logger at startup. Each
|
|
5
|
+
record it accepts is turned into a ``{"type": "log", ...}`` IPC message (over the
|
|
6
|
+
same PLOTAPP stdout channel ``emit`` uses) and appended to a bounded ring buffer
|
|
7
|
+
so a freshly-opened panel can backfill recent history.
|
|
8
|
+
|
|
9
|
+
Verbosity is controlled at runtime from the frontend (the level switcher) via the
|
|
10
|
+
``set_log_level`` staged handler. To keep the panel useful rather than flooded,
|
|
11
|
+
records from third-party loggers (dask/distributed/matplotlib/…) are only
|
|
12
|
+
forwarded at WARNING and above; the app's own records are forwarded at the selected
|
|
13
|
+
level. So picking DEBUG shows the app's own debug trail without drowning in library
|
|
14
|
+
chatter, while warnings/errors from anywhere always surface.
|
|
15
|
+
|
|
16
|
+
No Qt. The handler logs to the structured IPC channel; ordinary logging still
|
|
17
|
+
goes to stderr via whatever other handlers are configured.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import collections
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
import traceback
|
|
25
|
+
|
|
26
|
+
LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
27
|
+
|
|
28
|
+
_handler: "IPCLogHandler | None" = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Map a logger name to a short, stable AREA tag so the log panel can group and
|
|
32
|
+
# filter by subsystem (the user copies only the relevant area's lines). Ordered
|
|
33
|
+
# longest/most-specific prefix first; first match wins.
|
|
34
|
+
#
|
|
35
|
+
# The shell only knows about ITSELF and the libraries every app shares. An app's
|
|
36
|
+
# own subsystems are its business, so it registers them via `register_area_rules`
|
|
37
|
+
# at startup — those are consulted BEFORE these, so an app can also override a
|
|
38
|
+
# default (e.g. classify `de_shell.plotting` as something else).
|
|
39
|
+
_SHELL_AREA_RULES = (
|
|
40
|
+
("de_shell.plotting.selectors", "navigator"),
|
|
41
|
+
("de_shell.plotting", "plots"),
|
|
42
|
+
("de_shell.actions", "actions"),
|
|
43
|
+
("de_shell", "shell"),
|
|
44
|
+
("anyplotlib.tile", "plots"),
|
|
45
|
+
("anyplotlib", "plots"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# App-registered rules, consulted first. See `register_area_rules`.
|
|
49
|
+
_app_area_rules: tuple[tuple[str, str], ...] = ()
|
|
50
|
+
|
|
51
|
+
# Logger prefixes whose sub-WARNING records reach the panel. Warnings and above
|
|
52
|
+
# always surface, from anywhere; below that, only the app's and the shell's own
|
|
53
|
+
# loggers, so the panel isn't flooded by third-party INFO/DEBUG chatter.
|
|
54
|
+
_verbose_packages: tuple[str, ...] = ("de_shell",)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def register_area_rules(rules, verbose_packages=()) -> None:
|
|
58
|
+
"""Register an app's logger-name → area mapping (and which of its packages
|
|
59
|
+
are verbose enough to show below WARNING).
|
|
60
|
+
|
|
61
|
+
``rules`` is a sequence of ``(logger_prefix, area_tag)``, most-specific
|
|
62
|
+
first. Called once at backend startup, before ``install``. Idempotent in the
|
|
63
|
+
sense that a second call REPLACES the app's rules rather than appending —
|
|
64
|
+
there is one app per process.
|
|
65
|
+
"""
|
|
66
|
+
global _app_area_rules, _verbose_packages
|
|
67
|
+
_app_area_rules = tuple((str(p), str(a)) for p, a in rules)
|
|
68
|
+
_verbose_packages = ("de_shell",) + tuple(str(p) for p in verbose_packages)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _area_for(name: str) -> str:
|
|
72
|
+
"""Short subsystem tag for a logger name (e.g. 'navigator', 'dask')."""
|
|
73
|
+
for prefix, area in _app_area_rules + _SHELL_AREA_RULES:
|
|
74
|
+
if name == prefix or name.startswith(prefix + "."):
|
|
75
|
+
return area
|
|
76
|
+
# Fall back to the top-level package so unmapped third-party loggers still
|
|
77
|
+
# get a usable, filterable tag instead of nothing.
|
|
78
|
+
return name.split(".", 1)[0] or "other"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _coerce_level(level) -> int:
|
|
82
|
+
if isinstance(level, int):
|
|
83
|
+
return level
|
|
84
|
+
return getattr(logging, str(level).upper(), logging.INFO)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class IPCLogHandler(logging.Handler):
|
|
88
|
+
"""A logging handler that streams records to the Electron app-log panel."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, level: int = logging.INFO, maxlen: int = 2000):
|
|
91
|
+
super().__init__(level)
|
|
92
|
+
self.buffer: collections.deque = collections.deque(maxlen=maxlen)
|
|
93
|
+
# Re-entrancy guard: forwarding a record calls ipc.emit, and any logging
|
|
94
|
+
# that happens *inside* that path would recurse back into this handler.
|
|
95
|
+
self._guard = threading.local()
|
|
96
|
+
|
|
97
|
+
# ── record → IPC ─────────────────────────────────────────────────────────
|
|
98
|
+
def _entry(self, record: logging.LogRecord) -> dict:
|
|
99
|
+
try:
|
|
100
|
+
msg = record.getMessage()
|
|
101
|
+
except Exception:
|
|
102
|
+
msg = str(record.msg)
|
|
103
|
+
if record.exc_info:
|
|
104
|
+
msg = msg + "\n" + "".join(traceback.format_exception(*record.exc_info))
|
|
105
|
+
return {
|
|
106
|
+
"type": "log",
|
|
107
|
+
"level": record.levelname,
|
|
108
|
+
"name": record.name,
|
|
109
|
+
"area": _area_for(record.name),
|
|
110
|
+
"msg": msg,
|
|
111
|
+
"time": record.created,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def _accept(self, record: logging.LogRecord) -> bool:
|
|
115
|
+
# Always surface warnings+; otherwise only the app's and the shell's own
|
|
116
|
+
# loggers, so the panel isn't flooded by third-party INFO/DEBUG at low
|
|
117
|
+
# levels. See `register_area_rules(verbose_packages=…)`.
|
|
118
|
+
if record.levelno >= logging.WARNING:
|
|
119
|
+
return True
|
|
120
|
+
name = record.name
|
|
121
|
+
return any(name == p or name.startswith(p + ".") for p in _verbose_packages)
|
|
122
|
+
|
|
123
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
124
|
+
if getattr(self._guard, "active", False):
|
|
125
|
+
return
|
|
126
|
+
if not self._accept(record):
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
self._guard.active = True
|
|
130
|
+
entry = self._entry(record)
|
|
131
|
+
self.buffer.append(entry)
|
|
132
|
+
from de_shell.ipc import emit as _emit
|
|
133
|
+
_emit(entry)
|
|
134
|
+
except Exception:
|
|
135
|
+
# Logging must never crash the app, and handleError() would print to
|
|
136
|
+
# stderr (noise); swallow deliberately here — this IS the log path.
|
|
137
|
+
pass
|
|
138
|
+
finally:
|
|
139
|
+
self._guard.active = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def install(level="INFO") -> IPCLogHandler:
|
|
143
|
+
"""Attach the singleton IPC log handler to the root logger (idempotent).
|
|
144
|
+
|
|
145
|
+
Sets the root level so records at the chosen verbosity actually reach the
|
|
146
|
+
handler. Call once at backend startup, after ``redirect_stray_stdout``.
|
|
147
|
+
"""
|
|
148
|
+
global _handler
|
|
149
|
+
if _handler is not None:
|
|
150
|
+
return _handler
|
|
151
|
+
lv = _coerce_level(level)
|
|
152
|
+
_handler = IPCLogHandler(level=lv)
|
|
153
|
+
root = logging.getLogger()
|
|
154
|
+
root.addHandler(_handler)
|
|
155
|
+
# Root defaults to WARNING; lift it so INFO/DEBUG records propagate to us.
|
|
156
|
+
if root.level == logging.NOTSET or root.level > lv:
|
|
157
|
+
root.setLevel(lv)
|
|
158
|
+
return _handler
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def set_level(level) -> int:
|
|
162
|
+
"""Set the live verbosity (root logger + handler). Returns the numeric level."""
|
|
163
|
+
lv = _coerce_level(level)
|
|
164
|
+
if _handler is not None:
|
|
165
|
+
_handler.setLevel(lv)
|
|
166
|
+
logging.getLogger().setLevel(lv)
|
|
167
|
+
return lv
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def emit_backfill() -> None:
|
|
171
|
+
"""Re-emit buffered records so a freshly-opened panel shows recent history."""
|
|
172
|
+
if _handler is None:
|
|
173
|
+
return
|
|
174
|
+
from de_shell.ipc import emit
|
|
175
|
+
emit({"type": "log_backfill", "entries": list(_handler.buffer)})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── staged handler (session.py dispatch: fn(session, plot, payload)) ───────────
|
|
179
|
+
|
|
180
|
+
def set_log_level(session, plot, payload) -> None:
|
|
181
|
+
"""Frontend level switcher → set verbosity, backfill history, confirm level."""
|
|
182
|
+
lv = set_level(payload.get("level", "INFO"))
|
|
183
|
+
emit_backfill()
|
|
184
|
+
from de_shell.ipc import emit
|
|
185
|
+
emit({"type": "log_level", "level": logging.getLevelName(lv)})
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Colormap registry — names that anyplotlib accepts directly.
|
|
2
|
+
# anyplotlib uses colorcet internally; these names are passed verbatim to
|
|
3
|
+
# Plot2D.set_colormap() / the Electron colormap selector.
|
|
4
|
+
#
|
|
5
|
+
# Only visually DISTINCT maps belong here. anyplotlib's _CMAP_ALIASES maps
|
|
6
|
+
# "plasma" and "hot" onto colorcet's "fire", and colorcet's own CET_L3 is a
|
|
7
|
+
# synonym for "fire" while CET_L1 is its grey ramp — so fire/hot/CET_L3 render
|
|
8
|
+
# byte-identical to plasma, and CET_L1 to gray. Verified by diffing the full
|
|
9
|
+
# 256-entry LUTs from anyplotlib._utils._build_colormap_lut; re-check
|
|
10
|
+
# distinctness the same way before adding a name.
|
|
11
|
+
#
|
|
12
|
+
# Names use colorcet's underscore spelling (CET_R1, not CET-R1) — the dashed
|
|
13
|
+
# spelling silently resolves to a flat grey ramp via anyplotlib's fallback
|
|
14
|
+
# chain (colorcet miss -> matplotlib miss -> grey ramp).
|
|
15
|
+
|
|
16
|
+
COLORMAPS: dict[str, str] = {
|
|
17
|
+
"gray": "gray",
|
|
18
|
+
"viridis": "viridis",
|
|
19
|
+
"plasma": "plasma",
|
|
20
|
+
"inferno": "inferno",
|
|
21
|
+
"magma": "magma",
|
|
22
|
+
"cividis": "cividis",
|
|
23
|
+
"turbo": "turbo",
|
|
24
|
+
"CET_R1": "CET_R1", # diverging
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DEFAULT_COLORMAP = "gray"
|