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.
Files changed (57) hide show
  1. de_shell/__init__.py +25 -0
  2. de_shell/actions/__init__.py +0 -0
  3. de_shell/actions/context.py +62 -0
  4. de_shell/actions/figure_registry.py +53 -0
  5. de_shell/actions/lifecycle.py +295 -0
  6. de_shell/actions/registry.py +141 -0
  7. de_shell/actions/wizard.py +115 -0
  8. de_shell/app.py +170 -0
  9. de_shell/compute.py +103 -0
  10. de_shell/debug_flags.py +69 -0
  11. de_shell/ipc.py +236 -0
  12. de_shell/js/__init__.py +38 -0
  13. de_shell/js/__main__.py +4 -0
  14. de_shell/js/main/backendProcess.test.ts +70 -0
  15. de_shell/js/main/backendProcess.ts +330 -0
  16. de_shell/js/main/config.ts +53 -0
  17. de_shell/js/main/dialogs.ts +62 -0
  18. de_shell/js/main/envProgress.ts +126 -0
  19. de_shell/js/main/errorReport.ts +261 -0
  20. de_shell/js/main/index.ts +57 -0
  21. de_shell/js/main/problemLog.ts +53 -0
  22. de_shell/js/main/pythonEnv.test.ts +125 -0
  23. de_shell/js/main/pythonEnv.ts +442 -0
  24. de_shell/js/main/sentryEnvelope.test.ts +94 -0
  25. de_shell/js/main/sentryEnvelope.ts +100 -0
  26. de_shell/js/main/updater.ts +322 -0
  27. de_shell/js/main/updaterErrors.test.ts +111 -0
  28. de_shell/js/main/updaterErrors.ts +65 -0
  29. de_shell/js/main/window.ts +141 -0
  30. de_shell/js/package.json +5 -0
  31. de_shell/js/preload/index.ts +130 -0
  32. de_shell/js/renderer/FigureFrame.tsx +88 -0
  33. de_shell/js/renderer/figureBridge.react.ts +58 -0
  34. de_shell/js/renderer/figureBridge.test.ts +184 -0
  35. de_shell/js/renderer/figureBridge.ts +169 -0
  36. de_shell/js/renderer/index.ts +34 -0
  37. de_shell/js/renderer/protocol.ts +164 -0
  38. de_shell/js/renderer/shellState.test.ts +193 -0
  39. de_shell/js/renderer/shellState.ts +310 -0
  40. de_shell/js/testing/harness.cjs +244 -0
  41. de_shell/js/testing/harness.test.cjs +73 -0
  42. de_shell/log_stream.py +185 -0
  43. de_shell/plotting/__init__.py +0 -0
  44. de_shell/plotting/colormaps.py +27 -0
  45. de_shell/plotting/figure.py +601 -0
  46. de_shell/plotting/selectors/__init__.py +0 -0
  47. de_shell/plotting/selectors/utils.py +29 -0
  48. de_shell/plotting/stream.py +172 -0
  49. de_shell/process_guard.py +190 -0
  50. de_shell/session.py +211 -0
  51. de_shell/testing/__init__.py +0 -0
  52. de_shell/timing.py +28 -0
  53. de_shell-0.2.0.dist-info/METADATA +196 -0
  54. de_shell-0.2.0.dist-info/RECORD +57 -0
  55. de_shell-0.2.0.dist-info/WHEEL +5 -0
  56. de_shell-0.2.0.dist-info/licenses/LICENSE +21 -0
  57. 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"